mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: Stream chat message (#5498)
* chore: stream message * chore: stream message * chore: fix streaming * chore: fix clippy
This commit is contained in:
parent
94060a0a99
commit
bb3e9d5bd8
@ -1,42 +1,131 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'dart:async';
|
||||
|
||||
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_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_ai_message_bloc.freezed.dart';
|
||||
|
||||
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
ChatAIMessageBloc({
|
||||
required Message message,
|
||||
dynamic message,
|
||||
required this.chatId,
|
||||
required this.questionId,
|
||||
}) : 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>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
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
|
||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.initial() = Initial;
|
||||
const factory ChatAIMessageEvent.update(
|
||||
UserProfilePB userProfile,
|
||||
String deviceId,
|
||||
DocumentAwarenessStatesPB states,
|
||||
) = Update;
|
||||
const factory ChatAIMessageEvent.newText(String text) = _NewText;
|
||||
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
|
||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatAIMessageState with _$ChatAIMessageState {
|
||||
const factory ChatAIMessageState({
|
||||
required Message message,
|
||||
AnswerStream? stream,
|
||||
String? error,
|
||||
required String text,
|
||||
required LoadingState retryState,
|
||||
}) = _ChatAIMessageState;
|
||||
|
||||
factory ChatAIMessageState.initial(Message message) =>
|
||||
ChatAIMessageState(message: message);
|
||||
factory ChatAIMessageState.initial(dynamic text) {
|
||||
return ChatAIMessageState(
|
||||
text: text is String ? text : "",
|
||||
stream: text is AnswerStream ? text : null,
|
||||
retryState: const LoadingState.finish(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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/log.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/view.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:nanoid/nanoid.dart';
|
||||
import 'chat_message_listener.dart';
|
||||
|
||||
part 'chat_bloc.freezed.dart';
|
||||
|
||||
const canRetryKey = "canRetry";
|
||||
const sendMessageErrorKey = "sendMessageError";
|
||||
|
||||
class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
@ -26,78 +30,31 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
super(
|
||||
ChatState.initial(view, userProfile),
|
||||
) {
|
||||
_startListening();
|
||||
_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 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
|
||||
Future<void> close() {
|
||||
listener.stop();
|
||||
Future<void> close() async {
|
||||
if (state.answerStream != null) {
|
||||
await state.answerStream?.dispose();
|
||||
}
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@ -114,8 +71,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
startLoadingPrevMessage: () async {
|
||||
Int64? beforeMessageId;
|
||||
if (state.messages.isNotEmpty) {
|
||||
beforeMessageId = Int64.parseInt(state.messages.last.id);
|
||||
final oldestMessage = _getOlderstMessage();
|
||||
if (oldestMessage != null) {
|
||||
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
||||
}
|
||||
_loadPrevMessage(beforeMessageId);
|
||||
emit(
|
||||
@ -126,8 +84,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
didLoadPreviousMessages: (List<Message> messages, bool hasMore) {
|
||||
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));
|
||||
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@ -137,8 +100,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
},
|
||||
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));
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@ -146,55 +113,43 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
streaming: (List<Message> messages) {
|
||||
streaming: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
allMessages.insertAll(0, messages);
|
||||
emit(state.copyWith(messages: allMessages));
|
||||
},
|
||||
didFinishStreaming: () {
|
||||
allMessages.insert(0, message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
answerQuestionStatus: const LoadingState.finish(),
|
||||
messages: allMessages,
|
||||
streamingStatus: const LoadingState.loading(),
|
||||
),
|
||||
);
|
||||
},
|
||||
sendMessage: (String message) async {
|
||||
await _handleSentMessage(message, emit);
|
||||
|
||||
// Create a loading indicator
|
||||
final loadingMessage =
|
||||
_loadingMessage(state.userProfile.id.toString());
|
||||
final allMessages = List<Message>.from(state.messages)
|
||||
..insert(0, loadingMessage);
|
||||
|
||||
didFinishStreaming: () {
|
||||
emit(
|
||||
state.copyWith(streamingStatus: const LoadingState.finish()),
|
||||
);
|
||||
},
|
||||
receveMessage: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
// 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(
|
||||
state.copyWith(
|
||||
lastSentMessage: null,
|
||||
messages: allMessages,
|
||||
answerQuestionStatus: const LoadingState.loading(),
|
||||
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) {
|
||||
final allMessages = _perminentMessages();
|
||||
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.
|
||||
List<Message> _perminentMessages() {
|
||||
final allMessages = state.messages.where((element) {
|
||||
@ -238,6 +286,22 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
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) {
|
||||
final payload = LoadPrevChatMessagePB(
|
||||
chatId: state.view.id,
|
||||
@ -247,68 +311,91 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
ChatEventLoadPrevMessage(payload).send();
|
||||
}
|
||||
|
||||
Future<void> _handleSentMessage(
|
||||
Future<void> _startStreamingMessage(
|
||||
String message,
|
||||
Emitter<ChatState> emit,
|
||||
) 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,
|
||||
message: message,
|
||||
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(
|
||||
(_) {},
|
||||
(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) {
|
||||
if (!isClosed) {
|
||||
Log.error("Failed to send message: ${err.msg}");
|
||||
final metadata = OnetimeShotType.invalidSendMesssage.toMap();
|
||||
metadata[sendMessageErrorKey] = err.msg;
|
||||
if (err.code != ErrorCode.Internal) {
|
||||
metadata[sendMessageErrorKey] = err.msg;
|
||||
}
|
||||
|
||||
final error = CustomMessage(
|
||||
metadata: metadata,
|
||||
author: const User(id: "system"),
|
||||
id: 'system',
|
||||
);
|
||||
|
||||
add(ChatEvent.streaming([error]));
|
||||
add(ChatEvent.receveMessage(error));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleChatMessage(ChatMessagePB pb) {
|
||||
if (!isClosed) {
|
||||
final message = _createChatMessage(pb);
|
||||
final messages = pb.hasFollowing
|
||||
? [_loadingMessage(0.toString()), message]
|
||||
: [message];
|
||||
add(ChatEvent.streaming(messages));
|
||||
}
|
||||
}
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
final streamMessageId = nanoid();
|
||||
lastStreamMessageId = streamMessageId;
|
||||
|
||||
Message _loadingMessage(String id) {
|
||||
return CustomMessage(
|
||||
author: User(id: id),
|
||||
metadata: OnetimeShotType.loading.toMap(),
|
||||
// fake id
|
||||
id: nanoid(),
|
||||
return TextMessage(
|
||||
author: User(id: nanoid()),
|
||||
metadata: {
|
||||
"$AnswerStream": stream,
|
||||
"question": questionMessageId,
|
||||
"chatId": chatId,
|
||||
},
|
||||
id: streamMessageId,
|
||||
text: '',
|
||||
);
|
||||
}
|
||||
|
||||
Message _createChatMessage(ChatMessagePB message) {
|
||||
final messageId = message.messageId.toString();
|
||||
Message _createTextMessage(ChatMessagePB message) {
|
||||
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(
|
||||
author: User(id: message.authorId),
|
||||
id: messageId,
|
||||
text: message.content,
|
||||
createdAt: message.createdAt.toInt(),
|
||||
repliedMessage: _getReplyMessage(state.messages, messageId),
|
||||
);
|
||||
}
|
||||
|
||||
Message? _getReplyMessage(List<Message?> messages, String messageId) {
|
||||
return messages.firstWhereOrNull((element) => element?.id == messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -322,15 +409,20 @@ class ChatEvent with _$ChatEvent {
|
||||
) = _DidLoadPreviousMessages;
|
||||
const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =
|
||||
_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.didReceiveRelatedQuestion(
|
||||
List<RelatedQuestionPB> questions,
|
||||
) = _DidReceiveRelatedQueston;
|
||||
const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion;
|
||||
const factory ChatEvent.retryGenerate() = _RetryGenerate;
|
||||
const factory ChatEvent.didSentUserMessage(ChatMessagePB message) =
|
||||
_DidSendUserMessage;
|
||||
const factory ChatEvent.didUpdateAnswerStream(
|
||||
AnswerStream stream,
|
||||
) = _DidUpdateAnswerStream;
|
||||
const factory ChatEvent.stopStream() = _StopStream;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -347,13 +439,14 @@ class ChatState with _$ChatState {
|
||||
required LoadingState loadingPreviousStatus,
|
||||
// When sending a user message, the status will be set as loading.
|
||||
// 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.
|
||||
required bool hasMorePrevMessage,
|
||||
// The related questions that are received after the user message is sent.
|
||||
required List<RelatedQuestionPB> relatedQuestions,
|
||||
// The last user message that is sent to the server.
|
||||
ChatMessagePB? lastSentMessage,
|
||||
AnswerStream? answerStream,
|
||||
}) = _ChatState;
|
||||
|
||||
factory ChatState.initial(ViewPB view, UserProfilePB userProfile) =>
|
||||
@ -363,7 +456,7 @@ class ChatState with _$ChatState {
|
||||
userProfile: userProfile,
|
||||
initialLoadingStatus: const LoadingState.finish(),
|
||||
loadingPreviousStatus: const LoadingState.finish(),
|
||||
answerQuestionStatus: const LoadingState.finish(),
|
||||
streamingStatus: const LoadingState.finish(),
|
||||
hasMorePrevMessage: true,
|
||||
relatedQuestions: [],
|
||||
);
|
||||
@ -377,10 +470,8 @@ class LoadingState with _$LoadingState {
|
||||
|
||||
enum OnetimeShotType {
|
||||
unknown,
|
||||
loading,
|
||||
serverStreamError,
|
||||
relatedQuestion,
|
||||
invalidSendMesssage
|
||||
invalidSendMesssage,
|
||||
}
|
||||
|
||||
const onetimeShotType = "OnetimeShotType";
|
||||
@ -388,10 +479,6 @@ const onetimeShotType = "OnetimeShotType";
|
||||
extension OnetimeMessageTypeExtension on OnetimeShotType {
|
||||
static OnetimeShotType fromString(String value) {
|
||||
switch (value) {
|
||||
case 'OnetimeShotType.loading':
|
||||
return OnetimeShotType.loading;
|
||||
case 'OnetimeShotType.serverStreamError':
|
||||
return OnetimeShotType.serverStreamError;
|
||||
case 'OnetimeShotType.relatedQuestion':
|
||||
return OnetimeShotType.relatedQuestion;
|
||||
case 'OnetimeShotType.invalidSendMesssage':
|
||||
@ -402,7 +489,7 @@ extension OnetimeMessageTypeExtension on OnetimeShotType {
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> toMap() {
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
onetimeShotType: toString(),
|
||||
};
|
||||
@ -421,3 +508,43 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -28,26 +28,23 @@ class ChatMessageListener {
|
||||
ChatNotificationParser? _parser;
|
||||
|
||||
ChatMessageCallback? chatMessageCallback;
|
||||
ChatMessageCallback? lastUserSentMessageCallback;
|
||||
ChatErrorMessageCallback? chatErrorMessageCallback;
|
||||
LatestMessageCallback? latestMessageCallback;
|
||||
PrevMessageCallback? prevMessageCallback;
|
||||
void Function()? finishAnswerQuestionCallback;
|
||||
void Function()? finishStreamingCallback;
|
||||
|
||||
void start({
|
||||
ChatMessageCallback? chatMessageCallback,
|
||||
ChatErrorMessageCallback? chatErrorMessageCallback,
|
||||
LatestMessageCallback? latestMessageCallback,
|
||||
PrevMessageCallback? prevMessageCallback,
|
||||
ChatMessageCallback? lastUserSentMessageCallback,
|
||||
void Function()? finishAnswerQuestionCallback,
|
||||
void Function()? finishStreamingCallback,
|
||||
}) {
|
||||
this.chatMessageCallback = chatMessageCallback;
|
||||
this.chatErrorMessageCallback = chatErrorMessageCallback;
|
||||
this.latestMessageCallback = latestMessageCallback;
|
||||
this.prevMessageCallback = prevMessageCallback;
|
||||
this.lastUserSentMessageCallback = lastUserSentMessageCallback;
|
||||
this.finishAnswerQuestionCallback = finishAnswerQuestionCallback;
|
||||
this.finishStreamingCallback = finishStreamingCallback;
|
||||
}
|
||||
|
||||
void _callback(
|
||||
@ -59,9 +56,6 @@ class ChatMessageListener {
|
||||
case ChatNotification.DidReceiveChatMessage:
|
||||
chatMessageCallback?.call(ChatMessagePB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.LastUserSentMessage:
|
||||
lastUserSentMessageCallback?.call(ChatMessagePB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.StreamChatMessageError:
|
||||
chatErrorMessageCallback?.call(ChatMessageErrorPB.fromBuffer(r));
|
||||
break;
|
||||
@ -71,8 +65,8 @@ class ChatMessageListener {
|
||||
case ChatNotification.DidLoadPrevChatMessage:
|
||||
prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.FinishAnswerQuestion:
|
||||
finishAnswerQuestionCallback?.call();
|
||||
case ChatNotification.FinishStreaming:
|
||||
finishStreamingCallback?.call();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -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();
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.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/chat_streaming_error_message.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.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_backend/protobuf/flowy-folder/view.pb.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 'presentation/chat_input.dart';
|
||||
import 'presentation/chat_loading.dart';
|
||||
import 'presentation/chat_popmenu.dart';
|
||||
import 'presentation/chat_theme.dart';
|
||||
import 'presentation/chat_user_invalid_message.dart';
|
||||
import 'presentation/chat_welcome_page.dart';
|
||||
import 'presentation/message/ai_text_message.dart';
|
||||
import 'presentation/message/user_text_message.dart';
|
||||
|
||||
class AIChatUILayout {
|
||||
static EdgeInsets get chatPadding =>
|
||||
@ -108,7 +108,6 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
customBottomWidget: buildChatInput(blocContext),
|
||||
user: _user,
|
||||
theme: buildTheme(context),
|
||||
customMessageBuilder: _customMessageBuilder,
|
||||
onEndReached: () async {
|
||||
if (state.hasMorePrevMessage &&
|
||||
state.loadingPreviousStatus !=
|
||||
@ -138,6 +137,13 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
},
|
||||
),
|
||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||
textMessageBuilder: (
|
||||
textMessage, {
|
||||
required messageWidth,
|
||||
required showName,
|
||||
}) {
|
||||
return _buildAITextMessage(blocContext, textMessage);
|
||||
},
|
||||
bubbleBuilder: (
|
||||
child, {
|
||||
required message,
|
||||
@ -149,46 +155,7 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
final messageType = onetimeMessageTypeFromMeta(
|
||||
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,
|
||||
);
|
||||
return _buildAIBubble(message, blocContext, state, 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) {
|
||||
final isAuthor = message.author.id == _user.id;
|
||||
const borderRadius = BorderRadius.all(Radius.circular(6));
|
||||
|
||||
final childWithPadding = isAuthor
|
||||
? Padding(
|
||||
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) {
|
||||
return ClipRect(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: Column(
|
||||
children: [
|
||||
ChatInput(
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) => onSendPressed(context, message.text),
|
||||
BlocSelector<ChatBloc, ChatState, LoadingState>(
|
||||
selector: (state) => state.streamingStatus,
|
||||
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),
|
||||
Opacity(
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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/presentation/chat_avatar.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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -35,30 +33,22 @@ class ChatAIMessageBubble extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
const padding = EdgeInsets.symmetric(horizontal: _leftPadding);
|
||||
final childWithPadding = Padding(padding: padding, child: child);
|
||||
final widget = isMobile
|
||||
? _wrapPopMenu(childWithPadding)
|
||||
: _wrapHover(childWithPadding);
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => ChatAIMessageBloc(message: message),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
final widget = isMobile
|
||||
? _wrapPopMenu(childWithPadding)
|
||||
: _wrapHover(childWithPadding);
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const ChatBorderedCircleAvatar(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(24),
|
||||
),
|
||||
),
|
||||
Expanded(child: widget),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 40),
|
||||
padding: const EdgeInsets.only(bottom: 30),
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
@ -18,13 +18,17 @@ class ChatInput extends StatefulWidget {
|
||||
required this.onSendPressed,
|
||||
required this.chatId,
|
||||
this.options = const InputOptions(),
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
});
|
||||
|
||||
final bool? isAttachmentUploading;
|
||||
final VoidCallback? onAttachmentPressed;
|
||||
final void Function(types.PartialText) onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final InputOptions options;
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
@ -68,26 +72,23 @@ class _ChatInputState extends State<ChatInput> {
|
||||
|
||||
void _handleSendButtonVisibilityModeChange() {
|
||||
_textController.removeListener(_handleTextControllerChange);
|
||||
if (widget.options.sendButtonVisibilityMode ==
|
||||
SendButtonVisibilityMode.hidden) {
|
||||
_sendButtonVisible = false;
|
||||
} else if (widget.options.sendButtonVisibilityMode ==
|
||||
SendButtonVisibilityMode.editing) {
|
||||
_sendButtonVisible = _textController.text.trim() != '';
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
} else {
|
||||
_sendButtonVisible = true;
|
||||
}
|
||||
_sendButtonVisible =
|
||||
_textController.text.trim() != '' || widget.isStreaming;
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
}
|
||||
|
||||
void _handleSendPressed() {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
if (widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
} else {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -138,6 +139,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
padding: textPadding,
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
readOnly: widget.isStreaming,
|
||||
focusNode: _inputFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
@ -153,7 +155,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
autocorrect: widget.options.autocorrect,
|
||||
autofocus: widget.options.autofocus,
|
||||
enableSuggestions: widget.options.enableSuggestions,
|
||||
spellCheckConfiguration: const SpellCheckConfiguration(),
|
||||
keyboardType: widget.options.keyboardType,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
maxLines: 10,
|
||||
@ -173,8 +174,14 @@ class _ChatInputState extends State<ChatInput> {
|
||||
visible: _sendButtonVisible,
|
||||
child: Padding(
|
||||
padding: buttonPadding,
|
||||
child: SendButton(
|
||||
onPressed: _handleSendPressed,
|
||||
child: AccessoryButton(
|
||||
onSendPressed: () {
|
||||
_handleSendPressed();
|
||||
},
|
||||
onStopStreaming: () {
|
||||
widget.onStopStreaming();
|
||||
},
|
||||
isStreaming: widget.isStreaming,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -184,10 +191,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.options.sendButtonVisibilityMode !=
|
||||
oldWidget.options.sendButtonVisibilityMode) {
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -211,7 +215,6 @@ class InputOptions {
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.onTextChanged,
|
||||
this.onTextFieldTap,
|
||||
this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing,
|
||||
this.textEditingController,
|
||||
this.autocorrect = true,
|
||||
this.autofocus = false,
|
||||
@ -231,11 +234,6 @@ class InputOptions {
|
||||
/// Will be called on [TextField] tap.
|
||||
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
|
||||
/// [InputTextFieldController], which extends [TextEditingController] and has
|
||||
/// additional fatures like markdown support. If you want to keep additional
|
||||
@ -260,24 +258,46 @@ class InputOptions {
|
||||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
class SendButton extends StatelessWidget {
|
||||
const SendButton({required this.onPressed, super.key});
|
||||
class AccessoryButton extends StatelessWidget {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
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: onPressed,
|
||||
);
|
||||
if (isStreaming) {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_stream_stop_s,
|
||||
size: const Size.square(28),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onStopStreaming,
|
||||
radius: BorderRadius.circular(18),
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,15 +53,15 @@ class ContentPlaceholder extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
width: 140,
|
||||
height: 16.0,
|
||||
margin: const EdgeInsets.only(bottom: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
),
|
||||
// Container(
|
||||
// width: 140,
|
||||
// height: 16.0,
|
||||
// margin: const EdgeInsets.only(bottom: 8.0),
|
||||
// decoration: BoxDecoration(
|
||||
// color: AFThemeExtension.of(context).lightGreyHover,
|
||||
// borderRadius: BorderRadius.circular(4.0),
|
||||
// ),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,47 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.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:easy_localization/easy_localization.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';
|
||||
|
||||
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 {
|
||||
const RelatedQuestionList({
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,84 +1,83 @@
|
||||
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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
// import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
// import 'package:easy_localization/easy_localization.dart';
|
||||
// import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
|
||||
class ChatStreamingError extends StatelessWidget {
|
||||
const ChatStreamingError({
|
||||
required this.message,
|
||||
required this.onRetryPressed,
|
||||
super.key,
|
||||
});
|
||||
// class ChatStreamingError extends StatelessWidget {
|
||||
// const ChatStreamingError({
|
||||
// required this.message,
|
||||
// required this.onRetryPressed,
|
||||
// super.key,
|
||||
// });
|
||||
|
||||
final void Function() onRetryPressed;
|
||||
final Message message;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canRetry = message.metadata?[canRetryKey] != null;
|
||||
// final void Function() onRetryPressed;
|
||||
// final Message message;
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final canRetry = message.metadata?[canRetryKey] != null;
|
||||
|
||||
if (canRetry) {
|
||||
return Column(
|
||||
children: [
|
||||
const Divider(height: 4, thickness: 1),
|
||||
const VSpace(16),
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
_aiUnvaliable(),
|
||||
const VSpace(10),
|
||||
_retryButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(height: 20, thickness: 1),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_serverUnavailable.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// if (canRetry) {
|
||||
// return Column(
|
||||
// children: [
|
||||
// const Divider(height: 4, thickness: 1),
|
||||
// const VSpace(16),
|
||||
// Center(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// _aiUnvaliable(),
|
||||
// const VSpace(10),
|
||||
// _retryButton(),
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// );
|
||||
// } else {
|
||||
// return Center(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// const Divider(height: 20, thickness: 1),
|
||||
// Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_serverUnavailable.tr(),
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// ),
|
||||
// ],
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
// 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Padding _aiUnvaliable() {
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.chat_aiServerUnavailable.tr(),
|
||||
// fontSize: 14,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
@ -20,29 +20,33 @@ class ChatWelcomePage extends StatelessWidget {
|
||||
];
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(44),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isMobile ? 2 : 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 16.0 / 9.0,
|
||||
return AnimatedOpacity(
|
||||
opacity: 1.0,
|
||||
duration: const Duration(seconds: 3),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.flowy_ai_chat_logo_s,
|
||||
size: Size.square(44),
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => WelcomeQuestion(
|
||||
question: items[index],
|
||||
onSelected: onSelectedQuestion,
|
||||
const SizedBox(height: 40),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -680,6 +680,14 @@ packages:
|
||||
url: "https://github.com/LucasXu0/emoji_mart.git"
|
||||
source: git
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1066,7 +1074,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
isolates:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: isolates
|
||||
sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28
|
||||
@ -1209,6 +1217,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -141,6 +141,8 @@ dependencies:
|
||||
auto_size_text_field: ^2.2.3
|
||||
reorderable_tabbar: ^1.0.6
|
||||
shimmer: ^3.0.0
|
||||
isolates: ^3.0.3+8
|
||||
markdown_widget: ^2.3.2+6
|
||||
|
||||
# Window Manager for MacOS and Linux
|
||||
window_manager: ^0.3.9
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
44
frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart
Normal file
44
frontend/appflowy_flutter/test/bloc_test/chat_test/util.dart
Normal 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);
|
||||
}
|
@ -58,7 +58,6 @@ class AppFlowyUnitTest {
|
||||
}
|
||||
|
||||
WorkspacePB get currentWorkspace => workspace;
|
||||
|
||||
Future<void> _loadWorkspace() async {
|
||||
final result = await userService.getCurrentWorkspace();
|
||||
result.fold(
|
||||
@ -83,15 +82,6 @@ class AppFlowyUnitTest {
|
||||
(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() {
|
||||
|
47
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
47
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -117,6 +117,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@ -162,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -182,7 +192,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -289,6 +299,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.10"
|
||||
@ -756,7 +772,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
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 = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@ -803,7 +819,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
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 = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@ -1043,7 +1059,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -1068,7 +1084,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1425,7 +1441,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -1817,6 +1833,7 @@ dependencies = [
|
||||
name = "flowy-chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"flowy-chat-pub",
|
||||
@ -1828,6 +1845,7 @@ dependencies = [
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"log",
|
||||
"protobuf",
|
||||
"strum_macros 0.21.1",
|
||||
"tokio",
|
||||
@ -2835,7 +2853,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@ -2852,7 +2870,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -3284,7 +3302,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"reqwest",
|
||||
@ -3525,11 +3543,13 @@ dependencies = [
|
||||
name = "lib-infra"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"atomic_refcell",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"md5",
|
||||
"pin-project",
|
||||
@ -3643,9 +3663,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@ -5772,7 +5792,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -5783,6 +5803,7 @@ dependencies = [
|
||||
"database-entity",
|
||||
"futures",
|
||||
"gotrue-entity",
|
||||
"log",
|
||||
"pin-project",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
|
||||
# Run the script:
|
||||
# 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]
|
||||
serde_json.workspace = true
|
||||
|
28
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
28
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
@ -216,7 +216,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -236,7 +236,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -562,7 +562,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
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 = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@ -609,7 +609,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
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 = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@ -787,7 +787,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -812,7 +812,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1026,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -1881,7 +1881,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@ -1898,7 +1898,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -2199,7 +2199,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"reqwest",
|
||||
@ -2339,6 +2339,7 @@ dependencies = [
|
||||
"atomic_refcell",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"md5",
|
||||
"pin-project",
|
||||
@ -2427,9 +2428,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "mac"
|
||||
@ -3900,7 +3901,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -3911,6 +3912,7 @@ dependencies = [
|
||||
"database-entity",
|
||||
"futures",
|
||||
"gotrue-entity",
|
||||
"log",
|
||||
"pin-project",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -55,7 +55,7 @@ yrs = "0.18.8"
|
||||
# Run the script:
|
||||
# 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" }
|
||||
|
||||
|
||||
|
||||
|
43
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
43
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -108,6 +108,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@ -153,7 +163,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -173,7 +183,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -299,6 +309,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "atomic_refcell"
|
||||
version = "0.1.13"
|
||||
@ -730,7 +746,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
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 = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@ -777,7 +793,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
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 = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@ -1026,7 +1042,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -1051,7 +1067,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1412,7 +1428,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -1854,6 +1870,7 @@ dependencies = [
|
||||
name = "flowy-chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"flowy-chat-pub",
|
||||
@ -1865,6 +1882,7 @@ dependencies = [
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"log",
|
||||
"protobuf",
|
||||
"strum_macros 0.21.1",
|
||||
"tokio",
|
||||
@ -2909,7 +2927,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@ -2926,7 +2944,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -3363,7 +3381,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"reqwest",
|
||||
@ -3609,11 +3627,13 @@ dependencies = [
|
||||
name = "lib-infra"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"atomic_refcell",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures",
|
||||
"futures-core",
|
||||
"md5",
|
||||
"pin-project",
|
||||
@ -5867,7 +5887,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -5878,6 +5898,7 @@ dependencies = [
|
||||
"database-entity",
|
||||
"futures",
|
||||
"gotrue-entity",
|
||||
"log",
|
||||
"pin-project",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
|
||||
# Run the script:
|
||||
# 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]
|
||||
serde_json.workspace = true
|
||||
|
1
frontend/resources/flowy_icons/16x/ai_stream_stop.svg
Normal file
1
frontend/resources/flowy_icons/16x/ai_stream_stop.svg
Normal 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 |
52
frontend/rust-lib/Cargo.lock
generated
52
frontend/rust-lib/Cargo.lock
generated
@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
|
||||
[[package]]
|
||||
name = "app-error"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -183,7 +183,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "appflowy-ai-client"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -664,7 +664,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-api"
|
||||
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 = [
|
||||
"again",
|
||||
"anyhow",
|
||||
@ -711,7 +711,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "client-websocket"
|
||||
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 = [
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
@ -920,7 +920,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
@ -945,7 +945,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-rt-protocol"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1165,7 +1165,7 @@ dependencies = [
|
||||
"cssparser-macros",
|
||||
"dtoa-short",
|
||||
"itoa",
|
||||
"phf 0.8.0",
|
||||
"phf 0.11.2",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@ -1265,7 +1265,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
|
||||
[[package]]
|
||||
name = "database-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -1652,6 +1652,7 @@ dependencies = [
|
||||
name = "flowy-chat"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"bytes",
|
||||
"dashmap",
|
||||
"flowy-chat-pub",
|
||||
@ -1663,6 +1664,7 @@ dependencies = [
|
||||
"futures",
|
||||
"lib-dispatch",
|
||||
"lib-infra",
|
||||
"log",
|
||||
"protobuf",
|
||||
"strum_macros 0.21.1",
|
||||
"tokio",
|
||||
@ -2521,7 +2523,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"futures-util",
|
||||
@ -2538,7 +2540,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gotrue-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -2903,7 +2905,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "infra"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"reqwest",
|
||||
@ -3045,6 +3047,7 @@ dependencies = [
|
||||
name = "lib-infra"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"allo-isolate",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"atomic_refcell",
|
||||
@ -3156,9 +3159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@ -3778,7 +3781,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_macros 0.8.0",
|
||||
"phf_shared 0.8.0",
|
||||
"proc-macro-hack",
|
||||
]
|
||||
@ -3798,6 +3801,7 @@ version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_macros 0.11.2",
|
||||
"phf_shared 0.11.2",
|
||||
]
|
||||
|
||||
@ -3865,6 +3869,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
@ -4068,7 +4085,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"heck 0.4.1",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.11.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"once_cell",
|
||||
@ -4089,7 +4106,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.10.5",
|
||||
"itertools 0.11.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.47",
|
||||
@ -4986,7 +5003,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "shared-entity"
|
||||
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 = [
|
||||
"anyhow",
|
||||
"app-error",
|
||||
@ -4997,6 +5014,7 @@ dependencies = [
|
||||
"database-entity",
|
||||
"futures",
|
||||
"gotrue-entity",
|
||||
"log",
|
||||
"pin-project",
|
||||
"reqwest",
|
||||
"serde",
|
||||
|
@ -29,7 +29,8 @@ members = [
|
||||
"build-tool/flowy-codegen",
|
||||
"build-tool/flowy-derive",
|
||||
"flowy-search-pub",
|
||||
"flowy-chat", "flowy-chat-pub",
|
||||
"flowy-chat",
|
||||
"flowy-chat-pub",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
@ -93,7 +94,7 @@ yrs = "0.18.8"
|
||||
# Run the script.add_workspace_members:
|
||||
# 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]
|
||||
opt-level = 1
|
||||
|
@ -43,7 +43,7 @@ impl EventIntegrationTest {
|
||||
};
|
||||
|
||||
EventBuilder::new(self.clone())
|
||||
.event(ChatEvent::SendMessage)
|
||||
.event(ChatEvent::StreamMessage)
|
||||
.payload(payload)
|
||||
.async_send()
|
||||
.await;
|
||||
|
@ -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::{
|
||||
ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage,
|
||||
};
|
||||
@ -9,6 +9,7 @@ use lib_infra::async_trait::async_trait;
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
|
||||
pub type StreamAnswer = BoxStream<'static, Result<StringOrMessage, AppResponseError>>;
|
||||
#[async_trait]
|
||||
pub trait ChatCloudService: Send + Sync + 'static {
|
||||
fn create_chat(
|
||||
@ -26,6 +27,29 @@ pub trait ChatCloudService: Send + Sync + 'static {
|
||||
message_type: ChatMessageType,
|
||||
) -> 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(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
@ -19,12 +19,14 @@ strum_macros = "0.21"
|
||||
protobuf.workspace = true
|
||||
bytes.workspace = true
|
||||
validator = { version = "0.16.0", features = ["derive"] }
|
||||
lib-infra = { workspace = true }
|
||||
lib-infra = { workspace = true, features = ["isolate_flutter"] }
|
||||
flowy-chat-pub.workspace = true
|
||||
dashmap = "5.5"
|
||||
flowy-sqlite = { workspace = true }
|
||||
tokio.workspace = true
|
||||
futures.workspace = true
|
||||
allo-isolate = { version = "^0.1", features = ["catch-unwind"] }
|
||||
log = "0.4.21"
|
||||
|
||||
[build-dependencies]
|
||||
flowy-codegen.workspace = true
|
||||
|
@ -3,18 +3,18 @@ use crate::entities::{
|
||||
};
|
||||
use crate::manager::ChatUserService;
|
||||
use crate::notification::{send_notification, ChatNotification};
|
||||
use crate::persistence::{
|
||||
insert_answer_message, insert_chat_messages, select_chat_messages, ChatMessageTable,
|
||||
};
|
||||
use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable};
|
||||
use allo_isolate::Isolate;
|
||||
use flowy_chat_pub::cloud::{
|
||||
ChatAuthorType, ChatCloudService, ChatMessage, ChatMessageType, MessageCursor,
|
||||
ChatCloudService, ChatMessage, ChatMessageType, MessageCursor, StringOrMessage,
|
||||
};
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_sqlite::DBConnection;
|
||||
use futures::StreamExt;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use lib_infra::isolate_stream::IsolateSink;
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tracing::{error, instrument, trace};
|
||||
|
||||
enum PrevMessageState {
|
||||
@ -30,6 +30,8 @@ pub struct Chat {
|
||||
cloud_service: Arc<dyn ChatCloudService>,
|
||||
prev_message_state: Arc<RwLock<PrevMessageState>>,
|
||||
latest_message_id: Arc<AtomicI64>,
|
||||
stop_stream: Arc<AtomicBool>,
|
||||
steam_buffer: Arc<Mutex<String>>,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
@ -46,6 +48,8 @@ impl Chat {
|
||||
user_service,
|
||||
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
|
||||
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)]
|
||||
pub async fn send_chat_message(
|
||||
pub async fn stream_chat_message(
|
||||
&self,
|
||||
message: &str,
|
||||
message_type: ChatMessageType,
|
||||
) -> Result<(), FlowyError> {
|
||||
text_stream_port: i64,
|
||||
) -> Result<ChatMessagePB, FlowyError> {
|
||||
if message.len() > 2000 {
|
||||
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 workspace_id = self.user_service.workspace_id()?;
|
||||
stream_send_chat_messages(
|
||||
uid,
|
||||
workspace_id,
|
||||
self.chat_id.clone(),
|
||||
message.to_string(),
|
||||
message_type,
|
||||
self.cloud_service.clone(),
|
||||
self.user_service.clone(),
|
||||
);
|
||||
|
||||
let question = self
|
||||
.cloud_service
|
||||
.send_question(&workspace_id, &self.chat_id, message, message_type)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to send question: {}", err);
|
||||
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(())
|
||||
}
|
||||
@ -106,7 +216,7 @@ impl Chat {
|
||||
before_message_id: Option<i64>,
|
||||
) -> Result<ChatMessageListPB, FlowyError> {
|
||||
trace!(
|
||||
"Loading old messages: chat_id={}, limit={}, before_message_id={:?}",
|
||||
"[Chat] Loading messages from disk: chat_id={}, limit={}, before_message_id={:?}",
|
||||
self.chat_id,
|
||||
limit,
|
||||
before_message_id
|
||||
@ -116,13 +226,16 @@ impl Chat {
|
||||
.await?;
|
||||
|
||||
// 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 {
|
||||
return Ok(ChatMessageListPB {
|
||||
let pb = ChatMessageListPB {
|
||||
messages,
|
||||
has_more,
|
||||
has_more: true,
|
||||
total: 0,
|
||||
});
|
||||
};
|
||||
send_notification(&self.chat_id, ChatNotification::DidLoadPrevChatMessage)
|
||||
.payload(pb.clone())
|
||||
.send();
|
||||
return Ok(pb);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
@ -140,7 +253,7 @@ impl Chat {
|
||||
|
||||
Ok(ChatMessageListPB {
|
||||
messages,
|
||||
has_more,
|
||||
has_more: true,
|
||||
total: 0,
|
||||
})
|
||||
}
|
||||
@ -151,7 +264,7 @@ impl Chat {
|
||||
after_message_id: Option<i64>,
|
||||
) -> Result<ChatMessageListPB, FlowyError> {
|
||||
trace!(
|
||||
"Loading new messages: chat_id={}, limit={}, after_message_id={:?}",
|
||||
"[Chat] Loading new messages: chat_id={}, limit={}, after_message_id={:?}",
|
||||
self.chat_id,
|
||||
limit,
|
||||
after_message_id,
|
||||
@ -161,7 +274,7 @@ impl Chat {
|
||||
.await?;
|
||||
|
||||
trace!(
|
||||
"Loaded local chat messages: chat_id={}, messages={}",
|
||||
"[Chat] Loaded local chat messages: chat_id={}, messages={}",
|
||||
self.chat_id,
|
||||
messages.len()
|
||||
);
|
||||
@ -185,7 +298,7 @@ impl Chat {
|
||||
after_message_id: Option<i64>,
|
||||
) -> FlowyResult<()> {
|
||||
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,
|
||||
limit,
|
||||
before_message_id,
|
||||
@ -228,9 +341,11 @@ impl Chat {
|
||||
|
||||
let pb = ChatMessageListPB::from(resp);
|
||||
trace!(
|
||||
"Loaded chat messages from remote: chat_id={}, messages={}",
|
||||
"[Chat] Loaded messages from remote: chat_id={}, messages={}, hasMore: {}, cursor:{:?}",
|
||||
chat_id,
|
||||
pb.messages.len()
|
||||
pb.messages.len(),
|
||||
pb.has_more,
|
||||
cursor,
|
||||
);
|
||||
if matches!(cursor, MessageCursor::BeforeMessageId(_)) {
|
||||
if pb.has_more {
|
||||
@ -265,7 +380,7 @@ impl Chat {
|
||||
.await?;
|
||||
|
||||
trace!(
|
||||
"Related messages: chat_id={}, message_id={}, messages:{:?}",
|
||||
"[Chat] related messages: chat_id={}, message_id={}, messages:{:?}",
|
||||
self.chat_id,
|
||||
message_id,
|
||||
resp.items
|
||||
@ -275,20 +390,19 @@ impl Chat {
|
||||
|
||||
#[instrument(level = "debug", skip_all, err)]
|
||||
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 resp = self
|
||||
let answer = self
|
||||
.cloud_service
|
||||
.generate_answer(&workspace_id, &self.chat_id, question_message_id)
|
||||
.await?;
|
||||
|
||||
save_answer(
|
||||
self.user_service.sqlite_connection(self.uid)?,
|
||||
&self.chat_id,
|
||||
resp.clone(),
|
||||
question_message_id,
|
||||
)?;
|
||||
|
||||
let pb = ChatMessagePB::from(resp);
|
||||
Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
|
||||
let pb = ChatMessagePB::from(answer);
|
||||
Ok(pb)
|
||||
}
|
||||
|
||||
@ -314,7 +428,6 @@ impl Chat {
|
||||
created_at: record.created_at,
|
||||
author_type: record.author_type,
|
||||
author_id: record.author_id,
|
||||
has_following: false,
|
||||
reply_message_id: record.reply_message_id,
|
||||
})
|
||||
.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(
|
||||
conn: DBConnection,
|
||||
chat_id: &str,
|
||||
@ -451,21 +456,3 @@ fn save_chat_message(
|
||||
insert_chat_messages(conn, &records)?;
|
||||
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(())
|
||||
}
|
||||
|
@ -19,6 +19,30 @@ pub struct SendChatPayloadPB {
|
||||
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)]
|
||||
pub enum ChatMessageTypePB {
|
||||
#[default]
|
||||
@ -97,10 +121,7 @@ pub struct ChatMessagePB {
|
||||
#[pb(index = 5)]
|
||||
pub author_id: String,
|
||||
|
||||
#[pb(index = 6)]
|
||||
pub has_following: bool,
|
||||
|
||||
#[pb(index = 7, one_of)]
|
||||
#[pb(index = 6, one_of)]
|
||||
pub reply_message_id: Option<i64>,
|
||||
}
|
||||
|
||||
@ -110,9 +131,6 @@ pub struct ChatMessageErrorPB {
|
||||
pub chat_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub content: String,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
@ -124,7 +142,6 @@ impl From<ChatMessage> for ChatMessagePB {
|
||||
created_at: chat_message.created_at.timestamp(),
|
||||
author_type: chat_message.author.author_type as i64,
|
||||
author_id: chat_message.author.author_id.to_string(),
|
||||
has_following: false,
|
||||
reply_message_id: None,
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,10 @@ fn upgrade_chat_manager(
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
pub(crate) async fn send_chat_message_handler(
|
||||
data: AFPluginData<SendChatPayloadPB>,
|
||||
pub(crate) async fn stream_chat_message_handler(
|
||||
data: AFPluginData<StreamChatPayloadPB>,
|
||||
chat_manager: AFPluginState<Weak<ChatManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
) -> DataResult<ChatMessagePB, FlowyError> {
|
||||
let chat_manager = upgrade_chat_manager(chat_manager)?;
|
||||
let data = data.into_inner();
|
||||
data.validate()?;
|
||||
@ -30,10 +30,16 @@ pub(crate) async fn send_chat_message_handler(
|
||||
ChatMessageTypePB::System => ChatMessageType::System,
|
||||
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?;
|
||||
Ok(())
|
||||
data_result_ok(question)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip_all, err)]
|
||||
@ -91,3 +97,16 @@ pub(crate) async fn get_answer_handler(
|
||||
.await?;
|
||||
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(())
|
||||
}
|
||||
|
@ -12,11 +12,12 @@ pub fn init(chat_manager: Weak<ChatManager>) -> AFPlugin {
|
||||
AFPlugin::new()
|
||||
.name("Flowy-Chat")
|
||||
.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::LoadNextMessage, load_next_message_handler)
|
||||
.event(ChatEvent::GetRelatedQuestion, get_related_question_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)]
|
||||
@ -29,12 +30,15 @@ pub enum ChatEvent {
|
||||
#[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")]
|
||||
LoadNextMessage = 1,
|
||||
|
||||
#[event(input = "SendChatPayloadPB")]
|
||||
SendMessage = 2,
|
||||
#[event(input = "StreamChatPayloadPB", output = "ChatMessagePB")]
|
||||
StreamMessage = 2,
|
||||
|
||||
#[event(input = "StopStreamPB")]
|
||||
StopStream = 3,
|
||||
|
||||
#[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")]
|
||||
GetRelatedQuestion = 3,
|
||||
GetRelatedQuestion = 4,
|
||||
|
||||
#[event(input = "ChatMessageIdPB", output = "ChatMessagePB")]
|
||||
GetAnswerForQuestion = 4,
|
||||
GetAnswerForQuestion = 5,
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_sqlite::DBConnection;
|
||||
use lib_infra::util::timestamp;
|
||||
use std::sync::Arc;
|
||||
use tracing::{instrument, trace};
|
||||
use tracing::trace;
|
||||
|
||||
pub trait ChatUserService: Send + Sync + 'static {
|
||||
fn user_id(&self) -> Result<i64, FlowyError>;
|
||||
@ -79,16 +79,18 @@ impl ChatManager {
|
||||
Ok(chat)
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all, err)]
|
||||
pub async fn send_chat_message(
|
||||
pub async fn stream_chat_message(
|
||||
&self,
|
||||
chat_id: &str,
|
||||
message: &str,
|
||||
message_type: ChatMessageType,
|
||||
) -> Result<(), FlowyError> {
|
||||
text_stream_port: i64,
|
||||
) -> Result<ChatMessagePB, FlowyError> {
|
||||
let chat = self.get_or_create_chat_instance(chat_id).await?;
|
||||
chat.send_chat_message(message, message_type).await?;
|
||||
Ok(())
|
||||
let question = chat
|
||||
.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> {
|
||||
@ -168,6 +170,12 @@ impl ChatManager {
|
||||
let resp = chat.generate_answer(question_message_id).await?;
|
||||
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<()> {
|
||||
|
@ -11,8 +11,7 @@ pub enum ChatNotification {
|
||||
DidLoadPrevChatMessage = 2,
|
||||
DidReceiveChatMessage = 3,
|
||||
StreamChatMessageError = 4,
|
||||
FinishAnswerQuestion = 5,
|
||||
LastUserSentMessage = 6,
|
||||
FinishStreaming = 5,
|
||||
}
|
||||
|
||||
impl std::convert::From<ChatNotification> for i32 {
|
||||
@ -27,8 +26,7 @@ impl std::convert::From<i32> for ChatNotification {
|
||||
2 => ChatNotification::DidLoadPrevChatMessage,
|
||||
3 => ChatNotification::DidReceiveChatMessage,
|
||||
4 => ChatNotification::StreamChatMessageError,
|
||||
5 => ChatNotification::FinishAnswerQuestion,
|
||||
6 => ChatNotification::LastUserSentMessage,
|
||||
5 => ChatNotification::FinishStreaming,
|
||||
_ => ChatNotification::Unknown,
|
||||
}
|
||||
}
|
||||
|
@ -45,41 +45,6 @@ pub fn insert_chat_messages(
|
||||
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(
|
||||
mut conn: DBConnection,
|
||||
chat_id_val: &str,
|
||||
|
@ -30,6 +30,7 @@ use tokio::sync::RwLock;
|
||||
use crate::integrate::server::ServerProvider;
|
||||
|
||||
pub struct FolderDepsResolver();
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl FolderDepsResolver {
|
||||
pub async fn resolve(
|
||||
authenticate_user: Weak<AuthenticateUser>,
|
||||
|
@ -18,6 +18,7 @@ use collab_integrate::collab_builder::{
|
||||
};
|
||||
use flowy_chat_pub::cloud::{
|
||||
ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage,
|
||||
StreamAnswer,
|
||||
};
|
||||
use flowy_database_pub::cloud::{
|
||||
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
|
||||
@ -476,6 +477,60 @@ impl ChatCloudService for ServerProvider {
|
||||
.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(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
@ -1,9 +1,12 @@
|
||||
use crate::af_cloud::AFServer;
|
||||
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
|
||||
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 futures_util::StreamExt;
|
||||
use lib_infra::async_trait::async_trait;
|
||||
@ -50,22 +53,81 @@ where
|
||||
message: &str,
|
||||
message_type: ChatMessageType,
|
||||
) -> 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 params = CreateChatMessageParams {
|
||||
content: message,
|
||||
content: message.to_string(),
|
||||
message_type,
|
||||
};
|
||||
let stream = try_get_client?
|
||||
.create_chat_message(&workspace_id, &chat_id, params)
|
||||
.create_chat_qa_message(workspace_id, chat_id, params)
|
||||
.await
|
||||
.map_err(FlowyError::from)?;
|
||||
|
||||
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(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
@ -119,7 +181,7 @@ where
|
||||
|
||||
FutureResult::new(async move {
|
||||
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
|
||||
.map_err(FlowyError::from)?;
|
||||
Ok(resp)
|
||||
|
@ -1,6 +1,6 @@
|
||||
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
|
||||
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 lib_infra::async_trait::async_trait;
|
||||
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."))
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
_workspace_id: &str,
|
||||
|
@ -8,7 +8,7 @@ edition = "2018"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, default-features = false, features = ["clock"] }
|
||||
chrono = { workspace = true, default-features = false, features = ["clock"] }
|
||||
bytes = { version = "1.5" }
|
||||
pin-project = "1.1.3"
|
||||
futures-core = { version = "0.3" }
|
||||
@ -21,6 +21,8 @@ tempfile = "3.8.1"
|
||||
validator = "0.16.0"
|
||||
tracing.workspace = true
|
||||
atomic_refcell = "0.1"
|
||||
allo-isolate = { version = "^0.1", features = ["catch-unwind"], optional = true }
|
||||
futures = "0.3.30"
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5"
|
||||
@ -28,7 +30,8 @@ futures = "0.3.30"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
zip = { version = "0.6.6", features = ["deflate"] }
|
||||
brotli = { version = "3.4.0", optional = true }
|
||||
brotli = { version = "3.4.0", optional = true }
|
||||
|
||||
[features]
|
||||
compression = ["brotli"]
|
||||
compression = ["brotli"]
|
||||
isolate_flutter = ["allo-isolate"]
|
||||
|
44
frontend/rust-lib/lib-infra/src/isolate_stream.rs
Normal file
44
frontend/rust-lib/lib-infra/src/isolate_stream.rs
Normal 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(()))
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ if_wasm! {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "isolate_flutter")]
|
||||
pub mod isolate_stream;
|
||||
pub mod priority_task;
|
||||
pub mod ref_map;
|
||||
pub mod util;
|
||||
|
Loading…
Reference in New Issue
Block a user