chore: chat UI poblish (#5895)

* chore: update ui

* chore: update send state

* chore: workspace owner prompt

* chore: show other user

* chore: fix ui
This commit is contained in:
Nathan.fooo 2024-08-07 16:48:09 +08:00 committed by GitHub
parent 98b7882d43
commit e28a251e72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 652 additions and 221 deletions

View File

@ -26,6 +26,8 @@ part 'chat_bloc.g.dart';
part 'chat_bloc.freezed.dart';
const sendMessageErrorKey = "sendMessageError";
const systemUserId = "system";
const aiResponseUserId = "0";
class ChatBloc extends Bloc<ChatEvent, ChatState> {
ChatBloc({
@ -87,6 +89,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
},
);
},
// Loading messages
startLoadingPrevMessage: () async {
Int64? beforeMessageId;
final oldestMessage = _getOlderstMessage();
@ -130,21 +133,58 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
),
);
},
// streaming message
streaming: (Message message) {
final allMessages = _perminentMessages();
allMessages.insert(0, message);
emit(
state.copyWith(
messages: allMessages,
streamingStatus: const LoadingState.loading(),
streamingState: const StreamingState.streaming(),
canSendMessage: false,
),
);
},
didFinishStreaming: () {
finishStreaming: () {
emit(
state.copyWith(streamingStatus: const LoadingState.finish()),
state.copyWith(
streamingState: const StreamingState.done(),
canSendMessage:
state.sendingState == const SendMessageState.done(),
),
);
},
didUpdateAnswerStream: (AnswerStream stream) {
emit(state.copyWith(answerStream: stream));
},
stopStream: () async {
if (state.answerStream == null) {
return;
}
final payload = StopStreamPB(chatId: chatId);
await AIEventStopStream(payload).send();
final allMessages = _perminentMessages();
if (state.streamingState != const StreamingState.done()) {
// If the streaming is not started, remove the message from the list
if (!state.answerStream!.hasStarted) {
allMessages.removeWhere(
(element) => element.id == lastStreamMessageId,
);
lastStreamMessageId = "";
}
// when stop stream, we will set the answer stream to null. Which means the streaming
// is finished or canceled.
emit(
state.copyWith(
messages: allMessages,
answerStream: null,
streamingState: const StreamingState.done(),
),
);
}
},
receveMessage: (Message message) {
final allMessages = _perminentMessages();
// remove message with the same id
@ -164,15 +204,32 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
lastSentMessage: null,
messages: allMessages,
relatedQuestions: [],
sendingState: const SendMessageState.sending(),
canSendMessage: false,
),
);
},
finishSending: (ChatMessagePB message) {
emit(
state.copyWith(
lastSentMessage: message,
sendingState: const SendMessageState.done(),
canSendMessage:
state.streamingState == const StreamingState.done(),
),
);
},
// related question
didReceiveRelatedQuestion: (List<RelatedQuestionPB> questions) {
if (questions.isEmpty) {
return;
}
final allMessages = _perminentMessages();
final message = CustomMessage(
metadata: OnetimeShotType.relatedQuestion.toMap(),
author: const User(id: "system"),
id: 'system',
author: const User(id: systemUserId),
id: systemUserId,
);
allMessages.insert(0, message);
emit(
@ -189,44 +246,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
),
);
},
didSentUserMessage: (ChatMessagePB message) {
emit(
state.copyWith(
lastSentMessage: message,
),
);
},
didUpdateAnswerStream: (AnswerStream stream) {
emit(state.copyWith(answerStream: stream));
},
stopStream: () async {
if (state.answerStream == null) {
return;
}
final payload = StopStreamPB(chatId: chatId);
await AIEventStopStream(payload).send();
final allMessages = _perminentMessages();
if (state.streamingStatus != const LoadingState.finish()) {
// If the streaming is not started, remove the message from the list
if (!state.answerStream!.hasStarted) {
allMessages.removeWhere(
(element) => element.id == lastStreamMessageId,
);
lastStreamMessageId = "";
}
// when stop stream, we will set the answer stream to null. Which means the streaming
// is finished or canceled.
emit(
state.copyWith(
messages: allMessages,
answerStream: null,
streamingStatus: const LoadingState.finish(),
),
);
}
},
);
},
);
@ -250,7 +269,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
chatErrorMessageCallback: (err) {
if (!isClosed) {
Log.error("chat error: ${err.errorMessage}");
add(const ChatEvent.didFinishStreaming());
add(const ChatEvent.finishStreaming());
}
},
latestMessageCallback: (list) {
@ -267,7 +286,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
},
finishStreamingCallback: () {
if (!isClosed) {
add(const ChatEvent.didFinishStreaming());
add(const ChatEvent.finishStreaming());
// The answer strema will bet set to null after the streaming is finished or canceled.
// so if the answer stream is null, we will not get related question.
if (state.lastSentMessage != null && state.answerStream != null) {
@ -353,7 +372,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
result.fold(
(ChatMessagePB question) {
if (!isClosed) {
add(ChatEvent.didSentUserMessage(question));
add(ChatEvent.finishSending(question));
final questionMessageId = question.messageId;
final message = _createTextMessage(question);
@ -374,8 +393,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
final error = CustomMessage(
metadata: metadata,
author: const User(id: "system"),
id: 'system',
author: const User(id: systemUserId),
id: systemUserId,
);
add(ChatEvent.receveMessage(error));
@ -390,7 +409,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
lastStreamMessageId = streamMessageId;
return TextMessage(
author: User(id: nanoid()),
author: User(id: "streamId:${nanoid()}"),
metadata: {
"$AnswerStream": stream,
"question": questionMessageId,
@ -425,10 +444,21 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
@freezed
class ChatEvent with _$ChatEvent {
const factory ChatEvent.initialLoad() = _InitialLoadMessage;
// send message
const factory ChatEvent.sendMessage({
required String message,
Map<String, dynamic>? metadata,
}) = _SendMessage;
const factory ChatEvent.finishSending(ChatMessagePB message) =
_FinishSendMessage;
// receive message
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
const factory ChatEvent.finishStreaming() = _FinishStreamingMessage;
// loading messages
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
const factory ChatEvent.didLoadPreviousMessages(
List<Message> messages,
@ -436,16 +466,13 @@ class ChatEvent with _$ChatEvent {
) = _DidLoadPreviousMessages;
const factory ChatEvent.didLoadLatestMessages(List<Message> messages) =
_DidLoadMessages;
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
const factory ChatEvent.didFinishStreaming() = _FinishStreamingMessage;
// related questions
const factory ChatEvent.didReceiveRelatedQuestion(
List<RelatedQuestionPB> questions,
) = _DidReceiveRelatedQueston;
const factory ChatEvent.clearReleatedQuestion() = _ClearRelatedQuestion;
const factory ChatEvent.didSentUserMessage(ChatMessagePB message) =
_DidSendUserMessage;
const factory ChatEvent.didUpdateAnswerStream(
AnswerStream stream,
) = _DidUpdateAnswerStream;
@ -466,7 +493,8 @@ class ChatState with _$ChatState {
required LoadingState loadingPreviousStatus,
// When sending a user message, the status will be set as loading.
// After the message is sent, the status will be set as finished.
required LoadingState streamingStatus,
required StreamingState streamingState,
required SendMessageState sendingState,
// Indicate whether there are more previous messages to load.
required bool hasMorePrevMessage,
// The related questions that are received after the user message is sent.
@ -474,6 +502,7 @@ class ChatState with _$ChatState {
// The last user message that is sent to the server.
ChatMessagePB? lastSentMessage,
AnswerStream? answerStream,
@Default(true) bool canSendMessage,
}) = _ChatState;
factory ChatState.initial(ViewPB view, UserProfilePB userProfile) =>
@ -483,12 +512,19 @@ class ChatState with _$ChatState {
userProfile: userProfile,
initialLoadingStatus: const LoadingState.finish(),
loadingPreviousStatus: const LoadingState.finish(),
streamingStatus: const LoadingState.finish(),
streamingState: const StreamingState.done(),
sendingState: const SendMessageState.done(),
hasMorePrevMessage: true,
relatedQuestions: [],
);
}
bool isOtherUserMessage(Message message) {
return message.author.id != aiResponseUserId &&
message.author.id != systemUserId &&
!message.author.id.startsWith("streamId:");
}
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
@ -497,6 +533,7 @@ class LoadingState with _$LoadingState {
enum OnetimeShotType {
unknown,
sendingMessage,
relatedQuestion,
invalidSendMesssage,
}
@ -638,7 +675,9 @@ List<ChatMessageMetadata> chatMessageMetadataFromString(String? s) {
}
if (metadataJson is Map<String, dynamic>) {
metadata.add(ChatMessageMetadata.fromJson(metadataJson));
if (metadataJson.isNotEmpty) {
metadata.add(ChatMessageMetadata.fromJson(metadataJson));
}
} else if (metadataJson is List) {
metadata.addAll(
metadataJson.map(
@ -672,3 +711,15 @@ class ChatMessageMetadata {
Map<String, dynamic> toJson() => _$ChatMessageMetadataToJson(this);
}
@freezed
class StreamingState with _$StreamingState {
const factory StreamingState.streaming() = _Streaming;
const factory StreamingState.done({FlowyError? error}) = _StreamDone;
}
@freezed
class SendMessageState with _$SendMessageState {
const factory SendMessageState.sending() = _Sending;
const factory SendMessageState.done({FlowyError? error}) = _SendDone;
}

View File

@ -108,6 +108,14 @@ class ChatInputActionBloc
),
);
},
clear: () {
emit(
state.copyWith(
selectedPages: [],
filter: "",
),
);
},
);
}
}
@ -171,6 +179,7 @@ class ChatInputActionEvent with _$ChatInputActionEvent {
const factory ChatInputActionEvent.addPage(ChatInputActionPage page) =
_AddPage;
const factory ChatInputActionEvent.removePage(String text) = _RemovePage;
const factory ChatInputActionEvent.clear() = _Clear;
}
@freezed

View File

@ -35,10 +35,18 @@ class ChatInputActionControl extends ChatActionHandler {
List<String> get tags =>
_commandBloc.state.selectedPages.map((e) => e.title).toList();
ChatInputMetadata get metaData => _commandBloc.state.selectedPages.fold(
<String, ChatInputActionPage>{},
(map, page) => map..putIfAbsent(page.pageId, () => page),
);
ChatInputMetadata consumeMetaData() {
final metadata = _commandBloc.state.selectedPages.fold(
<String, ChatInputActionPage>{},
(map, page) => map..putIfAbsent(page.pageId, () => page),
);
if (metadata.isNotEmpty) {
_commandBloc.add(const ChatInputActionEvent.clear());
}
return metadata;
}
void handleKeyEvent(KeyEvent event) {
// ignore: deprecated_member_use

View File

@ -0,0 +1,80 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:equatable/equatable.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_member_bloc.freezed.dart';
class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
ChatMemberBloc() : super(const ChatMemberState()) {
on<ChatMemberEvent>(
(event, emit) async {
event.when(
initial: () {},
receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) {
final members = Map<String, ChatMember>.from(state.members);
members[id] = ChatMember(info: memberInfo);
emit(state.copyWith(members: members));
},
getMemberInfo: (String userId) {
if (state.members.containsKey(userId)) {
// Member info already exists. Debouncing refresh member info from backend would be better.
return;
}
final payload = WorkspaceMemberIdPB(
uid: Int64.parseInt(userId),
);
UserEventGetMemberInfo(payload).send().then((result) {
if (!isClosed) {
result.fold((member) {
add(
ChatMemberEvent.receiveMemberInfo(
userId,
member,
),
);
}, (err) {
Log.error("Error getting member info: $err");
});
}
});
},
);
},
);
}
}
@freezed
class ChatMemberEvent with _$ChatMemberEvent {
const factory ChatMemberEvent.initial() = Initial;
const factory ChatMemberEvent.getMemberInfo(
String userId,
) = _GetMemberInfo;
const factory ChatMemberEvent.receiveMemberInfo(
String id,
WorkspaceMemberPB memberInfo,
) = _ReceiveMemberInfo;
}
@freezed
class ChatMemberState with _$ChatMemberState {
const factory ChatMemberState({
@Default({}) Map<String, ChatMember> members,
}) = _ChatMemberState;
}
class ChatMember extends Equatable {
ChatMember({
required this.info,
});
final DateTime _date = DateTime.now();
final WorkspaceMemberPB info;
@override
List<Object?> get props => [_date, info];
}

View File

@ -7,7 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
Map<String, dynamic>? map,) async {
Map<String, dynamic>? map,
) async {
final List<ChatMessageMetaPB> metadata = [];
if (map != null) {
for (final entry in map.entries) {
@ -18,11 +19,14 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
final payload = OpenDocumentPayloadPB(documentId: view.id);
final result = await DocumentEventGetDocumentText(payload).send();
result.fold((pb) {
metadata.add(ChatMessageMetaPB(
id: view.id,
name: view.name,
text: pb.text,
),);
metadata.add(
ChatMessageMetaPB(
id: view.id,
name: view.name,
text: pb.text,
source: "appflowy document",
),
);
}, (err) {
Log.error('Failed to get document text: $err');
});

View File

@ -1,7 +1,4 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:fixnum/fixnum.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@ -12,25 +9,14 @@ class ChatUserMessageBloc
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
ChatUserMessageBloc({
required Message message,
}) : super(ChatUserMessageState.initial(message)) {
required ChatMember? member,
}) : super(ChatUserMessageState.initial(message, member)) {
on<ChatUserMessageEvent>(
(event, emit) async {
event.when(
initial: () {
final payload =
WorkspaceMemberIdPB(uid: Int64.parseInt(message.author.id));
UserEventGetMemberInfo(payload).send().then((result) {
if (!isClosed) {
result.fold((member) {
add(ChatUserMessageEvent.didReceiveMemberInfo(member));
}, (err) {
Log.error("Error getting member info: $err");
});
}
});
},
didReceiveMemberInfo: (WorkspaceMemberPB memberInfo) {
emit(state.copyWith(member: memberInfo));
initial: () {},
refreshMember: (ChatMember member) {
emit(state.copyWith(member: member));
},
);
},
@ -41,18 +27,20 @@ class ChatUserMessageBloc
@freezed
class ChatUserMessageEvent with _$ChatUserMessageEvent {
const factory ChatUserMessageEvent.initial() = Initial;
const factory ChatUserMessageEvent.didReceiveMemberInfo(
WorkspaceMemberPB memberInfo,
) = _MemberInfo;
const factory ChatUserMessageEvent.refreshMember(ChatMember member) =
_MemberInfo;
}
@freezed
class ChatUserMessageState with _$ChatUserMessageState {
const factory ChatUserMessageState({
required Message message,
WorkspaceMemberPB? member,
ChatMember? member,
}) = _ChatUserMessageState;
factory ChatUserMessageState.initial(Message message) =>
ChatUserMessageState(message: message);
factory ChatUserMessageState.initial(
Message message,
ChatMember? member,
) =>
ChatUserMessageState(message: message, member: member);
}

View File

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/other_user_message_bubble.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:desktop_drop/desktop_drop.dart';
@ -27,6 +28,7 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
import 'package:styled_widget/styled_widget.dart';
import 'application/chat_member_bloc.dart';
import 'application/chat_side_pannel_bloc.dart';
import 'presentation/chat_input/chat_input.dart';
import 'presentation/chat_popmenu.dart';
@ -101,6 +103,7 @@ class AIChatPage extends StatelessWidget {
ChatInputStateBloc()..add(const ChatInputStateEvent.started()),
),
BlocProvider(create: (_) => ChatSidePannelBloc(chatId: view.id)),
BlocProvider(create: (_) => ChatMemberBloc()),
],
child: BlocListener<ChatFileBloc, ChatFileState>(
listenWhen: (previous, current) =>
@ -298,6 +301,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
Widget buildChatWidget() {
return BlocBuilder<ChatBloc, ChatState>(
builder: (blocContext, state) => Chat(
key: ValueKey(widget.view.id),
messages: state.messages,
onSendPressed: (_) {
// We use custom bottom widget for chat input, so
@ -335,7 +339,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
required messageWidth,
required showName,
}) =>
_buildAITextMessage(blocContext, textMessage),
_buildTextMessage(blocContext, textMessage),
bubbleBuilder: (
child, {
required message,
@ -346,17 +350,21 @@ class _ChatContentPageState extends State<_ChatContentPage> {
message: message,
child: child,
);
} else if (isOtherUserMessage(message)) {
return OtherUserMessageBubble(
message: message,
child: child,
);
} else {
return _buildAIBubble(message, blocContext, state, child);
}
return _buildAIBubble(message, blocContext, state, child);
},
),
);
}
Widget _buildAITextMessage(BuildContext context, TextMessage message) {
final isAuthor = message.author.id == _user.id;
if (isAuthor) {
Widget _buildTextMessage(BuildContext context, TextMessage message) {
if (message.author.id == _user.id) {
return ChatTextMessageWidget(
user: message.author,
messageUserId: message.id,
@ -497,9 +505,9 @@ class _ChatContentPageState extends State<_ChatContentPage> {
return Column(
children: [
BlocSelector<ChatBloc, ChatState, LoadingState>(
selector: (state) => state.streamingStatus,
builder: (context, state) {
BlocSelector<ChatBloc, ChatState, bool>(
selector: (state) => state.canSendMessage,
builder: (context, canSendMessage) {
return ChatInput(
aiType: aiType,
chatId: widget.view.id,
@ -511,7 +519,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
),
);
},
isStreaming: state != const LoadingState.finish(),
isStreaming: !canSendMessage,
onStopStreaming: () {
context
.read<ChatBloc>()

View File

@ -55,11 +55,13 @@ class ChatUserAvatar extends StatelessWidget {
required this.name,
required this.size,
this.isHovering = false,
this.defaultName,
});
final String iconUrl;
final String name;
final double size;
final String? defaultName;
// If true, a border will be applied on top of the avatar
final bool isHovering;
@ -76,7 +78,8 @@ class ChatUserAvatar extends StatelessWidget {
}
Widget _buildEmptyAvatar(BuildContext context) {
final String nameOrDefault = _userName(name);
final String nameOrDefault = _userName(name, defaultName);
final Color color = ColorGenerator(name).toColor();
const initialsCount = 2;
@ -170,8 +173,8 @@ class ChatUserAvatar extends StatelessWidget {
/// 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;
String _userName(String name, String? defaultName) =>
name.isEmpty ? (defaultName ?? 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.

View File

@ -144,7 +144,7 @@ class _ChatInputState extends State<ChatInput> {
if (trimmedText != '') {
final partialText = types.PartialText(
text: trimmedText,
metadata: _inputActionControl.metaData,
metadata: _inputActionControl.consumeMetaData(),
);
widget.onSendPressed(partialText);
_textController.clear();

View File

@ -44,9 +44,12 @@ class AIMessageMetadata extends StatelessWidget {
),
useIntrinsicWidth: true,
radius: BorderRadius.circular(6),
text: FlowyText(
m.source,
fontSize: 14,
text: Opacity(
opacity: 0.5,
child: FlowyText(
m.name,
fontSize: 14,
),
),
onTap: () {
onSelectedMetadata(m);

View File

@ -0,0 +1,212 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.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 OtherUserMessageBubble extends StatelessWidget {
const OtherUserMessageBubble({
super.key,
required this.message,
required this.child,
});
final Message message;
final Widget child;
@override
Widget build(BuildContext context) {
const padding = EdgeInsets.symmetric(horizontal: _leftPadding);
final childWithPadding = Padding(padding: padding, child: child);
final widget = isMobile
? _wrapPopMenu(childWithPadding)
: _wrapHover(childWithPadding);
if (context.read<ChatMemberBloc>().state.members[message.author.id] ==
null) {
context
.read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id));
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
BlocConsumer<ChatMemberBloc, ChatMemberState>(
listenWhen: (previous, current) {
return previous.members[message.author.id] !=
current.members[message.author.id];
},
listener: (context, state) {},
builder: (context, state) {
final member = state.members[message.author.id];
return ChatUserAvatar(
iconUrl: member?.info.avatarUrl ?? "",
name: member?.info.name ?? "",
size: 36,
defaultName: "",
);
},
),
Expanded(child: widget),
],
);
}
OtherUserMessageHover _wrapHover(Padding child) {
return OtherUserMessageHover(
message: message,
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 OtherUserMessageHover extends StatefulWidget {
const OtherUserMessageHover({
super.key,
required this.child,
required this.message,
});
final Widget child;
final Message message;
final bool autoShowHover = true;
@override
State<OtherUserMessageHover> createState() => _OtherUserMessageHoverState();
}
class _OtherUserMessageHoverState extends State<OtherUserMessageHover> {
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) {
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.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: () async {
final document = customMarkdownToDocument(textMessage.text);
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: textMessage.text,
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
},
),
);
}
}

View File

@ -1,6 +1,6 @@
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/application/chat_member_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';
@ -27,49 +27,53 @@ class ChatUserMessageBubble extends StatelessWidget {
const borderRadius = BorderRadius.all(Radius.circular(6));
final backgroundColor =
Theme.of(context).colorScheme.surfaceContainerHighest;
if (context.read<ChatMemberBloc>().state.members[message.author.id] ==
null) {
context
.read<ChatMemberBloc>()
.add(ChatMemberEvent.getMemberInfo(message.author.id));
}
return BlocProvider(
create: (context) => ChatUserMessageBloc(message: message)
..add(const ChatUserMessageEvent.initial()),
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,
return BlocConsumer<ChatMemberBloc, ChatMemberState>(
listenWhen: (previous, current) {
return previous.members[message.author.id] !=
current.members[message.author.id];
},
listener: (context, state) {},
builder: (context, state) {
final member = state.members[message.author.id];
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,
),
);
},
),
// ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChatUserAvatar(
iconUrl: member?.info.avatarUrl ?? "",
name: member?.info.name ?? "",
size: 36,
),
],
);
},
),
),
],
);
},
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
@ -10,12 +11,12 @@ part 'local_ai_on_boarding_bloc.freezed.dart';
class LocalAIOnBoardingBloc
extends Bloc<LocalAIOnBoardingEvent, LocalAIOnBoardingState> {
LocalAIOnBoardingBloc(this.workspaceId)
LocalAIOnBoardingBloc(this.userProfile)
: super(const LocalAIOnBoardingState()) {
_dispatch();
}
final String workspaceId;
final UserProfilePB userProfile;
void _dispatch() {
on<LocalAIOnBoardingEvent>((event, emit) {
@ -44,7 +45,7 @@ class LocalAIOnBoardingBloc
}
void _loadSubscriptionPlans() {
final payload = UserWorkspaceIdPB()..workspaceId = workspaceId;
final payload = UserWorkspaceIdPB()..workspaceId = userProfile.workspaceId;
UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) {
if (!isClosed) {
add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result));

View File

@ -1,4 +1,5 @@
import 'package:appflowy/user/application/user_listener.dart';
import 'package:appflowy/user/application/user_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -10,14 +11,31 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_ai_bloc.freezed.dart';
class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(this.userProfile)
SettingsAIBloc(this.userProfile, WorkspaceMemberPB? member)
: _userListener = UserListener(userProfile: userProfile),
super(SettingsAIState(userProfile: userProfile)) {
_userService = UserBackendService(userId: userProfile.id),
super(SettingsAIState(userProfile: userProfile, member: member)) {
_dispatch();
if (member == null) {
_userService.getWorkspaceMember().then((result) {
result.fold(
(member) {
if (!isClosed) {
add(SettingsAIEvent.refreshMember(member));
}
},
(err) {
Log.error(err);
},
);
});
}
}
final UserListener _userListener;
final UserProfilePB userProfile;
final UserBackendService _userService;
@override
Future<void> close() async {
@ -62,6 +80,9 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
),
);
},
refreshMember: (member) {
emit(state.copyWith(member: member));
},
);
});
}
@ -112,6 +133,7 @@ class SettingsAIEvent with _$SettingsAIEvent {
) = _DidLoadWorkspaceSetting;
const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;
const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = _RefreshMember;
const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;
@ -125,6 +147,7 @@ class SettingsAIState with _$SettingsAIState {
const factory SettingsAIState({
required UserProfilePB userProfile,
UseAISettingPB? aiSettings,
WorkspaceMemberPB? member,
@Default(true) bool enableSearchIndexing,
}) = _SettingsAIState;
}

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/shared/af_role_pb_extension.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_on_boarding_bloc.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
@ -37,15 +38,20 @@ class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
}
class SettingsAIView extends StatelessWidget {
const SettingsAIView({super.key, required this.userProfile});
const SettingsAIView({
super.key,
required this.userProfile,
required this.member,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB? member;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
create: (_) => SettingsAIBloc(userProfile, member)
..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
final children = <Widget>[
@ -53,11 +59,15 @@ class SettingsAIView extends StatelessWidget {
];
children.add(const _AISearchToggle(value: false));
children.add(
_LocalAIOnBoarding(
workspaceId: userProfile.workspaceId,
),
);
if (state.member != null) {
children.add(
_LocalAIOnBoarding(
userProfile: userProfile,
member: state.member!,
),
);
}
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
@ -116,8 +126,12 @@ class _AISearchToggle extends StatelessWidget {
// ignore: unused_element
class _LocalAIOnBoarding extends StatelessWidget {
const _LocalAIOnBoarding({required this.workspaceId});
final String workspaceId;
const _LocalAIOnBoarding({
required this.userProfile,
required this.member,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB member;
@override
Widget build(BuildContext context) {
@ -125,7 +139,7 @@ class _LocalAIOnBoarding extends StatelessWidget {
return BillingGateGuard(
builder: (context) {
return BlocProvider(
create: (context) => LocalAIOnBoardingBloc(workspaceId)
create: (context) => LocalAIOnBoardingBloc(userProfile)
..add(const LocalAIOnBoardingEvent.started()),
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
@ -133,16 +147,20 @@ class _LocalAIOnBoarding extends StatelessWidget {
if (kDebugMode || state.isPurchaseAILocal) {
return const LocalAISetting();
} else {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
return _UpgradeToAILocalPlan(
onTap: () {
context.read<SettingsDialogBloc>().add(
const SettingsDialogEvent.setSelectedPage(
SettingsPage.plan,
),
);
},
);
if (member.role.isOwner) {
// Show the upgrade to AI Local plan button if the user has not purchased the AI Local plan
return _UpgradeToAILocalPlan(
onTap: () {
context.read<SettingsDialogBloc>().add(
const SettingsDialogEvent.setSelectedPage(
SettingsPage.plan,
),
);
},
);
} else {
return const _AskOwnerUpgradeToLocalAI();
}
}
},
),
@ -155,6 +173,18 @@ class _LocalAIOnBoarding extends StatelessWidget {
}
}
class _AskOwnerUpgradeToLocalAI extends StatelessWidget {
const _AskOwnerUpgradeToLocalAI();
@override
Widget build(BuildContext context) {
return FlowyText(
LocaleKeys.sideBar_askOwnerToUpgradeToLocalAI.tr(),
color: AFThemeExtension.of(context).strongText,
);
}
}
class _UpgradeToAILocalPlan extends StatefulWidget {
const _UpgradeToAILocalPlan({required this.onTap});

View File

@ -120,7 +120,7 @@ class SettingsDialog extends StatelessWidget {
return const SettingsShortcutsView();
case SettingsPage.ai:
if (user.authenticator == AuthenticatorPB.AppFlowyCloud) {
return SettingsAIView(userProfile: user);
return SettingsAIView(userProfile: user, member: member);
} else {
return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud();
}

View File

@ -172,7 +172,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -192,7 +192,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"again",
"anyhow",
@ -876,7 +876,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -888,7 +888,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"futures-channel",
"futures-util",
@ -1132,7 +1132,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -1157,7 +1157,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"async-trait",
@ -1532,7 +1532,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -3051,7 +3051,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"futures-util",
@ -3068,7 +3068,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -3500,7 +3500,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -6098,7 +6098,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",

View File

@ -53,7 +53,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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" }
[dependencies]
serde_json.workspace = true

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -800,7 +800,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"again",
"anyhow",
@ -850,7 +850,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -862,7 +862,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"futures-channel",
"futures-util",
@ -1115,7 +1115,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -1140,7 +1140,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"async-trait",
@ -1522,7 +1522,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -3118,7 +3118,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"futures-util",
@ -3135,7 +3135,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -3572,7 +3572,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -6162,7 +6162,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",

View File

@ -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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" }
[dependencies]
serde_json.workspace = true

View File

@ -301,7 +301,8 @@
"askOwnerToUpgradeToAIMax": "Your workspace is running out of free AI responses. Please ask your workspace owner to upgrade the plan or purchase AI add-ons",
"purchaseStorageSpace": "Purchase Storage Space",
"purchaseAIResponse": "Purchase ",
"upgradeToAILocal": "AI offline on your device"
"askOwnerToUpgradeToLocalAI": "Ask workspace owner to enable AI On-device",
"upgradeToAILocal": "AI On-device on your device"
},
"notifications": {
"export": {
@ -654,7 +655,7 @@
"menuLabel": "AI Settings",
"keys": {
"enableAISearchTitle": "AI Search",
"aiSettingsDescription": "Select or configure AI models used on @:appName. For best performance we recommend using the default model options",
"aiSettingsDescription": "Choose your preferred model to power AppFlowy AI. Now includes GPT 4-o, Claude 3,5, Llama 3.1, and Mistral 7B",
"loginToEnableAIFeature": "AI features are only enabled after logging in with @:appName Cloud. If you don't have an @:appName account, go to 'My Account' to sign up",
"llmModel": "Language Model",
"llmModelType": "Language Model Type",
@ -778,7 +779,7 @@
},
"aiOnDevice": {
"label": "AI On-device for Mac",
"description": "Unlock unlimited AI offline on your device",
"description": "Unlock unlimited AI On-device on your device",
"activeDescription": "Next invoice due on {}",
"canceledDescription": "AI On-device for Mac will be available until {}"
},

View File

@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -718,7 +718,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"again",
"anyhow",
@ -768,7 +768,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"collab-entity",
"collab-rt-entity",
@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"futures-channel",
"futures-util",
@ -993,7 +993,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bincode",
@ -1018,7 +1018,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"async-trait",
@ -1356,7 +1356,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -2730,7 +2730,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"futures-util",
@ -2747,7 +2747,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",
@ -3112,7 +3112,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"bytes",
@ -5307,7 +5307,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=a371912c61d79fa946ec78f0cb852fdd7d391356#a371912c61d79fa946ec78f0cb852fdd7d391356"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=40bd6b784eced6fe43c95941420b3ce444b0a60c#40bd6b784eced6fe43c95941420b3ce444b0a60c"
dependencies = [
"anyhow",
"app-error",

View File

@ -99,8 +99,8 @@ zip = "2.1.3"
# 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 = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "a371912c61d79fa946ec78f0cb852fdd7d391356" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" }
client-api-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "40bd6b784eced6fe43c95941420b3ce444b0a60c" }
[profile.dev]
opt-level = 0

View File

@ -54,6 +54,9 @@ pub struct ChatMessageMetaPB {
#[pb(index = 3)]
pub text: String,
#[pb(index = 4)]
pub source: String,
}
#[derive(Default, ProtoBuf, Validate, Clone, Debug)]

View File

@ -42,7 +42,7 @@ pub(crate) async fn stream_chat_message_handler(
data: ChatMetadataData::new_text(metadata.text),
id: metadata.id,
name: metadata.name.clone(),
source: metadata.name,
source: metadata.source,
})
.collect::<Vec<_>>();

View File

@ -304,6 +304,9 @@ pub enum ErrorCode {
#[error("AI offline not started")]
AIOfflineNotInstalled = 105,
#[error("Invalid Request")]
InvalidRequest = 106,
}
impl ErrorCode {

View File

@ -15,7 +15,7 @@ impl From<AppResponseError> for FlowyError {
AppErrorCode::MissingPayload => ErrorCode::MissingPayload,
AppErrorCode::OpenError => ErrorCode::Internal,
AppErrorCode::InvalidUrl => ErrorCode::InvalidURL,
AppErrorCode::InvalidRequest => ErrorCode::InvalidParams,
AppErrorCode::InvalidRequest => ErrorCode::InvalidRequest,
AppErrorCode::InvalidOAuthProvider => ErrorCode::InvalidAuthConfig,
AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized,
AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions,