feat: Stream chat message (#5498)

* chore: stream message

* chore: stream message

* chore: fix streaming

* chore: fix clippy
This commit is contained in:
Nathan.fooo 2024-06-09 14:02:32 +08:00 committed by GitHub
parent 94060a0a99
commit bb3e9d5bd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1691 additions and 870 deletions

View File

@ -1,42 +1,131 @@
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'dart:async';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.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 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_ai_message_bloc.freezed.dart'; part 'chat_ai_message_bloc.freezed.dart';
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> { class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
ChatAIMessageBloc({ ChatAIMessageBloc({
required Message message, dynamic message,
required this.chatId,
required this.questionId,
}) : super(ChatAIMessageState.initial(message)) { }) : super(ChatAIMessageState.initial(message)) {
if (state.stream != null) {
_subscription = state.stream!.listen((text) {
if (isClosed) {
return;
}
if (text.startsWith("data:")) {
add(ChatAIMessageEvent.newText(text.substring(5)));
} else if (text.startsWith("error:")) {
add(ChatAIMessageEvent.receiveError(text.substring(5)));
}
});
if (state.stream!.error != null) {
Future.delayed(const Duration(milliseconds: 300), () {
if (!isClosed) {
add(ChatAIMessageEvent.receiveError(state.stream!.error!));
}
});
}
}
on<ChatAIMessageEvent>( on<ChatAIMessageEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async {}, initial: () async {},
update: (userProfile, deviceId, states) {}, newText: (newText) {
emit(state.copyWith(text: state.text + newText, error: null));
},
receiveError: (error) {
emit(state.copyWith(error: error));
},
retry: () {
if (questionId is! Int64) {
Log.error("Question id is not Int64: $questionId");
return;
}
emit(
state.copyWith(
retryState: const LoadingState.loading(),
error: null,
),
);
final payload = ChatMessageIdPB(
chatId: chatId,
messageId: questionId,
);
ChatEventGetAnswerForQuestion(payload).send().then((result) {
if (!isClosed) {
result.fold(
(answer) {
add(ChatAIMessageEvent.retryResult(answer.content));
},
(err) {
Log.error("Failed to get answer: $err");
add(ChatAIMessageEvent.receiveError(err.toString()));
},
);
}
});
},
retryResult: (String text) {
emit(
state.copyWith(
text: text,
error: null,
retryState: const LoadingState.finish(),
),
);
},
); );
}, },
); );
} }
@override
Future<void> close() {
_subscription?.cancel();
return super.close();
}
StreamSubscription<AnswerStreamElement>? _subscription;
final String chatId;
final Int64? questionId;
} }
@freezed @freezed
class ChatAIMessageEvent with _$ChatAIMessageEvent { class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.initial() = Initial; const factory ChatAIMessageEvent.initial() = Initial;
const factory ChatAIMessageEvent.update( const factory ChatAIMessageEvent.newText(String text) = _NewText;
UserProfilePB userProfile, const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
String deviceId, const factory ChatAIMessageEvent.retry() = _Retry;
DocumentAwarenessStatesPB states, const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
) = Update;
} }
@freezed @freezed
class ChatAIMessageState with _$ChatAIMessageState { class ChatAIMessageState with _$ChatAIMessageState {
const factory ChatAIMessageState({ const factory ChatAIMessageState({
required Message message, AnswerStream? stream,
String? error,
required String text,
required LoadingState retryState,
}) = _ChatAIMessageState; }) = _ChatAIMessageState;
factory ChatAIMessageState.initial(Message message) => factory ChatAIMessageState.initial(dynamic text) {
ChatAIMessageState(message: message); return ChatAIMessageState(
text: text is String ? text : "",
stream: text is AnswerStream ? text : null,
retryState: const LoadingState.finish(),
);
}
} }

View File

@ -1,6 +1,12 @@
import 'dart:async';
import 'dart:collection';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@ -11,10 +17,8 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:nanoid/nanoid.dart'; import 'package:nanoid/nanoid.dart';
import 'chat_message_listener.dart'; import 'chat_message_listener.dart';
part 'chat_bloc.freezed.dart'; part 'chat_bloc.freezed.dart';
const canRetryKey = "canRetry";
const sendMessageErrorKey = "sendMessageError"; const sendMessageErrorKey = "sendMessageError";
class ChatBloc extends Bloc<ChatEvent, ChatState> { class ChatBloc extends Bloc<ChatEvent, ChatState> {
@ -26,78 +30,31 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
super( super(
ChatState.initial(view, userProfile), ChatState.initial(view, userProfile),
) { ) {
_startListening();
_dispatch(); _dispatch();
listener.start(
chatMessageCallback: _handleChatMessage,
lastUserSentMessageCallback: (message) {
if (!isClosed) {
add(ChatEvent.didSentUserMessage(message));
}
},
chatErrorMessageCallback: (err) {
if (!isClosed) {
Log.error("chat error: ${err.errorMessage}");
final metadata = OnetimeShotType.serverStreamError.toMap();
if (state.lastSentMessage != null) {
metadata[canRetryKey] = "true";
}
final error = CustomMessage(
metadata: metadata,
author: const User(id: "system"),
id: 'system',
);
add(ChatEvent.streaming([error]));
add(const ChatEvent.didFinishStreaming());
}
},
latestMessageCallback: (list) {
if (!isClosed) {
final messages = list.messages.map(_createChatMessage).toList();
add(ChatEvent.didLoadLatestMessages(messages));
}
},
prevMessageCallback: (list) {
if (!isClosed) {
final messages = list.messages.map(_createChatMessage).toList();
add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore));
}
},
finishAnswerQuestionCallback: () {
if (!isClosed) {
add(const ChatEvent.didFinishStreaming());
if (state.lastSentMessage != null) {
final payload = ChatMessageIdPB(
chatId: chatId,
messageId: state.lastSentMessage!.messageId,
);
// When user message was sent to the server, we start gettting related question
ChatEventGetRelatedQuestion(payload).send().then((result) {
if (!isClosed) {
result.fold(
(list) {
add(
ChatEvent.didReceiveRelatedQuestion(list.items),
);
},
(err) {
Log.error("Failed to get related question: $err");
},
);
}
});
}
}
},
);
} }
final ChatMessageListener listener; final ChatMessageListener listener;
final String chatId; final String chatId;
/// The last streaming message id
String lastStreamMessageId = '';
/// Using a temporary map to associate the real message ID with the last streaming message ID.
///
/// When a message is streaming, it does not have a real message ID. To maintain the relationship
/// between the real message ID and the last streaming message ID, we use this map to store the associations.
///
/// This map will be updated when receiving a message from the server and its author type
/// is 3 (AI response).
final HashMap<String, String> temporaryMessageIDMap = HashMap();
@override @override
Future<void> close() { Future<void> close() async {
listener.stop(); if (state.answerStream != null) {
await state.answerStream?.dispose();
}
await listener.stop();
return super.close(); return super.close();
} }
@ -114,8 +71,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
}, },
startLoadingPrevMessage: () async { startLoadingPrevMessage: () async {
Int64? beforeMessageId; Int64? beforeMessageId;
if (state.messages.isNotEmpty) { final oldestMessage = _getOlderstMessage();
beforeMessageId = Int64.parseInt(state.messages.last.id); if (oldestMessage != null) {
beforeMessageId = Int64.parseInt(oldestMessage.id);
} }
_loadPrevMessage(beforeMessageId); _loadPrevMessage(beforeMessageId);
emit( emit(
@ -126,8 +84,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
}, },
didLoadPreviousMessages: (List<Message> messages, bool hasMore) { didLoadPreviousMessages: (List<Message> messages, bool hasMore) {
Log.debug("did load previous messages: ${messages.length}"); Log.debug("did load previous messages: ${messages.length}");
final uniqueMessages = {...state.messages, ...messages}.toList() final onetimeMessages = _getOnetimeMessages();
final allMessages = _perminentMessages();
final uniqueMessages = {...allMessages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id)); ..sort((a, b) => b.id.compareTo(a.id));
uniqueMessages.insertAll(0, onetimeMessages);
emit( emit(
state.copyWith( state.copyWith(
messages: uniqueMessages, messages: uniqueMessages,
@ -137,8 +100,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
); );
}, },
didLoadLatestMessages: (List<Message> messages) { didLoadLatestMessages: (List<Message> messages) {
final uniqueMessages = {...state.messages, ...messages}.toList() final onetimeMessages = _getOnetimeMessages();
final allMessages = _perminentMessages();
final uniqueMessages = {...allMessages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id)); ..sort((a, b) => b.id.compareTo(a.id));
uniqueMessages.insertAll(0, onetimeMessages);
emit( emit(
state.copyWith( state.copyWith(
messages: uniqueMessages, messages: uniqueMessages,
@ -146,55 +113,43 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
), ),
); );
}, },
streaming: (List<Message> messages) { streaming: (Message message) {
final allMessages = _perminentMessages(); final allMessages = _perminentMessages();
allMessages.insertAll(0, messages); allMessages.insert(0, message);
emit(state.copyWith(messages: allMessages));
},
didFinishStreaming: () {
emit( emit(
state.copyWith( state.copyWith(
answerQuestionStatus: const LoadingState.finish(), messages: allMessages,
streamingStatus: const LoadingState.loading(),
), ),
); );
}, },
sendMessage: (String message) async { didFinishStreaming: () {
await _handleSentMessage(message, emit); emit(
state.copyWith(streamingStatus: const LoadingState.finish()),
// Create a loading indicator );
final loadingMessage = },
_loadingMessage(state.userProfile.id.toString()); receveMessage: (Message message) {
final allMessages = List<Message>.from(state.messages) final allMessages = _perminentMessages();
..insert(0, loadingMessage); // remove message with the same id
allMessages.removeWhere((element) => element.id == message.id);
allMessages.insert(0, message);
emit(
state.copyWith(
messages: allMessages,
),
);
},
sendMessage: (String message) {
_startStreamingMessage(message, emit);
final allMessages = _perminentMessages();
emit( emit(
state.copyWith( state.copyWith(
lastSentMessage: null, lastSentMessage: null,
messages: allMessages, messages: allMessages,
answerQuestionStatus: const LoadingState.loading(),
relatedQuestions: [], relatedQuestions: [],
), ),
); );
}, },
retryGenerate: () {
if (state.lastSentMessage == null) {
return;
}
final payload = ChatMessageIdPB(
chatId: chatId,
messageId: state.lastSentMessage!.messageId,
);
ChatEventGetAnswerForQuestion(payload).send().then((result) {
if (!isClosed) {
result.fold(
(answer) => _handleChatMessage(answer),
(err) {
Log.error("Failed to get answer: $err");
},
);
}
});
},
didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) { didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) {
final allMessages = _perminentMessages(); final allMessages = _perminentMessages();
final message = CustomMessage( final message = CustomMessage(
@ -224,11 +179,104 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
), ),
); );
}, },
didUpdateAnswerStream: (AnswerStream stream) {
emit(state.copyWith(answerStream: stream));
},
stopStream: () async {
if (state.answerStream == null) {
return;
}
final payload = StopStreamPB(chatId: chatId);
await ChatEventStopStream(payload).send();
final allMessages = _perminentMessages();
if (state.streamingStatus != const LoadingState.finish()) {
// If the streaming is not started, remove the message from the list
if (!state.answerStream!.hasStarted) {
allMessages.removeWhere(
(element) => element.id == lastStreamMessageId,
);
lastStreamMessageId = "";
}
// when stop stream, we will set the answer stream to null. Which means the streaming
// is finished or canceled.
emit(
state.copyWith(
messages: allMessages,
answerStream: null,
streamingStatus: const LoadingState.finish(),
),
);
}
},
); );
}, },
); );
} }
void _startListening() {
listener.start(
chatMessageCallback: (pb) {
if (!isClosed) {
// 3 mean message response from AI
if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) {
temporaryMessageIDMap[pb.messageId.toString()] =
lastStreamMessageId;
lastStreamMessageId = "";
}
final message = _createTextMessage(pb);
add(ChatEvent.receveMessage(message));
}
},
chatErrorMessageCallback: (err) {
if (!isClosed) {
Log.error("chat error: ${err.errorMessage}");
add(const ChatEvent.didFinishStreaming());
}
},
latestMessageCallback: (list) {
if (!isClosed) {
final messages = list.messages.map(_createTextMessage).toList();
add(ChatEvent.didLoadLatestMessages(messages));
}
},
prevMessageCallback: (list) {
if (!isClosed) {
final messages = list.messages.map(_createTextMessage).toList();
add(ChatEvent.didLoadPreviousMessages(messages, list.hasMore));
}
},
finishStreamingCallback: () {
if (!isClosed) {
add(const ChatEvent.didFinishStreaming());
// 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) {
final payload = ChatMessageIdPB(
chatId: chatId,
messageId: state.lastSentMessage!.messageId,
);
// When user message was sent to the server, we start gettting related question
ChatEventGetRelatedQuestion(payload).send().then((result) {
if (!isClosed) {
result.fold(
(list) {
add(ChatEvent.didReceiveRelatedQuestion(list.items));
},
(err) {
Log.error("Failed to get related question: $err");
},
);
}
});
}
}
},
);
}
// Returns the list of messages that are not include one-time messages. // Returns the list of messages that are not include one-time messages.
List<Message> _perminentMessages() { List<Message> _perminentMessages() {
final allMessages = state.messages.where((element) { final allMessages = state.messages.where((element) {
@ -238,6 +286,22 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
return allMessages; return allMessages;
} }
List<Message> _getOnetimeMessages() {
final messages = state.messages.where((element) {
return (element.metadata?.containsKey(onetimeShotType) == true);
}).toList();
return messages;
}
Message? _getOlderstMessage() {
// get the last message that is not a one-time message
final message = state.messages.lastWhereOrNull((element) {
return !(element.metadata?.containsKey(onetimeShotType) == true);
});
return message;
}
void _loadPrevMessage(Int64? beforeMessageId) { void _loadPrevMessage(Int64? beforeMessageId) {
final payload = LoadPrevChatMessagePB( final payload = LoadPrevChatMessagePB(
chatId: state.view.id, chatId: state.view.id,
@ -247,68 +311,91 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
ChatEventLoadPrevMessage(payload).send(); ChatEventLoadPrevMessage(payload).send();
} }
Future<void> _handleSentMessage( Future<void> _startStreamingMessage(
String message, String message,
Emitter<ChatState> emit, Emitter<ChatState> emit,
) async { ) async {
final payload = SendChatPayloadPB( if (state.answerStream != null) {
await state.answerStream?.dispose();
}
final answerStream = AnswerStream();
add(ChatEvent.didUpdateAnswerStream(answerStream));
final payload = StreamChatPayloadPB(
chatId: state.view.id, chatId: state.view.id,
message: message, message: message,
messageType: ChatMessageTypePB.User, messageType: ChatMessageTypePB.User,
textStreamPort: Int64(answerStream.nativePort),
); );
final result = await ChatEventSendMessage(payload).send();
// Stream message to the server
final result = await ChatEventStreamMessage(payload).send();
result.fold( result.fold(
(_) {}, (ChatMessagePB question) {
if (!isClosed) {
add(ChatEvent.didSentUserMessage(question));
final questionMessageId = question.messageId;
final message = _createTextMessage(question);
add(ChatEvent.receveMessage(message));
final streamAnswer =
_createStreamMessage(answerStream, questionMessageId);
add(ChatEvent.streaming(streamAnswer));
}
},
(err) { (err) {
if (!isClosed) { if (!isClosed) {
Log.error("Failed to send message: ${err.msg}"); Log.error("Failed to send message: ${err.msg}");
final metadata = OnetimeShotType.invalidSendMesssage.toMap(); final metadata = OnetimeShotType.invalidSendMesssage.toMap();
metadata[sendMessageErrorKey] = err.msg; if (err.code != ErrorCode.Internal) {
metadata[sendMessageErrorKey] = err.msg;
}
final error = CustomMessage( final error = CustomMessage(
metadata: metadata, metadata: metadata,
author: const User(id: "system"), author: const User(id: "system"),
id: 'system', id: 'system',
); );
add(ChatEvent.streaming([error])); add(ChatEvent.receveMessage(error));
} }
}, },
); );
} }
void _handleChatMessage(ChatMessagePB pb) { Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
if (!isClosed) { final streamMessageId = nanoid();
final message = _createChatMessage(pb); lastStreamMessageId = streamMessageId;
final messages = pb.hasFollowing
? [_loadingMessage(0.toString()), message]
: [message];
add(ChatEvent.streaming(messages));
}
}
Message _loadingMessage(String id) { return TextMessage(
return CustomMessage( author: User(id: nanoid()),
author: User(id: id), metadata: {
metadata: OnetimeShotType.loading.toMap(), "$AnswerStream": stream,
// fake id "question": questionMessageId,
id: nanoid(), "chatId": chatId,
},
id: streamMessageId,
text: '',
); );
} }
Message _createChatMessage(ChatMessagePB message) { Message _createTextMessage(ChatMessagePB message) {
final messageId = message.messageId.toString(); String messageId = message.messageId.toString();
/// If the message id is in the temporary map, we will use the previous fake message id
if (temporaryMessageIDMap.containsKey(messageId)) {
messageId = temporaryMessageIDMap[messageId]!;
}
return TextMessage( return TextMessage(
author: User(id: message.authorId), author: User(id: message.authorId),
id: messageId, id: messageId,
text: message.content, text: message.content,
createdAt: message.createdAt.toInt(), createdAt: message.createdAt.toInt(),
repliedMessage: _getReplyMessage(state.messages, messageId),
); );
} }
Message? _getReplyMessage(List<Message?> messages, String messageId) {
return messages.firstWhereOrNull((element) => element?.id == messageId);
}
} }
@freezed @freezed
@ -322,15 +409,20 @@ class ChatEvent with _$ChatEvent {
) = _DidLoadPreviousMessages; ) = _DidLoadPreviousMessages;
const factory ChatEvent.didLoadLatestMessages(List<Message> messages) = const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =
_DidLoadMessages; _DidLoadMessages;
const factory ChatEvent.streaming(List<Message> messages) = _DidStreamMessage; const factory ChatEvent.streaming(Message message) = _StreamingMessage;
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage; const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage;
const factory ChatEvent.didReceiveRelatedQuestion( const factory ChatEvent.didReceiveRelatedQuestion(
List<RelatedQuestionPB> questions, List<RelatedQuestionPB> questions,
) = _DidReceiveRelatedQueston; ) = _DidReceiveRelatedQueston;
const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion; const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion;
const factory ChatEvent.retryGenerate() = _RetryGenerate;
const factory ChatEvent.didSentUserMessage(ChatMessagePB message) = const factory ChatEvent.didSentUserMessage(ChatMessagePB message) =
_DidSendUserMessage; _DidSendUserMessage;
const factory ChatEvent.didUpdateAnswerStream(
AnswerStream stream,
) = _DidUpdateAnswerStream;
const factory ChatEvent.stopStream() = _StopStream;
} }
@freezed @freezed
@ -347,13 +439,14 @@ class ChatState with _$ChatState {
required LoadingState loadingPreviousStatus, required LoadingState loadingPreviousStatus,
// When sending a user message, the status will be set as loading. // When sending a user message, the status will be set as loading.
// After the message is sent, the status will be set as finished. // After the message is sent, the status will be set as finished.
required LoadingState answerQuestionStatus, required LoadingState streamingStatus,
// Indicate whether there are more previous messages to load. // Indicate whether there are more previous messages to load.
required bool hasMorePrevMessage, required bool hasMorePrevMessage,
// The related questions that are received after the user message is sent. // The related questions that are received after the user message is sent.
required List<RelatedQuestionPB> relatedQuestions, required List<RelatedQuestionPB> relatedQuestions,
// The last user message that is sent to the server. // The last user message that is sent to the server.
ChatMessagePB? lastSentMessage, ChatMessagePB? lastSentMessage,
AnswerStream? answerStream,
}) = _ChatState; }) = _ChatState;
factory ChatState.initial(ViewPB view, UserProfilePB userProfile) => factory ChatState.initial(ViewPB view, UserProfilePB userProfile) =>
@ -363,7 +456,7 @@ class ChatState with _$ChatState {
userProfile: userProfile, userProfile: userProfile,
initialLoadingStatus: const LoadingState.finish(), initialLoadingStatus: const LoadingState.finish(),
loadingPreviousStatus: const LoadingState.finish(), loadingPreviousStatus: const LoadingState.finish(),
answerQuestionStatus: const LoadingState.finish(), streamingStatus: const LoadingState.finish(),
hasMorePrevMessage: true, hasMorePrevMessage: true,
relatedQuestions: [], relatedQuestions: [],
); );
@ -377,10 +470,8 @@ class LoadingState with _$LoadingState {
enum OnetimeShotType { enum OnetimeShotType {
unknown, unknown,
loading,
serverStreamError,
relatedQuestion, relatedQuestion,
invalidSendMesssage invalidSendMesssage,
} }
const onetimeShotType = "OnetimeShotType"; const onetimeShotType = "OnetimeShotType";
@ -388,10 +479,6 @@ const onetimeShotType = "OnetimeShotType";
extension OnetimeMessageTypeExtension on OnetimeShotType { extension OnetimeMessageTypeExtension on OnetimeShotType {
static OnetimeShotType fromString(String value) { static OnetimeShotType fromString(String value) {
switch (value) { switch (value) {
case 'OnetimeShotType.loading':
return OnetimeShotType.loading;
case 'OnetimeShotType.serverStreamError':
return OnetimeShotType.serverStreamError;
case 'OnetimeShotType.relatedQuestion': case 'OnetimeShotType.relatedQuestion':
return OnetimeShotType.relatedQuestion; return OnetimeShotType.relatedQuestion;
case 'OnetimeShotType.invalidSendMesssage': case 'OnetimeShotType.invalidSendMesssage':
@ -402,7 +489,7 @@ extension OnetimeMessageTypeExtension on OnetimeShotType {
} }
} }
Map<String, String> toMap() { Map<String, dynamic> toMap() {
return { return {
onetimeShotType: toString(), onetimeShotType: toString(),
}; };
@ -421,3 +508,43 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
} }
return null; return null;
} }
typedef AnswerStreamElement = String;
class AnswerStream {
AnswerStream() {
_port.handler = _controller.add;
_subscription = _controller.stream.listen(
(event) {
if (event.startsWith("data:")) {
_hasStarted = true;
} else if (event.startsWith("error:")) {
_error = event.substring(5);
}
},
);
}
final RawReceivePort _port = RawReceivePort();
final StreamController<AnswerStreamElement> _controller =
StreamController.broadcast();
late StreamSubscription<AnswerStreamElement> _subscription;
bool _hasStarted = false;
String? _error;
int get nativePort => _port.sendPort.nativePort;
bool get hasStarted => _hasStarted;
String? get error => _error;
Future<void> dispose() async {
await _controller.close();
await _subscription.cancel();
_port.close();
}
StreamSubscription<AnswerStreamElement> listen(
void Function(AnswerStreamElement event)? onData,
) {
return _controller.stream.listen(onData);
}
}

View File

@ -28,26 +28,23 @@ class ChatMessageListener {
ChatNotificationParser? _parser; ChatNotificationParser? _parser;
ChatMessageCallback? chatMessageCallback; ChatMessageCallback? chatMessageCallback;
ChatMessageCallback? lastUserSentMessageCallback;
ChatErrorMessageCallback? chatErrorMessageCallback; ChatErrorMessageCallback? chatErrorMessageCallback;
LatestMessageCallback? latestMessageCallback; LatestMessageCallback? latestMessageCallback;
PrevMessageCallback? prevMessageCallback; PrevMessageCallback? prevMessageCallback;
void Function()? finishAnswerQuestionCallback; void Function()? finishStreamingCallback;
void start({ void start({
ChatMessageCallback? chatMessageCallback, ChatMessageCallback? chatMessageCallback,
ChatErrorMessageCallback? chatErrorMessageCallback, ChatErrorMessageCallback? chatErrorMessageCallback,
LatestMessageCallback? latestMessageCallback, LatestMessageCallback? latestMessageCallback,
PrevMessageCallback? prevMessageCallback, PrevMessageCallback? prevMessageCallback,
ChatMessageCallback? lastUserSentMessageCallback, void Function()? finishStreamingCallback,
void Function()? finishAnswerQuestionCallback,
}) { }) {
this.chatMessageCallback = chatMessageCallback; this.chatMessageCallback = chatMessageCallback;
this.chatErrorMessageCallback = chatErrorMessageCallback; this.chatErrorMessageCallback = chatErrorMessageCallback;
this.latestMessageCallback = latestMessageCallback; this.latestMessageCallback = latestMessageCallback;
this.prevMessageCallback = prevMessageCallback; this.prevMessageCallback = prevMessageCallback;
this.lastUserSentMessageCallback = lastUserSentMessageCallback; this.finishStreamingCallback = finishStreamingCallback;
this.finishAnswerQuestionCallback = finishAnswerQuestionCallback;
} }
void _callback( void _callback(
@ -59,9 +56,6 @@ class ChatMessageListener {
case ChatNotification.DidReceiveChatMessage: case ChatNotification.DidReceiveChatMessage:
chatMessageCallback?.call(ChatMessagePB.fromBuffer(r)); chatMessageCallback?.call(ChatMessagePB.fromBuffer(r));
break; break;
case ChatNotification.LastUserSentMessage:
lastUserSentMessageCallback?.call(ChatMessagePB.fromBuffer(r));
break;
case ChatNotification.StreamChatMessageError: case ChatNotification.StreamChatMessageError:
chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r)); chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r));
break; break;
@ -71,8 +65,8 @@ class ChatMessageListener {
case ChatNotification.DidLoadPrevChatMessage: case ChatNotification.DidLoadPrevChatMessage:
prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r)); prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r));
break; break;
case ChatNotification.FinishAnswerQuestion: case ChatNotification.FinishStreaming:
finishAnswerQuestionCallback?.call(); finishStreamingCallback?.call();
break; break;
default: default:
break; break;

View File

@ -1,103 +0,0 @@
import 'package:appflowy/plugins/ai_chat/application/chat_message_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_related_question_bloc.freezed.dart';
class ChatRelatedMessageBloc
extends Bloc<ChatRelatedMessageEvent, ChatRelatedMessageState> {
ChatRelatedMessageBloc({
required String chatId,
}) : listener = ChatMessageListener(chatId: chatId),
super(ChatRelatedMessageState.initial()) {
on<ChatRelatedMessageEvent>(
(event, emit) async {
await event.when(
initial: () async {
listener.start(
lastUserSentMessageCallback: (message) {
if (!isClosed) {
add(ChatRelatedMessageEvent.updateLastSentMessage(message));
}
},
);
},
didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) {
Log.debug("Related questions: $questions");
emit(
state.copyWith(
relatedQuestions: questions,
),
);
},
updateLastSentMessage: (ChatMessagePB message) {
final payload =
ChatMessageIdPB(chatId: chatId, messageId: message.messageId);
ChatEventGetRelatedQuestion(payload).send().then((result) {
if (!isClosed) {
result.fold(
(list) {
add(
ChatRelatedMessageEvent.didReceiveRelatedQuestion(
list.items,
),
);
},
(err) {
Log.error("Failed to get related question: $err");
},
);
}
});
emit(
state.copyWith(
lastSentMessage: message,
relatedQuestions: [],
),
);
},
clear: () {
emit(
state.copyWith(
relatedQuestions: [],
),
);
},
);
},
);
}
final ChatMessageListener listener;
@override
Future<void> close() {
listener.stop();
return super.close();
}
}
@freezed
class ChatRelatedMessageEvent with _$ChatRelatedMessageEvent {
const factory ChatRelatedMessageEvent.initial() = Initial;
const factory ChatRelatedMessageEvent.updateLastSentMessage(
ChatMessagePB message,
) = _LastSentMessage;
const factory ChatRelatedMessageEvent.didReceiveRelatedQuestion(
List<RelatedQuestionPB> questions,
) = _RelatedQuestion;
const factory ChatRelatedMessageEvent.clear() = _Clear;
}
@freezed
class ChatRelatedMessageState with _$ChatRelatedMessageState {
const factory ChatRelatedMessageState({
ChatMessagePB? lastSentMessage,
@Default([]) List<RelatedQuestionPB> relatedQuestions,
}) = _ChatRelatedMessageState;
factory ChatRelatedMessageState.initial() => const ChatRelatedMessageState();
}

View File

@ -1,9 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_ai_message.dart'; import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_streaming_error_message.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_user_message.dart'; import 'package:appflowy/plugins/ai_chat/presentation/user_message_bubble.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
@ -19,11 +18,12 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'presentation/chat_input.dart'; import 'presentation/chat_input.dart';
import 'presentation/chat_loading.dart';
import 'presentation/chat_popmenu.dart'; import 'presentation/chat_popmenu.dart';
import 'presentation/chat_theme.dart'; import 'presentation/chat_theme.dart';
import 'presentation/chat_user_invalid_message.dart'; import 'presentation/chat_user_invalid_message.dart';
import 'presentation/chat_welcome_page.dart'; import 'presentation/chat_welcome_page.dart';
import 'presentation/message/ai_text_message.dart';
import 'presentation/message/user_text_message.dart';
class AIChatUILayout { class AIChatUILayout {
static EdgeInsets get chatPadding => static EdgeInsets get chatPadding =>
@ -108,7 +108,6 @@ class _AIChatPageState extends State<AIChatPage> {
customBottomWidget: buildChatInput(blocContext), customBottomWidget: buildChatInput(blocContext),
user: _user, user: _user,
theme: buildTheme(context), theme: buildTheme(context),
customMessageBuilder: _customMessageBuilder,
onEndReached: () async { onEndReached: () async {
if (state.hasMorePrevMessage && if (state.hasMorePrevMessage &&
state.loadingPreviousStatus != state.loadingPreviousStatus !=
@ -138,6 +137,13 @@ class _AIChatPageState extends State<AIChatPage> {
}, },
), ),
messageWidthRatio: AIChatUILayout.messageWidthRatio, messageWidthRatio: AIChatUILayout.messageWidthRatio,
textMessageBuilder: (
textMessage, {
required messageWidth,
required showName,
}) {
return _buildAITextMessage(blocContext, textMessage);
},
bubbleBuilder: ( bubbleBuilder: (
child, { child, {
required message, required message,
@ -149,46 +155,7 @@ class _AIChatPageState extends State<AIChatPage> {
child: child, child: child,
); );
} else { } else {
final messageType = onetimeMessageTypeFromMeta( return _buildAIBubble(message, blocContext, state, child);
message.metadata,
);
if (messageType == OnetimeShotType.serverStreamError) {
return ChatStreamingError(
message: message,
onRetryPressed: () {
blocContext
.read<ChatBloc>()
.add(const ChatEvent.retryGenerate());
},
);
}
if (messageType == OnetimeShotType.invalidSendMesssage) {
return ChatInvalidUserMessage(
message: message,
);
}
if (messageType == OnetimeShotType.relatedQuestion) {
return RelatedQuestionList(
onQuestionSelected: (question) {
blocContext
.read<ChatBloc>()
.add(ChatEvent.sendMessage(question));
blocContext
.read<ChatBloc>()
.add(const ChatEvent.clearReleatedQuestion());
},
chatId: widget.view.id,
relatedQuestions: state.relatedQuestions,
);
}
return ChatAIMessageBubble(
message: message,
customMessageType: messageType,
child: child,
);
} }
}, },
); );
@ -199,10 +166,67 @@ class _AIChatPageState extends State<AIChatPage> {
); );
} }
Widget _buildAITextMessage(BuildContext context, TextMessage message) {
final isAuthor = message.author.id == _user.id;
if (isAuthor) {
return ChatTextMessageWidget(
user: message.author,
messageUserId: message.id,
text: message.text,
);
} else {
final stream = message.metadata?["$AnswerStream"];
final questionId = message.metadata?["question"];
return ChatAITextMessageWidget(
user: message.author,
messageUserId: message.id,
text: stream is AnswerStream ? stream : message.text,
key: ValueKey(message.id),
questionId: questionId,
chatId: widget.view.id,
);
}
}
Widget _buildAIBubble(
Message message,
BuildContext blocContext,
ChatState state,
Widget child,
) {
final messageType = onetimeMessageTypeFromMeta(
message.metadata,
);
if (messageType == OnetimeShotType.invalidSendMesssage) {
return ChatInvalidUserMessage(
message: message,
);
}
if (messageType == OnetimeShotType.relatedQuestion) {
return RelatedQuestionList(
onQuestionSelected: (question) {
blocContext.read<ChatBloc>().add(ChatEvent.sendMessage(question));
blocContext
.read<ChatBloc>()
.add(const ChatEvent.clearReleatedQuestion());
},
chatId: widget.view.id,
relatedQuestions: state.relatedQuestions,
);
}
return ChatAIMessageBubble(
message: message,
customMessageType: messageType,
child: child,
);
}
Widget buildBubble(Message message, Widget child) { Widget buildBubble(Message message, Widget child) {
final isAuthor = message.author.id == _user.id; final isAuthor = message.author.id == _user.id;
const borderRadius = BorderRadius.all(Radius.circular(6)); const borderRadius = BorderRadius.all(Radius.circular(6));
final childWithPadding = isAuthor final childWithPadding = isAuthor
? Padding( ? Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
@ -261,33 +285,25 @@ class _AIChatPageState extends State<AIChatPage> {
} }
} }
Widget _customMessageBuilder(
types.CustomMessage message, {
required int messageWidth,
}) {
// iteration custom message type
final messageType = onetimeMessageTypeFromMeta(message.metadata);
if (messageType == null) {
return const SizedBox.shrink();
}
switch (messageType) {
case OnetimeShotType.loading:
return const ChatAILoading();
default:
return const SizedBox.shrink();
}
}
Widget buildChatInput(BuildContext context) { Widget buildChatInput(BuildContext context) {
return ClipRect( return ClipRect(
child: Padding( child: Padding(
padding: AIChatUILayout.safeAreaInsets(context), padding: AIChatUILayout.safeAreaInsets(context),
child: Column( child: Column(
children: [ children: [
ChatInput( BlocSelector<ChatBloc, ChatState, LoadingState>(
chatId: widget.view.id, selector: (state) => state.streamingStatus,
onSendPressed: (message) => onSendPressed(context, message.text), builder: (context, state) {
return ChatInput(
chatId: widget.view.id,
onSendPressed: (message) =>
onSendPressed(context, message.text),
isStreaming: state != const LoadingState.finish(),
onStopStreaming: () {
context.read<ChatBloc>().add(const ChatEvent.stopStream());
},
);
},
), ),
const VSpace(6), const VSpace(6),
Opacity( Opacity(

View File

@ -1,6 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
@ -13,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@ -35,30 +33,22 @@ class ChatAIMessageBubble extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(horizontal: _leftPadding); const padding = EdgeInsets.symmetric(horizontal: _leftPadding);
final childWithPadding = Padding(padding: padding, child: child); final childWithPadding = Padding(padding: padding, child: child);
final widget = isMobile
? _wrapPopMenu(childWithPadding)
: _wrapHover(childWithPadding);
return BlocProvider( return Row(
create: (context) => ChatAIMessageBloc(message: message), mainAxisSize: MainAxisSize.min,
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>( crossAxisAlignment: CrossAxisAlignment.start,
builder: (context, state) { children: [
final widget = isMobile const ChatBorderedCircleAvatar(
? _wrapPopMenu(childWithPadding) child: FlowySvg(
: _wrapHover(childWithPadding); FlowySvgs.flowy_ai_chat_logo_s,
size: Size.square(24),
return Row( ),
mainAxisSize: MainAxisSize.min, ),
crossAxisAlignment: CrossAxisAlignment.start, Expanded(child: widget),
children: [ ],
const ChatBorderedCircleAvatar(
child: FlowySvg(
FlowySvgs.flowy_ai_chat_logo_s,
size: Size.square(24),
),
),
Expanded(child: widget),
],
);
},
),
); );
} }
@ -118,7 +108,7 @@ class _ChatAIMessageHoverState extends State<ChatAIMessageHover> {
borderRadius: Corners.s6Border, borderRadius: Corners.s6Border,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.only(bottom: 40), padding: const EdgeInsets.only(bottom: 30),
child: widget.child, child: widget.child,
), ),
), ),

View File

@ -18,13 +18,17 @@ class ChatInput extends StatefulWidget {
required this.onSendPressed, required this.onSendPressed,
required this.chatId, required this.chatId,
this.options = const InputOptions(), this.options = const InputOptions(),
required this.isStreaming,
required this.onStopStreaming,
}); });
final bool? isAttachmentUploading; final bool? isAttachmentUploading;
final VoidCallback? onAttachmentPressed; final VoidCallback? onAttachmentPressed;
final void Function(types.PartialText) onSendPressed; final void Function(types.PartialText) onSendPressed;
final void Function() onStopStreaming;
final InputOptions options; final InputOptions options;
final String chatId; final String chatId;
final bool isStreaming;
@override @override
State<ChatInput> createState() => _ChatInputState(); State<ChatInput> createState() => _ChatInputState();
@ -68,26 +72,23 @@ class _ChatInputState extends State<ChatInput> {
void _handleSendButtonVisibilityModeChange() { void _handleSendButtonVisibilityModeChange() {
_textController.removeListener(_handleTextControllerChange); _textController.removeListener(_handleTextControllerChange);
if (widget.options.sendButtonVisibilityMode == _sendButtonVisible =
SendButtonVisibilityMode.hidden) { _textController.text.trim() != '' || widget.isStreaming;
_sendButtonVisible = false; _textController.addListener(_handleTextControllerChange);
} else if (widget.options.sendButtonVisibilityMode ==
SendButtonVisibilityMode.editing) {
_sendButtonVisible = _textController.text.trim() != '';
_textController.addListener(_handleTextControllerChange);
} else {
_sendButtonVisible = true;
}
} }
void _handleSendPressed() { void _handleSendPressed() {
final trimmedText = _textController.text.trim(); if (widget.isStreaming) {
if (trimmedText != '') { widget.onStopStreaming();
final partialText = types.PartialText(text: trimmedText); } else {
widget.onSendPressed(partialText); final trimmedText = _textController.text.trim();
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
if (widget.options.inputClearMode == InputClearMode.always) { if (widget.options.inputClearMode == InputClearMode.always) {
_textController.clear(); _textController.clear();
}
} }
} }
} }
@ -138,6 +139,7 @@ class _ChatInputState extends State<ChatInput> {
padding: textPadding, padding: textPadding,
child: TextField( child: TextField(
controller: _textController, controller: _textController,
readOnly: widget.isStreaming,
focusNode: _inputFocusNode, focusNode: _inputFocusNode,
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
@ -153,7 +155,6 @@ class _ChatInputState extends State<ChatInput> {
autocorrect: widget.options.autocorrect, autocorrect: widget.options.autocorrect,
autofocus: widget.options.autofocus, autofocus: widget.options.autofocus,
enableSuggestions: widget.options.enableSuggestions, enableSuggestions: widget.options.enableSuggestions,
spellCheckConfiguration: const SpellCheckConfiguration(),
keyboardType: widget.options.keyboardType, keyboardType: widget.options.keyboardType,
textCapitalization: TextCapitalization.sentences, textCapitalization: TextCapitalization.sentences,
maxLines: 10, maxLines: 10,
@ -173,8 +174,14 @@ class _ChatInputState extends State<ChatInput> {
visible: _sendButtonVisible, visible: _sendButtonVisible,
child: Padding( child: Padding(
padding: buttonPadding, padding: buttonPadding,
child: SendButton( child: AccessoryButton(
onPressed: _handleSendPressed, onSendPressed: () {
_handleSendPressed();
},
onStopStreaming: () {
widget.onStopStreaming();
},
isStreaming: widget.isStreaming,
), ),
), ),
), ),
@ -184,10 +191,7 @@ class _ChatInputState extends State<ChatInput> {
@override @override
void didUpdateWidget(covariant ChatInput oldWidget) { void didUpdateWidget(covariant ChatInput oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.options.sendButtonVisibilityMode != _handleSendButtonVisibilityModeChange();
oldWidget.options.sendButtonVisibilityMode) {
_handleSendButtonVisibilityModeChange();
}
} }
@override @override
@ -211,7 +215,6 @@ class InputOptions {
this.keyboardType = TextInputType.multiline, this.keyboardType = TextInputType.multiline,
this.onTextChanged, this.onTextChanged,
this.onTextFieldTap, this.onTextFieldTap,
this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing,
this.textEditingController, this.textEditingController,
this.autocorrect = true, this.autocorrect = true,
this.autofocus = false, this.autofocus = false,
@ -231,11 +234,6 @@ class InputOptions {
/// Will be called on [TextField] tap. /// Will be called on [TextField] tap.
final VoidCallback? onTextFieldTap; final VoidCallback? onTextFieldTap;
/// Controls the visibility behavior of the [SendButton] based on the
/// [TextField] state inside the [ChatInput] widget.
/// Defaults to [SendButtonVisibilityMode.editing].
final SendButtonVisibilityMode sendButtonVisibilityMode;
/// Custom [TextEditingController]. If not provided, defaults to the /// Custom [TextEditingController]. If not provided, defaults to the
/// [InputTextFieldController], which extends [TextEditingController] and has /// [InputTextFieldController], which extends [TextEditingController] and has
/// additional fatures like markdown support. If you want to keep additional /// additional fatures like markdown support. If you want to keep additional
@ -260,24 +258,46 @@ class InputOptions {
final isMobile = defaultTargetPlatform == TargetPlatform.android || final isMobile = defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS; defaultTargetPlatform == TargetPlatform.iOS;
class SendButton extends StatelessWidget { class AccessoryButton extends StatelessWidget {
const SendButton({required this.onPressed, super.key}); const AccessoryButton({
required this.onSendPressed,
required this.onStopStreaming,
required this.isStreaming,
super.key,
});
final void Function() onPressed; final void Function() onSendPressed;
final void Function() onStopStreaming;
final bool isStreaming;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyIconButton( if (isStreaming) {
width: 36, return FlowyIconButton(
fillColor: AFThemeExtension.of(context).lightGreyHover, width: 36,
hoverColor: AFThemeExtension.of(context).lightGreyHover, icon: FlowySvg(
radius: BorderRadius.circular(18), FlowySvgs.ai_stream_stop_s,
icon: FlowySvg( size: const Size.square(28),
FlowySvgs.send_s, color: Theme.of(context).colorScheme.primary,
size: const Size.square(24), ),
color: Theme.of(context).colorScheme.primary, onPressed: onStopStreaming,
), radius: BorderRadius.circular(18),
onPressed: onPressed, fillColor: AFThemeExtension.of(context).lightGreyHover,
); hoverColor: AFThemeExtension.of(context).lightGreyHover,
);
} else {
return FlowyIconButton(
width: 36,
fillColor: AFThemeExtension.of(context).lightGreyHover,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
radius: BorderRadius.circular(18),
icon: FlowySvg(
FlowySvgs.send_s,
size: const Size.square(24),
color: Theme.of(context).colorScheme.primary,
),
onPressed: onSendPressed,
);
}
} }
} }

View File

@ -53,15 +53,15 @@ class ContentPlaceholder extends StatelessWidget {
), ),
], ],
), ),
Container( // Container(
width: 140, // width: 140,
height: 16.0, // height: 16.0,
margin: const EdgeInsets.only(bottom: 8.0), // margin: const EdgeInsets.only(bottom: 8.0),
decoration: BoxDecoration( // decoration: BoxDecoration(
color: AFThemeExtension.of(context).lightGreyHover, // color: AFThemeExtension.of(context).lightGreyHover,
borderRadius: BorderRadius.circular(4.0), // borderRadius: BorderRadius.circular(4.0),
), // ),
), // ),
], ],
), ),
); );

View File

@ -1,47 +1,11 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_related_question_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RelatedQuestionPage extends StatefulWidget {
const RelatedQuestionPage({
required this.chatId,
required this.onQuestionSelected,
super.key,
});
final String chatId;
final Function(String) onQuestionSelected;
@override
State<RelatedQuestionPage> createState() => _RelatedQuestionPageState();
}
class _RelatedQuestionPageState extends State<RelatedQuestionPage> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChatRelatedMessageBloc(chatId: widget.chatId)
..add(
const ChatRelatedMessageEvent.initial(),
),
child: BlocBuilder<ChatRelatedMessageBloc, ChatRelatedMessageState>(
builder: (blocContext, state) {
return RelatedQuestionList(
chatId: widget.chatId,
onQuestionSelected: widget.onQuestionSelected,
relatedQuestions: state.relatedQuestions,
);
},
),
);
}
}
class RelatedQuestionList extends StatelessWidget { class RelatedQuestionList extends StatelessWidget {
const RelatedQuestionList({ const RelatedQuestionList({

View File

@ -0,0 +1,10 @@
import 'package:flutter/widgets.dart';
class StreamTextField extends StatelessWidget {
const StreamTextField({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -1,84 +1,83 @@
import 'package:appflowy/generated/locale_keys.g.dart'; // import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart'; // import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization/easy_localization.dart'; // import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; // import 'package:flutter/material.dart';
import 'package:flutter/material.dart'; // import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
class ChatStreamingError extends StatelessWidget { // class ChatStreamingError extends StatelessWidget {
const ChatStreamingError({ // const ChatStreamingError({
required this.message, // required this.message,
required this.onRetryPressed, // required this.onRetryPressed,
super.key, // super.key,
}); // });
final void Function() onRetryPressed; // final void Function() onRetryPressed;
final Message message; // final Message message;
@override // @override
Widget build(BuildContext context) { // Widget build(BuildContext context) {
final canRetry = message.metadata?[canRetryKey] != null; // final canRetry = message.metadata?[canRetryKey] != null;
if (canRetry) { // if (canRetry) {
return Column( // return Column(
children: [ // children: [
const Divider(height: 4, thickness: 1), // const Divider(height: 4, thickness: 1),
const VSpace(16), // const VSpace(16),
Center( // Center(
child: Column( // child: Column(
children: [ // children: [
_aiUnvaliable(), // _aiUnvaliable(),
const VSpace(10), // const VSpace(10),
_retryButton(), // _retryButton(),
], // ],
), // ),
), // ),
], // ],
); // );
} else { // } else {
return Center( // return Center(
child: Column( // child: Column(
children: [ // children: [
const Divider(height: 20, thickness: 1), // const Divider(height: 20, thickness: 1),
Padding( // Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: FlowyText( // child: FlowyText(
LocaleKeys.chat_serverUnavailable.tr(), // LocaleKeys.chat_serverUnavailable.tr(),
fontSize: 14, // fontSize: 14,
), // ),
), // ),
], // ],
), // ),
); // );
} // }
} // }
FlowyButton _retryButton() { // FlowyButton _retryButton() {
return FlowyButton( // return FlowyButton(
radius: BorderRadius.circular(20), // radius: BorderRadius.circular(20),
useIntrinsicWidth: true, // useIntrinsicWidth: true,
text: Padding( // text: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), // padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: FlowyText( // child: FlowyText(
LocaleKeys.chat_regenerateAnswer.tr(), // LocaleKeys.chat_regenerateAnswer.tr(),
fontSize: 14, // fontSize: 14,
), // ),
), // ),
onTap: onRetryPressed, // onTap: onRetryPressed,
iconPadding: 0, // iconPadding: 0,
leftIcon: const Icon( // leftIcon: const Icon(
Icons.refresh, // Icons.refresh,
size: 20, // size: 20,
), // ),
); // );
} // }
Padding _aiUnvaliable() { // Padding _aiUnvaliable() {
return Padding( // return Padding(
padding: const EdgeInsets.all(8.0), // padding: const EdgeInsets.all(8.0),
child: FlowyText( // child: FlowyText(
LocaleKeys.chat_aiServerUnavailable.tr(), // LocaleKeys.chat_aiServerUnavailable.tr(),
fontSize: 14, // fontSize: 14,
), // ),
); // );
} // }
} // }

View File

@ -20,29 +20,33 @@ class ChatWelcomePage extends StatelessWidget {
]; ];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return AnimatedOpacity(
mainAxisAlignment: MainAxisAlignment.center, opacity: 1.0,
children: [ duration: const Duration(seconds: 3),
const FlowySvg( child: Column(
FlowySvgs.flowy_ai_chat_logo_s, mainAxisAlignment: MainAxisAlignment.center,
size: Size.square(44), children: [
), const FlowySvg(
const SizedBox(height: 40), FlowySvgs.flowy_ai_chat_logo_s,
GridView.builder( size: Size.square(44),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 16.0 / 9.0,
), ),
itemCount: items.length, const SizedBox(height: 40),
itemBuilder: (context, index) => WelcomeQuestion( GridView.builder(
question: items[index], shrinkWrap: true,
onSelected: onSelectedQuestion, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4,
crossAxisSpacing: 6,
mainAxisSpacing: 6,
childAspectRatio: 16.0 / 9.0,
),
itemCount: items.length,
itemBuilder: (context, index) => WelcomeQuestion(
question: items[index],
onSelected: onSelectedQuestion,
),
), ),
), ],
], ),
); );
} }
} }

View File

@ -0,0 +1,295 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:markdown_widget/markdown_widget.dart';
class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({
super.key,
required this.user,
required this.messageUserId,
required this.text,
required this.questionId,
required this.chatId,
});
final User user;
final String messageUserId;
final dynamic text;
final Int64? questionId;
final String chatId;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => ChatAIMessageBloc(
message: text,
chatId: chatId,
questionId: questionId,
)..add(const ChatAIMessageEvent.initial()),
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
builder: (context, state) {
if (state.error != null) {
return StreamingError(
onRetryPressed: () {
context.read<ChatAIMessageBloc>().add(
const ChatAIMessageEvent.retry(),
);
},
);
}
if (state.retryState == const LoadingState.loading()) {
return const ChatAILoading();
}
if (state.text.isEmpty) {
return const ChatAILoading();
} else {
return _textWidgetBuilder(user, context, state.text);
}
},
),
);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return MarkdownWidget(
data: text,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.all(
Radius.circular(8.0),
),
),
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig.darkConfig,
],
);
}
}
class ChatH1Config extends HeadingConfig {
const ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class ChatH2Config extends HeadingConfig {
const ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class ChatH3Config extends HeadingConfig {
const ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class StreamingError extends StatelessWidget {
const StreamingError({
required this.onRetryPressed,
super.key,
});
final void Function() onRetryPressed;
@override
Widget build(BuildContext context) {
return Column(
children: [
const Divider(height: 4, thickness: 1),
const VSpace(16),
Center(
child: Column(
children: [
_aiUnvaliable(),
const VSpace(10),
_retryButton(),
],
),
),
],
);
}
FlowyButton _retryButton() {
return FlowyButton(
radius: BorderRadius.circular(20),
useIntrinsicWidth: true,
text: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: FlowyText(
LocaleKeys.chat_regenerateAnswer.tr(),
fontSize: 14,
),
),
onTap: onRetryPressed,
iconPadding: 0,
leftIcon: const Icon(
Icons.refresh,
size: 20,
),
);
}
Padding _aiUnvaliable() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FlowyText(
LocaleKeys.chat_aiServerUnavailable.tr(),
fontSize: 14,
),
);
}
}

View File

@ -0,0 +1,60 @@
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
class ChatTextMessageWidget extends StatelessWidget {
const ChatTextMessageWidget({
super.key,
required this.user,
required this.messageUserId,
required this.text,
});
final User user;
final String messageUserId;
final String text;
@override
Widget build(BuildContext context) {
return _textWidgetBuilder(user, context, text);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextMessageText(
text: text,
),
],
);
}
}
/// Widget to reuse the markdown capabilities, e.g., for previews.
class TextMessageText extends StatelessWidget {
const TextMessageText({
super.key,
required this.text,
});
/// Text that is shown as markdown.
final String text;
@override
Widget build(BuildContext context) {
return FlowyText(
text,
fontSize: 16,
fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: 2000,
color: AFThemeExtension.of(context).textColor,
);
}
}

View File

@ -680,6 +680,14 @@ packages:
url: "https://github.com/LucasXu0/emoji_mart.git" url: "https://github.com/LucasXu0/emoji_mart.git"
source: git source: git
version: "1.0.2" version: "1.0.2"
flutter_highlight:
dependency: transitive
description:
name: flutter_highlight
sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c"
url: "https://pub.dev"
source: hosted
version: "0.7.0"
flutter_link_previewer: flutter_link_previewer:
dependency: transitive dependency: transitive
description: description:
@ -1066,7 +1074,7 @@ packages:
source: hosted source: hosted
version: "0.6.0" version: "0.6.0"
isolates: isolates:
dependency: transitive dependency: "direct main"
description: description:
name: isolates name: isolates
sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28 sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28
@ -1209,6 +1217,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.2.2" version: "7.2.2"
markdown_widget:
dependency: "direct main"
description:
name: markdown_widget
sha256: "216dced98962d7699a265344624bc280489d739654585ee881c95563a3252fac"
url: "https://pub.dev"
source: hosted
version: "2.3.2+6"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -141,6 +141,8 @@ dependencies:
auto_size_text_field: ^2.2.3 auto_size_text_field: ^2.2.3
reorderable_tabbar: ^1.0.6 reorderable_tabbar: ^1.0.6
shimmer: ^3.0.0 shimmer: ^3.0.0
isolates: ^3.0.3+8
markdown_widget: ^2.3.2+6
# Window Manager for MacOS and Linux # Window Manager for MacOS and Linux
window_manager: ^0.3.9 window_manager: ^0.3.9

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
void main() {
// ignore: unused_local_variable
late AppFlowyChatTest chatTest;
setUpAll(() async {
chatTest = await AppFlowyChatTest.ensureInitialized();
});
test('send message', () async {
// final context = await chatTest.createChat();
});
}

View File

@ -0,0 +1,44 @@
import 'package:appflowy/plugins/ai_chat/chat.dart';
import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import '../../util.dart';
class AppFlowyChatTest {
AppFlowyChatTest({required this.unitTest});
final AppFlowyUnitTest unitTest;
static Future<AppFlowyChatTest> ensureInitialized() async {
final inner = await AppFlowyUnitTest.ensureInitialized();
return AppFlowyChatTest(unitTest: inner);
}
Future<ViewPB> createChat() async {
final app = await unitTest.createWorkspace();
final builder = AIChatPluginBuilder();
return ViewBackendService.createView(
parentViewId: app.id,
name: "Test Chat",
layoutType: builder.layoutType,
openAfterCreate: true,
).then((result) {
return result.fold(
(view) async {
return view;
},
(error) {
throw Exception();
},
);
});
}
}
Future<void> boardResponseFuture() {
return Future.delayed(boardResponseDuration());
}
Duration boardResponseDuration({int milliseconds = 200}) {
return Duration(milliseconds: milliseconds);
}

View File

@ -58,7 +58,6 @@ class AppFlowyUnitTest {
} }
WorkspacePB get currentWorkspace => workspace; WorkspacePB get currentWorkspace => workspace;
Future<void> _loadWorkspace() async { Future<void> _loadWorkspace() async {
final result = await userService.getCurrentWorkspace(); final result = await userService.getCurrentWorkspace();
result.fold( result.fold(
@ -83,15 +82,6 @@ class AppFlowyUnitTest {
(error) => throw Exception(error), (error) => throw Exception(error),
); );
} }
Future<List<ViewPB>> loadApps() async {
final result = await workspaceService.getPublicViews();
return result.fold(
(apps) => apps,
(error) => throw Exception(error),
);
}
} }
void _pathProviderInitialized() { void _pathProviderInitialized() {

View File

@ -117,6 +117,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allo-isolate"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509"
dependencies = [
"atomic",
"pin-project",
]
[[package]] [[package]]
name = "alloc-no-stdlib" name = "alloc-no-stdlib"
version = "2.0.4" version = "2.0.4"
@ -162,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -182,7 +192,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -289,6 +299,12 @@ dependencies = [
"system-deps 6.1.1", "system-deps 6.1.1",
] ]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]] [[package]]
name = "atomic_refcell" name = "atomic_refcell"
version = "0.1.10" version = "0.1.10"
@ -756,7 +772,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -803,7 +819,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -1043,7 +1059,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1068,7 +1084,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1425,7 +1441,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1817,6 +1833,7 @@ dependencies = [
name = "flowy-chat" name = "flowy-chat"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"bytes", "bytes",
"dashmap", "dashmap",
"flowy-chat-pub", "flowy-chat-pub",
@ -1828,6 +1845,7 @@ dependencies = [
"futures", "futures",
"lib-dispatch", "lib-dispatch",
"lib-infra", "lib-infra",
"log",
"protobuf", "protobuf",
"strum_macros 0.21.1", "strum_macros 0.21.1",
"tokio", "tokio",
@ -2835,7 +2853,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2852,7 +2870,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3284,7 +3302,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3525,11 +3543,13 @@ dependencies = [
name = "lib-infra" name = "lib-infra"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"anyhow", "anyhow",
"async-trait", "async-trait",
"atomic_refcell", "atomic_refcell",
"bytes", "bytes",
"chrono", "chrono",
"futures",
"futures-core", "futures-core",
"md5", "md5",
"pin-project", "pin-project",
@ -3643,9 +3663,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "loom" name = "loom"
@ -5772,7 +5792,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -5783,6 +5803,7 @@ dependencies = [
"database-entity", "database-entity",
"futures", "futures",
"gotrue-entity", "gotrue-entity",
"log",
"pin-project", "pin-project",
"reqwest", "reqwest",
"serde", "serde",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3f55cea9ca386875a1668ef30600c83cd6a1ffe2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true

View File

@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -236,7 +236,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -562,7 +562,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -609,7 +609,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -787,7 +787,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -812,7 +812,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1026,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1881,7 +1881,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -1898,7 +1898,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -2199,7 +2199,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -2339,6 +2339,7 @@ dependencies = [
"atomic_refcell", "atomic_refcell",
"bytes", "bytes",
"chrono", "chrono",
"futures",
"futures-core", "futures-core",
"md5", "md5",
"pin-project", "pin-project",
@ -2427,9 +2428,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "mac" name = "mac"
@ -3900,7 +3901,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3911,6 +3912,7 @@ dependencies = [
"database-entity", "database-entity",
"futures", "futures",
"gotrue-entity", "gotrue-entity",
"log",
"pin-project", "pin-project",
"reqwest", "reqwest",
"serde", "serde",

View File

@ -55,7 +55,7 @@ yrs = "0.18.8"
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3f55cea9ca386875a1668ef30600c83cd6a1ffe2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }

View File

@ -108,6 +108,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "allo-isolate"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b6d794345b06592d0ebeed8e477e41b71e5a0a49df4fc0e4184d5938b99509"
dependencies = [
"atomic",
"pin-project",
]
[[package]] [[package]]
name = "alloc-no-stdlib" name = "alloc-no-stdlib"
version = "2.0.4" version = "2.0.4"
@ -153,7 +163,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -173,7 +183,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -299,6 +309,12 @@ dependencies = [
"system-deps 6.2.2", "system-deps 6.2.2",
] ]
[[package]]
name = "atomic"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]] [[package]]
name = "atomic_refcell" name = "atomic_refcell"
version = "0.1.13" version = "0.1.13"
@ -730,7 +746,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -777,7 +793,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -1026,7 +1042,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -1051,7 +1067,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1412,7 +1428,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1854,6 +1870,7 @@ dependencies = [
name = "flowy-chat" name = "flowy-chat"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"bytes", "bytes",
"dashmap", "dashmap",
"flowy-chat-pub", "flowy-chat-pub",
@ -1865,6 +1882,7 @@ dependencies = [
"futures", "futures",
"lib-dispatch", "lib-dispatch",
"lib-infra", "lib-infra",
"log",
"protobuf", "protobuf",
"strum_macros 0.21.1", "strum_macros 0.21.1",
"tokio", "tokio",
@ -2909,7 +2927,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2926,7 +2944,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3363,7 +3381,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3609,11 +3627,13 @@ dependencies = [
name = "lib-infra" name = "lib-infra"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"anyhow", "anyhow",
"async-trait", "async-trait",
"atomic_refcell", "atomic_refcell",
"bytes", "bytes",
"chrono", "chrono",
"futures",
"futures-core", "futures-core",
"md5", "md5",
"pin-project", "pin-project",
@ -5867,7 +5887,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -5878,6 +5898,7 @@ dependencies = [
"database-entity", "database-entity",
"futures", "futures",
"gotrue-entity", "gotrue-entity",
"log",
"pin-project", "pin-project",
"reqwest", "reqwest",
"serde", "serde",

View File

@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3f55cea9ca386875a1668ef30600c83cd6a1ffe2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
[dependencies] [dependencies]
serde_json.workspace = true serde_json.workspace = true

View File

@ -0,0 +1 @@
<svg width="64px" height="64px" viewBox="0 0 24.00 24.00" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000000" stroke-width="0.00024000000000000003" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: #e8e6e3;"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path fill-rule="evenodd" clip-rule="evenodd" d="M9 8C8.44772 8 8 8.44772 8 9V15C8 15.5523 8.44772 16 9 16H15C15.5523 16 16 15.5523 16 15V9C16 8.44772 15.5523 8 15 8H9ZM6 9C6 7.34315 7.34315 6 9 6H15C16.6569 6 18 7.34315 18 9V15C18 16.6569 16.6569 18 15 18H9C7.34315 18 6 16.6569 6 15V9Z" fill="#333333" style="--darkreader-inline-fill: #262a2b;" data-darkreader-inline-fill=""></path> </g></svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]] [[package]]
name = "appflowy-ai-client" name = "appflowy-ai-client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -664,7 +664,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -711,7 +711,7 @@ dependencies = [
[[package]] [[package]]
name = "client-websocket" name = "client-websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -920,7 +920,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-entity" name = "collab-rt-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -945,7 +945,7 @@ dependencies = [
[[package]] [[package]]
name = "collab-rt-protocol" name = "collab-rt-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1165,7 +1165,7 @@ dependencies = [
"cssparser-macros", "cssparser-macros",
"dtoa-short", "dtoa-short",
"itoa", "itoa",
"phf 0.8.0", "phf 0.11.2",
"smallvec", "smallvec",
] ]
@ -1265,7 +1265,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1652,6 +1652,7 @@ dependencies = [
name = "flowy-chat" name = "flowy-chat"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"bytes", "bytes",
"dashmap", "dashmap",
"flowy-chat-pub", "flowy-chat-pub",
@ -1663,6 +1664,7 @@ dependencies = [
"futures", "futures",
"lib-dispatch", "lib-dispatch",
"lib-infra", "lib-infra",
"log",
"protobuf", "protobuf",
"strum_macros 0.21.1", "strum_macros 0.21.1",
"tokio", "tokio",
@ -2521,7 +2523,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2538,7 +2540,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -2903,7 +2905,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3045,6 +3047,7 @@ dependencies = [
name = "lib-infra" name = "lib-infra"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"allo-isolate",
"anyhow", "anyhow",
"async-trait", "async-trait",
"atomic_refcell", "atomic_refcell",
@ -3156,9 +3159,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]] [[package]]
name = "loom" name = "loom"
@ -3778,7 +3781,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [ dependencies = [
"phf_macros", "phf_macros 0.8.0",
"phf_shared 0.8.0", "phf_shared 0.8.0",
"proc-macro-hack", "proc-macro-hack",
] ]
@ -3798,6 +3801,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [ dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2", "phf_shared 0.11.2",
] ]
@ -3865,6 +3869,19 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.47",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.8.0" version = "0.8.0"
@ -4068,7 +4085,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [ dependencies = [
"bytes", "bytes",
"heck 0.4.1", "heck 0.4.1",
"itertools 0.10.5", "itertools 0.11.0",
"log", "log",
"multimap", "multimap",
"once_cell", "once_cell",
@ -4089,7 +4106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools 0.10.5", "itertools 0.11.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.47", "syn 2.0.47",
@ -4986,7 +5003,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=3f55cea9ca386875a1668ef30600c83cd6a1ffe2#3f55cea9ca386875a1668ef30600c83cd6a1ffe2" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d0467e7e2e8ee4b925556b5510fb6ed7322dde8c#d0467e7e2e8ee4b925556b5510fb6ed7322dde8c"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -4997,6 +5014,7 @@ dependencies = [
"database-entity", "database-entity",
"futures", "futures",
"gotrue-entity", "gotrue-entity",
"log",
"pin-project", "pin-project",
"reqwest", "reqwest",
"serde", "serde",

View File

@ -29,7 +29,8 @@ members = [
"build-tool/flowy-codegen", "build-tool/flowy-codegen",
"build-tool/flowy-derive", "build-tool/flowy-derive",
"flowy-search-pub", "flowy-search-pub",
"flowy-chat", "flowy-chat-pub", "flowy-chat",
"flowy-chat-pub",
] ]
resolver = "2" resolver = "2"
@ -93,7 +94,7 @@ yrs = "0.18.8"
# Run the script.add_workspace_members: # Run the script.add_workspace_members:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "3f55cea9ca386875a1668ef30600c83cd6a1ffe2" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d0467e7e2e8ee4b925556b5510fb6ed7322dde8c" }
[profile.dev] [profile.dev]
opt-level = 1 opt-level = 1

View File

@ -43,7 +43,7 @@ impl EventIntegrationTest {
}; };
EventBuilder::new(self.clone()) EventBuilder::new(self.clone())
.event(ChatEvent::SendMessage) .event(ChatEvent::StreamMessage)
.payload(payload) .payload(payload)
.async_send() .async_send()
.await; .await;

View File

@ -1,4 +1,4 @@
pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion}; pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion, StringOrMessage};
pub use client_api::entity::{ pub use client_api::entity::{
ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage, ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage,
}; };
@ -9,6 +9,7 @@ use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>; pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
pub type StreamAnswer = BoxStream<'static, Result<StringOrMessage, AppResponseError>>;
#[async_trait] #[async_trait]
pub trait ChatCloudService: Send + Sync + 'static { pub trait ChatCloudService: Send + Sync + 'static {
fn create_chat( fn create_chat(
@ -26,6 +27,29 @@ pub trait ChatCloudService: Send + Sync + 'static {
message_type: ChatMessageType, message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError>; ) -> Result<ChatMessageStream, FlowyError>;
fn send_question(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> FutureResult<ChatMessage, FlowyError>;
fn save_answer(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
question_id: i64,
) -> FutureResult<ChatMessage, FlowyError>;
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> Result<StreamAnswer, FlowyError>;
fn get_chat_messages( fn get_chat_messages(
&self, &self,
workspace_id: &str, workspace_id: &str,

View File

@ -19,12 +19,14 @@ strum_macros = "0.21"
protobuf.workspace = true protobuf.workspace = true
bytes.workspace = true bytes.workspace = true
validator = { version = "0.16.0", features = ["derive"] } validator = { version = "0.16.0", features = ["derive"] }
lib-infra = { workspace = true } lib-infra = { workspace = true, features = ["isolate_flutter"] }
flowy-chat-pub.workspace = true flowy-chat-pub.workspace = true
dashmap = "5.5" dashmap = "5.5"
flowy-sqlite = { workspace = true } flowy-sqlite = { workspace = true }
tokio.workspace = true tokio.workspace = true
futures.workspace = true futures.workspace = true
allo-isolate = { version = "^0.1", features = ["catch-unwind"] }
log = "0.4.21"
[build-dependencies] [build-dependencies]
flowy-codegen.workspace = true flowy-codegen.workspace = true

View File

@ -3,18 +3,18 @@ use crate::entities::{
}; };
use crate::manager::ChatUserService; use crate::manager::ChatUserService;
use crate::notification::{send_notification, ChatNotification}; use crate::notification::{send_notification, ChatNotification};
use crate::persistence::{ use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
insert_answer_message, insert_chat_messages, select_chat_messages, ChatMessageTable, use allo_isolate::Isolate;
};
use flowy_chat_pub::cloud::{ use flowy_chat_pub::cloud::{
ChatAuthorType, ChatCloudService, ChatMessage, ChatMessageType, MessageCursor, ChatCloudService, ChatMessage, ChatMessageType, MessageCursor, StringOrMessage,
}; };
use flowy_error::{FlowyError, FlowyResult}; use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::DBConnection; use flowy_sqlite::DBConnection;
use futures::StreamExt; use futures::{SinkExt, StreamExt};
use std::sync::atomic::AtomicI64; use lib_infra::isolate_stream::IsolateSink;
use std::sync::atomic::{AtomicBool, AtomicI64};
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::{Mutex, RwLock};
use tracing::{error, instrument, trace}; use tracing::{error, instrument, trace};
enum PrevMessageState { enum PrevMessageState {
@ -30,6 +30,8 @@ pub struct Chat {
cloud_service: Arc<dyn ChatCloudService>, cloud_service: Arc<dyn ChatCloudService>,
prev_message_state: Arc<RwLock<PrevMessageState>>, prev_message_state: Arc<RwLock<PrevMessageState>>,
latest_message_id: Arc<AtomicI64>, latest_message_id: Arc<AtomicI64>,
stop_stream: Arc<AtomicBool>,
steam_buffer: Arc<Mutex<String>>,
} }
impl Chat { impl Chat {
@ -46,6 +48,8 @@ impl Chat {
user_service, user_service,
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)), prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
latest_message_id: Default::default(), latest_message_id: Default::default(),
stop_stream: Arc::new(AtomicBool::new(false)),
steam_buffer: Arc::new(Mutex::new("".to_string())),
} }
} }
@ -63,27 +67,133 @@ impl Chat {
} }
} }
pub async fn stop_stream_message(&self) {
self
.stop_stream
.store(true, std::sync::atomic::Ordering::SeqCst);
}
#[instrument(level = "info", skip_all, err)] #[instrument(level = "info", skip_all, err)]
pub async fn send_chat_message( pub async fn stream_chat_message(
&self, &self,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
) -> Result<(), FlowyError> { text_stream_port: i64,
) -> Result<ChatMessagePB, FlowyError> {
if message.len() > 2000 { if message.len() > 2000 {
return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length")); return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length"));
} }
// clear
self
.stop_stream
.store(false, std::sync::atomic::Ordering::SeqCst);
self.steam_buffer.lock().await.clear();
let stream_buffer = self.steam_buffer.clone();
let uid = self.user_service.user_id()?; let uid = self.user_service.user_id()?;
let workspace_id = self.user_service.workspace_id()?; let workspace_id = self.user_service.workspace_id()?;
stream_send_chat_messages(
uid, let question = self
workspace_id, .cloud_service
self.chat_id.clone(), .send_question(&workspace_id, &self.chat_id, message, message_type)
message.to_string(), .await
message_type, .map_err(|err| {
self.cloud_service.clone(), error!("Failed to send question: {}", err);
self.user_service.clone(), FlowyError::server_error()
); })?;
save_chat_message(
self.user_service.sqlite_connection(uid)?,
&self.chat_id,
vec![question.clone()],
)?;
let stop_stream = self.stop_stream.clone();
let chat_id = self.chat_id.clone();
let question_id = question.message_id;
let cloud_service = self.cloud_service.clone();
let user_service = self.user_service.clone();
tokio::spawn(async move {
let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port));
match cloud_service
.stream_answer(&workspace_id, &chat_id, question_id)
.await
{
Ok(mut stream) => {
while let Some(message) = stream.next().await {
match message {
Ok(message) => match message {
StringOrMessage::Left(s) => {
if stop_stream.load(std::sync::atomic::Ordering::Relaxed) {
send_notification(&chat_id, ChatNotification::FinishStreaming).send();
trace!("[Chat] stop streaming message");
let answer = cloud_service
.save_answer(
&workspace_id,
&chat_id,
&stream_buffer.lock().await,
question_id,
)
.await?;
Self::save_answer(uid, &chat_id, &user_service, answer)?;
break;
}
stream_buffer.lock().await.push_str(&s);
let _ = text_sink.send(format!("data:{}", s)).await;
},
StringOrMessage::Right(answer) => {
trace!("[Chat] received final answer: {:?}", answer);
send_notification(&chat_id, ChatNotification::FinishStreaming).send();
Self::save_answer(uid, &chat_id, &user_service, answer)?;
},
},
Err(err) => {
error!("[Chat] failed to stream answer: {}", err);
let _ = text_sink.send(format!("error:{}", err)).await;
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
error_message: err.to_string(),
};
send_notification(&chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
break;
},
}
}
},
Err(err) => {
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
error_message: err.to_string(),
};
send_notification(&chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
},
}
Ok::<(), FlowyError>(())
});
let question_pb = ChatMessagePB::from(question);
Ok(question_pb)
}
fn save_answer(
uid: i64,
chat_id: &str,
user_service: &Arc<dyn ChatUserService>,
answer: ChatMessage,
) -> Result<(), FlowyError> {
save_chat_message(
user_service.sqlite_connection(uid)?,
chat_id,
vec![answer.clone()],
)?;
let pb = ChatMessagePB::from(answer);
send_notification(chat_id, ChatNotification::DidReceiveChatMessage)
.payload(pb)
.send();
Ok(()) Ok(())
} }
@ -106,7 +216,7 @@ impl Chat {
before_message_id: Option<i64>, before_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> { ) -> Result<ChatMessageListPB, FlowyError> {
trace!( trace!(
"Loading old messages: chat_id={}, limit={}, before_message_id={:?}", "[Chat] Loading messages from disk: chat_id={}, limit={}, before_message_id={:?}",
self.chat_id, self.chat_id,
limit, limit,
before_message_id before_message_id
@ -116,13 +226,16 @@ impl Chat {
.await?; .await?;
// If the number of messages equals the limit, then no need to load more messages from remote // If the number of messages equals the limit, then no need to load more messages from remote
let has_more = !messages.is_empty();
if messages.len() == limit as usize { if messages.len() == limit as usize {
return Ok(ChatMessageListPB { let pb = ChatMessageListPB {
messages, messages,
has_more, has_more: true,
total: 0, total: 0,
}); };
send_notification(&self.chat_id, ChatNotification::DidLoadPrevChatMessage)
.payload(pb.clone())
.send();
return Ok(pb);
} }
if matches!( if matches!(
@ -140,7 +253,7 @@ impl Chat {
Ok(ChatMessageListPB { Ok(ChatMessageListPB {
messages, messages,
has_more, has_more: true,
total: 0, total: 0,
}) })
} }
@ -151,7 +264,7 @@ impl Chat {
after_message_id: Option<i64>, after_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> { ) -> Result<ChatMessageListPB, FlowyError> {
trace!( trace!(
"Loading new messages: chat_id={}, limit={}, after_message_id={:?}", "[Chat] Loading new messages: chat_id={}, limit={}, after_message_id={:?}",
self.chat_id, self.chat_id,
limit, limit,
after_message_id, after_message_id,
@ -161,7 +274,7 @@ impl Chat {
.await?; .await?;
trace!( trace!(
"Loaded local chat messages: chat_id={}, messages={}", "[Chat] Loaded local chat messages: chat_id={}, messages={}",
self.chat_id, self.chat_id,
messages.len() messages.len()
); );
@ -185,7 +298,7 @@ impl Chat {
after_message_id: Option<i64>, after_message_id: Option<i64>,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
trace!( trace!(
"Loading chat messages from remote: chat_id={}, limit={}, before_message_id={:?}, after_message_id={:?}", "[Chat] start loading messages from remote: chat_id={}, limit={}, before_message_id={:?}, after_message_id={:?}",
self.chat_id, self.chat_id,
limit, limit,
before_message_id, before_message_id,
@ -228,9 +341,11 @@ impl Chat {
let pb = ChatMessageListPB::from(resp); let pb = ChatMessageListPB::from(resp);
trace!( trace!(
"Loaded chat messages from remote: chat_id={}, messages={}", "[Chat] Loaded messages from remote: chat_id={}, messages={}, hasMore: {}, cursor:{:?}",
chat_id, chat_id,
pb.messages.len() pb.messages.len(),
pb.has_more,
cursor,
); );
if matches!(cursor, MessageCursor::BeforeMessageId(_)) { if matches!(cursor, MessageCursor::BeforeMessageId(_)) {
if pb.has_more { if pb.has_more {
@ -265,7 +380,7 @@ impl Chat {
.await?; .await?;
trace!( trace!(
"Related messages: chat_id={}, message_id={}, messages:{:?}", "[Chat] related messages: chat_id={}, message_id={}, messages:{:?}",
self.chat_id, self.chat_id,
message_id, message_id,
resp.items resp.items
@ -275,20 +390,19 @@ impl Chat {
#[instrument(level = "debug", skip_all, err)] #[instrument(level = "debug", skip_all, err)]
pub async fn generate_answer(&self, question_message_id: i64) -> FlowyResult<ChatMessagePB> { pub async fn generate_answer(&self, question_message_id: i64) -> FlowyResult<ChatMessagePB> {
trace!(
"[Chat] generate answer: chat_id={}, question_message_id={}",
self.chat_id,
question_message_id
);
let workspace_id = self.user_service.workspace_id()?; let workspace_id = self.user_service.workspace_id()?;
let resp = self let answer = self
.cloud_service .cloud_service
.generate_answer(&workspace_id, &self.chat_id, question_message_id) .generate_answer(&workspace_id, &self.chat_id, question_message_id)
.await?; .await?;
save_answer( Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
self.user_service.sqlite_connection(self.uid)?, let pb = ChatMessagePB::from(answer);
&self.chat_id,
resp.clone(),
question_message_id,
)?;
let pb = ChatMessagePB::from(resp);
Ok(pb) Ok(pb)
} }
@ -314,7 +428,6 @@ impl Chat {
created_at: record.created_at, created_at: record.created_at,
author_type: record.author_type, author_type: record.author_type,
author_id: record.author_id, author_id: record.author_id,
has_following: false,
reply_message_id: record.reply_message_id, reply_message_id: record.reply_message_id,
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -323,114 +436,6 @@ impl Chat {
} }
} }
fn stream_send_chat_messages(
uid: i64,
workspace_id: String,
chat_id: String,
message_content: String,
message_type: ChatMessageType,
cloud_service: Arc<dyn ChatCloudService>,
user_service: Arc<dyn ChatUserService>,
) {
tokio::spawn(async move {
trace!(
"Sending chat message: chat_id={}, message={}, type={:?}",
chat_id,
message_content,
message_type
);
let mut messages = Vec::with_capacity(2);
let stream_result = cloud_service
.send_chat_message(&workspace_id, &chat_id, &message_content, message_type)
.await;
// By default, stream only returns two messages:
// 1. user message
// 2. ai response message
match stream_result {
Ok(mut stream) => {
while let Some(result) = stream.next().await {
match result {
Ok(message) => {
let mut pb = ChatMessagePB::from(message.clone());
if matches!(message.author.author_type, ChatAuthorType::Human) {
pb.has_following = true;
send_notification(&chat_id, ChatNotification::LastUserSentMessage)
.payload(pb.clone())
.send();
}
//
send_notification(&chat_id, ChatNotification::DidReceiveChatMessage)
.payload(pb)
.send();
messages.push(message);
},
Err(err) => {
error!("stream chat message error: {}", err);
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
content: message_content.clone(),
error_message: "Service Temporarily Unavailable".to_string(),
};
send_notification(&chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
break;
},
}
}
},
Err(err) => {
error!("Failed to send chat message: {}", err);
let pb = ChatMessageErrorPB {
chat_id: chat_id.clone(),
content: message_content.clone(),
error_message: err.to_string(),
};
send_notification(&chat_id, ChatNotification::StreamChatMessageError)
.payload(pb)
.send();
return;
},
}
if messages.is_empty() {
return;
}
trace!(
"Saving chat messages to local disk: chat_id={}, messages:{:?}",
chat_id,
messages
);
// Insert chat messages to local disk
if let Err(err) = user_service.sqlite_connection(uid).and_then(|conn| {
let records = messages
.into_iter()
.map(|message| ChatMessageTable {
message_id: message.message_id,
chat_id: chat_id.clone(),
content: message.content,
created_at: message.created_at.timestamp(),
author_type: message.author.author_type as i64,
author_id: message.author.author_id.to_string(),
reply_message_id: message.reply_message_id,
})
.collect::<Vec<_>>();
insert_chat_messages(conn, &records)?;
// Mark chat as finished
send_notification(&chat_id, ChatNotification::FinishAnswerQuestion).send();
Ok(())
}) {
error!("Failed to save chat messages: {}", err);
}
});
}
fn save_chat_message( fn save_chat_message(
conn: DBConnection, conn: DBConnection,
chat_id: &str, chat_id: &str,
@ -451,21 +456,3 @@ fn save_chat_message(
insert_chat_messages(conn, &records)?; insert_chat_messages(conn, &records)?;
Ok(()) Ok(())
} }
fn save_answer(
conn: DBConnection,
chat_id: &str,
message: ChatMessage,
question_message_id: i64,
) -> FlowyResult<()> {
let record = ChatMessageTable {
message_id: message.message_id,
chat_id: chat_id.to_string(),
content: message.content,
created_at: message.created_at.timestamp(),
author_type: message.author.author_type as i64,
author_id: message.author.author_id.to_string(),
reply_message_id: message.reply_message_id,
};
insert_answer_message(conn, question_message_id, record)?;
Ok(())
}

View File

@ -19,6 +19,30 @@ pub struct SendChatPayloadPB {
pub message_type: ChatMessageTypePB, pub message_type: ChatMessageTypePB,
} }
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct StreamChatPayloadPB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub chat_id: String,
#[pb(index = 2)]
#[validate(custom = "required_not_empty_str")]
pub message: String,
#[pb(index = 3)]
pub message_type: ChatMessageTypePB,
#[pb(index = 4)]
pub text_stream_port: i64,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct StopStreamPB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub chat_id: String,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)] #[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum ChatMessageTypePB { pub enum ChatMessageTypePB {
#[default] #[default]
@ -97,10 +121,7 @@ pub struct ChatMessagePB {
#[pb(index = 5)] #[pb(index = 5)]
pub author_id: String, pub author_id: String,
#[pb(index = 6)] #[pb(index = 6, one_of)]
pub has_following: bool,
#[pb(index = 7, one_of)]
pub reply_message_id: Option<i64>, pub reply_message_id: Option<i64>,
} }
@ -110,9 +131,6 @@ pub struct ChatMessageErrorPB {
pub chat_id: String, pub chat_id: String,
#[pb(index = 2)] #[pb(index = 2)]
pub content: String,
#[pb(index = 3)]
pub error_message: String, pub error_message: String,
} }
@ -124,7 +142,6 @@ impl From<ChatMessage> for ChatMessagePB {
created_at: chat_message.created_at.timestamp(), created_at: chat_message.created_at.timestamp(),
author_type: chat_message.author.author_type as i64, author_type: chat_message.author.author_type as i64,
author_id: chat_message.author.author_id.to_string(), author_id: chat_message.author.author_id.to_string(),
has_following: false,
reply_message_id: None, reply_message_id: None,
} }
} }

View File

@ -18,10 +18,10 @@ fn upgrade_chat_manager(
} }
#[tracing::instrument(level = "debug", skip_all, err)] #[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn send_chat_message_handler( pub(crate) async fn stream_chat_message_handler(
data: AFPluginData<SendChatPayloadPB>, data: AFPluginData<StreamChatPayloadPB>,
chat_manager: AFPluginState<Weak<ChatManager>>, chat_manager: AFPluginState<Weak<ChatManager>>,
) -> Result<(), FlowyError> { ) -> DataResult<ChatMessagePB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?; let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner(); let data = data.into_inner();
data.validate()?; data.validate()?;
@ -30,10 +30,16 @@ pub(crate) async fn send_chat_message_handler(
ChatMessageTypePB::System => ChatMessageType::System, ChatMessageTypePB::System => ChatMessageType::System,
ChatMessageTypePB::User => ChatMessageType::User, ChatMessageTypePB::User => ChatMessageType::User,
}; };
chat_manager
.send_chat_message(&data.chat_id, &data.message, message_type) let question = chat_manager
.stream_chat_message(
&data.chat_id,
&data.message,
message_type,
data.text_stream_port,
)
.await?; .await?;
Ok(()) data_result_ok(question)
} }
#[tracing::instrument(level = "debug", skip_all, err)] #[tracing::instrument(level = "debug", skip_all, err)]
@ -91,3 +97,16 @@ pub(crate) async fn get_answer_handler(
.await?; .await?;
data_result_ok(message) data_result_ok(message)
} }
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn stop_stream_handler(
data: AFPluginData<StopStreamPB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> Result<(), FlowyError> {
let data = data.into_inner();
data.validate()?;
let chat_manager = upgrade_chat_manager(chat_manager)?;
chat_manager.stop_stream(&data.chat_id).await?;
Ok(())
}

View File

@ -12,11 +12,12 @@ pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
AFPlugin::new() AFPlugin::new()
.name("Flowy-Chat") .name("Flowy-Chat")
.state(chat_manager) .state(chat_manager)
.event(ChatEvent::SendMessage, send_chat_message_handler) .event(ChatEvent::StreamMessage, stream_chat_message_handler)
.event(ChatEvent::LoadPrevMessage, load_prev_message_handler) .event(ChatEvent::LoadPrevMessage, load_prev_message_handler)
.event(ChatEvent::LoadNextMessage, load_next_message_handler) .event(ChatEvent::LoadNextMessage, load_next_message_handler)
.event(ChatEvent::GetRelatedQuestion, get_related_question_handler) .event(ChatEvent::GetRelatedQuestion, get_related_question_handler)
.event(ChatEvent::GetAnswerForQuestion, get_answer_handler) .event(ChatEvent::GetAnswerForQuestion, get_answer_handler)
.event(ChatEvent::StopStream, stop_stream_handler)
} }
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
@ -29,12 +30,15 @@ pub enum ChatEvent {
#[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")] #[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")]
LoadNextMessage = 1, LoadNextMessage = 1,
#[event(input = "SendChatPayloadPB")] #[event(input = "StreamChatPayloadPB", output = "ChatMessagePB")]
SendMessage = 2, StreamMessage = 2,
#[event(input = "StopStreamPB")]
StopStream = 3,
#[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")] #[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")]
GetRelatedQuestion = 3, GetRelatedQuestion = 4,
#[event(input = "ChatMessageIdPB", output = "ChatMessagePB")] #[event(input = "ChatMessageIdPB", output = "ChatMessagePB")]
GetAnswerForQuestion = 4, GetAnswerForQuestion = 5,
} }

View File

@ -7,7 +7,7 @@ use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::DBConnection; use flowy_sqlite::DBConnection;
use lib_infra::util::timestamp; use lib_infra::util::timestamp;
use std::sync::Arc; use std::sync::Arc;
use tracing::{instrument, trace}; use tracing::trace;
pub trait ChatUserService: Send + Sync + 'static { pub trait ChatUserService: Send + Sync + 'static {
fn user_id(&self) -> Result<i64, FlowyError>; fn user_id(&self) -> Result<i64, FlowyError>;
@ -79,16 +79,18 @@ impl ChatManager {
Ok(chat) Ok(chat)
} }
#[instrument(level = "info", skip_all, err)] pub async fn stream_chat_message(
pub async fn send_chat_message(
&self, &self,
chat_id: &str, chat_id: &str,
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
) -> Result<(), FlowyError> { text_stream_port: i64,
) -> Result<ChatMessagePB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?; let chat = self.get_or_create_chat_instance(chat_id).await?;
chat.send_chat_message(message, message_type).await?; let question = chat
Ok(()) .stream_chat_message(message, message_type, text_stream_port)
.await?;
Ok(question)
} }
pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> { pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
@ -168,6 +170,12 @@ impl ChatManager {
let resp = chat.generate_answer(question_message_id).await?; let resp = chat.generate_answer(question_message_id).await?;
Ok(resp) Ok(resp)
} }
pub async fn stop_stream(&self, chat_id: &str) -> Result<(), FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
chat.stop_stream_message().await;
Ok(())
}
} }
fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> { fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {

View File

@ -11,8 +11,7 @@ pub enum ChatNotification {
DidLoadPrevChatMessage = 2, DidLoadPrevChatMessage = 2,
DidReceiveChatMessage = 3, DidReceiveChatMessage = 3,
StreamChatMessageError = 4, StreamChatMessageError = 4,
FinishAnswerQuestion = 5, FinishStreaming = 5,
LastUserSentMessage = 6,
} }
impl std::convert::From<ChatNotification> for i32 { impl std::convert::From<ChatNotification> for i32 {
@ -27,8 +26,7 @@ impl std::convert::From<i32> for ChatNotification {
2 => ChatNotification::DidLoadPrevChatMessage, 2 => ChatNotification::DidLoadPrevChatMessage,
3 => ChatNotification::DidReceiveChatMessage, 3 => ChatNotification::DidReceiveChatMessage,
4 => ChatNotification::StreamChatMessageError, 4 => ChatNotification::StreamChatMessageError,
5 => ChatNotification::FinishAnswerQuestion, 5 => ChatNotification::FinishStreaming,
6 => ChatNotification::LastUserSentMessage,
_ => ChatNotification::Unknown, _ => ChatNotification::Unknown,
} }
} }

View File

@ -45,41 +45,6 @@ pub fn insert_chat_messages(
Ok(()) Ok(())
} }
pub fn insert_answer_message(
mut conn: DBConnection,
question_message_id: i64,
message: ChatMessageTable,
) -> FlowyResult<()> {
conn.immediate_transaction(|conn| {
// Step 1: Get the message with the given question_message_id
let question_message = dsl::chat_message_table
.filter(chat_message_table::message_id.eq(question_message_id))
.first::<ChatMessageTable>(conn)?;
// Step 2: Use reply_message_id from the retrieved message to delete the existing message
if let Some(reply_id) = question_message.reply_message_id {
diesel::delete(dsl::chat_message_table.filter(chat_message_table::message_id.eq(reply_id)))
.execute(conn)?;
}
// Step 3: Insert the new message
let _ = insert_into(chat_message_table::table)
.values(message)
.on_conflict(chat_message_table::message_id)
.do_update()
.set((
chat_message_table::content.eq(excluded(chat_message_table::content)),
chat_message_table::created_at.eq(excluded(chat_message_table::created_at)),
chat_message_table::author_type.eq(excluded(chat_message_table::author_type)),
chat_message_table::author_id.eq(excluded(chat_message_table::author_id)),
chat_message_table::reply_message_id.eq(excluded(chat_message_table::reply_message_id)),
))
.execute(conn)?;
Ok::<(), FlowyError>(())
})?;
Ok(())
}
pub fn select_chat_messages( pub fn select_chat_messages(
mut conn: DBConnection, mut conn: DBConnection,
chat_id_val: &str, chat_id_val: &str,

View File

@ -30,6 +30,7 @@ use tokio::sync::RwLock;
use crate::integrate::server::ServerProvider; use crate::integrate::server::ServerProvider;
pub struct FolderDepsResolver(); pub struct FolderDepsResolver();
#[allow(clippy::too_many_arguments)]
impl FolderDepsResolver { impl FolderDepsResolver {
pub async fn resolve( pub async fn resolve(
authenticate_user: Weak<AuthenticateUser>, authenticate_user: Weak<AuthenticateUser>,

View File

@ -18,6 +18,7 @@ use collab_integrate::collab_builder::{
}; };
use flowy_chat_pub::cloud::{ use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage, ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage,
StreamAnswer,
}; };
use flowy_database_pub::cloud::{ use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent, CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
@ -476,6 +477,60 @@ impl ChatCloudService for ServerProvider {
.await .await
} }
fn send_question(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let message = message.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.send_question(&workspace_id, &chat_id, &message, message_type)
.await
})
}
fn save_answer(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
question_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let message = message.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.save_answer(&workspace_id, &chat_id, &message, question_id)
.await
})
}
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> Result<StreamAnswer, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server()?;
server
.chat_service()
.stream_answer(&workspace_id, &chat_id, message_id)
.await
}
fn get_chat_messages( fn get_chat_messages(
&self, &self,
workspace_id: &str, workspace_id: &str,

View File

@ -1,9 +1,12 @@
use crate::af_cloud::AFServer; use crate::af_cloud::AFServer;
use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::{ use client_api::entity::{
CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage, CreateAnswerMessageParams, CreateChatMessageParams, CreateChatParams, MessageCursor,
RepeatedChatMessage,
};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType, StreamAnswer,
}; };
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType};
use flowy_error::FlowyError; use flowy_error::FlowyError;
use futures_util::StreamExt; use futures_util::StreamExt;
use lib_infra::async_trait::async_trait; use lib_infra::async_trait::async_trait;
@ -50,22 +53,81 @@ where
message: &str, message: &str,
message_type: ChatMessageType, message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError> { ) -> Result<ChatMessageStream, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let message = message.to_string();
let try_get_client = self.inner.try_get_client(); let try_get_client = self.inner.try_get_client();
let params = CreateChatMessageParams { let params = CreateChatMessageParams {
content: message, content: message.to_string(),
message_type, message_type,
}; };
let stream = try_get_client? let stream = try_get_client?
.create_chat_message(&workspace_id, &chat_id, params) .create_chat_qa_message(workspace_id, chat_id, params)
.await .await
.map_err(FlowyError::from)?; .map_err(FlowyError::from)?;
Ok(stream.boxed()) Ok(stream.boxed())
} }
fn send_question(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
let params = CreateChatMessageParams {
content: message.to_string(),
message_type,
};
FutureResult::new(async move {
let message = try_get_client?
.create_question(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(message)
})
}
fn save_answer(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
question_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
let params = CreateAnswerMessageParams {
content: message.to_string(),
question_message_id: question_id,
};
FutureResult::new(async move {
let message = try_get_client?
.create_answer(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(message)
})
}
async fn stream_answer(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> Result<StreamAnswer, FlowyError> {
let try_get_client = self.inner.try_get_client();
let stream = try_get_client?
.stream_answer(workspace_id, chat_id, message_id)
.await
.map_err(FlowyError::from)?;
Ok(stream.boxed())
}
fn get_chat_messages( fn get_chat_messages(
&self, &self,
workspace_id: &str, workspace_id: &str,
@ -119,7 +181,7 @@ where
FutureResult::new(async move { FutureResult::new(async move {
let resp = try_get_client? let resp = try_get_client?
.generate_question_answer(&workspace_id, &chat_id, question_message_id) .get_answer(&workspace_id, &chat_id, question_message_id)
.await .await
.map_err(FlowyError::from)?; .map_err(FlowyError::from)?;
Ok(resp) Ok(resp)

View File

@ -1,6 +1,6 @@
use client_api::entity::ai_dto::RepeatedRelatedQuestion; use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage}; use client_api::entity::{ChatMessageType, MessageCursor, RepeatedChatMessage};
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream}; use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, StreamAnswer};
use flowy_error::FlowyError; use flowy_error::FlowyError;
use lib_infra::async_trait::async_trait; use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult; use lib_infra::future::FutureResult;
@ -30,6 +30,39 @@ impl ChatCloudService for DefaultChatCloudServiceImpl {
Err(FlowyError::not_support().with_context("Chat is not supported in local server.")) Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
} }
fn send_question(
&self,
_workspace_id: &str,
_chat_id: &str,
_message: &str,
_message_type: ChatMessageType,
) -> FutureResult<ChatMessage, FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
}
fn save_answer(
&self,
_workspace_id: &str,
_chat_id: &str,
_message: &str,
_question_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
FutureResult::new(async move {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
})
}
async fn stream_answer(
&self,
_workspace_id: &str,
_chat_id: &str,
_message_id: i64,
) -> Result<StreamAnswer, FlowyError> {
Err(FlowyError::not_support().with_context("Chat is not supported in local server."))
}
fn get_chat_messages( fn get_chat_messages(
&self, &self,
_workspace_id: &str, _workspace_id: &str,

View File

@ -8,7 +8,7 @@ edition = "2018"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
chrono = { workspace = true, default-features = false, features = ["clock"] } chrono = { workspace = true, default-features = false, features = ["clock"] }
bytes = { version = "1.5" } bytes = { version = "1.5" }
pin-project = "1.1.3" pin-project = "1.1.3"
futures-core = { version = "0.3" } futures-core = { version = "0.3" }
@ -21,6 +21,8 @@ tempfile = "3.8.1"
validator = "0.16.0" validator = "0.16.0"
tracing.workspace = true tracing.workspace = true
atomic_refcell = "0.1" atomic_refcell = "0.1"
allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true }
futures = "0.3.30"
[dev-dependencies] [dev-dependencies]
rand = "0.8.5" rand = "0.8.5"
@ -28,7 +30,8 @@ futures = "0.3.30"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
zip = { version = "0.6.6", features = ["deflate"] } zip = { version = "0.6.6", features = ["deflate"] }
brotli = { version = "3.4.0", optional = true } brotli = { version = "3.4.0", optional = true }
[features] [features]
compression = ["brotli"] compression = ["brotli"]
isolate_flutter = ["allo-isolate"]

View File

@ -0,0 +1,44 @@
use allo_isolate::{IntoDart, Isolate};
use futures::Sink;
use pin_project::pin_project;
use std::pin::Pin;
use std::task::{Context, Poll};
#[pin_project]
pub struct IsolateSink {
isolate: Isolate,
}
impl IsolateSink {
pub fn new(isolate: Isolate) -> Self {
Self { isolate }
}
}
impl<T> Sink<T> for IsolateSink
where
T: IntoDart,
{
type Error = ();
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn start_send(self: Pin<&mut Self>, item: T) -> Result<(), Self::Error> {
let this = self.project();
if this.isolate.post(item) {
Ok(())
} else {
Err(())
}
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
}

View File

@ -19,6 +19,8 @@ if_wasm! {
} }
} }
#[cfg(feature = "isolate_flutter")]
pub mod isolate_stream;
pub mod priority_task; pub mod priority_task;
pub mod ref_map; pub mod ref_map;
pub mod util; pub mod util;