feat: AI chat (#5383)

* chore: ai type

* chore: use patch to fix version issue

* chore: update

* chore: update

* chore: integrate client api

* chore: add schema

* chore: setup event

* chore: add event test

* chore: add test

* chore: update test

* chore: load chat message

* chore: load chat message

* chore: chat ui

* chore: disable create chat

* chore: update client api

* chore: disable chat

* chore: ui theme

* chore: ui theme

* chore: copy message

* chore: fix test

* chore: show error

* chore: update bloc

* chore: update test

* chore: lint

* chore: icon

* chore: hover

* chore: show unsupported page

* chore: adjust mobile ui

* chore: adjust view title bar

* chore: return related question

* chore: error page

* chore: error page

* chore: code format

* chore: prompt

* chore: fix test

* chore: ui adjust

* chore: disable create chat

* chore: add loading page

* chore: fix test

* chore: disable chat action

* chore: add maximum text limit
This commit is contained in:
Nathan.fooo 2024-06-03 14:27:28 +08:00 committed by GitHub
parent 4d42c9ea68
commit aec7bc847e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
114 changed files with 5473 additions and 282 deletions

View File

@ -89,6 +89,7 @@ jobs:
with:
os: ${{ matrix.os }}
flutter_version: ${{ env.FLUTTER_VERSION }}
DISABLE_CI_TEST_LOG: "true"
rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
@ -202,6 +203,7 @@ jobs:
- name: Run Flutter unit tests
env:
DISABLE_EVENT_LOG: true
DISABLE_CI_TEST_LOG: "true"
working-directory: frontend
run: |
if [ "$RUNNER_OS" == "macOS" ]; then

View File

@ -18,6 +18,9 @@ void main() {
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,
@ -46,6 +49,9 @@ void main() {
// create document, board, grid and calendar views
for (final value in ViewLayoutPB.values) {
if (value == ViewLayoutPB.Chat) {
continue;
}
await tester.createNewPageWithNameUnderParent(
name: value.name,
parentName: gettingStarted,

View File

@ -39,6 +39,9 @@ void main() {
await tester.tapAnonymousSignInButton();
for (final layout in ViewLayoutPB.values) {
if (layout == ViewLayoutPB.Chat) {
continue;
}
// create a new page
final name = 'AppFlowy_$layout';
await tester.createNewPageWithNameUnderParent(
@ -66,6 +69,8 @@ void main() {
case ViewLayoutPB.Calendar:
expect(find.byType(CalendarPage), findsOneWidget);
break;
case ViewLayoutPB.Chat:
break;
}
await tester.openPage(gettingStarted);

View File

@ -200,7 +200,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -227,4 +227,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2
COCOAPODS: 1.11.3

View File

@ -1,15 +1,17 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
extension MobileRouter on BuildContext {
@ -37,6 +39,9 @@ extension on ViewPB {
return MobileCalendarScreen.routeName;
case ViewLayoutPB.Board:
return MobileBoardScreen.routeName;
case ViewLayoutPB.Chat:
return MobileChatScreen.routeName;
default:
throw UnimplementedError('routeName for $this is not implemented');
}
@ -65,6 +70,11 @@ extension on ViewPB {
MobileBoardScreen.viewId: id,
MobileBoardScreen.viewTitle: name,
};
case ViewLayoutPB.Chat:
return {
MobileChatScreen.viewId: id,
MobileChatScreen.viewTitle: name,
};
default:
throw UnimplementedError(
'queryParameters for $this is not implemented',

View File

@ -8,6 +8,7 @@ import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/plugins/shared/sync_indicator.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@ -154,7 +155,10 @@ class _MobileViewPageState extends State<MobileViewPage> {
(view) {
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
return plugin.widgetBuilder.buildWidget(
shrinkWrap: false,
context: PluginContext(userProfile: state.userProfilePB),
);
},
(error) {
return FlowyMobileStateContainer.error(

View File

@ -0,0 +1,28 @@
import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
class MobileChatScreen extends StatelessWidget {
const MobileChatScreen({
super.key,
required this.id,
this.title,
});
/// view id
final String id;
final String? title;
static const routeName = '/chat';
static const viewId = 'id';
static const viewTitle = 'title';
@override
Widget build(BuildContext context) {
return MobileViewPage(
id: id,
title: title,
viewLayout: ViewLayoutPB.Chat,
);
}
}

View File

@ -0,0 +1,42 @@
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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,
}) : super(ChatAIMessageState.initial(message)) {
on<ChatAIMessageEvent>(
(event, emit) async {
await event.when(
initial: () async {},
update: (userProfile, deviceId, states) {},
);
},
);
}
}
@freezed
class ChatAIMessageEvent with _$ChatAIMessageEvent {
const factory ChatAIMessageEvent.initial() = Initial;
const factory ChatAIMessageEvent.update(
UserProfilePB userProfile,
String deviceId,
DocumentAwarenessStatesPB states,
) = Update;
}
@freezed
class ChatAIMessageState with _$ChatAIMessageState {
const factory ChatAIMessageState({
required Message message,
}) = _ChatAIMessageState;
factory ChatAIMessageState.initial(Message message) =>
ChatAIMessageState(message: message);
}

View File

@ -0,0 +1,423 @@
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-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:collection/collection.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';
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> {
ChatBloc({
required ViewPB view,
required UserProfilePB userProfile,
}) : listener = ChatMessageListener(chatId: view.id),
chatId = view.id,
super(
ChatState.initial(view, userProfile),
) {
_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;
@override
Future<void> close() {
listener.stop();
return super.close();
}
void _dispatch() {
on<ChatEvent>(
(event, emit) async {
await event.when(
initialLoad: () {
final payload = LoadNextChatMessagePB(
chatId: state.view.id,
limit: Int64(10),
);
ChatEventLoadNextMessage(payload).send();
},
startLoadingPrevMessage: () async {
Int64? beforeMessageId;
if (state.messages.isNotEmpty) {
beforeMessageId = Int64.parseInt(state.messages.last.id);
}
_loadPrevMessage(beforeMessageId);
emit(
state.copyWith(
loadingPreviousStatus: const LoadingState.loading(),
),
);
},
didLoadPreviousMessages: (List<Message> messages, bool hasMore) {
Log.debug("did load previous messages: ${messages.length}");
final uniqueMessages = {...state.messages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id));
emit(
state.copyWith(
messages: uniqueMessages,
loadingPreviousStatus: const LoadingState.finish(),
hasMorePrevMessage: hasMore,
),
);
},
didLoadLatestMessages: (List<Message> messages) {
final uniqueMessages = {...state.messages, ...messages}.toList()
..sort((a, b) => b.id.compareTo(a.id));
emit(
state.copyWith(
messages: uniqueMessages,
initialLoadingStatus: const LoadingState.finish(),
),
);
},
streaming: (List<Message> messages) {
final allMessages = _perminentMessages();
allMessages.insertAll(0, messages);
emit(state.copyWith(messages: allMessages));
},
didFinishStreaming: () {
emit(
state.copyWith(
answerQuestionStatus: const LoadingState.finish(),
),
);
},
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);
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(
metadata: OnetimeShotType.relatedQuestion.toMap(),
author: const User(id: "system"),
id: 'system',
);
allMessages.insert(0, message);
emit(
state.copyWith(
messages: allMessages,
relatedQuestions: questions,
),
);
},
clearReleatedQuestion: () {
emit(
state.copyWith(
relatedQuestions: [],
),
);
},
didSentUserMessage: (ChatMessagePB message) {
emit(
state.copyWith(
lastSentMessage: message,
),
);
},
);
},
);
}
// Returns the list of messages that are not include one-time messages.
List<Message> _perminentMessages() {
final allMessages = state.messages.where((element) {
return !(element.metadata?.containsKey(onetimeShotType) == true);
}).toList();
return allMessages;
}
void _loadPrevMessage(Int64? beforeMessageId) {
final payload = LoadPrevChatMessagePB(
chatId: state.view.id,
limit: Int64(10),
beforeMessageId: beforeMessageId,
);
ChatEventLoadPrevMessage(payload).send();
}
Future<void> _handleSentMessage(
String message,
Emitter<ChatState> emit,
) async {
final payload = SendChatPayloadPB(
chatId: state.view.id,
message: message,
messageType: ChatMessageTypePB.User,
);
final result = await ChatEventSendMessage(payload).send();
result.fold(
(_) {},
(err) {
if (!isClosed) {
Log.error("Failed to send message: ${err.msg}");
final metadata = OnetimeShotType.invalidSendMesssage.toMap();
metadata[sendMessageErrorKey] = err.msg;
final error = CustomMessage(
metadata: metadata,
author: const User(id: "system"),
id: 'system',
);
add(ChatEvent.streaming([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 _loadingMessage(String id) {
return CustomMessage(
author: User(id: id),
metadata: OnetimeShotType.loading.toMap(),
// fake id
id: nanoid(),
);
}
Message _createChatMessage(ChatMessagePB message) {
final messageId = message.messageId.toString();
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
class ChatEvent with _$ChatEvent {
const factory ChatEvent.initialLoad() = _InitialLoadMessage;
const factory ChatEvent.sendMessage(String message) = _SendMessage;
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
const factory ChatEvent.didLoadPreviousMessages(
List<Message> messages,
bool hasMore,
) = _DidLoadPreviousMessages;
const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =
_DidLoadMessages;
const factory ChatEvent.streaming(List<Message> messages) = _DidStreamMessage;
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;
}
@freezed
class ChatState with _$ChatState {
const factory ChatState({
required ViewPB view,
required List<Message> messages,
required UserProfilePB userProfile,
// When opening the chat, the initial loading status will be set as loading.
//After the initial loading is done, the status will be set as finished.
required LoadingState initialLoadingStatus,
// When loading previous messages, the status will be set as loading.
// After the loading is done, the status will be set as finished.
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,
// 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,
}) = _ChatState;
factory ChatState.initial(ViewPB view, UserProfilePB userProfile) =>
ChatState(
view: view,
messages: [],
userProfile: userProfile,
initialLoadingStatus: const LoadingState.finish(),
loadingPreviousStatus: const LoadingState.finish(),
answerQuestionStatus: const LoadingState.finish(),
hasMorePrevMessage: true,
relatedQuestions: [],
);
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
}
enum OnetimeShotType {
unknown,
loading,
serverStreamError,
relatedQuestion,
invalidSendMesssage
}
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':
return OnetimeShotType.invalidSendMesssage;
default:
Log.error('Unknown OnetimeShotType: $value');
return OnetimeShotType.unknown;
}
}
Map<String, String> toMap() {
return {
onetimeShotType: toString(),
};
}
}
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
if (metadata == null) {
return null;
}
for (final entry in metadata.entries) {
if (entry.key == onetimeShotType) {
return OnetimeMessageTypeExtension.fromString(entry.value as String);
}
}
return null;
}

View File

@ -0,0 +1,87 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'chat_notification.dart';
typedef ChatMessageCallback = void Function(ChatMessagePB message);
typedef ChatErrorMessageCallback = void Function(ChatMessageErrorPB message);
typedef LatestMessageCallback = void Function(ChatMessageListPB list);
typedef PrevMessageCallback = void Function(ChatMessageListPB list);
class ChatMessageListener {
ChatMessageListener({required this.chatId}) {
_parser = ChatNotificationParser(id: chatId, callback: _callback);
_subscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
final String chatId;
StreamSubscription<SubscribeObject>? _subscription;
ChatNotificationParser? _parser;
ChatMessageCallback? chatMessageCallback;
ChatMessageCallback? lastUserSentMessageCallback;
ChatErrorMessageCallback? chatErrorMessageCallback;
LatestMessageCallback? latestMessageCallback;
PrevMessageCallback? prevMessageCallback;
void Function()? finishAnswerQuestionCallback;
void start({
ChatMessageCallback? chatMessageCallback,
ChatErrorMessageCallback? chatErrorMessageCallback,
LatestMessageCallback? latestMessageCallback,
PrevMessageCallback? prevMessageCallback,
ChatMessageCallback? lastUserSentMessageCallback,
void Function()? finishAnswerQuestionCallback,
}) {
this.chatMessageCallback = chatMessageCallback;
this.chatErrorMessageCallback = chatErrorMessageCallback;
this.latestMessageCallback = latestMessageCallback;
this.prevMessageCallback = prevMessageCallback;
this.lastUserSentMessageCallback = lastUserSentMessageCallback;
this.finishAnswerQuestionCallback = finishAnswerQuestionCallback;
}
void _callback(
ChatNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
result.map((r) {
switch (ty) {
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;
case ChatNotification.DidLoadLatestChatMessage:
latestMessageCallback?.call(ChatMessageListPB.fromBuffer(r));
break;
case ChatNotification.DidLoadPrevChatMessage:
prevMessageCallback?.call(ChatMessageListPB.fromBuffer(r));
break;
case ChatNotification.FinishAnswerQuestion:
finishAnswerQuestionCallback?.call();
break;
default:
break;
}
});
}
Future<void> stop() async {
await _subscription?.cancel();
_subscription = null;
}
}

View File

@ -0,0 +1,45 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/core/notification/notification_helper.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
class ChatNotificationParser
extends NotificationParser<ChatNotification, FlowyError> {
ChatNotificationParser({
super.id,
required super.callback,
}) : super(
tyParser: (ty, source) =>
source == "Chat" ? ChatNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
typedef ChatNotificationHandler = Function(
ChatNotification ty,
FlowyResult<Uint8List, FlowyError> result,
);
class ChatNotificationListener {
ChatNotificationListener({
required String objectId,
required ChatNotificationHandler handler,
}) : _parser = ChatNotificationParser(id: objectId, callback: handler) {
_subscription =
RustStreamReceiver.listen((observable) => _parser?.parse(observable));
}
ChatNotificationParser? _parser;
StreamSubscription<SubscribeObject>? _subscription;
Future<void> stop() async {
_parser = null;
await _subscription?.cancel();
_subscription = null;
}
}

View File

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

View File

@ -0,0 +1,44 @@
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.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_user_message_bloc.freezed.dart';
class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({
required Message message,
}) : super(ChatUserMessageState.initial(message)) {
on<ChatUserMessageEvent>(
(event, emit) async {
await event.when(
initial: () async {},
update: (userProfile, deviceId, states) {},
);
},
);
}
}
@freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.update(
UserProfilePB userProfile,
String deviceId,
DocumentAwarenessStatesPB states,
) = Update;
}
@freezed
class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({
required Message message,
WorkspaceMemberPB? member,
}) = _ChatUserMessageState;
factory ChatUserMessageState.initial(Message message) =>
ChatUserMessageState(message: message);
}

View File

@ -0,0 +1,114 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
import 'package:appflowy/plugins/util.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIChatPluginBuilder extends PluginBuilder {
@override
Plugin build(dynamic data) {
if (data is ViewPB) {
return AIChatPagePlugin(view: data);
}
throw FlowyPluginException.invalidData;
}
@override
String get menuName => "AIChat";
@override
FlowySvgData get icon => FlowySvgs.chat_ai_page_s;
@override
PluginType get pluginType => PluginType.chat;
@override
ViewLayoutPB get layoutType => ViewLayoutPB.Chat;
}
class AIChatPluginConfig implements PluginConfig {
@override
bool get creatable => false;
}
class AIChatPagePlugin extends Plugin {
AIChatPagePlugin({
required ViewPB view,
}) : notifier = ViewPluginNotifier(view: view);
late final ViewInfoBloc _viewInfoBloc;
@override
final ViewPluginNotifier notifier;
@override
PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(
bloc: _viewInfoBloc,
notifier: notifier,
);
@override
PluginId get id => notifier.view.id;
@override
PluginType get pluginType => PluginType.chat;
@override
void init() {
_viewInfoBloc = ViewInfoBloc(view: notifier.view)
..add(const ViewInfoEvent.started());
}
}
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
with NavigationItem {
AIChatPagePluginWidgetBuilder({
required this.bloc,
required this.notifier,
});
final ViewInfoBloc bloc;
final ViewPluginNotifier notifier;
int? deletedViewIndex;
@override
Widget get leftBarItem =>
ViewTitleBar(key: ValueKey(notifier.view.id), view: notifier.view);
@override
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
@override
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) {
notifier.isDeleted.addListener(() {
final deletedView = notifier.isDeleted.value;
if (deletedView != null && deletedView.hasIndex()) {
deletedViewIndex = deletedView.index;
}
});
return BlocProvider<ViewInfoBloc>.value(
value: bloc,
child: AIChatPage(
userProfile: context.userProfile!,
key: ValueKey(notifier.view.id),
view: notifier.view,
onDeleted: () =>
context.onDeleted?.call(notifier.view, deletedViewIndex),
),
);
}
@override
List<NavigationItem> get navigationItems => [this];
}

View File

@ -0,0 +1,332 @@
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/chat_related_question.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_user_message.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';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.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/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
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';
class AIChatPage extends StatefulWidget {
const AIChatPage({
super.key,
required this.view,
required this.onDeleted,
required this.userProfile,
});
final ViewPB view;
final VoidCallback onDeleted;
final UserProfilePB userProfile;
@override
State<AIChatPage> createState() => _AIChatPageState();
}
class _AIChatPageState extends State<AIChatPage> {
late types.User _user;
@override
void initState() {
super.initState();
_user = types.User(id: widget.userProfile.id.toString());
}
@override
Widget build(BuildContext context) {
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
return buildChatWidget();
} else {
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
}
Widget buildChatWidget() {
return SizedBox.expand(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 60),
child: BlocProvider(
create: (context) => ChatBloc(
view: widget.view,
userProfile: widget.userProfile,
)..add(const ChatEvent.initialLoad()),
child: BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) {
return Chat(
messages: state.messages,
onAttachmentPressed: () {},
onSendPressed: (types.PartialText message) {
// We use custom bottom widget for chat input, so
// do not need to handle this event.
},
customBottomWidget: buildChatInput(blocContext),
user: _user,
theme: buildTheme(context),
customMessageBuilder: _customMessageBuilder,
onEndReached: () async {
if (state.hasMorePrevMessage &&
state.loadingPreviousStatus !=
const LoadingState.loading()) {
blocContext
.read<ChatBloc>()
.add(const ChatEvent.startLoadingPrevMessage());
}
},
emptyState: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.initialLoadingStatus ==
const LoadingState.finish()
? const ChatWelcomePage()
: const Center(
child: CircularProgressIndicator.adaptive(),
);
},
),
messageWidthRatio: isMobile ? 0.8 : 0.86,
bubbleBuilder: (
child, {
required message,
required nextMessageInGroup,
}) {
if (message.author.id == _user.id) {
return ChatUserMessageBubble(
message: message,
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,
);
}
},
);
},
),
),
),
);
}
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),
child: child,
)
: Padding(
padding: const EdgeInsets.all(8),
child: child,
);
// If the message is from the author, we will decorate it with a different color
final decoratedChild = isAuthor
? DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
color: !isAuthor || message.type == types.MessageType.image
? AFThemeExtension.of(context).tint1
: Theme.of(context).colorScheme.secondary,
),
child: childWithPadding,
)
: childWithPadding;
// If the message is from the author, no further actions are needed
if (isAuthor) {
return ClipRRect(
borderRadius: borderRadius,
child: decoratedChild,
);
} else {
if (isMobile) {
return ChatPopupMenu(
onAction: (action) {
switch (action) {
case ChatMessageAction.copy:
if (message is TextMessage) {
Clipboard.setData(ClipboardData(text: message.text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
break;
}
},
builder: (context) =>
ClipRRect(borderRadius: borderRadius, child: decoratedChild),
);
} else {
// Show hover effect only on desktop
return ClipRRect(
borderRadius: borderRadius,
child: ChatAIMessageHover(
message: message,
child: decoratedChild,
),
);
}
}
}
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) {
final query = MediaQuery.of(context);
final safeAreaInsets = isMobile
? EdgeInsets.fromLTRB(
query.padding.left,
0,
query.padding.right,
query.viewInsets.bottom + query.padding.bottom,
)
: EdgeInsets.zero;
return Column(
children: [
ClipRect(
child: Padding(
padding: safeAreaInsets,
child: ChatInput(
chatId: widget.view.id,
onSendPressed: (message) => onSendPressed(context, message.text),
),
),
),
const VSpace(6),
Opacity(
opacity: 0.6,
child: FlowyText(
LocaleKeys.chat_aiMistakePrompt.tr(),
fontSize: 12,
),
),
],
);
}
AFDefaultChatTheme buildTheme(BuildContext context) {
return AFDefaultChatTheme(
backgroundColor: AFThemeExtension.of(context).background,
primaryColor: Theme.of(context).colorScheme.primary,
secondaryColor: AFThemeExtension.of(context).tint1,
receivedMessageDocumentIconColor: Theme.of(context).primaryColor,
receivedMessageCaptionTextStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
receivedMessageBodyTextStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
receivedMessageLinkTitleTextStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
receivedMessageBodyLinkTextStyle: const TextStyle(
color: Colors.lightBlue,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
sentMessageBodyTextStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
sentMessageBodyLinkTextStyle: const TextStyle(
color: Colors.blue,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
inputElevation: 2,
);
}
void onSendPressed(BuildContext context, String message) {
context.read<ChatBloc>().add(ChatEvent.sendMessage(message));
}
}

View File

@ -0,0 +1,197 @@
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';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
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';
const _leftPadding = 16.0;
class ChatAIMessageBubble extends StatelessWidget {
const ChatAIMessageBubble({
super.key,
required this.message,
required this.child,
this.customMessageType,
});
final Message message;
final Widget child;
final OnetimeShotType? customMessageType;
@override
Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(horizontal: _leftPadding);
final childWithPadding = Padding(padding: padding, child: child);
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: [
ChatBorderedCircleAvatar(
backgroundColor: Theme.of(context).colorScheme.secondary,
child: const FlowySvg(
FlowySvgs.flowy_ai_chat_logo_s,
size: Size.square(24),
),
),
Expanded(child: widget),
],
);
},
),
);
}
ChatAIMessageHover _wrapHover(Padding child) {
return ChatAIMessageHover(
message: message,
customMessageType: customMessageType,
child: child,
);
}
ChatPopupMenu _wrapPopMenu(Padding childWithPadding) {
return ChatPopupMenu(
onAction: (action) {
if (action == ChatMessageAction.copy && message is TextMessage) {
Clipboard.setData(ClipboardData(text: (message as TextMessage).text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
}
},
builder: (context) => childWithPadding,
);
}
}
class ChatAIMessageHover extends StatefulWidget {
const ChatAIMessageHover({
super.key,
required this.child,
required this.message,
this.customMessageType,
});
final Widget child;
final Message message;
final bool autoShowHover = true;
final OnetimeShotType? customMessageType;
@override
State<ChatAIMessageHover> createState() => _ChatAIMessageHoverState();
}
class _ChatAIMessageHoverState extends State<ChatAIMessageHover> {
bool _isHover = false;
@override
void initState() {
super.initState();
_isHover = widget.autoShowHover ? false : true;
}
@override
Widget build(BuildContext context) {
final List<Widget> children = [
DecoratedBox(
decoration: const BoxDecoration(
color: Colors.transparent,
borderRadius: Corners.s6Border,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 40),
child: widget.child,
),
),
];
if (_isHover) {
children.addAll(_buildOnHoverItems());
}
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = true;
}
}),
onExit: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = false;
}
}),
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: children,
),
);
}
List<Widget> _buildOnHoverItems() {
final List<Widget> children = [];
if (widget.customMessageType != null) {
//
} else {
if (widget.message is TextMessage) {
children.add(
CopyButton(
textMessage: widget.message as TextMessage,
).positioned(left: _leftPadding, bottom: 0),
);
}
}
return children;
}
}
class CopyButton extends StatelessWidget {
const CopyButton({
super.key,
required this.textMessage,
});
final TextMessage textMessage;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: FlowyIconButton(
width: 24,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
size: const Size.square(14),
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: textMessage.text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
},
),
);
}
}

View File

@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/util/color_generator/color_generator.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:string_validator/string_validator.dart';
class ChatChatUserAvatar extends StatelessWidget {
const ChatChatUserAvatar({required this.userId, super.key});
final String userId;
@override
Widget build(BuildContext context) {
return const ChatBorderedCircleAvatar();
}
}
class ChatBorderedCircleAvatar extends StatelessWidget {
const ChatBorderedCircleAvatar({
super.key,
this.border = const BorderSide(),
this.backgroundImage,
this.backgroundColor,
this.child,
});
final BorderSide border;
final ImageProvider<Object>? backgroundImage;
final Color? backgroundColor;
final Widget? child;
@override
Widget build(BuildContext context) {
return CircleAvatar(
backgroundColor: border.color,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: CircleAvatar(
backgroundImage: backgroundImage,
backgroundColor: backgroundColor,
child: child,
),
),
);
}
}
class ChatUserAvatar extends StatelessWidget {
const ChatUserAvatar({
super.key,
required this.iconUrl,
required this.name,
required this.size,
this.isHovering = false,
});
final String iconUrl;
final String name;
final double size;
// If true, a border will be applied on top of the avatar
final bool isHovering;
@override
Widget build(BuildContext context) {
if (iconUrl.isEmpty) {
return _buildEmptyAvatar(context);
} else if (isURL(iconUrl)) {
return _buildUrlAvatar(context);
} else {
return _buildEmojiAvatar(context);
}
}
Widget _buildEmptyAvatar(BuildContext context) {
final String nameOrDefault = _userName(name);
final Color color = ColorGenerator(name).toColor();
const initialsCount = 2;
// Taking the first letters of the name components and limiting to 2 elements
final nameInitials = nameOrDefault
.split(' ')
.where((element) => element.isNotEmpty)
.take(initialsCount)
.map((element) => element[0].toUpperCase())
.join();
return Container(
width: size,
height: size,
alignment: Alignment.center,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: isHovering
? Border.all(
color: _darken(color),
width: 4,
)
: null,
),
child: FlowyText.regular(
nameInitials,
color: Colors.black,
),
);
}
Widget _buildUrlAvatar(BuildContext context) {
return SizedBox.square(
dimension: size,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: isHovering
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
),
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: Image.network(
iconUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
_buildEmptyAvatar(context),
),
),
),
),
);
}
Widget _buildEmojiAvatar(BuildContext context) {
return SizedBox.square(
dimension: size,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: isHovering
? Border.all(
color: Theme.of(context).colorScheme.primary,
width: 4,
)
: null,
),
child: ClipRRect(
borderRadius: Corners.s5Border,
child: CircleAvatar(
backgroundColor: Colors.transparent,
child: builtInSVGIcons.contains(iconUrl)
? FlowySvg(
FlowySvgData('emoji/$iconUrl'),
blendMode: null,
)
: FlowyText.emoji(iconUrl),
),
),
),
);
}
/// Return the user name, if the user name is empty,
/// return the default user name.
///
String _userName(String name) =>
name.isEmpty ? LocaleKeys.defaultUsername.tr() : name;
/// Used to darken the generated color for the hover border effect.
/// The color is darkened by 15% - Hence the 0.15 value.
///
Color _darken(Color color) {
final hsl = HSLColor.fromColor(color);
return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor();
}
}

View File

@ -0,0 +1,257 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
class ChatInput extends StatefulWidget {
/// Creates [ChatInput] widget.
const ChatInput({
super.key,
this.isAttachmentUploading,
this.onAttachmentPressed,
required this.onSendPressed,
required this.chatId,
this.options = const InputOptions(),
});
final bool? isAttachmentUploading;
final VoidCallback? onAttachmentPressed;
final void Function(types.PartialText) onSendPressed;
final InputOptions options;
final String chatId;
@override
State<ChatInput> createState() => _ChatInputState();
}
/// [ChatInput] widget state.
class _ChatInputState extends State<ChatInput> {
late final _inputFocusNode = FocusNode(
onKeyEvent: (node, event) {
if (event.physicalKey == PhysicalKeyboardKey.enter &&
!HardwareKeyboard.instance.physicalKeysPressed.any(
(el) => <PhysicalKeyboardKey>{
PhysicalKeyboardKey.shiftLeft,
PhysicalKeyboardKey.shiftRight,
}.contains(el),
)) {
if (kIsWeb && _textController.value.isComposingRangeValid) {
return KeyEventResult.ignored;
}
if (event is KeyDownEvent) {
_handleSendPressed();
}
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
);
bool _sendButtonVisible = false;
late TextEditingController _textController;
@override
void initState() {
super.initState();
_textController =
widget.options.textEditingController ?? InputTextFieldController();
_handleSendButtonVisibilityModeChange();
}
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;
}
}
void _handleSendPressed() {
final trimmedText = _textController.text.trim();
if (trimmedText != '') {
final partialText = types.PartialText(text: trimmedText);
widget.onSendPressed(partialText);
if (widget.options.inputClearMode == InputClearMode.always) {
_textController.clear();
}
}
}
void _handleTextControllerChange() {
if (_textController.value.isComposingRangeValid) {
return;
}
setState(() {
_sendButtonVisible = _textController.text.trim() != '';
});
}
Widget _inputBuilder() {
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
const inputPadding = EdgeInsets.all(6);
return Focus(
autofocus: !widget.options.autofocus,
child: Padding(
padding: inputPadding,
child: Material(
borderRadius: BorderRadius.circular(12),
color: isMobile
? Theme.of(context).colorScheme.surfaceContainer
: Theme.of(context).colorScheme.surfaceContainerHighest,
elevation: 0.6,
child: Row(
children: [
if (widget.onAttachmentPressed != null)
AttachmentButton(
isLoading: widget.isAttachmentUploading ?? false,
onPressed: widget.onAttachmentPressed,
padding: buttonPadding,
),
Expanded(child: _inputTextField(textPadding)),
_sendButton(buttonPadding),
],
),
),
),
);
}
Padding _inputTextField(EdgeInsets textPadding) {
return Padding(
padding: textPadding,
child: TextField(
controller: _textController,
focusNode: _inputFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
hintText: LocaleKeys.chat_inputMessageHint.tr(),
hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
),
),
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
),
enabled: widget.options.enabled,
autocorrect: widget.options.autocorrect,
autofocus: widget.options.autofocus,
enableSuggestions: widget.options.enableSuggestions,
spellCheckConfiguration: const SpellCheckConfiguration(),
keyboardType: widget.options.keyboardType,
textCapitalization: TextCapitalization.sentences,
maxLines: 10,
minLines: 1,
onChanged: widget.options.onTextChanged,
onTap: widget.options.onTextFieldTap,
),
);
}
ConstrainedBox _sendButton(EdgeInsets buttonPadding) {
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: buttonPadding.bottom + buttonPadding.top + 24,
),
child: Visibility(
visible: _sendButtonVisible,
child: SendButton(
onPressed: _handleSendPressed,
padding: buttonPadding,
),
),
);
}
@override
void didUpdateWidget(covariant ChatInput oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.options.sendButtonVisibilityMode !=
oldWidget.options.sendButtonVisibilityMode) {
_handleSendButtonVisibilityModeChange();
}
}
@override
void dispose() {
_inputFocusNode.dispose();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () => _inputFocusNode.requestFocus(),
child: _inputBuilder(),
);
}
@immutable
class InputOptions {
const InputOptions({
this.inputClearMode = InputClearMode.always,
this.keyboardType = TextInputType.multiline,
this.onTextChanged,
this.onTextFieldTap,
this.sendButtonVisibilityMode = SendButtonVisibilityMode.editing,
this.textEditingController,
this.autocorrect = true,
this.autofocus = false,
this.enableSuggestions = true,
this.enabled = true,
});
/// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always].
final InputClearMode inputClearMode;
/// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline].
final TextInputType keyboardType;
/// Will be called whenever the text inside [TextField] changes.
final void Function(String)? onTextChanged;
/// 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
/// features but still need some methods from the default [TextEditingController],
/// you can create your own [InputTextFieldController] (imported from this lib)
/// and pass it here.
final TextEditingController? textEditingController;
/// Controls the [TextInput] autocorrect behavior. Defaults to [true].
final bool autocorrect;
/// Whether [TextInput] should have focus. Defaults to [false].
final bool autofocus;
/// Controls the [TextInput] enableSuggestions behavior. Defaults to [true].
final bool enableSuggestions;
/// Controls the [TextInput] enabled behavior. Defaults to [true].
final bool enabled;
}
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
defaultTargetPlatform == TargetPlatform.iOS;

View File

@ -0,0 +1,69 @@
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class ChatAILoading extends StatelessWidget {
const ChatAILoading({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: AFThemeExtension.of(context).lightGreyHover,
highlightColor:
AFThemeExtension.of(context).lightGreyHover.withOpacity(0.5),
period: const Duration(seconds: 3),
child: const ContentPlaceholder(),
);
}
}
class ContentPlaceholder extends StatelessWidget {
const ContentPlaceholder({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 30,
height: 16.0,
margin: const EdgeInsets.only(bottom: 8.0),
decoration: BoxDecoration(
color: AFThemeExtension.of(context).lightGreyHover,
borderRadius: BorderRadius.circular(4.0),
),
),
const HSpace(10),
Container(
width: 100,
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),
),
),
],
),
);
}
}

View File

@ -0,0 +1,70 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class ChatPopupMenu extends StatefulWidget {
const ChatPopupMenu({
super.key,
required this.onAction,
required this.builder,
});
final Function(ChatMessageAction) onAction;
final Widget Function(BuildContext context) builder;
@override
State<ChatPopupMenu> createState() => _ChatPopupMenuState();
}
class _ChatPopupMenuState extends State<ChatPopupMenu> {
@override
Widget build(BuildContext context) {
return PopoverActionList<ChatMessageActionWrapper>(
asBarrier: true,
actions: ChatMessageAction.values
.map((action) => ChatMessageActionWrapper(action))
.toList(),
buildChild: (controller) {
return GestureDetector(
onLongPress: () {
controller.show();
},
child: widget.builder(context),
);
},
onSelected: (action, controller) async {
widget.onAction(action.inner);
controller.close();
},
direction: PopoverDirection.bottomWithCenterAligned,
);
}
}
enum ChatMessageAction {
copy,
}
class ChatMessageActionWrapper extends ActionCell {
ChatMessageActionWrapper(this.inner);
final ChatMessageAction inner;
@override
Widget? leftIcon(Color iconColor) => null;
@override
String get name => inner.name;
}
extension ChatMessageActionExtension on ChatMessageAction {
String get name {
switch (this) {
case ChatMessageAction.copy:
return LocaleKeys.document_plugins_contextMenu_copy.tr();
}
}
}

View File

@ -0,0 +1,147 @@
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({
required this.chatId,
required this.onQuestionSelected,
required this.relatedQuestions,
super.key,
});
final String chatId;
final Function(String) onQuestionSelected;
final List<RelatedQuestionPB> relatedQuestions;
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: relatedQuestions.length,
itemBuilder: (context, index) {
final question = relatedQuestions[index];
if (index == 0) {
return Column(
children: [
const Divider(height: 36),
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
const FlowySvg(
FlowySvgs.ai_summary_generate_s,
size: Size.square(24),
),
const HSpace(6),
FlowyText(
LocaleKeys.chat_relatedQuestion.tr(),
fontSize: 18,
),
],
),
),
const Divider(height: 6),
RelatedQuestionItem(
question: question,
onQuestionSelected: onQuestionSelected,
),
],
);
} else {
return RelatedQuestionItem(
question: question,
onQuestionSelected: onQuestionSelected,
);
}
},
);
}
}
class RelatedQuestionItem extends StatefulWidget {
const RelatedQuestionItem({
required this.question,
required this.onQuestionSelected,
super.key,
});
final RelatedQuestionPB question;
final Function(String) onQuestionSelected;
@override
State<RelatedQuestionItem> createState() => _RelatedQuestionItemState();
}
class _RelatedQuestionItemState extends State<RelatedQuestionItem> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
),
title: Text(
widget.question.content,
style: TextStyle(
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
fontSize: 14,
),
),
onTap: () {
widget.onQuestionSelected(widget.question.content);
},
trailing: FlowySvg(
FlowySvgs.add_m,
color: Theme.of(context).colorScheme.primary,
),
),
);
}
}

View File

@ -0,0 +1,84 @@
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';
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;
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,
),
);
}
Padding _aiUnvaliable() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: FlowyText(
LocaleKeys.chat_aiServerUnavailable.tr(),
fontSize: 14,
),
);
}
}

View File

@ -0,0 +1,222 @@
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
// For internal usage only. Use values from theme itself.
/// See [ChatTheme.userAvatarNameColors].
const colors = [
Color(0xffff6767),
Color(0xff66e0da),
Color(0xfff5a2d9),
Color(0xfff0c722),
Color(0xff6a85e5),
Color(0xfffd9a6f),
Color(0xff92db6e),
Color(0xff73b8e5),
Color(0xfffd7590),
Color(0xffc78ae5),
];
/// Dark.
const dark = Color(0xff1f1c38);
/// Error.
const error = Color(0xffff6767);
/// N0.
const neutral0 = Color(0xff1d1c21);
/// N1.
const neutral1 = Color(0xff615e6e);
/// N2.
const neutral2 = Color(0xff9e9cab);
/// N7.
const neutral7 = Color(0xffffffff);
/// N7 with opacity.
const neutral7WithOpacity = Color(0x80ffffff);
/// Primary.
const primary = Color(0xff6f61e8);
/// Secondary.
const secondary = Color(0xfff5f5f7);
/// Secondary dark.
const secondaryDark = Color(0xff2b2250);
/// Default chat theme which extends [ChatTheme].
@immutable
class AFDefaultChatTheme extends ChatTheme {
/// Creates a default chat theme. Use this constructor if you want to
/// override only a couple of properties, otherwise create a new class
/// which extends [ChatTheme].
const AFDefaultChatTheme({
super.attachmentButtonIcon,
super.attachmentButtonMargin,
super.backgroundColor = neutral7,
super.bubbleMargin,
super.dateDividerMargin = const EdgeInsets.only(
bottom: 32,
top: 16,
),
super.dateDividerTextStyle = const TextStyle(
color: neutral2,
fontSize: 12,
fontWeight: FontWeight.w800,
height: 1.333,
),
super.deliveredIcon,
super.documentIcon,
super.emptyChatPlaceholderTextStyle = const TextStyle(
color: neutral2,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
super.errorColor = error,
super.errorIcon,
super.inputBackgroundColor = neutral0,
super.inputSurfaceTintColor = neutral0,
super.inputElevation = 0,
super.inputBorderRadius = const BorderRadius.vertical(
top: Radius.circular(20),
),
super.inputContainerDecoration,
super.inputMargin = EdgeInsets.zero,
super.inputPadding = const EdgeInsets.fromLTRB(14, 20, 14, 20),
super.inputTextColor = neutral7,
super.inputTextCursorColor,
super.inputTextDecoration = const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
isCollapsed: true,
),
super.inputTextStyle = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
super.messageBorderRadius = 20,
super.messageInsetsHorizontal = 0,
super.messageInsetsVertical = 0,
super.messageMaxWidth = 1000,
super.primaryColor = primary,
super.receivedEmojiMessageTextStyle = const TextStyle(fontSize: 40),
super.receivedMessageBodyBoldTextStyle,
super.receivedMessageBodyCodeTextStyle,
super.receivedMessageBodyLinkTextStyle,
super.receivedMessageBodyTextStyle = const TextStyle(
color: neutral0,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
super.receivedMessageCaptionTextStyle = const TextStyle(
color: neutral2,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.333,
),
super.receivedMessageDocumentIconColor = primary,
super.receivedMessageLinkDescriptionTextStyle = const TextStyle(
color: neutral0,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.428,
),
super.receivedMessageLinkTitleTextStyle = const TextStyle(
color: neutral0,
fontSize: 16,
fontWeight: FontWeight.w800,
height: 1.375,
),
super.secondaryColor = secondary,
super.seenIcon,
super.sendButtonIcon,
super.sendButtonMargin,
super.sendingIcon,
super.sentEmojiMessageTextStyle = const TextStyle(fontSize: 40),
super.sentMessageBodyBoldTextStyle,
super.sentMessageBodyCodeTextStyle,
super.sentMessageBodyLinkTextStyle,
super.sentMessageBodyTextStyle = const TextStyle(
color: neutral7,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
super.sentMessageCaptionTextStyle = const TextStyle(
color: neutral7WithOpacity,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.333,
),
super.sentMessageDocumentIconColor = neutral7,
super.sentMessageLinkDescriptionTextStyle = const TextStyle(
color: neutral7,
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.428,
),
super.sentMessageLinkTitleTextStyle = const TextStyle(
color: neutral7,
fontSize: 16,
fontWeight: FontWeight.w800,
height: 1.375,
),
super.statusIconPadding = const EdgeInsets.symmetric(horizontal: 4),
super.systemMessageTheme = const SystemMessageTheme(
margin: EdgeInsets.only(
bottom: 24,
top: 8,
left: 8,
right: 8,
),
textStyle: TextStyle(
color: neutral2,
fontSize: 12,
fontWeight: FontWeight.w800,
height: 1.333,
),
),
super.typingIndicatorTheme = const TypingIndicatorTheme(
animatedCirclesColor: neutral1,
animatedCircleSize: 5.0,
bubbleBorder: BorderRadius.all(Radius.circular(27.0)),
bubbleColor: neutral7,
countAvatarColor: primary,
countTextColor: secondary,
multipleUserTextStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: neutral2,
),
),
super.unreadHeaderTheme = const UnreadHeaderTheme(
color: secondary,
textStyle: TextStyle(
color: neutral2,
fontSize: 12,
fontWeight: FontWeight.w500,
height: 1.333,
),
),
super.userAvatarImageBackgroundColor = Colors.transparent,
super.userAvatarNameColors = colors,
super.userAvatarTextStyle = const TextStyle(
color: neutral7,
fontSize: 12,
fontWeight: FontWeight.w800,
height: 1.333,
),
super.userNameTextStyle = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
height: 1.333,
),
super.highlightMessageColor,
});
}

View File

@ -0,0 +1,31 @@
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.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 ChatInvalidUserMessage extends StatelessWidget {
const ChatInvalidUserMessage({
required this.message,
super.key,
});
final Message message;
@override
Widget build(BuildContext context) {
final errorMessage = message.metadata?[sendMessageErrorKey] ?? "";
return Center(
child: Column(
children: [
const Divider(height: 20, thickness: 1),
Padding(
padding: const EdgeInsets.all(8.0),
child: FlowyText(
errorMessage,
fontSize: 14,
),
),
],
),
);
}
}

View File

@ -0,0 +1,169 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
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_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:styled_widget/styled_widget.dart';
class ChatUserMessageBubble extends StatelessWidget {
const ChatUserMessageBubble({
super.key,
required this.message,
required this.child,
});
final Message message;
final Widget child;
@override
Widget build(BuildContext context) {
const borderRadius = BorderRadius.all(Radius.circular(6));
final backgroundColor = Theme.of(context).colorScheme.secondary;
return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message),
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// _wrapHover(
Flexible(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: borderRadius,
color: backgroundColor,
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: child,
),
),
),
// ),
BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChatUserAvatar(
iconUrl: state.member?.avatarUrl ?? "",
name: state.member?.name ?? "",
size: 36,
),
);
},
),
],
);
},
),
);
}
}
class ChatUserMessageHover extends StatefulWidget {
const ChatUserMessageHover({
super.key,
required this.child,
required this.message,
});
final Widget child;
final Message message;
final bool autoShowHover = true;
@override
State<ChatUserMessageHover> createState() => _ChatUserMessageHoverState();
}
class _ChatUserMessageHoverState extends State<ChatUserMessageHover> {
bool _isHover = false;
@override
void initState() {
super.initState();
_isHover = widget.autoShowHover ? false : true;
}
@override
Widget build(BuildContext context) {
final List<Widget> children = [
DecoratedBox(
decoration: const BoxDecoration(
color: Colors.transparent,
borderRadius: Corners.s6Border,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 30),
child: widget.child,
),
),
];
if (_isHover) {
if (widget.message is TextMessage) {
children.add(
EditButton(
textMessage: widget.message as TextMessage,
).positioned(right: 0, bottom: 0),
);
}
}
return MouseRegion(
cursor: SystemMouseCursors.click,
opaque: false,
onEnter: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = true;
}
}),
onExit: (p) => setState(() {
if (widget.autoShowHover) {
_isHover = false;
}
}),
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: children,
),
);
}
}
class EditButton extends StatelessWidget {
const EditButton({
super.key,
required this.textMessage,
});
final TextMessage textMessage;
@override
Widget build(BuildContext context) {
return FlowyTooltip(
message: LocaleKeys.settings_menu_clickToCopy.tr(),
child: FlowyIconButton(
width: 24,
hoverColor: AFThemeExtension.of(context).lightGreyHover,
fillColor: Theme.of(context).cardColor,
icon: FlowySvg(
FlowySvgs.ai_copy_s,
size: const Size.square(14),
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {},
),
);
}
}

View File

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

View File

@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@ -20,6 +21,9 @@ class BlankPluginBuilder extends PluginBuilder {
@override
PluginType get pluginType => PluginType.blank;
@override
ViewLayoutPB get layoutType => ViewLayoutPB.Document;
}
class BlankPluginConfig implements PluginConfig {
@ -47,7 +51,10 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
Widget tabBarItem(String pluginId) => leftBarItem;
@override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) =>
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) =>
const BlankPage();
@override

View File

@ -25,7 +25,7 @@ class BoardPluginBuilder implements PluginBuilder {
PluginType get pluginType => PluginType.board;
@override
ViewLayoutPB? get layoutType => ViewLayoutPB.Board;
ViewLayoutPB get layoutType => ViewLayoutPB.Board;
}
class BoardPluginConfig implements PluginConfig {

View File

@ -25,7 +25,7 @@ class CalendarPluginBuilder extends PluginBuilder {
PluginType get pluginType => PluginType.calendar;
@override
ViewLayoutPB? get layoutType => ViewLayoutPB.Calendar;
ViewLayoutPB get layoutType => ViewLayoutPB.Calendar;
}
class CalendarPluginConfig implements PluginConfig {

View File

@ -25,7 +25,7 @@ class GridPluginBuilder implements PluginBuilder {
PluginType get pluginType => PluginType.grid;
@override
ViewLayoutPB? get layoutType => ViewLayoutPB.Grid;
ViewLayoutPB get layoutType => ViewLayoutPB.Grid;
}
class GridPluginConfig implements PluginConfig {

View File

@ -243,11 +243,14 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
Widget tabBarItem(String pluginId) => ViewTabBarItem(view: notifier.view);
@override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) {
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) {
notifier.isDeleted.addListener(() {
final deletedView = notifier.isDeleted.value;
if (deletedView != null && deletedView.hasIndex()) {
context?.onDeleted(notifier.view, deletedView.index);
context.onDeleted?.call(notifier.view, deletedView.index);
}
});

View File

@ -54,6 +54,7 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
valueListenable: _layoutTypeChangeNotifier,
builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget(
shrinkWrap: widget.shrinkWrap,
context: PluginContext(),
),
);
}

View File

@ -48,6 +48,9 @@ class DatabaseDocumentPluginBuilder extends PluginBuilder {
@override
PluginType get pluginType => PluginType.databaseDocument;
@override
ViewLayoutPB get layoutType => ViewLayoutPB.Document;
}
class DatabaseDocumentPlugin extends Plugin {
@ -98,7 +101,10 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder
EdgeInsets get contentPadding => EdgeInsets.zero;
@override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) {
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) {
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
builder: (_, state) => DatabaseDocumentPage(
key: ValueKey(documentId),

View File

@ -43,7 +43,7 @@ class DocumentPluginBuilder extends PluginBuilder {
PluginType get pluginType => PluginType.document;
@override
ViewLayoutPB? get layoutType => ViewLayoutPB.Document;
ViewLayoutPB get layoutType => ViewLayoutPB.Document;
}
class DocumentPlugin extends Plugin {
@ -107,7 +107,10 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
EdgeInsets get contentPadding => EdgeInsets.zero;
@override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) {
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) {
notifier.isDeleted.addListener(() {
final deletedView = notifier.isDeleted.value;
if (deletedView != null && deletedView.hasIndex()) {
@ -121,7 +124,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
builder: (_, state) => DocumentPage(
key: ValueKey(view.id),
view: view,
onDeleted: () => context?.onDeleted(view, deletedViewIndex),
onDeleted: () => context.onDeleted?.call(view, deletedViewIndex),
initialSelection: initialSelection,
),
),

View File

@ -5,6 +5,7 @@ export "./src/trash_header.dart";
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
@ -26,6 +27,9 @@ class TrashPluginBuilder extends PluginBuilder {
@override
PluginType get pluginType => PluginType.trash;
@override
ViewLayoutPB get layoutType => ViewLayoutPB.Document;
}
class TrashPluginConfig implements PluginConfig {
@ -59,7 +63,10 @@ class TrashPluginDisplay extends PluginWidgetBuilder {
Widget? get rightBarItem => null;
@override
Widget buildWidget({PluginContext? context, required bool shrinkWrap}) =>
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
}) =>
const TrashPage(
key: ValueKey('TrashPage'),
);

View File

@ -1,5 +1,6 @@
library flowy_plugin;
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:flutter/widgets.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@ -18,6 +19,7 @@ enum PluginType {
board,
calendar,
databaseDocument,
chat,
}
typedef PluginId = String;
@ -57,7 +59,7 @@ abstract class PluginBuilder {
/// The layoutType is used in the backend to determine the layout of the view.
/// Currently, AppFlowy supports 4 layout types: Document, Grid, Board, Calendar.
ViewLayoutPB? get layoutType => ViewLayoutPB.Document;
ViewLayoutPB? get layoutType;
}
abstract class PluginConfig {
@ -71,14 +73,21 @@ abstract class PluginWidgetBuilder with NavigationItem {
EdgeInsets get contentPadding =>
const EdgeInsets.symmetric(horizontal: 40, vertical: 28);
Widget buildWidget({PluginContext? context, required bool shrinkWrap});
Widget buildWidget({
required PluginContext context,
required bool shrinkWrap,
});
}
class PluginContext {
PluginContext({required this.onDeleted});
PluginContext({
this.userProfile,
this.onDeleted,
});
// calls when widget of the plugin get deleted
final Function(ViewPB, int?) onDeleted;
final Function(ViewPB, int?)? onDeleted;
final UserProfilePB? userProfile;
}
void registerPlugin({required PluginBuilder builder, PluginConfig? config}) {

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart';
@ -61,6 +62,7 @@ GoRouter generateRouter(Widget child) {
_mobileGridScreenRoute(),
_mobileBoardScreenRoute(),
_mobileCalendarScreenRoute(),
_mobileChatScreenRoute(),
// card detail page
_mobileCardDetailScreenRoute(),
_mobileDateCellEditScreenRoute(),
@ -488,6 +490,21 @@ GoRoute _mobileEditorScreenRoute() {
);
}
GoRoute _mobileChatScreenRoute() {
return GoRoute(
path: MobileChatScreen.routeName,
parentNavigatorKey: AppGlobals.rootNavKey,
pageBuilder: (context, state) {
final id = state.uri.queryParameters[MobileChatScreen.viewId]!;
final title = state.uri.queryParameters[MobileChatScreen.viewTitle];
return MaterialExtendedPage(
child: MobileChatScreen(id: id, title: title),
);
},
);
}
GoRoute _mobileGridScreenRoute() {
return GoRoute(
path: MobileGridScreen.routeName,

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/ai_chat/chat.dart';
import 'package:appflowy/plugins/database/calendar/calendar.dart';
import 'package:appflowy/plugins/database/board/board.dart';
import 'package:appflowy/plugins/database/grid/grid.dart';
@ -29,6 +30,14 @@ class PluginLoadTask extends LaunchTask {
builder: DatabaseDocumentPluginBuilder(),
config: DatabaseDocumentPluginConfig(),
);
registerPlugin(
builder: DatabaseDocumentPluginBuilder(),
config: DatabaseDocumentPluginConfig(),
);
registerPlugin(
builder: AIChatPluginBuilder(),
config: AIChatPluginConfig(),
);
}
@override

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/plugins/ai_chat/chat.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
@ -44,6 +45,7 @@ extension ViewExtension on ViewPB {
ViewLayoutPB.Calendar => FlowySvgs.calendar_s,
ViewLayoutPB.Grid => FlowySvgs.grid_s,
ViewLayoutPB.Document => FlowySvgs.document_s,
ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,
_ => FlowySvgs.document_s,
},
);
@ -53,6 +55,7 @@ extension ViewExtension on ViewPB {
ViewLayoutPB.Calendar => PluginType.calendar,
ViewLayoutPB.Document => PluginType.document,
ViewLayoutPB.Grid => PluginType.grid,
ViewLayoutPB.Chat => PluginType.chat,
_ => throw UnimplementedError(),
};
@ -79,6 +82,8 @@ extension ViewExtension on ViewPB {
pluginType: pluginType,
initialSelection: initialSelection,
);
case ViewLayoutPB.Chat:
return AIChatPagePlugin(view: this);
}
throw UnimplementedError;
}
@ -161,11 +166,13 @@ extension ViewLayoutExtension on ViewLayoutPB {
ViewLayoutPB.Board => FlowySvgs.board_s,
ViewLayoutPB.Calendar => FlowySvgs.date_s,
ViewLayoutPB.Document => FlowySvgs.document_s,
ViewLayoutPB.Chat => FlowySvgs.chat_ai_page_s,
_ => throw Exception('Unknown layout type'),
};
bool get isDocumentView => switch (this) {
ViewLayoutPB.Document => true,
ViewLayoutPB.Chat ||
ViewLayoutPB.Grid ||
ViewLayoutPB.Board ||
ViewLayoutPB.Calendar =>
@ -178,7 +185,7 @@ extension ViewLayoutExtension on ViewLayoutPB {
ViewLayoutPB.Board ||
ViewLayoutPB.Calendar =>
true,
ViewLayoutPB.Document => false,
ViewLayoutPB.Document || ViewLayoutPB.Chat => false,
_ => throw Exception('Unknown layout type'),
};
}

View File

@ -153,6 +153,7 @@ class DesktopHomeScreen extends StatelessWidget {
final homeStack = HomeStack(
layout: layout,
delegate: DesktopHomeScreenStackAdaptor(context),
userProfile: userProfile,
);
final menu = _buildHomeSidebar(
context,

View File

@ -1,3 +1,4 @@
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy/core/frameless_window.dart';
import 'package:appflowy/plugins/blank/blank.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@ -29,10 +30,12 @@ class HomeStack extends StatelessWidget {
super.key,
required this.delegate,
required this.layout,
required this.userProfile,
});
final HomeStackDelegate delegate;
final HomeLayout layout;
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
@ -55,7 +58,11 @@ class HomeStack extends StatelessWidget {
controller: pageController,
children: state.pageManagers
.map(
(pm) => PageStack(pageManager: pm, delegate: delegate),
(pm) => PageStack(
pageManager: pm,
delegate: delegate,
userProfile: userProfile,
),
)
.toList(),
),
@ -73,11 +80,13 @@ class PageStack extends StatefulWidget {
super.key,
required this.pageManager,
required this.delegate,
required this.userProfile,
});
final PageManager pageManager;
final HomeStackDelegate delegate;
final UserProfilePB userProfile;
@override
State<PageStack> createState() => _PageStackState();
@ -93,6 +102,7 @@ class _PageStackState extends State<PageStack>
color: Theme.of(context).colorScheme.surface,
child: FocusTraversalGroup(
child: widget.pageManager.stackWidget(
userProfile: widget.userProfile,
onDeleted: (view, index) {
widget.delegate.didDeleteStackWidget(view, index);
},
@ -227,7 +237,10 @@ class PageManager {
);
}
Widget stackWidget({required Function(ViewPB, int?) onDeleted}) {
Widget stackWidget({
required UserProfilePB userProfile,
required Function(ViewPB, int?) onDeleted,
}) {
return MultiProvider(
providers: [ChangeNotifierProvider.value(value: _notifier)],
child: Consumer(
@ -239,7 +252,10 @@ class PageManager {
if (pluginType == notifier.plugin.pluginType) {
final builder = notifier.plugin.widgetBuilder;
final pluginWidget = builder.buildWidget(
context: PluginContext(onDeleted: onDeleted),
context: PluginContext(
onDeleted: onDeleted,
userProfile: userProfile,
),
shrinkWrap: false,
);

View File

@ -2,7 +2,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
class MenuSharedState {
MenuSharedState({ViewPB? view}) {
MenuSharedState({
ViewPB? view,
}) {
_latestOpenView.value = view;
}

View File

@ -681,6 +681,8 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
return LocaleKeys.newBoardText.tr();
case ViewLayoutPB.Calendar:
return LocaleKeys.newCalendarText.tr();
case ViewLayoutPB.Chat:
return LocaleKeys.chat_newChat.tr();
}
return LocaleKeys.newPageText.tr();
}

View File

@ -63,31 +63,48 @@ class ViewMoreActionButton extends StatelessWidget {
List<ViewMoreActionType> _buildActionTypes() {
final List<ViewMoreActionType> actionTypes = [];
switch (spaceType) {
case FolderSpaceType.favorite:
if (spaceType == FolderSpaceType.favorite) {
actionTypes.addAll([
ViewMoreActionType.unFavorite,
ViewMoreActionType.divider,
ViewMoreActionType.rename,
ViewMoreActionType.openInNewTab,
]);
} else {
actionTypes.add(
view.isFavorite
? ViewMoreActionType.unFavorite
: ViewMoreActionType.favorite,
);
actionTypes.addAll([
ViewMoreActionType.divider,
ViewMoreActionType.rename,
]);
// Chat doesn't change icon and duplicate
if (view.layout != ViewLayoutPB.Chat) {
actionTypes.addAll([
ViewMoreActionType.unFavorite,
ViewMoreActionType.divider,
ViewMoreActionType.rename,
ViewMoreActionType.openInNewTab,
]);
break;
default:
actionTypes.addAll([
view.isFavorite
? ViewMoreActionType.unFavorite
: ViewMoreActionType.favorite,
ViewMoreActionType.divider,
ViewMoreActionType.rename,
ViewMoreActionType.changeIcon,
ViewMoreActionType.duplicate,
ViewMoreActionType.delete,
ViewMoreActionType.divider,
ViewMoreActionType.collapseAllPages,
ViewMoreActionType.divider,
ViewMoreActionType.openInNewTab,
]);
}
actionTypes.addAll([
ViewMoreActionType.delete,
ViewMoreActionType.divider,
]);
// Chat doesn't change collapse
if (view.layout != ViewLayoutPB.Chat) {
actionTypes.add(ViewMoreActionType.collapseAllPages);
actionTypes.add(ViewMoreActionType.divider);
}
actionTypes.add(ViewMoreActionType.openInNewTab);
}
return actionTypes;
}
}

View File

@ -16,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-search/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/protobuf.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:ffi/ffi.dart';
import 'package:isolates/isolates.dart';
@ -36,6 +37,7 @@ part 'dart_event/flowy-document/dart_event.dart';
part 'dart_event/flowy-config/dart_event.dart';
part 'dart_event/flowy-date/dart_event.dart';
part 'dart_event/flowy-search/dart_event.dart';
part 'dart_event/flowy-chat/dart_event.dart';
enum FFIException {
RequestIsEmpty,

View File

@ -106,7 +106,7 @@ class FlowyButton extends StatelessWidget {
}
if (rightIcon != null) {
children.add(const HSpace(6));
children.add(HSpace(iconPadding));
// No need to define the size of rightIcon. Just use its intrinsic width
children.add(rightIcon!);
}

View File

@ -393,6 +393,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.1"
diffutil_dart:
dependency: transitive
description:
name: diffutil_dart
sha256: "5e74883aedf87f3b703cb85e815bdc1ed9208b33501556e4a8a5572af9845c81"
url: "https://pub.dev"
source: hosted
version: "4.0.1"
dotted_border:
dependency: "direct main"
description:
@ -595,13 +603,21 @@ packages:
source: git
version: "3.3.1"
flutter_chat_types:
dependency: transitive
dependency: "direct main"
description:
name: flutter_chat_types
sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9
url: "https://pub.dev"
source: hosted
version: "3.6.2"
flutter_chat_ui:
dependency: "direct main"
description:
name: flutter_chat_ui
sha256: "40fb37acc328dd179eadc3d67bf8bd2d950dc0da34464aa8d48e8707e0234c09"
url: "https://pub.dev"
source: hosted
version: "1.6.13"
flutter_colorpicker:
dependency: "direct main"
description:
@ -661,6 +677,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.2"
flutter_parsed_text:
dependency: transitive
description:
name: flutter_parsed_text
sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -1473,6 +1497,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
photo_view:
dependency: transitive
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
pixel_snap:
dependency: transitive
description:
@ -1689,6 +1721,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.9"
scroll_to_index:
dependency: transitive
description:
name: scroll_to_index
sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176
url: "https://pub.dev"
source: hosted
version: "3.0.1"
scrollable_positioned_list:
dependency: "direct main"
description:
@ -1810,6 +1850,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.4"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
simple_gesture_detector:
dependency: transitive
description:

View File

@ -135,9 +135,12 @@ dependencies:
numerus: ^2.1.2
flutter_animate: ^4.5.0
permission_handler: ^11.3.1
flutter_chat_ui: ^1.6.13
flutter_chat_types: ^3.6.2
scaled_app: ^2.3.0
auto_size_text_field: ^2.2.3
reorderable_tabbar: ^1.0.6
shimmer: ^3.0.0
dev_dependencies:
flutter_lints: ^3.0.1

View File

@ -29,7 +29,7 @@ class AppFlowyBoardTest {
return ViewBackendService.createView(
parentViewId: app.id,
name: "Test Board",
layoutType: builder.layoutType!,
layoutType: builder.layoutType,
openAfterCreate: true,
).then((result) {
return result.fold(

View File

@ -213,6 +213,9 @@ void main() {
const layouts = ViewLayoutPB.values;
for (var i = 0; i < layouts.length; i++) {
final layout = layouts[i];
if (layout == ViewLayoutPB.Chat) {
continue;
}
viewBloc.add(
ViewEvent.createView(
'Test $layout',

View File

@ -155,14 +155,14 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -179,12 +179,27 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "appflowy_tauri"
version = "0.0.0"
dependencies = [
"bytes",
"dotenv",
"flowy-chat",
"flowy-config",
"flowy-core",
"flowy-date",
@ -194,6 +209,7 @@ dependencies = [
"flowy-search",
"flowy-user",
"lib-dispatch",
"semver",
"serde",
"serde_json",
"tauri",
@ -529,9 +545,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
dependencies = [
"serde",
]
@ -740,7 +756,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"again",
"anyhow",
@ -770,6 +786,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"serde_urlencoded",
"shared-entity",
"thiserror",
"tokio",
@ -786,7 +803,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"futures-channel",
"futures-util",
@ -860,7 +877,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -884,7 +901,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -914,7 +931,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -933,7 +950,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"bytes",
@ -948,7 +965,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"chrono",
@ -986,7 +1003,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-stream",
@ -1025,7 +1042,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -1050,7 +1067,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"async-trait",
@ -1067,7 +1084,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -1296,7 +1313,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.6",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1407,7 +1424,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -1485,6 +1502,7 @@ dependencies = [
"diesel_derives",
"libsqlite3-sys",
"r2d2",
"serde_json",
"time",
]
@ -1794,6 +1812,39 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "flowy-chat"
version = "0.1.0"
dependencies = [
"bytes",
"dashmap",
"flowy-chat-pub",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"futures",
"lib-dispatch",
"lib-infra",
"protobuf",
"strum_macros 0.21.1",
"tokio",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "flowy-chat-pub"
version = "0.1.0"
dependencies = [
"client-api",
"flowy-error",
"futures",
"lib-infra",
]
[[package]]
name = "flowy-codegen"
version = "0.1.0"
@ -1845,6 +1896,8 @@ dependencies = [
"collab-integrate",
"collab-plugins",
"diesel",
"flowy-chat",
"flowy-chat-pub",
"flowy-config",
"flowy-database-pub",
"flowy-database2",
@ -2175,6 +2228,7 @@ dependencies = [
"collab-entity",
"collab-folder",
"collab-plugins",
"flowy-chat-pub",
"flowy-database-pub",
"flowy-document-pub",
"flowy-encrypt",
@ -2195,6 +2249,7 @@ dependencies = [
"postgrest",
"rand 0.8.5",
"reqwest",
"semver",
"serde",
"serde_json",
"thiserror",
@ -2777,7 +2832,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"futures-util",
@ -2794,7 +2849,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -3226,7 +3281,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"reqwest",
@ -4729,7 +4784,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4750,7 +4805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.47",
@ -5524,27 +5579,27 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
@ -5714,13 +5769,15 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
"appflowy-ai-client",
"chrono",
"collab-entity",
"database-entity",
"futures",
"gotrue-entity",
"reqwest",
"serde",
@ -6566,18 +6623,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",

View File

@ -20,7 +20,7 @@ bytes = "1.5.0"
serde = "1.0"
serde_json = "1.0.108"
protobuf = { version = "2.28.0" }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2"] }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] }
uuid = { version = "1.5.0", features = ["serde", "v4"] }
serde_repr = "0.1"
parking_lot = "0.12"
@ -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 = "ef96b42e43c7b929a928f6c334967c7edffc1319" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b36715dc2427e23a15fa81c629e1817d3dbf1e1a" }
[dependencies]
serde_json.workspace = true
@ -75,6 +75,7 @@ flowy-core = { path = "../../rust-lib/flowy-core", features = [
flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] }
flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] }
flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] }
flowy-chat = { path = "../../rust-lib/flowy-chat", features = ["tauri_ts"] }
flowy-error = { path = "../../rust-lib/flowy-error", features = [
"impl_from_sqlite",
"impl_from_dispatch_error",
@ -105,10 +106,10 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }

View File

@ -209,14 +209,14 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -233,6 +233,20 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
@ -428,9 +442,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
dependencies = [
"serde",
]
@ -548,7 +562,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"again",
"anyhow",
@ -578,6 +592,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"serde_urlencoded",
"shared-entity",
"thiserror",
"tokio",
@ -594,7 +609,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"futures-channel",
"futures-util",
@ -638,7 +653,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -662,7 +677,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -681,7 +696,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"bytes",
@ -696,7 +711,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"chrono",
@ -734,7 +749,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-stream",
@ -772,7 +787,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -797,7 +812,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"async-trait",
@ -814,7 +829,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -1011,7 +1026,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -1241,6 +1256,16 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "flowy-chat-pub"
version = "0.1.0"
dependencies = [
"client-api",
"flowy-error",
"futures",
"lib-infra",
]
[[package]]
name = "flowy-codegen"
version = "0.1.0"
@ -1463,6 +1488,7 @@ dependencies = [
"collab-entity",
"collab-folder",
"collab-plugins",
"flowy-chat-pub",
"flowy-database-pub",
"flowy-document-pub",
"flowy-encrypt",
@ -1483,6 +1509,7 @@ dependencies = [
"postgrest",
"rand 0.8.5",
"reqwest",
"semver",
"serde",
"serde_json",
"thiserror",
@ -1788,7 +1815,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"futures-util",
@ -1805,7 +1832,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -2106,7 +2133,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"reqwest",
@ -3617,15 +3644,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
@ -3643,9 +3670,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@ -3746,13 +3773,15 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
"appflowy-ai-client",
"chrono",
"collab-entity",
"database-entity",
"futures",
"gotrue-entity",
"reqwest",
"serde",
@ -4033,18 +4062,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@ -4235,6 +4264,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -5009,4 +5039,4 @@ dependencies = [
[[patch.unused]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2c430e0#2c430e05fff6ca541cfef9836dc72e66a7847b6e"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"

View File

@ -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 = "ef96b42e43c7b929a928f6c334967c7edffc1319" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b36715dc2427e23a15fa81c629e1817d3dbf1e1a" }
@ -70,10 +70,10 @@ opt-level = 3
codegen-units = 1
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }

View File

@ -153,7 +153,7 @@ checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -170,12 +170,27 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "appflowy_tauri"
version = "0.0.0"
dependencies = [
"bytes",
"dotenv",
"flowy-chat",
"flowy-config",
"flowy-core",
"flowy-date",
@ -184,6 +199,7 @@ dependencies = [
"flowy-notification",
"flowy-user",
"lib-dispatch",
"semver",
"serde",
"serde_json",
"tauri",
@ -714,7 +730,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"again",
"anyhow",
@ -744,6 +760,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"serde_urlencoded",
"shared-entity",
"thiserror",
"tokio",
@ -760,7 +777,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"futures-channel",
"futures-util",
@ -843,7 +860,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -867,7 +884,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -897,7 +914,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -916,7 +933,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"bytes",
@ -931,7 +948,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"chrono",
@ -969,7 +986,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-stream",
@ -1008,7 +1025,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -1033,7 +1050,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"async-trait",
@ -1050,7 +1067,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -1283,7 +1300,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa 1.0.10",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1394,7 +1411,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -1493,6 +1510,7 @@ dependencies = [
"diesel_derives",
"libsqlite3-sys",
"r2d2",
"serde_json",
"time",
]
@ -1831,6 +1849,39 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "flowy-chat"
version = "0.1.0"
dependencies = [
"bytes",
"dashmap",
"flowy-chat-pub",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"futures",
"lib-dispatch",
"lib-infra",
"protobuf",
"strum_macros 0.21.1",
"tokio",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "flowy-chat-pub"
version = "0.1.0"
dependencies = [
"client-api",
"flowy-error",
"futures",
"lib-infra",
]
[[package]]
name = "flowy-codegen"
version = "0.1.0"
@ -1882,6 +1933,8 @@ dependencies = [
"collab-integrate",
"collab-plugins",
"diesel",
"flowy-chat",
"flowy-chat-pub",
"flowy-config",
"flowy-database-pub",
"flowy-database2",
@ -2212,6 +2265,7 @@ dependencies = [
"collab-entity",
"collab-folder",
"collab-plugins",
"flowy-chat-pub",
"flowy-database-pub",
"flowy-document-pub",
"flowy-encrypt",
@ -2232,6 +2286,7 @@ dependencies = [
"postgrest",
"rand 0.8.5",
"reqwest",
"semver",
"serde",
"serde_json",
"thiserror",
@ -2851,7 +2906,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"futures-util",
@ -2868,7 +2923,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -3305,7 +3360,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"reqwest",
@ -4810,7 +4865,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4831,7 +4886,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.55",
@ -5616,27 +5671,27 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.22"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.197"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.197"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@ -5809,13 +5864,15 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
"appflowy-ai-client",
"chrono",
"collab-entity",
"database-entity",
"futures",
"gotrue-entity",
"reqwest",
"serde",

View File

@ -20,7 +20,7 @@ bytes = "1.5.0"
serde = "1.0"
serde_json = "1.0.108"
protobuf = { version = "2.28.0" }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2"] }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] }
uuid = { version = "1.5.0", features = ["serde", "v4"] }
serde_repr = "0.1"
parking_lot = "0.12"
@ -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 = "ef96b42e43c7b929a928f6c334967c7edffc1319" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b36715dc2427e23a15fa81c629e1817d3dbf1e1a" }
[dependencies]
serde_json.workspace = true
@ -89,6 +89,9 @@ flowy-document = { path = "../../rust-lib/flowy-document", features = [
flowy-notification = { path = "../../rust-lib/flowy-notification", features = [
"tauri_ts",
] }
flowy-chat = { path = "../../rust-lib/flowy-chat", features = [
"tauri_ts",
] }
uuid = "1.5.0"
tauri-plugin-deep-link = "0.1.2"
@ -104,10 +107,10 @@ default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
[patch.crates-io]
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }

View File

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 3.33782C15.5291 2.48697 13.8214 2 12 2C6.47715 2 2 6.47715 2 12C2 13.5997 2.37562 15.1116 3.04346 16.4525C3.22094 16.8088 3.28001 17.2161 3.17712 17.6006L2.58151 19.8267C2.32295 20.793 3.20701 21.677 4.17335 21.4185L6.39939 20.8229C6.78393 20.72 7.19121 20.7791 7.54753 20.9565C8.88837 21.6244 10.4003 22 12 22C17.5228 22 22 17.5228 22 12C22 10.1786 21.513 8.47087 20.6622 7" stroke="#171717" stroke-opacity="0.7" stroke-width="1.125" stroke-linecap="round"/>
<path d="M8 12H8.009M11.991 12H12M15.991 12H16" stroke="#171717" stroke-opacity="0.7" stroke-width="1.05" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 735 B

View File

@ -0,0 +1,13 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_214_2)">
<path d="M15.9881 9.7645C15.9849 9.37128 15.8725 8.98667 15.6633 8.65368C15.5916 8.5528 15.5001 8.46754 15.3945 8.40307C15.2888 8.33861 15.1711 8.29629 15.0486 8.27868C13.9636 8.09182 12.0538 9.31863 10.1517 10.6717L9.8811 10.8637C9.80636 10.9166 9.73033 10.9733 9.65816 11.0338C8.45713 12.0209 7.66332 13.8019 8.47517 14.9578C8.68981 15.2616 8.97311 15.5106 9.30204 15.6843C9.63097 15.8581 9.99626 15.9518 10.3682 15.9578C10.3943 15.9671 10.4217 15.9719 10.4494 15.972H14.9481C15.0858 15.9756 15.2227 15.9516 15.3509 15.9014C15.4792 15.8512 15.596 15.7759 15.6946 15.6798C15.7933 15.5837 15.8716 15.4688 15.9251 15.3419C15.9786 15.215 16.0061 15.0787 16.0061 14.9411V9.84053C16.0035 9.8145 15.9974 9.78893 15.9881 9.7645ZM14.9481 15.414H12.4829C12.8152 15.241 13.1355 15.046 13.4417 14.8302C13.4816 14.8006 13.5216 14.7722 13.5628 14.74C14.3846 14.0944 15.0307 13.2524 15.4417 12.2916V14.9346C15.4419 14.9988 15.4291 15.0624 15.4042 15.1216C15.3793 15.1808 15.3427 15.2344 15.2967 15.2791C15.2506 15.3238 15.196 15.3588 15.1361 15.382C15.0762 15.4052 15.0123 15.4161 14.9481 15.414ZM10.4739 11.1266C11.4649 10.4217 13.7226 8.81734 14.8025 8.81734C14.853 8.81711 14.9034 8.82099 14.9533 8.82894C15.0042 8.83319 15.0536 8.84841 15.0981 8.87355C15.1425 8.89868 15.1811 8.93314 15.211 8.97456C15.8759 9.92043 15.1633 12.8496 13.2303 14.2954C13.1955 14.3225 13.162 14.347 13.1272 14.3702C11.2948 15.6588 9.72775 15.7581 8.93909 14.6279C8.32955 13.7619 9.0061 12.2877 10.0203 11.4539C10.0821 11.4024 10.1491 11.3547 10.2084 11.3109L10.4739 11.1266Z" fill="black"/>
<path d="M5.52315 0.172868C4.63269 0.172868 3.63656 0.559466 2.55924 1.31591C2.51671 1.34555 2.47547 1.37519 2.44197 1.40225C0.359495 2.95508 -0.537412 6.25276 0.331144 7.48859C0.40288 7.58938 0.494582 7.67435 0.60055 7.73819C0.706519 7.80204 0.824488 7.84341 0.947124 7.85972C1.02378 7.87243 1.10139 7.87847 1.17908 7.87776C2.29248 7.87776 4.07341 6.7257 5.8479 5.4641L6.11465 5.2824C6.18939 5.22957 6.26284 5.17416 6.3363 5.11359C7.53991 4.12261 8.33372 2.34168 7.52058 1.18447C7.29845 0.862617 6.99932 0.601456 6.65045 0.42477C6.30158 0.248084 5.91405 0.161487 5.52315 0.172868ZM5.98192 4.68318C5.92006 4.73472 5.85305 4.78112 5.79377 4.82622L5.52444 5.01694C4.48449 5.75663 2.04506 7.49246 1.0412 7.3172C0.990214 7.3132 0.940726 7.29809 0.896203 7.27293C0.85168 7.24778 0.8132 7.21318 0.783464 7.17158C0.128825 6.22184 0.836299 3.29529 2.77315 1.84426L2.87496 1.76952C3.84145 1.09039 4.75511 0.730858 5.51671 0.730858C5.81859 0.719971 6.11837 0.785059 6.38856 0.920152C6.65874 1.05524 6.89068 1.25602 7.0631 1.50405C7.67264 2.37132 6.99609 3.84555 5.98063 4.67673L5.98192 4.68318Z" fill="black"/>
<path d="M10.897 6.1209C10.9317 6.16858 10.9665 6.21626 11.0026 6.26265C11.6341 7.07451 12.8132 7.86059 13.8969 7.86059C14.2875 7.86618 14.6701 7.74948 14.991 7.52683C16.0503 6.78198 16.598 5.12348 14.9601 2.7008C14.9279 2.65441 14.8957 2.60673 14.8622 2.56033C14.1637 1.56291 13.0155 0.740747 11.6985 0.301314C10.4601 -0.112346 9.30674 -0.0994591 8.6869 0.336108C8.58553 0.407326 8.49993 0.498659 8.43541 0.604414C8.37089 0.71017 8.32885 0.828081 8.3119 0.950799C8.12504 2.03585 9.35056 3.94564 10.7037 5.84771L10.897 6.1209ZM8.86216 1.04616C8.86616 0.995178 8.88127 0.94569 8.90642 0.901167C8.93158 0.856644 8.96617 0.818164 9.00778 0.788428C9.23329 0.629922 9.57865 0.551314 9.9936 0.551314C10.5146 0.563122 11.0305 0.657508 11.522 0.830953C12.723 1.23302 13.7732 1.97915 14.406 2.87992C14.4382 2.92503 14.4678 2.96884 14.4975 3.00879C15.6959 4.7807 15.7604 6.29745 14.6701 7.0642C13.7823 7.6892 12.2565 6.96111 11.4485 5.91472C11.4163 5.87477 11.3866 5.83353 11.3583 5.79487C11.3029 5.71369 11.2372 5.62219 11.1637 5.52039C10.4189 4.4843 8.68948 2.05131 8.86216 1.04616Z" fill="black"/>
<path d="M5.09906 10.0211C5.06427 9.9734 5.02947 9.92572 4.99339 9.87933C4.13514 8.77752 2.27174 7.73113 1.00499 8.61773C-0.0555802 9.36257 -0.603261 11.0237 1.04236 13.4515L1.05138 13.4631C1.07844 13.503 1.1055 13.5443 1.13385 13.5829C2.27432 15.2066 4.42509 16.1487 5.98308 16.1487C6.51658 16.1487 6.9805 16.0378 7.30911 15.8072C7.41048 15.7359 7.49608 15.6446 7.5606 15.5389C7.62512 15.4331 7.66716 15.3152 7.68411 15.1925C7.86968 14.1074 6.64416 12.1963 5.29107 10.2956L5.09906 10.0211ZM7.13385 15.0971C7.12985 15.1481 7.11474 15.1976 7.08959 15.2421C7.06443 15.2866 7.02984 15.3251 6.98823 15.3548C6.02045 16.034 2.99339 15.2582 1.58875 13.2659L1.50628 13.1461L1.49854 13.1345C0.298801 11.3561 0.234368 9.84067 1.32586 9.07391C2.21504 8.44891 3.73952 9.17701 4.54751 10.2221C4.57973 10.2621 4.60937 10.3033 4.63772 10.3432L4.83102 10.6152C5.57457 11.6551 7.30653 14.0907 7.13385 15.0971Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_214_2">
<rect width="16" height="16.1495" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -147,6 +147,17 @@
"newGridText": "New grid",
"newCalendarText": "New calendar",
"newBoardText": "New board",
"chat": {
"newChat": "New chat",
"inputMessageHint": "Message AppFlowy AI",
"unsupportedCloudPrompt": "This feature is only available when using AppFlowy Cloud",
"relatedQuestion": "Related",
"serverUnavailable": "Service Temporarily Unavailable. Please try again later.",
"aiServerUnavailable": "There was an error generating a response.",
"clickToRetry": "Click to retry",
"regenerateAnswer": "Regenerate",
"aiMistakePrompt": "AI can make mistakes. Check important info."
},
"trash": {
"text": "Trash",
"restoreAll": "Restore All",

View File

@ -156,14 +156,14 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -180,6 +180,20 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bytes",
"futures",
"serde",
"serde_json",
"serde_repr",
"thiserror",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
@ -530,9 +544,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.5.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
dependencies = [
"serde",
]
@ -650,7 +664,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"again",
"anyhow",
@ -680,6 +694,7 @@ dependencies = [
"serde",
"serde_json",
"serde_repr",
"serde_urlencoded",
"shared-entity",
"thiserror",
"tokio",
@ -696,7 +711,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"futures-channel",
"futures-util",
@ -739,7 +754,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -763,7 +778,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-trait",
@ -793,7 +808,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -812,7 +827,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"bytes",
@ -827,7 +842,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"chrono",
@ -865,7 +880,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"async-stream",
@ -904,7 +919,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"bincode",
@ -929,7 +944,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"async-trait",
@ -946,7 +961,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=0b52164#0b521646f5d609abe11a213ab063402c32496e9b"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=cca5135f0010fa5de22a298cbed939e21575538c#cca5135f0010fa5de22a298cbed939e21575538c"
dependencies = [
"anyhow",
"collab",
@ -1149,7 +1164,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.11.2",
"phf 0.8.0",
"smallvec",
]
@ -1249,7 +1264,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -1340,6 +1355,7 @@ dependencies = [
"diesel_derives",
"libsqlite3-sys",
"r2d2",
"serde_json",
"time",
]
@ -1487,6 +1503,8 @@ dependencies = [
"collab-folder",
"collab-plugins",
"dotenv",
"flowy-chat",
"flowy-chat-pub",
"flowy-core",
"flowy-database-pub",
"flowy-database2",
@ -1629,6 +1647,39 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "flowy-chat"
version = "0.1.0"
dependencies = [
"bytes",
"dashmap",
"flowy-chat-pub",
"flowy-codegen",
"flowy-derive",
"flowy-error",
"flowy-notification",
"flowy-sqlite",
"futures",
"lib-dispatch",
"lib-infra",
"protobuf",
"strum_macros 0.21.1",
"tokio",
"tracing",
"uuid",
"validator",
]
[[package]]
name = "flowy-chat-pub"
version = "0.1.0"
dependencies = [
"client-api",
"flowy-error",
"futures",
"lib-infra",
]
[[package]]
name = "flowy-codegen"
version = "0.1.0"
@ -1681,6 +1732,8 @@ dependencies = [
"collab-plugins",
"console-subscriber",
"diesel",
"flowy-chat",
"flowy-chat-pub",
"flowy-config",
"flowy-database-pub",
"flowy-database2",
@ -2017,6 +2070,7 @@ dependencies = [
"collab-folder",
"collab-plugins",
"dotenv",
"flowy-chat-pub",
"flowy-database-pub",
"flowy-document-pub",
"flowy-encrypt",
@ -2464,7 +2518,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"futures-util",
@ -2481,7 +2535,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
@ -2846,7 +2900,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"reqwest",
@ -3721,7 +3775,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros 0.8.0",
"phf_macros",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -3741,7 +3795,6 @@ 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",
]
@ -3809,19 +3862,6 @@ 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"
@ -4025,7 +4065,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@ -4046,7 +4086,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.47",
@ -4809,18 +4849,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
@ -4943,13 +4983,15 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=ef96b42e43c7b929a928f6c334967c7edffc1319#ef96b42e43c7b929a928f6c334967c7edffc1319"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=b36715dc2427e23a15fa81c629e1817d3dbf1e1a#b36715dc2427e23a15fa81c629e1817d3dbf1e1a"
dependencies = [
"anyhow",
"app-error",
"appflowy-ai-client",
"chrono",
"collab-entity",
"database-entity",
"futures",
"gotrue-entity",
"reqwest",
"serde",
@ -5454,18 +5496,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.56"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",

View File

@ -29,6 +29,7 @@ members = [
"build-tool/flowy-codegen",
"build-tool/flowy-derive",
"flowy-search-pub",
"flowy-chat", "flowy-chat-pub",
]
resolver = "2"
@ -61,13 +62,15 @@ flowy-search = { workspace = true, path = "flowy-search" }
flowy-search-pub = { workspace = true, path = "flowy-search-pub" }
collab-integrate = { workspace = true, path = "collab-integrate" }
flowy-date = { workspace = true, path = "flowy-date" }
flowy-chat = { workspace = true, path = "flowy-chat" }
flowy-chat-pub = { workspace = true, path = "flowy-chat-pub" }
anyhow = "1.0"
tracing = "0.1.40"
bytes = "1.5.0"
serde_json = "1.0.108"
serde = "1.0.194"
protobuf = { version = "2.28.0" }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2"] }
diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2", "serde_json"] }
uuid = { version = "1.5.0", features = ["serde", "v4", "v5"] }
serde_repr = "0.1"
parking_lot = "0.12"
@ -90,7 +93,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 = "ef96b42e43c7b929a928f6c334967c7edffc1319" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b36715dc2427e23a15fa81c629e1817d3dbf1e1a" }
[profile.dev]
opt-level = 1
@ -129,10 +132,10 @@ rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec1
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "0b52164" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "cca5135f0010fa5de22a298cbed939e21575538c" }

View File

@ -32,8 +32,8 @@ lib-dispatch = { workspace = true }
# Core
#flowy-core = { workspace = true, features = ["profiling"] }
flowy-core = { workspace = true, features = ["verbose_log"] }
#flowy-core = { workspace = true }
#flowy-core = { workspace = true, features = ["verbose_log"] }
flowy-core = { workspace = true }
flowy-notification = { workspace = true, features = ["dart"] }
flowy-document = { workspace = true, features = ["dart"] }

View File

@ -16,6 +16,7 @@ flowy-database-pub = { workspace = true }
flowy-document = { path = "../flowy-document" }
flowy-document-pub = { workspace = true }
flowy-encrypt = { workspace = true }
flowy-chat = { workspace = true }
lib-dispatch = { workspace = true }
lib-infra = { workspace = true }
flowy-server = { path = "../flowy-server" }
@ -56,6 +57,7 @@ chrono = "0.4.31"
zip = "0.6.6"
walkdir = "2.5.0"
futures = "0.3.30"
flowy-chat-pub = { workspace = true }
[features]
default = ["supabase_cloud_test"]

View File

@ -0,0 +1,89 @@
use crate::event_builder::EventBuilder;
use crate::EventIntegrationTest;
use flowy_chat::entities::{
ChatMessageListPB, ChatMessageTypePB, LoadNextChatMessagePB, LoadPrevChatMessagePB,
SendChatPayloadPB,
};
use flowy_chat::event_map::ChatEvent;
use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
use flowy_folder::event_map::FolderEvent;
impl EventIntegrationTest {
pub async fn create_chat(&self, parent_id: &str) -> ViewPB {
let payload = CreateViewPayloadPB {
parent_view_id: parent_id.to_string(),
name: "chat".to_string(),
desc: "".to_string(),
thumbnail: None,
layout: ViewLayoutPB::Chat,
initial_data: vec![],
meta: Default::default(),
set_as_current: true,
index: None,
section: None,
};
EventBuilder::new(self.clone())
.event(FolderEvent::CreateView)
.payload(payload)
.async_send()
.await
.parse::<ViewPB>()
}
pub async fn send_message(
&self,
chat_id: &str,
message: impl ToString,
message_type: ChatMessageTypePB,
) {
let payload = SendChatPayloadPB {
chat_id: chat_id.to_string(),
message: message.to_string(),
message_type,
};
EventBuilder::new(self.clone())
.event(ChatEvent::SendMessage)
.payload(payload)
.async_send()
.await;
}
pub async fn load_prev_message(
&self,
chat_id: &str,
limit: i64,
before_message_id: Option<i64>,
) -> ChatMessageListPB {
let payload = LoadPrevChatMessagePB {
chat_id: chat_id.to_string(),
limit,
before_message_id,
};
EventBuilder::new(self.clone())
.event(ChatEvent::LoadPrevMessage)
.payload(payload)
.async_send()
.await
.parse::<ChatMessageListPB>()
}
pub async fn load_next_message(
&self,
chat_id: &str,
limit: i64,
after_message_id: Option<i64>,
) -> ChatMessageListPB {
let payload = LoadNextChatMessagePB {
chat_id: chat_id.to_string(),
limit,
after_message_id,
};
EventBuilder::new(self.clone())
.event(ChatEvent::LoadNextMessage)
.payload(payload)
.async_send()
.await
.parse::<ChatMessageListPB>()
}
}

View File

@ -24,6 +24,7 @@ use lib_dispatch::runtime::AFPluginRuntime;
use crate::user_event::TestNotificationSender;
mod chat_event;
pub mod database_event;
pub mod document;
pub mod document_event;

View File

@ -378,6 +378,26 @@ impl TestNotificationSender {
rx
}
pub fn subscribe_without_payload(
&self,
id: &str,
ty: impl Into<i32> + Send,
) -> tokio::sync::mpsc::Receiver<()> {
let id = id.to_string();
let (tx, rx) = tokio::sync::mpsc::channel::<()>(10);
let mut receiver = self.sender.subscribe();
let ty = ty.into();
af_spawn(async move {
// DatabaseNotification::DidUpdateDatabaseSnapshotState
while let Ok(value) = receiver.recv().await {
if value.id == id && value.ty == ty {
let _ = tx.send(()).await;
}
}
});
rx
}
pub fn subscribe_with_condition<T, F>(&self, id: &str, when: F) -> tokio::sync::mpsc::Receiver<T>
where
T: TryFrom<Bytes, Error = ProtobufError> + Send + 'static,

View File

@ -0,0 +1,161 @@
use crate::util::receive_with_timeout;
use event_integration_test::user_event::user_localhost_af_cloud;
use event_integration_test::EventIntegrationTest;
use flowy_chat::entities::{ChatMessageListPB, ChatMessageTypePB};
use flowy_chat::notification::ChatNotification;
use flowy_chat_pub::cloud::ChatMessageType;
use futures_util::StreamExt;
use std::time::Duration;
#[tokio::test]
async fn af_cloud_create_chat_message_test() {
user_localhost_af_cloud().await;
let test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
let current_workspace = test.get_current_workspace().await;
let view = test.create_chat(&current_workspace.id).await;
let chat_id = view.id.clone();
let chat_service = test.server_provider.get_server().unwrap().chat_service();
for i in 0..10 {
let mut stream = chat_service
.send_chat_message(
&current_workspace.id,
&chat_id,
&format!("hello world {}", i),
ChatMessageType::System,
)
.await
.unwrap();
while let Some(message) = stream.next().await {
message.unwrap();
}
}
let rx = test
.notification_sender
.subscribe::<ChatMessageListPB>(&chat_id, ChatNotification::DidLoadLatestChatMessage);
let _ = test.load_next_message(&chat_id, 10, None).await;
let all = receive_with_timeout(rx, Duration::from_secs(30))
.await
.unwrap();
assert_eq!(all.messages.len(), 10);
// in desc order
assert_eq!(all.messages[4].content, "hello world 5");
assert_eq!(all.messages[5].content, "hello world 4");
let list = test
.load_next_message(&chat_id, 5, Some(all.messages[4].message_id))
.await;
assert_eq!(list.messages.len(), 4);
assert_eq!(list.messages[0].content, "hello world 9");
assert_eq!(list.messages[1].content, "hello world 8");
assert_eq!(list.messages[2].content, "hello world 7");
assert_eq!(list.messages[3].content, "hello world 6");
assert_eq!(all.messages[6].content, "hello world 3");
// Load from local
let list = test
.load_prev_message(&chat_id, 5, Some(all.messages[6].message_id))
.await;
assert_eq!(list.messages.len(), 3);
assert_eq!(list.messages[0].content, "hello world 2");
assert_eq!(list.messages[1].content, "hello world 1");
assert_eq!(list.messages[2].content, "hello world 0");
}
#[tokio::test]
async fn af_cloud_load_remote_system_message_test() {
user_localhost_af_cloud().await;
let test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
let current_workspace = test.get_current_workspace().await;
let view = test.create_chat(&current_workspace.id).await;
let chat_id = view.id.clone();
let chat_service = test.server_provider.get_server().unwrap().chat_service();
for i in 0..10 {
let mut stream = chat_service
.send_chat_message(
&current_workspace.id,
&chat_id,
&format!("hello server {}", i),
ChatMessageType::System,
)
.await
.unwrap();
while let Some(message) = stream.next().await {
message.unwrap();
}
}
let rx = test
.notification_sender
.subscribe::<ChatMessageListPB>(&chat_id, ChatNotification::DidLoadLatestChatMessage);
// Previous messages were created by the server, so there are no messages in the local cache.
// It will try to load messages in the background.
let all = test.load_next_message(&chat_id, 5, None).await;
assert!(all.messages.is_empty());
// Wait for the messages to be loaded.
let next_back_five = receive_with_timeout(rx, Duration::from_secs(60))
.await
.unwrap();
assert_eq!(next_back_five.messages.len(), 5);
assert!(next_back_five.has_more);
assert_eq!(next_back_five.total, 10);
assert_eq!(next_back_five.messages[0].content, "hello server 9");
assert_eq!(next_back_five.messages[1].content, "hello server 8");
assert_eq!(next_back_five.messages[2].content, "hello server 7");
assert_eq!(next_back_five.messages[3].content, "hello server 6");
assert_eq!(next_back_five.messages[4].content, "hello server 5");
// Load first five messages
let rx = test
.notification_sender
.subscribe::<ChatMessageListPB>(&chat_id, ChatNotification::DidLoadPrevChatMessage);
test
.load_prev_message(&chat_id, 5, Some(next_back_five.messages[4].message_id))
.await;
let first_five_messages = receive_with_timeout(rx, Duration::from_secs(60))
.await
.unwrap();
assert!(!first_five_messages.has_more);
assert_eq!(first_five_messages.messages[0].content, "hello server 4");
assert_eq!(first_five_messages.messages[1].content, "hello server 3");
assert_eq!(first_five_messages.messages[2].content, "hello server 2");
assert_eq!(first_five_messages.messages[3].content, "hello server 1");
assert_eq!(first_five_messages.messages[4].content, "hello server 0");
}
#[tokio::test]
async fn af_cloud_load_remote_user_message_test() {
user_localhost_af_cloud().await;
let test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
let current_workspace = test.get_current_workspace().await;
let view = test.create_chat(&current_workspace.id).await;
let chat_id = view.id.clone();
let rx = test
.notification_sender
.subscribe_without_payload(&chat_id, ChatNotification::FinishAnswerQuestion);
test
.send_message(&chat_id, "hello world", ChatMessageTypePB::User)
.await;
let _ = receive_with_timeout(rx, Duration::from_secs(60))
.await
.unwrap();
let all = test.load_next_message(&chat_id, 5, None).await;
assert_eq!(all.messages.len(), 2);
// 3 means AI
assert_eq!(all.messages[0].author_type, 3);
// 2 means User
assert_eq!(all.messages[1].author_type, 1);
// The message ID is incremented by 1.
assert_eq!(all.messages[1].message_id + 1, all.messages[0].message_id);
}

View File

@ -0,0 +1 @@
mod chat_message_test;

View File

@ -6,3 +6,5 @@ mod folder;
// mod search;
mod user;
pub mod util;
mod chat;

View File

@ -0,0 +1,12 @@
[package]
name = "flowy-chat-pub"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
lib-infra = { workspace = true }
flowy-error = { workspace = true }
client-api = { workspace = true }
futures.workspace = true

View File

@ -0,0 +1,50 @@
pub use client_api::entity::ai_dto::{RelatedQuestion, RepeatedRelatedQuestion};
pub use client_api::entity::{
ChatAuthorType, ChatMessage, ChatMessageType, MessageCursor, QAChatMessage, RepeatedChatMessage,
};
use client_api::error::AppResponseError;
use flowy_error::FlowyError;
use futures::stream::BoxStream;
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
pub type ChatMessageStream = BoxStream<'static, Result<ChatMessage, AppResponseError>>;
#[async_trait]
pub trait ChatCloudService: Send + Sync + 'static {
fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), FlowyError>;
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> Result<ChatMessageStream, FlowyError>;
fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
offset: MessageCursor,
limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError>;
fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError>;
fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError>;
}

View File

@ -0,0 +1 @@
pub mod cloud;

View File

@ -0,0 +1,35 @@
[package]
name = "flowy-chat"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
flowy-derive.workspace = true
flowy-notification = { workspace = true }
flowy-error = { path = "../flowy-error", features = [
"impl_from_dispatch_error",
"impl_from_collab_folder",
] }
lib-dispatch = { workspace = true }
tracing.workspace = true
uuid.workspace = true
strum_macros = "0.21"
protobuf.workspace = true
bytes.workspace = true
validator = { version = "0.16.0", features = ["derive"] }
lib-infra = { workspace = true }
flowy-chat-pub.workspace = true
dashmap = "5.5"
flowy-sqlite = { workspace = true }
tokio.workspace = true
futures.workspace = true
[build-dependencies]
flowy-codegen.workspace = true
[features]
dart = ["flowy-codegen/dart", "flowy-notification/dart"]
tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"]
web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"]

View File

@ -0,0 +1,3 @@
# Check out the FlowyConfig (located in flowy_toml.rs) for more details.
proto_input = ["src/entities.rs", "src/event_map.rs", "src/notification.rs"]
event_files = ["src/event_map.rs"]

View File

@ -0,0 +1,40 @@
fn main() {
#[cfg(feature = "dart")]
{
flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME"));
flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME"));
}
#[cfg(feature = "tauri_ts")]
{
flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri);
flowy_codegen::protobuf_file::ts_gen(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
flowy_codegen::Project::Tauri,
);
flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri);
flowy_codegen::protobuf_file::ts_gen(
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_NAME"),
flowy_codegen::Project::TauriApp,
);
}
#[cfg(feature = "web_ts")]
{
flowy_codegen::ts_event::gen(
"folder",
flowy_codegen::Project::Web {
relative_path: "../../".to_string(),
},
);
flowy_codegen::protobuf_file::ts_gen(
env!("CARGO_PKG_NAME"),
"folder",
flowy_codegen::Project::Web {
relative_path: "../../".to_string(),
},
);
}
}

View File

@ -0,0 +1,471 @@
use crate::entities::{
ChatMessageErrorPB, ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB,
};
use crate::manager::ChatUserService;
use crate::notification::{send_notification, ChatNotification};
use crate::persistence::{
insert_answer_message, insert_chat_messages, select_chat_messages, ChatMessageTable,
};
use flowy_chat_pub::cloud::{
ChatAuthorType, ChatCloudService, ChatMessage, ChatMessageType, MessageCursor,
};
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::DBConnection;
use futures::StreamExt;
use std::sync::atomic::AtomicI64;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{error, instrument, trace};
enum PrevMessageState {
HasMore,
NoMore,
Loading,
}
pub struct Chat {
chat_id: String,
uid: i64,
user_service: Arc<dyn ChatUserService>,
cloud_service: Arc<dyn ChatCloudService>,
prev_message_state: Arc<RwLock<PrevMessageState>>,
latest_message_id: Arc<AtomicI64>,
}
impl Chat {
pub fn new(
uid: i64,
chat_id: String,
user_service: Arc<dyn ChatUserService>,
cloud_service: Arc<dyn ChatCloudService>,
) -> Chat {
Chat {
uid,
chat_id,
cloud_service,
user_service,
prev_message_state: Arc::new(RwLock::new(PrevMessageState::HasMore)),
latest_message_id: Default::default(),
}
}
pub fn close(&self) {}
#[allow(dead_code)]
pub async fn pull_latest_message(&self, limit: i64) {
let latest_message_id = self
.latest_message_id
.load(std::sync::atomic::Ordering::Relaxed);
if latest_message_id > 0 {
let _ = self
.load_remote_chat_messages(limit, None, Some(latest_message_id))
.await;
}
}
#[instrument(level = "info", skip_all, err)]
pub async fn send_chat_message(
&self,
message: &str,
message_type: ChatMessageType,
) -> Result<(), FlowyError> {
if message.len() > 2000 {
return Err(FlowyError::text_too_long().with_context("Exceeds maximum message 2000 length"));
}
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(),
);
Ok(())
}
/// Load chat messages for a given `chat_id`.
///
/// 1. When opening a chat:
/// - Loads local chat messages.
/// - `after_message_id` and `before_message_id` are `None`.
/// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded.
///
/// 2. Loading more messages in an existing chat with `after_message_id`:
/// - `after_message_id` is the last message ID in the current chat messages.
///
/// 3. Loading more messages in an existing chat with `before_message_id`:
/// - `before_message_id` is the first message ID in the current chat messages.
pub async fn load_prev_chat_messages(
&self,
limit: i64,
before_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
trace!(
"Loading old messages: chat_id={}, limit={}, before_message_id={:?}",
self.chat_id,
limit,
before_message_id
);
let messages = self
.load_local_chat_messages(limit, None, before_message_id)
.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 {
messages,
has_more,
total: 0,
});
}
if matches!(
*self.prev_message_state.read().await,
PrevMessageState::HasMore
) {
*self.prev_message_state.write().await = PrevMessageState::Loading;
if let Err(err) = self
.load_remote_chat_messages(limit, before_message_id, None)
.await
{
error!("Failed to load previous chat messages: {}", err);
}
}
Ok(ChatMessageListPB {
messages,
has_more,
total: 0,
})
}
pub async fn load_latest_chat_messages(
&self,
limit: i64,
after_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
trace!(
"Loading new messages: chat_id={}, limit={}, after_message_id={:?}",
self.chat_id,
limit,
after_message_id,
);
let messages = self
.load_local_chat_messages(limit, after_message_id, None)
.await?;
trace!(
"Loaded local chat messages: chat_id={}, messages={}",
self.chat_id,
messages.len()
);
// If the number of messages equals the limit, then no need to load more messages from remote
let has_more = !messages.is_empty();
let _ = self
.load_remote_chat_messages(limit, None, after_message_id)
.await;
Ok(ChatMessageListPB {
messages,
has_more,
total: 0,
})
}
async fn load_remote_chat_messages(
&self,
limit: i64,
before_message_id: Option<i64>,
after_message_id: Option<i64>,
) -> FlowyResult<()> {
trace!(
"Loading chat messages from remote: chat_id={}, limit={}, before_message_id={:?}, after_message_id={:?}",
self.chat_id,
limit,
before_message_id,
after_message_id
);
let workspace_id = self.user_service.workspace_id()?;
let chat_id = self.chat_id.clone();
let cloud_service = self.cloud_service.clone();
let user_service = self.user_service.clone();
let uid = self.uid;
let prev_message_state = self.prev_message_state.clone();
let latest_message_id = self.latest_message_id.clone();
tokio::spawn(async move {
let cursor = match (before_message_id, after_message_id) {
(Some(bid), _) => MessageCursor::BeforeMessageId(bid),
(_, Some(aid)) => MessageCursor::AfterMessageId(aid),
_ => MessageCursor::NextBack,
};
match cloud_service
.get_chat_messages(&workspace_id, &chat_id, cursor.clone(), limit as u64)
.await
{
Ok(resp) => {
// Save chat messages to local disk
if let Err(err) = save_chat_message(
user_service.sqlite_connection(uid)?,
&chat_id,
resp.messages.clone(),
) {
error!("Failed to save chat:{} messages: {}", chat_id, err);
}
// Update latest message ID
if !resp.messages.is_empty() {
latest_message_id.store(
resp.messages[0].message_id,
std::sync::atomic::Ordering::Relaxed,
);
}
let pb = ChatMessageListPB::from(resp);
trace!(
"Loaded chat messages from remote: chat_id={}, messages={}",
chat_id,
pb.messages.len()
);
if matches!(cursor, MessageCursor::BeforeMessageId(_)) {
if pb.has_more {
*prev_message_state.write().await = PrevMessageState::HasMore;
} else {
*prev_message_state.write().await = PrevMessageState::NoMore;
}
send_notification(&chat_id, ChatNotification::DidLoadPrevChatMessage)
.payload(pb)
.send();
} else {
send_notification(&chat_id, ChatNotification::DidLoadLatestChatMessage)
.payload(pb)
.send();
}
},
Err(err) => error!("Failed to load chat messages: {}", err),
}
Ok::<(), FlowyError>(())
});
Ok(())
}
pub async fn get_related_question(
&self,
message_id: i64,
) -> Result<RepeatedRelatedQuestionPB, FlowyError> {
let workspace_id = self.user_service.workspace_id()?;
let resp = self
.cloud_service
.get_related_message(&workspace_id, &self.chat_id, message_id)
.await?;
trace!(
"Related messages: chat_id={}, message_id={}, messages:{:?}",
self.chat_id,
message_id,
resp.items
);
Ok(RepeatedRelatedQuestionPB::from(resp))
}
#[instrument(level = "debug", skip_all, err)]
pub async fn generate_answer(&self, question_message_id: i64) -> FlowyResult<ChatMessagePB> {
let workspace_id = self.user_service.workspace_id()?;
let resp = 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);
Ok(pb)
}
async fn load_local_chat_messages(
&self,
limit: i64,
after_message_id: Option<i64>,
before_message_id: Option<i64>,
) -> Result<Vec<ChatMessagePB>, FlowyError> {
let conn = self.user_service.sqlite_connection(self.uid)?;
let records = select_chat_messages(
conn,
&self.chat_id,
limit,
after_message_id,
before_message_id,
)?;
let messages = records
.into_iter()
.map(|record| ChatMessagePB {
message_id: record.message_id,
content: record.content,
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<_>>();
Ok(messages)
}
}
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!("Failed to send chat message: {}", 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,
messages: Vec<ChatMessage>,
) -> FlowyResult<()> {
let records = messages
.into_iter()
.map(|message| 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,
})
.collect::<Vec<_>>();
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(())
}

View File

@ -0,0 +1,190 @@
use flowy_chat_pub::cloud::{
ChatMessage, RelatedQuestion, RepeatedChatMessage, RepeatedRelatedQuestion,
};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use lib_infra::validator_fn::required_not_empty_str;
use validator::Validate;
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct SendChatPayloadPB {
#[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,
}
#[derive(Debug, Default, Clone, ProtoBuf_Enum, PartialEq, Eq, Copy)]
pub enum ChatMessageTypePB {
#[default]
System = 0,
User = 1,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct LoadPrevChatMessagePB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub chat_id: String,
#[pb(index = 2)]
pub limit: i64,
#[pb(index = 4, one_of)]
pub before_message_id: Option<i64>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct LoadNextChatMessagePB {
#[pb(index = 1)]
#[validate(custom = "required_not_empty_str")]
pub chat_id: String,
#[pb(index = 2)]
pub limit: i64,
#[pb(index = 4, one_of)]
pub after_message_id: Option<i64>,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]
pub struct ChatMessageListPB {
#[pb(index = 1)]
pub has_more: bool,
#[pb(index = 2)]
pub messages: Vec<ChatMessagePB>,
/// If the total number of messages is 0, then the total number of messages is unknown.
#[pb(index = 3)]
pub total: i64,
}
impl From<RepeatedChatMessage> for ChatMessageListPB {
fn from(repeated_chat_message: RepeatedChatMessage) -> Self {
let messages = repeated_chat_message
.messages
.into_iter()
.map(ChatMessagePB::from)
.collect();
ChatMessageListPB {
has_more: repeated_chat_message.has_more,
messages,
total: repeated_chat_message.total,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessagePB {
#[pb(index = 1)]
pub message_id: i64,
#[pb(index = 2)]
pub content: String,
#[pb(index = 3)]
pub created_at: i64,
#[pb(index = 4)]
pub author_type: i64,
#[pb(index = 5)]
pub author_id: String,
#[pb(index = 6)]
pub has_following: bool,
#[pb(index = 7, one_of)]
pub reply_message_id: Option<i64>,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessageErrorPB {
#[pb(index = 1)]
pub chat_id: String,
#[pb(index = 2)]
pub content: String,
#[pb(index = 3)]
pub error_message: String,
}
impl From<ChatMessage> for ChatMessagePB {
fn from(chat_message: ChatMessage) -> Self {
ChatMessagePB {
message_id: chat_message.message_id,
content: chat_message.content,
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,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedChatMessagePB {
#[pb(index = 1)]
items: Vec<ChatMessagePB>,
}
impl From<Vec<ChatMessage>> for RepeatedChatMessagePB {
fn from(messages: Vec<ChatMessage>) -> Self {
RepeatedChatMessagePB {
items: messages.into_iter().map(ChatMessagePB::from).collect(),
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChatMessageIdPB {
#[pb(index = 1)]
pub chat_id: String,
#[pb(index = 2)]
pub message_id: i64,
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RelatedQuestionPB {
#[pb(index = 1)]
pub content: String,
}
impl From<RelatedQuestion> for RelatedQuestionPB {
fn from(value: RelatedQuestion) -> Self {
RelatedQuestionPB {
content: value.content,
}
}
}
#[derive(Debug, Clone, Default, ProtoBuf)]
pub struct RepeatedRelatedQuestionPB {
#[pb(index = 1)]
pub message_id: i64,
#[pb(index = 2)]
pub items: Vec<RelatedQuestionPB>,
}
impl From<RepeatedRelatedQuestion> for RepeatedRelatedQuestionPB {
fn from(value: RepeatedRelatedQuestion) -> Self {
RepeatedRelatedQuestionPB {
message_id: value.message_id,
items: value
.items
.into_iter()
.map(RelatedQuestionPB::from)
.collect(),
}
}
}

View File

@ -0,0 +1,93 @@
use flowy_chat_pub::cloud::ChatMessageType;
use std::sync::{Arc, Weak};
use validator::Validate;
use flowy_error::{FlowyError, FlowyResult};
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
use crate::entities::*;
use crate::manager::ChatManager;
fn upgrade_chat_manager(
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> FlowyResult<Arc<ChatManager>> {
let chat_manager = chat_manager
.upgrade()
.ok_or(FlowyError::internal().with_context("The chat manager is already dropped"))?;
Ok(chat_manager)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn send_chat_message_handler(
data: AFPluginData<SendChatPayloadPB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> Result<(), FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
data.validate()?;
let message_type = match data.message_type {
ChatMessageTypePB::System => ChatMessageType::System,
ChatMessageTypePB::User => ChatMessageType::User,
};
chat_manager
.send_chat_message(&data.chat_id, &data.message, message_type)
.await?;
Ok(())
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn load_prev_message_handler(
data: AFPluginData<LoadPrevChatMessagePB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<ChatMessageListPB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
data.validate()?;
let messages = chat_manager
.load_prev_chat_messages(&data.chat_id, data.limit, data.before_message_id)
.await?;
data_result_ok(messages)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn load_next_message_handler(
data: AFPluginData<LoadNextChatMessagePB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<ChatMessageListPB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
data.validate()?;
let messages = chat_manager
.load_latest_chat_messages(&data.chat_id, data.limit, data.after_message_id)
.await?;
data_result_ok(messages)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_related_question_handler(
data: AFPluginData<ChatMessageIdPB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<RepeatedRelatedQuestionPB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
let messages = chat_manager
.get_related_questions(&data.chat_id, data.message_id)
.await?;
data_result_ok(messages)
}
#[tracing::instrument(level = "debug", skip_all, err)]
pub(crate) async fn get_answer_handler(
data: AFPluginData<ChatMessageIdPB>,
chat_manager: AFPluginState<Weak<ChatManager>>,
) -> DataResult<ChatMessagePB, FlowyError> {
let chat_manager = upgrade_chat_manager(chat_manager)?;
let data = data.into_inner();
let message = chat_manager
.generate_answer(&data.chat_id, data.message_id)
.await?;
data_result_ok(message)
}

View File

@ -0,0 +1,40 @@
use std::sync::Weak;
use strum_macros::Display;
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
use lib_dispatch::prelude::*;
use crate::event_handler::*;
use crate::manager::ChatManager;
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::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)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
#[event_err = "FlowyError"]
pub enum ChatEvent {
/// Create a new workspace
#[event(input = "LoadPrevChatMessagePB", output = "ChatMessageListPB")]
LoadPrevMessage = 0,
#[event(input = "LoadNextChatMessagePB", output = "ChatMessageListPB")]
LoadNextMessage = 1,
#[event(input = "SendChatPayloadPB")]
SendMessage = 2,
#[event(input = "ChatMessageIdPB", output = "RepeatedRelatedQuestionPB")]
GetRelatedQuestion = 3,
#[event(input = "ChatMessageIdPB", output = "ChatMessagePB")]
GetAnswerForQuestion = 4,
}

View File

@ -0,0 +1,9 @@
mod event_handler;
pub mod event_map;
mod chat;
pub mod entities;
pub mod manager;
pub mod notification;
mod persistence;
mod protobuf;

View File

@ -0,0 +1,182 @@
use crate::chat::Chat;
use crate::entities::{ChatMessageListPB, ChatMessagePB, RepeatedRelatedQuestionPB};
use crate::persistence::{insert_chat, ChatTable};
use dashmap::DashMap;
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessageType};
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::DBConnection;
use lib_infra::util::timestamp;
use std::sync::Arc;
use tracing::{instrument, trace};
pub trait ChatUserService: Send + Sync + 'static {
fn user_id(&self) -> Result<i64, FlowyError>;
fn device_id(&self) -> Result<String, FlowyError>;
fn workspace_id(&self) -> Result<String, FlowyError>;
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError>;
}
pub struct ChatManager {
cloud_service: Arc<dyn ChatCloudService>,
user_service: Arc<dyn ChatUserService>,
chats: Arc<DashMap<String, Arc<Chat>>>,
}
impl ChatManager {
pub fn new(
cloud_service: Arc<dyn ChatCloudService>,
user_service: impl ChatUserService,
) -> ChatManager {
let user_service = Arc::new(user_service);
Self {
cloud_service,
user_service,
chats: Arc::new(DashMap::new()),
}
}
pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
trace!("open chat: {}", chat_id);
self.chats.entry(chat_id.to_string()).or_insert_with(|| {
Arc::new(Chat::new(
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
))
});
Ok(())
}
pub async fn close_chat(&self, _chat_id: &str) -> Result<(), FlowyError> {
Ok(())
}
pub async fn delete_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
if let Some((_, chat)) = self.chats.remove(chat_id) {
chat.close();
}
Ok(())
}
pub async fn create_chat(&self, uid: &i64, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
let workspace_id = self.user_service.workspace_id()?;
self
.cloud_service
.create_chat(uid, &workspace_id, chat_id)
.await?;
save_chat(self.user_service.sqlite_connection(*uid)?, chat_id)?;
let chat = Arc::new(Chat::new(
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
Ok(chat)
}
#[instrument(level = "info", skip_all, err)]
pub async fn send_chat_message(
&self,
chat_id: &str,
message: &str,
message_type: ChatMessageType,
) -> Result<(), FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
chat.send_chat_message(message, message_type).await?;
Ok(())
}
pub async fn get_or_create_chat_instance(&self, chat_id: &str) -> Result<Arc<Chat>, FlowyError> {
let chat = self.chats.get(chat_id).as_deref().cloned();
match chat {
None => {
let chat = Arc::new(Chat::new(
self.user_service.user_id().unwrap(),
chat_id.to_string(),
self.user_service.clone(),
self.cloud_service.clone(),
));
self.chats.insert(chat_id.to_string(), chat.clone());
Ok(chat)
},
Some(chat) => Ok(chat),
}
}
/// Load chat messages for a given `chat_id`.
///
/// 1. When opening a chat:
/// - Loads local chat messages.
/// - `after_message_id` and `before_message_id` are `None`.
/// - Spawns a task to load messages from the remote server, notifying the user when the remote messages are loaded.
///
/// 2. Loading more messages in an existing chat with `after_message_id`:
/// - `after_message_id` is the last message ID in the current chat messages.
///
/// 3. Loading more messages in an existing chat with `before_message_id`:
/// - `before_message_id` is the first message ID in the current chat messages.
///
/// 4. `after_message_id` and `before_message_id` cannot be specified at the same time.
pub async fn load_prev_chat_messages(
&self,
chat_id: &str,
limit: i64,
before_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
let list = chat
.load_prev_chat_messages(limit, before_message_id)
.await?;
Ok(list)
}
pub async fn load_latest_chat_messages(
&self,
chat_id: &str,
limit: i64,
after_message_id: Option<i64>,
) -> Result<ChatMessageListPB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
let list = chat
.load_latest_chat_messages(limit, after_message_id)
.await?;
Ok(list)
}
pub async fn get_related_questions(
&self,
chat_id: &str,
message_id: i64,
) -> Result<RepeatedRelatedQuestionPB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
let resp = chat.get_related_question(message_id).await?;
Ok(resp)
}
pub async fn generate_answer(
&self,
chat_id: &str,
question_message_id: i64,
) -> Result<ChatMessagePB, FlowyError> {
let chat = self.get_or_create_chat_instance(chat_id).await?;
let resp = chat.generate_answer(question_message_id).await?;
Ok(resp)
}
}
fn save_chat(conn: DBConnection, chat_id: &str) -> FlowyResult<()> {
let row = ChatTable {
chat_id: chat_id.to_string(),
created_at: timestamp(),
name: "".to_string(),
};
insert_chat(conn, &row)?;
Ok(())
}

View File

@ -0,0 +1,40 @@
use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder;
const CHAT_OBSERVABLE_SOURCE: &str = "Chat";
#[derive(ProtoBuf_Enum, Debug, Default)]
pub enum ChatNotification {
#[default]
Unknown = 0,
DidLoadLatestChatMessage = 1,
DidLoadPrevChatMessage = 2,
DidReceiveChatMessage = 3,
StreamChatMessageError = 4,
FinishAnswerQuestion = 5,
LastUserSentMessage = 6,
}
impl std::convert::From<ChatNotification> for i32 {
fn from(notification: ChatNotification) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for ChatNotification {
fn from(notification: i32) -> Self {
match notification {
1 => ChatNotification::DidLoadLatestChatMessage,
2 => ChatNotification::DidLoadPrevChatMessage,
3 => ChatNotification::DidReceiveChatMessage,
4 => ChatNotification::StreamChatMessageError,
5 => ChatNotification::FinishAnswerQuestion,
6 => ChatNotification::LastUserSentMessage,
_ => ChatNotification::Unknown,
}
}
}
#[tracing::instrument(level = "trace")]
pub(crate) fn send_notification(id: &str, ty: ChatNotification) -> NotificationBuilder {
NotificationBuilder::new(id, ty, CHAT_OBSERVABLE_SOURCE)
}

View File

@ -0,0 +1,106 @@
use flowy_error::{FlowyError, FlowyResult};
use flowy_sqlite::upsert::excluded;
use flowy_sqlite::{
diesel, insert_into,
query_dsl::*,
schema::{chat_message_table, chat_message_table::dsl},
DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable,
};
#[derive(Queryable, Insertable, Identifiable)]
#[diesel(table_name = chat_message_table)]
#[diesel(primary_key(message_id))]
pub struct ChatMessageTable {
pub message_id: i64,
pub chat_id: String,
pub content: String,
pub created_at: i64,
pub author_type: i64,
pub author_id: String,
pub reply_message_id: Option<i64>,
}
pub fn insert_chat_messages(
mut conn: DBConnection,
new_messages: &[ChatMessageTable],
) -> FlowyResult<()> {
conn.immediate_transaction(|conn| {
for message in new_messages {
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 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,
limit_val: i64,
after_message_id: Option<i64>,
before_message_id: Option<i64>,
) -> QueryResult<Vec<ChatMessageTable>> {
let mut query = dsl::chat_message_table
.filter(chat_message_table::chat_id.eq(chat_id_val))
.into_boxed();
if let Some(after_message_id) = after_message_id {
query = query.filter(chat_message_table::message_id.gt(after_message_id));
}
if let Some(before_message_id) = before_message_id {
query = query.filter(chat_message_table::message_id.lt(before_message_id));
}
query = query
.order((chat_message_table::message_id.desc(),))
.limit(limit_val);
let messages: Vec<ChatMessageTable> = query.load::<ChatMessageTable>(&mut *conn)?;
Ok(messages)
}

View File

@ -0,0 +1,52 @@
use flowy_sqlite::upsert::excluded;
use flowy_sqlite::{
diesel,
query_dsl::*,
schema::{chat_table, chat_table::dsl},
DBConnection, ExpressionMethods, Identifiable, Insertable, QueryResult, Queryable,
};
#[derive(Clone, Default, Queryable, Insertable, Identifiable)]
#[diesel(table_name = chat_table)]
#[diesel(primary_key(chat_id))]
pub struct ChatTable {
pub chat_id: String,
pub created_at: i64,
pub name: String,
}
pub fn insert_chat(mut conn: DBConnection, new_chat: &ChatTable) -> QueryResult<usize> {
diesel::insert_into(chat_table::table)
.values(new_chat)
.on_conflict(chat_table::chat_id)
.do_update()
.set((
chat_table::created_at.eq(excluded(chat_table::created_at)),
chat_table::name.eq(excluded(chat_table::name)),
))
.execute(&mut *conn)
}
#[allow(dead_code)]
pub fn read_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<ChatTable> {
let row = dsl::chat_table
.filter(chat_table::chat_id.eq(chat_id_val))
.first::<ChatTable>(&mut *conn)?;
Ok(row)
}
#[allow(dead_code)]
pub fn update_chat_name(
mut conn: DBConnection,
chat_id_val: &str,
new_name: &str,
) -> QueryResult<usize> {
diesel::update(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val)))
.set(chat_table::name.eq(new_name))
.execute(&mut *conn)
}
#[allow(dead_code)]
pub fn delete_chat(mut conn: DBConnection, chat_id_val: &str) -> QueryResult<usize> {
diesel::delete(dsl::chat_table.filter(chat_table::chat_id.eq(chat_id_val))).execute(&mut *conn)
}

View File

@ -0,0 +1,5 @@
mod chat_message_sql;
mod chat_sql;
pub use chat_message_sql::*;
pub use chat_sql::*;

View File

@ -31,6 +31,8 @@ diesel.workspace = true
uuid.workspace = true
flowy-storage = { workspace = true }
client-api.workspace = true
flowy-chat = { workspace = true }
flowy-chat-pub = { workspace = true }
tracing.workspace = true
futures-core = { version = "0.3", default-features = false }
@ -61,6 +63,7 @@ dart = [
"flowy-search/dart",
"flowy-folder/dart",
"flowy-database2/dart",
"flowy-chat/dart",
]
ts = [
"flowy-user/tauri_ts",
@ -68,6 +71,7 @@ ts = [
"flowy-search/tauri_ts",
"flowy-database2/ts",
"flowy-config/tauri_ts",
"flowy-chat/tauri_ts",
]
openssl_vendored = ["flowy-sqlite/openssl_vendored"]

View File

@ -0,0 +1,47 @@
use flowy_chat::manager::{ChatManager, ChatUserService};
use flowy_chat_pub::cloud::ChatCloudService;
use flowy_error::FlowyError;
use flowy_sqlite::DBConnection;
use flowy_user::services::authenticate_user::AuthenticateUser;
use std::sync::{Arc, Weak};
pub struct ChatDepsResolver;
impl ChatDepsResolver {
pub fn resolve(
authenticate_user: Weak<AuthenticateUser>,
cloud_service: Arc<dyn ChatCloudService>,
) -> Arc<ChatManager> {
let user_service = ChatUserServiceImpl(authenticate_user);
Arc::new(ChatManager::new(cloud_service, user_service))
}
}
struct ChatUserServiceImpl(Weak<AuthenticateUser>);
impl ChatUserServiceImpl {
fn upgrade_user(&self) -> Result<Arc<AuthenticateUser>, FlowyError> {
let user = self
.0
.upgrade()
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?;
Ok(user)
}
}
impl ChatUserService for ChatUserServiceImpl {
fn user_id(&self) -> Result<i64, FlowyError> {
self.upgrade_user()?.user_id()
}
fn device_id(&self) -> Result<String, FlowyError> {
self.upgrade_user()?.device_id()
}
fn workspace_id(&self) -> Result<String, FlowyError> {
self.upgrade_user()?.workspace_id()
}
fn sqlite_connection(&self, uid: i64) -> Result<DBConnection, FlowyError> {
self.upgrade_user()?.get_sqlite_connection(uid)
}
}

View File

@ -1,6 +1,7 @@
use bytes::Bytes;
use collab_integrate::collab_builder::AppFlowyCollabBuilder;
use collab_integrate::CollabKVDB;
use flowy_chat::manager::ChatManager;
use flowy_database2::entities::DatabaseLayoutPB;
use flowy_database2::services::share::csv::CSVFormat;
use flowy_database2::template::{make_default_board, make_default_calendar, make_default_grid};
@ -9,10 +10,11 @@ use flowy_document::entities::DocumentDataPB;
use flowy_document::manager::DocumentManager;
use flowy_document::parser::json::parser::JsonToDocumentParser;
use flowy_error::FlowyError;
use flowy_folder::entities::ViewLayoutPB;
use flowy_folder::manager::{FolderManager, FolderUser};
use flowy_folder::share::ImportType;
use flowy_folder::view_operation::{FolderOperationHandler, FolderOperationHandlers, View};
use flowy_folder::view_operation::{
FolderOperationHandler, FolderOperationHandlers, View, ViewData,
};
use flowy_folder::ViewLayout;
use flowy_folder_pub::folder_builder::NestedViewBuilder;
use flowy_search::folder::indexer::FolderIndexManagerImpl;
@ -35,12 +37,17 @@ impl FolderDepsResolver {
collab_builder: Arc<AppFlowyCollabBuilder>,
server_provider: Arc<ServerProvider>,
folder_indexer: Arc<FolderIndexManagerImpl>,
chat_manager: &Arc<ChatManager>,
) -> Arc<FolderManager> {
let user: Arc<dyn FolderUser> = Arc::new(FolderUserImpl {
authenticate_user: authenticate_user.clone(),
});
let handlers = folder_operation_handlers(document_manager.clone(), database_manager.clone());
let handlers = folder_operation_handlers(
document_manager.clone(),
database_manager.clone(),
chat_manager.clone(),
);
Arc::new(
FolderManager::new(
user.clone(),
@ -58,6 +65,7 @@ impl FolderDepsResolver {
fn folder_operation_handlers(
document_manager: Arc<DocumentManager>,
database_manager: Arc<DatabaseManager>,
chat_manager: Arc<ChatManager>,
) -> FolderOperationHandlers {
let mut map: HashMap<ViewLayout, Arc<dyn FolderOperationHandler + Send + Sync>> = HashMap::new();
@ -65,9 +73,11 @@ fn folder_operation_handlers(
map.insert(ViewLayout::Document, document_folder_operation);
let database_folder_operation = Arc::new(DatabaseFolderOperation(database_manager));
let chat_folder_operation = Arc::new(ChatFolderOperation(chat_manager));
map.insert(ViewLayout::Board, database_folder_operation.clone());
map.insert(ViewLayout::Grid, database_folder_operation.clone());
map.insert(ViewLayout::Calendar, database_folder_operation);
map.insert(ViewLayout::Chat, chat_folder_operation);
Arc::new(map)
}
@ -315,7 +325,15 @@ impl FolderOperationHandler for DatabaseFolderOperation {
},
Some(params) => {
let database_manager = self.0.clone();
let layout = layout_type_from_view_layout(layout.into());
let layout = match layout {
ViewLayout::Board => DatabaseLayoutPB::Board,
ViewLayout::Calendar => DatabaseLayoutPB::Calendar,
ViewLayout::Grid => DatabaseLayoutPB::Grid,
ViewLayout::Document | ViewLayout::Chat => {
return FutureResult::new(async move { Err(FlowyError::not_support()) });
},
};
let name = name.to_string();
let database_view_id = view_id.to_string();
@ -351,6 +369,10 @@ impl FolderOperationHandler for DatabaseFolderOperation {
Err(FlowyError::internal().with_context(format!("Can't handle {:?} layout type", layout)))
});
},
ViewLayout::Chat => {
// TODO(nathan): AI
todo!("AI")
},
};
FutureResult::new(async move {
let result = database_manager.create_database_with_params(data).await;
@ -413,7 +435,7 @@ impl FolderOperationHandler for DatabaseFolderOperation {
fn did_update_view(&self, old: &View, new: &View) -> FutureResult<(), FlowyError> {
let database_layout = match new.layout {
ViewLayout::Document => {
ViewLayout::Document | ViewLayout::Chat => {
return FutureResult::new(async {
Err(FlowyError::internal().with_context("Can't handle document layout type"))
});
@ -450,11 +472,83 @@ impl CreateDatabaseExtParams {
}
}
pub fn layout_type_from_view_layout(layout: ViewLayoutPB) -> DatabaseLayoutPB {
match layout {
ViewLayoutPB::Grid => DatabaseLayoutPB::Grid,
ViewLayoutPB::Board => DatabaseLayoutPB::Board,
ViewLayoutPB::Calendar => DatabaseLayoutPB::Calendar,
ViewLayoutPB::Document => DatabaseLayoutPB::Grid,
struct ChatFolderOperation(Arc<ChatManager>);
impl FolderOperationHandler for ChatFolderOperation {
fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.open_chat(&view_id).await?;
Ok(())
})
}
fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.close_chat(&view_id).await?;
Ok(())
})
}
fn delete_view(&self, view_id: &str) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.delete_chat(&view_id).await?;
Ok(())
})
}
fn duplicate_view(&self, _view_id: &str) -> FutureResult<ViewData, FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn create_view_with_view_data(
&self,
_user_id: i64,
_view_id: &str,
_name: &str,
_data: Vec<u8>,
_layout: ViewLayout,
_meta: HashMap<String, String>,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn create_built_in_view(
&self,
user_id: i64,
view_id: &str,
_name: &str,
_layout: ViewLayout,
) -> FutureResult<(), FlowyError> {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.create_chat(&user_id, &view_id).await?;
Ok(())
})
}
fn import_from_bytes(
&self,
_uid: i64,
_view_id: &str,
_name: &str,
_import_type: ImportType,
_bytes: Vec<u8>,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
fn import_from_file_path(
&self,
_view_id: &str,
_name: &str,
_path: String,
) -> FutureResult<(), FlowyError> {
FutureResult::new(async move { Err(FlowyError::not_support()) })
}
}

View File

@ -1,3 +1,4 @@
pub use chat_deps::*;
pub use collab_deps::*;
pub use database_deps::*;
pub use document_deps::*;
@ -9,6 +10,7 @@ mod collab_deps;
mod document_deps;
mod folder_deps;
mod chat_deps;
mod database_deps;
mod search_deps;
mod user_deps;

View File

@ -52,6 +52,7 @@ pub fn create_log_filter(level: String, with_crates: Vec<String>, platform: Plat
filters.push(format!("flowy_notification={}", "info"));
filters.push(format!("lib_infra={}", level));
filters.push(format!("flowy_search={}", level));
filters.push(format!("flowy_chat={}", level));
// Enable the frontend logs. DO NOT DISABLE.
// These logs are essential for debugging and verifying frontend behavior.
filters.push(format!("dart_ffi={}", level));

View File

@ -3,8 +3,10 @@ use std::sync::Arc;
use anyhow::Error;
use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin};
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::ChatMessageType;
use collab::core::origin::{CollabClient, CollabOrigin};
use collab::preclude::CollabPlugin;
use collab_entity::CollabType;
use collab_plugins::cloud_storage::postgres::SupabaseDBPlugin;
@ -14,6 +16,9 @@ use tracing::debug;
use collab_integrate::collab_builder::{
CollabCloudPluginProvider, CollabPluginProviderContext, CollabPluginProviderType,
};
use flowy_chat_pub::cloud::{
ChatCloudService, ChatMessage, ChatMessageStream, MessageCursor, RepeatedChatMessage,
};
use flowy_database_pub::cloud::{
CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot, SummaryRowContent,
};
@ -28,6 +33,7 @@ use flowy_server_pub::supabase_config::SupabaseConfiguration;
use flowy_storage::ObjectValue;
use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider};
use flowy_user_pub::entities::{Authenticator, UserTokenState};
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
use crate::integrate::server::{Server, ServerProvider};
@ -372,7 +378,12 @@ impl CollabCloudPluginProvider for ServerProvider {
collab_object.uid,
collab_object.device_id.clone(),
));
let sync_object = SyncObject::from(collab_object);
let sync_object = SyncObject::new(
&collab_object.object_id,
&collab_object.workspace_id,
collab_object.collab_type,
&collab_object.device_id,
);
let (sink, stream) = (channel.sink(), channel.stream());
let sink_config = SinkConfig::new().send_timeout(8);
let sync_plugin = SyncPlugin::new(
@ -427,3 +438,93 @@ impl CollabCloudPluginProvider for ServerProvider {
*self.user_enable_sync.read()
}
}
#[async_trait]
impl ChatCloudService for ServerProvider {
fn create_chat(
&self,
uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), FlowyError> {
let workspace_id = workspace_id.to_string();
let server = self.get_server();
let chat_id = chat_id.to_string();
let uid = *uid;
FutureResult::new(async move {
server?
.chat_service()
.create_chat(&uid, &workspace_id, &chat_id)
.await
})
}
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
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 server = self.get_server()?;
server
.chat_service()
.send_chat_message(&workspace_id, &chat_id, &message, message_type)
.await
}
fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
offset: MessageCursor,
limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.get_chat_messages(&workspace_id, &chat_id, offset, limit)
.await
})
}
fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.get_related_message(&workspace_id, &chat_id, message_id)
.await
})
}
fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_id: i64,
) -> FutureResult<ChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let server = self.get_server();
FutureResult::new(async move {
server?
.chat_service()
.generate_answer(&workspace_id, &chat_id, question_message_id)
.await
})
}
}

View File

@ -10,6 +10,7 @@ use tokio::sync::RwLock;
use tracing::{debug, error, event, info, instrument};
use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabPluginProviderType};
use flowy_chat::manager::ChatManager;
use flowy_database2::DatabaseManager;
use flowy_document::manager::DocumentManager;
use flowy_error::{FlowyError, FlowyResult};
@ -57,6 +58,7 @@ pub struct AppFlowyCore {
pub task_dispatcher: Arc<RwLock<TaskDispatcher>>,
pub store_preference: Arc<StorePreferences>,
pub search_manager: Arc<SearchManager>,
pub chat_manager: Arc<ChatManager>,
}
impl AppFlowyCore {
@ -137,6 +139,7 @@ impl AppFlowyCore {
document_manager,
collab_builder,
search_manager,
chat_manager,
) = async {
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [CollabPluginConfig].
@ -164,6 +167,8 @@ impl AppFlowyCore {
Arc::downgrade(&(server_provider.clone() as Arc<dyn ObjectStorageService>)),
);
let chat_manager =
ChatDepsResolver::resolve(Arc::downgrade(&authenticate_user), server_provider.clone());
let folder_indexer = Arc::new(FolderIndexManagerImpl::new(None));
let folder_manager = FolderDepsResolver::resolve(
Arc::downgrade(&authenticate_user),
@ -172,6 +177,7 @@ impl AppFlowyCore {
collab_builder.clone(),
server_provider.clone(),
folder_indexer.clone(),
&chat_manager,
)
.await;
@ -195,6 +201,7 @@ impl AppFlowyCore {
document_manager,
collab_builder,
search_manager,
chat_manager,
)
}
.await;
@ -230,6 +237,7 @@ impl AppFlowyCore {
Arc::downgrade(&user_manager),
Arc::downgrade(&document_manager),
Arc::downgrade(&search_manager),
Arc::downgrade(&chat_manager),
),
));
@ -244,6 +252,7 @@ impl AppFlowyCore {
task_dispatcher,
store_preference,
search_manager,
chat_manager,
}
}

View File

@ -1,3 +1,4 @@
use flowy_chat::manager::ChatManager;
use std::sync::Weak;
use flowy_database2::DatabaseManager;
@ -13,6 +14,7 @@ pub fn make_plugins(
user_session: Weak<UserManager>,
document_manager2: Weak<DocumentManager2>,
search_manager: Weak<SearchManager>,
chat_manager: Weak<ChatManager>,
) -> Vec<AFPlugin> {
let store_preferences = user_session
.upgrade()
@ -25,6 +27,7 @@ pub fn make_plugins(
let config_plugin = flowy_config::event_map::init(store_preferences);
let date_plugin = flowy_date::event_map::init();
let search_plugin = flowy_search::event_map::init(search_manager);
let chat_plugin = flowy_chat::event_map::init(chat_manager);
vec![
user_plugin,
folder_plugin,
@ -33,5 +36,6 @@ pub fn make_plugins(
config_plugin,
date_plugin,
search_plugin,
chat_plugin,
]
}

View File

@ -136,6 +136,7 @@ pub enum ViewLayoutPB {
Grid = 1,
Board = 2,
Calendar = 3,
Chat = 4,
}
impl ViewLayoutPB {
@ -154,6 +155,7 @@ impl std::convert::From<ViewLayout> for ViewLayoutPB {
ViewLayout::Board => ViewLayoutPB::Board,
ViewLayout::Document => ViewLayoutPB::Document,
ViewLayout::Calendar => ViewLayoutPB::Calendar,
ViewLayout::Chat => ViewLayoutPB::Chat,
}
}
}

View File

@ -796,7 +796,10 @@ impl FolderManager {
if let Some(view) = &view {
let view_layout: ViewLayout = view.layout.clone().into();
if let Some(handle) = self.operation_handlers.get(&view_layout) {
let _ = handle.open_view(view_id).await;
info!("Open view: {}", view.id);
if let Err(err) = handle.open_view(view_id).await {
error!("Open view error: {:?}", err);
}
}
}

View File

@ -115,6 +115,7 @@ impl From<ViewLayoutPB> for ViewLayout {
ViewLayoutPB::Grid => ViewLayout::Grid,
ViewLayoutPB::Board => ViewLayout::Board,
ViewLayoutPB::Calendar => ViewLayout::Calendar,
ViewLayoutPB::Chat => ViewLayout::Chat,
}
}
}

View File

@ -41,6 +41,7 @@ flowy-error = { workspace = true, features = ["impl_from_serde", "impl_from_reqw
flowy-server-pub = { workspace = true }
flowy-encrypt = { workspace = true }
flowy-storage = { workspace = true }
flowy-chat-pub = { workspace = true }
mime_guess = "2.0"
url = "2.4"
tokio-util = "0.7"

View File

@ -0,0 +1,128 @@
use crate::af_cloud::AFServer;
use client_api::entity::ai_dto::RepeatedRelatedQuestion;
use client_api::entity::{
CreateChatMessageParams, CreateChatParams, MessageCursor, RepeatedChatMessage,
};
use flowy_chat_pub::cloud::{ChatCloudService, ChatMessage, ChatMessageStream, ChatMessageType};
use flowy_error::FlowyError;
use futures_util::StreamExt;
use lib_infra::async_trait::async_trait;
use lib_infra::future::FutureResult;
pub(crate) struct AFCloudChatCloudServiceImpl<T> {
pub inner: T,
}
#[async_trait]
impl<T> ChatCloudService for AFCloudChatCloudServiceImpl<T>
where
T: AFServer,
{
fn create_chat(
&self,
_uid: &i64,
workspace_id: &str,
chat_id: &str,
) -> FutureResult<(), FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let params = CreateChatParams {
chat_id,
name: "".to_string(),
rag_ids: vec![],
};
try_get_client?
.create_chat(&workspace_id, params)
.await
.map_err(FlowyError::from)?;
Ok(())
})
}
async fn send_chat_message(
&self,
workspace_id: &str,
chat_id: &str,
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,
message_type,
};
let stream = try_get_client?
.create_chat_message(&workspace_id, &chat_id, params)
.await
.map_err(FlowyError::from)?;
Ok(stream.boxed())
}
fn get_chat_messages(
&self,
workspace_id: &str,
chat_id: &str,
offset: MessageCursor,
limit: u64,
) -> FutureResult<RepeatedChatMessage, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let resp = try_get_client?
.get_chat_messages(&workspace_id, &chat_id, offset, limit)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
fn get_related_message(
&self,
workspace_id: &str,
chat_id: &str,
message_id: i64,
) -> FutureResult<RepeatedRelatedQuestion, FlowyError> {
let workspace_id = workspace_id.to_string();
let chat_id = chat_id.to_string();
let try_get_client = self.inner.try_get_client();
FutureResult::new(async move {
let resp = try_get_client?
.get_chat_related_question(&workspace_id, &chat_id, message_id)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
fn generate_answer(
&self,
workspace_id: &str,
chat_id: &str,
question_message_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();
FutureResult::new(async move {
let resp = try_get_client?
.generate_question_answer(&workspace_id, &chat_id, question_message_id)
.await
.map_err(FlowyError::from)?;
Ok(resp)
})
}
}

View File

@ -42,10 +42,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: object_id.clone(),
collab_type: collab_type.clone(),
},
inner: QueryCollab::new(object_id.clone(), collab_type.clone()),
};
match try_get_client?.get_collab(params).await {
Ok(data) => {
@ -81,10 +78,7 @@ where
let client = try_get_client?;
let params = object_ids
.into_iter()
.map(|object_id| QueryCollab {
object_id,
collab_type: object_ty.clone(),
})
.map(|object_id| QueryCollab::new(object_id, object_ty.clone()))
.collect();
let results = client.batch_get_collab(&workspace_id, params).await?;
check_request_workspace_id_is_match(

View File

@ -37,10 +37,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: document_id.to_string(),
collab_type: CollabType::Document,
},
inner: QueryCollab::new(document_id.to_string(), CollabType::Document),
};
let doc_state = try_get_client?
.get_collab(params)
@ -82,10 +79,7 @@ where
FutureResult::new(async move {
let params = QueryCollabParams {
workspace_id: workspace_id.clone(),
inner: QueryCollab {
object_id: document_id.clone(),
collab_type: CollabType::Document,
},
inner: QueryCollab::new(document_id.clone(), CollabType::Document),
};
let doc_state = try_get_client?
.get_collab(params)

Some files were not shown because too many files have changed in this diff Show More