mirror of
				https://github.com/AppFlowy-IO/AppFlowy.git
				synced 2025-11-04 12:03:28 +00:00 
			
		
		
		
	feat: show indicator when send chat message with attachment/mention etc (#5919)
* chore: adjust line height * chore: send stream message * chore: index file * chore: clippy
This commit is contained in:
		
							parent
							
								
									758c304a74
								
							
						
					
					
						commit
						7abe9f4661
					
				@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
 | 
			
		||||
              value.text,
 | 
			
		||||
              fontSize: 14.0,
 | 
			
		||||
              maxLines: 2,
 | 
			
		||||
              lineHeight: 1.0,
 | 
			
		||||
              overflow: TextOverflow.ellipsis,
 | 
			
		||||
              textAlign: TextAlign.center,
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
 | 
			
		||||
        return Row(
 | 
			
		||||
          children: [
 | 
			
		||||
            FlowyText(
 | 
			
		||||
              lineHeight: 1.0,
 | 
			
		||||
              databaseLayoutFromViewLayout(view.layout).layoutName,
 | 
			
		||||
              color: Theme.of(context).hintColor,
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
 | 
			
		||||
        mainAxisSize: MainAxisSize.min,
 | 
			
		||||
        children: [
 | 
			
		||||
          FlowyText(
 | 
			
		||||
            lineHeight: 1.0,
 | 
			
		||||
            name,
 | 
			
		||||
            color: theme.colorScheme.onSurface,
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
 | 
			
		||||
                mainAxisSize: MainAxisSize.min,
 | 
			
		||||
                children: [
 | 
			
		||||
                  FlowyText(
 | 
			
		||||
                    lineHeight: 1.0,
 | 
			
		||||
                    languageFromLocale(locale),
 | 
			
		||||
                    color: theme.colorScheme.onSurface,
 | 
			
		||||
                  ),
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
 | 
			
		||||
  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<ChatEvent, ChatState> {
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          // 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<ChatEvent, ChatState> {
 | 
			
		||||
              // 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<ChatEvent, ChatState> {
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
          },
 | 
			
		||||
          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<String, dynamic>? 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<ChatEvent, ChatState> {
 | 
			
		||||
      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<ChatEvent, ChatState> {
 | 
			
		||||
      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<ChatEvent, ChatState> {
 | 
			
		||||
      },
 | 
			
		||||
      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<ChatEvent, ChatState> {
 | 
			
		||||
              if (!isClosed) {
 | 
			
		||||
                result.fold(
 | 
			
		||||
                  (list) {
 | 
			
		||||
                    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<ChatEvent, ChatState> {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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<ChatEvent, ChatState> {
 | 
			
		||||
        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<ChatEvent, ChatState> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<ChatEvent, ChatState> {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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<RelatedQuestionPB> relatedQuestions,
 | 
			
		||||
    @Default(false) bool acceptRelatedQuestion,
 | 
			
		||||
    // The last user message that is sent to the server.
 | 
			
		||||
    ChatMessagePB? lastSentMessage,
 | 
			
		||||
    AnswerStream? answerStream,
 | 
			
		||||
 | 
			
		||||
@ -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<String> _controller = StreamController.broadcast();
 | 
			
		||||
  late StreamSubscription<String> _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<void> 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<ChatUserMessageEvent, ChatUserMessageState> {
 | 
			
		||||
  ChatUserMessageBloc({
 | 
			
		||||
    required Message message,
 | 
			
		||||
    required String? metadata,
 | 
			
		||||
    required dynamic message,
 | 
			
		||||
  }) : super(
 | 
			
		||||
          ChatUserMessageState.initial(
 | 
			
		||||
            message,
 | 
			
		||||
            chatFilesFromMetadataString(metadata),
 | 
			
		||||
          ),
 | 
			
		||||
        ) {
 | 
			
		||||
    on<ChatUserMessageEvent>(
 | 
			
		||||
      (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<ChatFile> files,
 | 
			
		||||
    required String text,
 | 
			
		||||
    QuestionStream? stream,
 | 
			
		||||
    String? messageId,
 | 
			
		||||
    @Default(QuestionMessageState.finish()) QuestionMessageState messageState,
 | 
			
		||||
  }) = _ChatUserMessageState;
 | 
			
		||||
 | 
			
		||||
  factory ChatUserMessageState.initial(
 | 
			
		||||
    Message message,
 | 
			
		||||
    List<ChatFile> 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<ChatUserMessageBubbleEvent, ChatUserMessageBubbleState> {
 | 
			
		||||
  ChatUserMessageBubbleBloc({
 | 
			
		||||
    required Message message,
 | 
			
		||||
    required String? metadata,
 | 
			
		||||
  }) : super(
 | 
			
		||||
          ChatUserMessageBubbleState.initial(
 | 
			
		||||
            message,
 | 
			
		||||
            chatFilesFromMetadataString(metadata),
 | 
			
		||||
          ),
 | 
			
		||||
        ) {
 | 
			
		||||
    on<ChatUserMessageBubbleEvent>(
 | 
			
		||||
      (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<ChatFile> files,
 | 
			
		||||
  }) = _ChatUserMessageBubbleState;
 | 
			
		||||
 | 
			
		||||
  factory ChatUserMessageBubbleState.initial(
 | 
			
		||||
    Message message,
 | 
			
		||||
    List<ChatFile> files,
 | 
			
		||||
  ) =>
 | 
			
		||||
      ChatUserMessageBubbleState(message: message, files: files);
 | 
			
		||||
}
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -49,6 +49,8 @@ class AIMessageMetadata extends StatelessWidget {
 | 
			
		||||
                      child: FlowyText(
 | 
			
		||||
                        m.name,
 | 
			
		||||
                        fontSize: 14,
 | 
			
		||||
                        lineHeight: 1.0,
 | 
			
		||||
                        overflow: TextOverflow.ellipsis,
 | 
			
		||||
                      ),
 | 
			
		||||
                    ),
 | 
			
		||||
                    onTap: () => onSelectedMetadata(m),
 | 
			
		||||
 | 
			
		||||
@ -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<String>
 | 
			
		||||
  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: () {
 | 
			
		||||
 | 
			
		||||
@ -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<ChatUserMessageBloc, ChatUserMessageState>(
 | 
			
		||||
      child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>(
 | 
			
		||||
        builder: (context, state) {
 | 
			
		||||
          return Column(
 | 
			
		||||
            mainAxisSize: MainAxisSize.min,
 | 
			
		||||
 | 
			
		||||
@ -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<ChatUserMessageBloc, ChatUserMessageState>(
 | 
			
		||||
        builder: (context, state) {
 | 
			
		||||
          final List<Widget> 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,
 | 
			
		||||
          );
 | 
			
		||||
        },
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -237,6 +237,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
 | 
			
		||||
                  leftIcon: FlowySvg(action.icon),
 | 
			
		||||
                  text: FlowyText.medium(
 | 
			
		||||
                    action.text,
 | 
			
		||||
                    lineHeight: 1.0,
 | 
			
		||||
                    overflow: TextOverflow.ellipsis,
 | 
			
		||||
                  ),
 | 
			
		||||
                  onTap: () {
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -167,6 +167,7 @@ class _CalculateCellState extends State<CalculateCell> {
 | 
			
		||||
                  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<CalculateCell> {
 | 
			
		||||
                    if (widget.calculation!.value.isNotEmpty) ...[
 | 
			
		||||
                      const HSpace(8),
 | 
			
		||||
                      FlowyText(
 | 
			
		||||
                        lineHeight: 1.0,
 | 
			
		||||
                        calculateValue,
 | 
			
		||||
                        color: AFThemeExtension.of(context).textColor,
 | 
			
		||||
                        overflow: TextOverflow.ellipsis,
 | 
			
		||||
 | 
			
		||||
@ -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();
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
 | 
			
		||||
        height: 28,
 | 
			
		||||
        child: FlowyButton(
 | 
			
		||||
          text: FlowyText(
 | 
			
		||||
            lineHeight: 1.0,
 | 
			
		||||
            LocaleKeys.grid_settings_addFilter.tr(),
 | 
			
		||||
            color: AFThemeExtension.of(context).textColor,
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      text: FlowyText(
 | 
			
		||||
        lineHeight: 1.0,
 | 
			
		||||
        LocaleKeys.grid_row_newRow.tr(),
 | 
			
		||||
        color: Theme.of(context).hintColor,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -195,6 +195,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
 | 
			
		||||
      margin: GridSize.cellContentInsets,
 | 
			
		||||
      radius: BorderRadius.zero,
 | 
			
		||||
      text: FlowyText(
 | 
			
		||||
        lineHeight: 1.0,
 | 
			
		||||
        LocaleKeys.grid_field_newProperty.tr(),
 | 
			
		||||
        overflow: TextOverflow.ellipsis,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
 | 
			
		||||
      useIntrinsicWidth: true,
 | 
			
		||||
      text: FlowyText(
 | 
			
		||||
        text,
 | 
			
		||||
        lineHeight: 1.0,
 | 
			
		||||
        color: AFThemeExtension.of(context).textColor,
 | 
			
		||||
        overflow: TextOverflow.ellipsis,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget {
 | 
			
		||||
            ),
 | 
			
		||||
            text: FlowyText(
 | 
			
		||||
              view.name,
 | 
			
		||||
              lineHeight: 1.0,
 | 
			
		||||
              fontSize: FontSizes.s11,
 | 
			
		||||
              textAlign: TextAlign.center,
 | 
			
		||||
              overflow: TextOverflow.ellipsis,
 | 
			
		||||
 | 
			
		||||
@ -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<SwitchFieldButton> {
 | 
			
		||||
                },
 | 
			
		||||
                text: FlowyText.medium(
 | 
			
		||||
                  state.field.fieldType.i18n,
 | 
			
		||||
                  lineHeight: 1.0,
 | 
			
		||||
                  color: isPrimary ? Theme.of(context).disabledColor : null,
 | 
			
		||||
                ),
 | 
			
		||||
                leftIcon: FlowySvg(
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
              ),
 | 
			
		||||
 | 
			
		||||
@ -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: () {
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
 | 
			
		||||
          text: FlowyText.medium(
 | 
			
		||||
            name,
 | 
			
		||||
            color: AFThemeExtension.of(context).textColor,
 | 
			
		||||
            lineHeight: 1.0,
 | 
			
		||||
          ),
 | 
			
		||||
          leftIcon: icon != null
 | 
			
		||||
              ? FlowySvg(
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
 | 
			
		||||
@ -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<CreateRowFieldButton> {
 | 
			
		||||
        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,
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
@ -150,6 +150,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
 | 
			
		||||
        child: FlowyButton(
 | 
			
		||||
          hoverColor: AFThemeExtension.of(context).lightGreyHover,
 | 
			
		||||
          text: FlowyText.medium(
 | 
			
		||||
            lineHeight: 1.0,
 | 
			
		||||
            widget.fieldInfo.name,
 | 
			
		||||
            color: AFThemeExtension.of(context).textColor,
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
@ -163,7 +163,6 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
 | 
			
		||||
                            fontSize: 16,
 | 
			
		||||
                            maxLines: 2,
 | 
			
		||||
                            textAlign: TextAlign.center,
 | 
			
		||||
                            lineHeight: 1.5,
 | 
			
		||||
                          ),
 | 
			
		||||
                        ],
 | 
			
		||||
                      ],
 | 
			
		||||
 | 
			
		||||
@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
 | 
			
		||||
              size: Size(20, 20),
 | 
			
		||||
            ),
 | 
			
		||||
            text: FlowyText(
 | 
			
		||||
              lineHeight: 1.0,
 | 
			
		||||
              LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
@ -101,7 +101,10 @@ class _TrashPageState extends State<TrashPage> {
 | 
			
		||||
          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<TrashPage> {
 | 
			
		||||
          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(
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -626,7 +626,6 @@ class _Heading extends StatelessWidget {
 | 
			
		||||
                  description!,
 | 
			
		||||
                  fontSize: 12,
 | 
			
		||||
                  maxLines: 5,
 | 
			
		||||
                  lineHeight: 1.5,
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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:
 | 
			
		||||
 | 
			
		||||
@ -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<ChatMessageMetadata>,
 | 
			
		||||
  ) -> Result<ChatMessagePB, FlowyError> {
 | 
			
		||||
    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)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -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<ChatMessageMetadata>,
 | 
			
		||||
  ) -> Result<ChatMessagePB, FlowyError> {
 | 
			
		||||
    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)
 | 
			
		||||
 | 
			
		||||
@ -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<ChatMessageMetaPB>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -10,3 +10,4 @@ mod middleware;
 | 
			
		||||
pub mod notification;
 | 
			
		||||
mod persistence;
 | 
			
		||||
mod protobuf;
 | 
			
		||||
mod stream_message;
 | 
			
		||||
 | 
			
		||||
@ -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<String> + Unpin),
 | 
			
		||||
  ) -> FlowyResult<()> {
 | 
			
		||||
    for metadata in metadata_list {
 | 
			
		||||
      let mut index_metadata = HashMap::new();
 | 
			
		||||
@ -351,47 +353,89 @@ 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(
 | 
			
		||||
      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()),
 | 
			
		||||
                  Some(index_metadata),
 | 
			
		||||
            metadata,
 | 
			
		||||
            &index_metadata,
 | 
			
		||||
            index_process_sink,
 | 
			
		||||
          )
 | 
			
		||||
                .await
 | 
			
		||||
              {
 | 
			
		||||
                error!("[AI Plugin] failed to index file: {:?}", err);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          _ => {
 | 
			
		||||
          .await?;
 | 
			
		||||
      } else {
 | 
			
		||||
        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),
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async fn process_index_file(
 | 
			
		||||
    &self,
 | 
			
		||||
    chat_id: &str,
 | 
			
		||||
    file_path: Option<PathBuf>,
 | 
			
		||||
    content: Option<String>,
 | 
			
		||||
    metadata: &ChatMessageMetadata,
 | 
			
		||||
    index_metadata: &HashMap<String, serde_json::Value>,
 | 
			
		||||
    index_process_sink: &mut (impl Sink<String> + Unpin),
 | 
			
		||||
  ) -> Result<(), FlowyError> {
 | 
			
		||||
    let _ = index_process_sink
 | 
			
		||||
      .send(
 | 
			
		||||
        StreamMessage::StartIndexFile {
 | 
			
		||||
          file_name: metadata.name.clone(),
 | 
			
		||||
        }
 | 
			
		||||
        .to_string(),
 | 
			
		||||
      )
 | 
			
		||||
              .await
 | 
			
		||||
            {
 | 
			
		||||
              error!("[AI Plugin] failed to index file: {:?}", err);
 | 
			
		||||
            }
 | 
			
		||||
      .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(())
 | 
			
		||||
 | 
			
		||||
@ -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<String> + 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(())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										34
									
								
								frontend/rust-lib/flowy-ai/src/stream_message.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/rust-lib/flowy-ai/src/stream_message.rs
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user