Skip to content

Commit

Permalink
feat: save messages as a new page
Browse files Browse the repository at this point in the history
  • Loading branch information
Xazin committed Dec 14, 2024
1 parent 6a6fac7 commit d63a74c
Show file tree
Hide file tree
Showing 17 changed files with 521 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,8 @@ extension CommonOperations on WidgetTester {
Future<void> duplicateByMoreViewActions() async {
final button = find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewMoreActionType.duplicate,
widget is CommonViewAction &&
widget.type == ViewMoreActionType.duplicate,
);
await tap(button);
await pump();
Expand All @@ -769,7 +770,8 @@ extension CommonOperations on WidgetTester {
of: find.byType(ListView),
matching: find.byWidgetPredicate(
(widget) =>
widget is ViewAction && widget.type == ViewMoreActionType.delete,
widget is CommonViewAction &&
widget.type == ViewMoreActionType.delete,
),
);
await tap(button);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,28 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
ChatMemberBloc() : super(const ChatMemberState()) {
on<ChatMemberEvent>(
(event, emit) async {
event.when(
await event.when(
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) {
getMemberInfo: (String userId) async {
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");
});
}
});
final payload = WorkspaceMemberIdPB(uid: Int64.parseInt(userId));
final memberOrFailure =
await UserEventGetMemberInfo(payload).send();

if (!isClosed) {
memberOrFailure.fold(
(info) => add(ChatMemberEvent.receiveMemberInfo(userId, info)),
(err) => Log.error("Error getting member info: $err"),
);
}
},
);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'chat_message_selector_bloc.freezed.dart';

class ChatMessageSelectorBloc
extends Bloc<ChatMessageSelectorEvent, ChatMessageSelectorState> {
ChatMessageSelectorBloc({required this.parentViewId})
: super(const ChatMessageSelectorState()) {
on<ChatMessageSelectorEvent>(
(event, emit) {
event.when(
toggleSelectingMessages: () {
if (state.isSelectingMessages) {
// Clear selected messages when exiting selection mode
return emit(
state.copyWith(
selectedMessages: [],
isSelectingMessages: false,
),
);
}

emit(state.copyWith(isSelectingMessages: true));
},
toggleSelectMessage: (Message message) {
if (state.selectedMessages.contains(message)) {
emit(
state.copyWith(
selectedMessages: state.selectedMessages
.where((m) => m != message)
.toList(),
),
);
} else {
emit(
state.copyWith(
selectedMessages: [...state.selectedMessages, message],
),
);
}
},
selectAllMessages: (List<Message> messages) {
final filtered = messages.where(isAIMessage).toList();
emit(state.copyWith(selectedMessages: filtered));
},
unselectAllMessages: () {
emit(state.copyWith(selectedMessages: const []));
},
saveAsPage: () {
String completeMessage = '';
for (final message in state.selectedMessages) {
if (message is TextMessage) {
completeMessage += '${message.text}\n\n';
}
}

if (completeMessage.isEmpty) {
return;
}

final document = customMarkdownToDocument(completeMessage);
final initialBytes =
DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
if (initialBytes != null) {
ViewBackendService.createView(
// TODO: Better name for the document?
name: 'Test document',
layoutType: ViewLayoutPB.Document,
parentViewId: parentViewId,
initialDataBytes: DocumentDataPBFromTo.fromDocument(document)
?.writeToBuffer(),
// TODO: Consider the location of this document?
);
}

// Reset state when saving as page
emit(const ChatMessageSelectorState());
},
);
},
);
}

final String parentViewId;

bool isMessageSelected(String messageId) =>
state.selectedMessages.any((m) => m.id == messageId);

bool isAIMessage(Message message) {
return message.author.id == aiResponseUserId ||
message.author.id == systemUserId ||
message.author.id.startsWith("streamId:");
}
}

@freezed
class ChatMessageSelectorEvent with _$ChatMessageSelectorEvent {
const factory ChatMessageSelectorEvent.toggleSelectingMessages() =
_ToggleSelectingMessages;
const factory ChatMessageSelectorEvent.toggleSelectMessage(Message message) =
_ToggleSelectMessage;
const factory ChatMessageSelectorEvent.selectAllMessages(
List<Message> messages,
) = _SelectAllMessages;
const factory ChatMessageSelectorEvent.unselectAllMessages() =
_UnselectAllMessages;
const factory ChatMessageSelectorEvent.saveAsPage() = _SaveAsPage;
}

@freezed
class ChatMessageSelectorState with _$ChatMessageSelectorState {
const factory ChatMessageSelectorState({
@Default(false) bool isSelectingMessages,
@Default([]) List<Message> selectedMessages,
}) = _ChatMessageSelectorState;
}
68 changes: 63 additions & 5 deletions frontend/appflowy_flutter/lib/plugins/ai_chat/chat.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_message_selector_bloc.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/view_ext.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/home/menu/view/view_action_type.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.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/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

Expand Down Expand Up @@ -46,13 +52,17 @@ class AIChatPagePlugin extends Plugin {
}) : notifier = ViewPluginNotifier(view: view);

late final ViewInfoBloc _viewInfoBloc;
late final _chatMessageSelectorBloc = ChatMessageSelectorBloc(
parentViewId: notifier.view.parentViewId,
);

@override
final ViewPluginNotifier notifier;

@override
PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(
bloc: _viewInfoBloc,
viewInfoBloc: _viewInfoBloc,
chatMessageSelectorBloc: _chatMessageSelectorBloc,
notifier: notifier,
);

Expand All @@ -71,18 +81,21 @@ class AIChatPagePlugin extends Plugin {
@override
void dispose() {
_viewInfoBloc.close();
_chatMessageSelectorBloc.close();
notifier.dispose();
}
}

class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
with NavigationItem {
AIChatPagePluginWidgetBuilder({
required this.bloc,
required this.viewInfoBloc,
required this.chatMessageSelectorBloc,
required this.notifier,
});

final ViewInfoBloc bloc;
final ViewInfoBloc viewInfoBloc;
final ChatMessageSelectorBloc chatMessageSelectorBloc;
final ViewPluginNotifier notifier;
int? deletedViewIndex;

Expand Down Expand Up @@ -110,8 +123,11 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
return const SizedBox();
}

return BlocProvider.value(
value: bloc,
return MultiBlocProvider(
providers: [
BlocProvider.value(value: chatMessageSelectorBloc),
BlocProvider.value(value: viewInfoBloc),
],
child: AIChatPage(
userProfile: context.userProfile!,
key: ValueKey(notifier.view.id),
Expand All @@ -134,4 +150,46 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder

@override
EdgeInsets get contentPadding => EdgeInsets.zero;

@override
Widget? get rightBarItem => MultiBlocProvider(
providers: [
BlocProvider.value(value: viewInfoBloc),
BlocProvider.value(value: chatMessageSelectorBloc),
],
child: BlocBuilder<ChatMessageSelectorBloc, ChatMessageSelectorState>(
builder: (context, state) {
if (state.isSelectingMessages) {
return const SizedBox.shrink();
}

return Row(
mainAxisSize: MainAxisSize.min,
children: [
MoreViewActions(
key: ValueKey(notifier.view.id),
view: notifier.view,
customActions: [
ViewAction(
view: notifier.view,
leftIcon: FlowySvgs.download_s,
label: LocaleKeys.moreAction_saveAsNewPage.tr(),
onTap: () {
chatMessageSelectorBloc.add(
const ChatMessageSelectorEvent
.toggleSelectingMessages(),
);
},
),
CommonViewAction(
type: ViewMoreActionType.divider,
view: notifier.view,
),
],
),
],
);
},
),
);
}
Loading

0 comments on commit d63a74c

Please sign in to comment.