mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
4d42c9ea68
commit
aec7bc847e
2
.github/workflows/flutter_ci.yaml
vendored
2
.github/workflows/flutter_ci.yaml
vendored
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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',
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
114
frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart
Normal file
114
frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart
Normal 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];
|
||||
}
|
332
frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart
Normal file
332
frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart
Normal 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));
|
||||
}
|
||||
}
|
@ -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());
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -54,6 +54,7 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
|
||||
valueListenable: _layoutTypeChangeNotifier,
|
||||
builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget(
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
context: PluginContext(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
@ -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'),
|
||||
);
|
||||
|
@ -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}) {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
};
|
||||
}
|
||||
|
@ -153,6 +153,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
final homeStack = HomeStack(
|
||||
layout: layout,
|
||||
delegate: DesktopHomeScreenStackAdaptor(context),
|
||||
userProfile: userProfile,
|
||||
);
|
||||
final menu = _buildHomeSidebar(
|
||||
context,
|
||||
|
@ -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,
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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!);
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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',
|
||||
|
125
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
125
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
92
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
92
frontend/appflowy_web/wasm-libs/Cargo.lock
generated
@ -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"
|
||||
|
@ -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" }
|
109
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
109
frontend/appflowy_web_app/src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
4
frontend/resources/flowy_icons/16x/chat_ai_page.svg
Normal file
4
frontend/resources/flowy_icons/16x/chat_ai_page.svg
Normal 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 |
13
frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg
Normal file
13
frontend/resources/flowy_icons/16x/flowy_ai_chat_logo.svg
Normal 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 |
@ -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",
|
||||
|
136
frontend/rust-lib/Cargo.lock
generated
136
frontend/rust-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -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" }
|
||||
|
@ -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"] }
|
||||
|
@ -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"]
|
||||
|
89
frontend/rust-lib/event-integration-test/src/chat_event.rs
Normal file
89
frontend/rust-lib/event-integration-test/src/chat_event.rs
Normal 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>()
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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(¤t_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(
|
||||
¤t_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(¤t_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(
|
||||
¤t_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(¤t_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);
|
||||
}
|
@ -0,0 +1 @@
|
||||
mod chat_message_test;
|
@ -6,3 +6,5 @@ mod folder;
|
||||
// mod search;
|
||||
mod user;
|
||||
pub mod util;
|
||||
|
||||
mod chat;
|
||||
|
12
frontend/rust-lib/flowy-chat-pub/Cargo.toml
Normal file
12
frontend/rust-lib/flowy-chat-pub/Cargo.toml
Normal 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
|
50
frontend/rust-lib/flowy-chat-pub/src/cloud.rs
Normal file
50
frontend/rust-lib/flowy-chat-pub/src/cloud.rs
Normal 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>;
|
||||
}
|
1
frontend/rust-lib/flowy-chat-pub/src/lib.rs
Normal file
1
frontend/rust-lib/flowy-chat-pub/src/lib.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod cloud;
|
35
frontend/rust-lib/flowy-chat/Cargo.toml
Normal file
35
frontend/rust-lib/flowy-chat/Cargo.toml
Normal 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"]
|
3
frontend/rust-lib/flowy-chat/Flowy.toml
Normal file
3
frontend/rust-lib/flowy-chat/Flowy.toml
Normal 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"]
|
40
frontend/rust-lib/flowy-chat/build.rs
Normal file
40
frontend/rust-lib/flowy-chat/build.rs
Normal 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(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
471
frontend/rust-lib/flowy-chat/src/chat.rs
Normal file
471
frontend/rust-lib/flowy-chat/src/chat.rs
Normal 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(())
|
||||
}
|
190
frontend/rust-lib/flowy-chat/src/entities.rs
Normal file
190
frontend/rust-lib/flowy-chat/src/entities.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
93
frontend/rust-lib/flowy-chat/src/event_handler.rs
Normal file
93
frontend/rust-lib/flowy-chat/src/event_handler.rs
Normal 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)
|
||||
}
|
40
frontend/rust-lib/flowy-chat/src/event_map.rs
Normal file
40
frontend/rust-lib/flowy-chat/src/event_map.rs
Normal 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,
|
||||
}
|
9
frontend/rust-lib/flowy-chat/src/lib.rs
Normal file
9
frontend/rust-lib/flowy-chat/src/lib.rs
Normal 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;
|
182
frontend/rust-lib/flowy-chat/src/manager.rs
Normal file
182
frontend/rust-lib/flowy-chat/src/manager.rs
Normal 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(())
|
||||
}
|
40
frontend/rust-lib/flowy-chat/src/notification.rs
Normal file
40
frontend/rust-lib/flowy-chat/src/notification.rs
Normal 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)
|
||||
}
|
106
frontend/rust-lib/flowy-chat/src/persistence/chat_message_sql.rs
Normal file
106
frontend/rust-lib/flowy-chat/src/persistence/chat_message_sql.rs
Normal 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)
|
||||
}
|
52
frontend/rust-lib/flowy-chat/src/persistence/chat_sql.rs
Normal file
52
frontend/rust-lib/flowy-chat/src/persistence/chat_sql.rs
Normal 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)
|
||||
}
|
5
frontend/rust-lib/flowy-chat/src/persistence/mod.rs
Normal file
5
frontend/rust-lib/flowy-chat/src/persistence/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod chat_message_sql;
|
||||
mod chat_sql;
|
||||
|
||||
pub use chat_message_sql::*;
|
||||
pub use chat_sql::*;
|
@ -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"]
|
||||
|
||||
|
47
frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs
Normal file
47
frontend/rust-lib/flowy-core/src/deps_resolve/chat_deps.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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()) })
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
]
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
128
frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs
Normal file
128
frontend/rust-lib/flowy-server/src/af_cloud/impls/chat.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user