mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-17 10:14:47 +00:00
feat: save messages as a new page (#7224)
This commit is contained in:
parent
63c7f7b6fe
commit
1723886f3a
@ -14,6 +14,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
|
||||||
class ChatEditDocumentService {
|
class ChatEditDocumentService {
|
||||||
|
const ChatEditDocumentService._();
|
||||||
|
|
||||||
static Future<ViewPB?> saveMessagesToNewPage(
|
static Future<ViewPB?> saveMessagesToNewPage(
|
||||||
String chatPageName,
|
String chatPageName,
|
||||||
String parentViewId,
|
String parentViewId,
|
||||||
@ -45,12 +47,13 @@ class ChatEditDocumentService {
|
|||||||
).toNullable();
|
).toNullable();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> addMessageToPage(
|
static Future<void> addMessagesToPage(
|
||||||
String documentId,
|
String documentId,
|
||||||
TextMessage message,
|
List<TextMessage> messages,
|
||||||
) async {
|
) async {
|
||||||
if (message.text.isEmpty) {
|
// Convert messages to markdown and trim the last empty newline.
|
||||||
Log.error('Message is empty');
|
final completeMessage = messages.map((m) => m.text).join('\n').trimRight();
|
||||||
|
if (completeMessage.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,7 +72,7 @@ class ChatEditDocumentService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final messageDocument = customMarkdownToDocument(message.text);
|
final messageDocument = customMarkdownToDocument(completeMessage);
|
||||||
if (messageDocument.isEmpty) {
|
if (messageDocument.isEmpty) {
|
||||||
Log.error('Failed to convert message to document');
|
Log.error('Failed to convert message to document');
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -12,13 +12,13 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
|||||||
ChatMemberBloc() : super(const ChatMemberState()) {
|
ChatMemberBloc() : super(const ChatMemberState()) {
|
||||||
on<ChatMemberEvent>(
|
on<ChatMemberEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
event.when(
|
await event.when(
|
||||||
receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) {
|
receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) {
|
||||||
final members = Map<String, ChatMember>.from(state.members);
|
final members = Map<String, ChatMember>.from(state.members);
|
||||||
members[id] = ChatMember(info: memberInfo);
|
members[id] = ChatMember(info: memberInfo);
|
||||||
emit(state.copyWith(members: members));
|
emit(state.copyWith(members: members));
|
||||||
},
|
},
|
||||||
getMemberInfo: (String userId) {
|
getMemberInfo: (String userId) async {
|
||||||
if (state.members.containsKey(userId)) {
|
if (state.members.containsKey(userId)) {
|
||||||
// Member info already exists. Debouncing refresh member info from backend would be better.
|
// Member info already exists. Debouncing refresh member info from backend would be better.
|
||||||
return;
|
return;
|
||||||
@ -27,19 +27,15 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
|||||||
final payload = WorkspaceMemberIdPB(
|
final payload = WorkspaceMemberIdPB(
|
||||||
uid: Int64.parseInt(userId),
|
uid: Int64.parseInt(userId),
|
||||||
);
|
);
|
||||||
UserEventGetMemberInfo(payload).send().then((result) {
|
await UserEventGetMemberInfo(payload).send().then((result) {
|
||||||
if (!isClosed) {
|
result.fold(
|
||||||
result.fold((member) {
|
(member) {
|
||||||
add(
|
if (!isClosed) {
|
||||||
ChatMemberEvent.receiveMemberInfo(
|
add(ChatMemberEvent.receiveMemberInfo(userId, member));
|
||||||
userId,
|
}
|
||||||
member,
|
},
|
||||||
),
|
(err) => Log.error("Error getting member info: $err"),
|
||||||
);
|
);
|
||||||
}, (err) {
|
|
||||||
Log.error("Error getting member info: $err");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/util.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_select_message_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ChatSelectMessageBloc
|
||||||
|
extends Bloc<ChatSelectMessageEvent, ChatSelectMessageState> {
|
||||||
|
ChatSelectMessageBloc({required this.viewNotifier})
|
||||||
|
: super(ChatSelectMessageState.initial()) {
|
||||||
|
_dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewPluginNotifier viewNotifier;
|
||||||
|
|
||||||
|
void _dispatch() {
|
||||||
|
on<ChatSelectMessageEvent>(
|
||||||
|
(event, emit) {
|
||||||
|
event.when(
|
||||||
|
toggleSelectingMessages: () {
|
||||||
|
if (state.isSelectingMessages) {
|
||||||
|
emit(ChatSelectMessageState.initial());
|
||||||
|
} else {
|
||||||
|
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: () {
|
||||||
|
emit(ChatSelectMessageState.initial());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ChatSelectMessageEvent with _$ChatSelectMessageEvent {
|
||||||
|
const factory ChatSelectMessageEvent.toggleSelectingMessages() =
|
||||||
|
_ToggleSelectingMessages;
|
||||||
|
const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) =
|
||||||
|
_ToggleSelectMessage;
|
||||||
|
const factory ChatSelectMessageEvent.selectAllMessages(
|
||||||
|
List<Message> messages,
|
||||||
|
) = _SelectAllMessages;
|
||||||
|
const factory ChatSelectMessageEvent.unselectAllMessages() =
|
||||||
|
_UnselectAllMessages;
|
||||||
|
const factory ChatSelectMessageEvent.saveAsPage() = _SaveAsPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class ChatSelectMessageState with _$ChatSelectMessageState {
|
||||||
|
const factory ChatSelectMessageState({
|
||||||
|
required bool isSelectingMessages,
|
||||||
|
required List<Message> selectedMessages,
|
||||||
|
}) = _ChatSelectMessageState;
|
||||||
|
|
||||||
|
factory ChatSelectMessageState.initial() => const ChatSelectMessageState(
|
||||||
|
isSelectingMessages: false,
|
||||||
|
selectedMessages: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,22 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
|
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
|
||||||
import 'package:appflowy/plugins/util.dart';
|
import 'package:appflowy/plugins/util.dart';
|
||||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_ext.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/application/view_info/view_info_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/home_stack.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/favorite_button.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/tab_bar_item.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -46,13 +54,16 @@ class AIChatPagePlugin extends Plugin {
|
|||||||
}) : notifier = ViewPluginNotifier(view: view);
|
}) : notifier = ViewPluginNotifier(view: view);
|
||||||
|
|
||||||
late final ViewInfoBloc _viewInfoBloc;
|
late final ViewInfoBloc _viewInfoBloc;
|
||||||
|
late final _chatMessageSelectorBloc =
|
||||||
|
ChatSelectMessageBloc(viewNotifier: notifier);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final ViewPluginNotifier notifier;
|
final ViewPluginNotifier notifier;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(
|
PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(
|
||||||
bloc: _viewInfoBloc,
|
viewInfoBloc: _viewInfoBloc,
|
||||||
|
chatMessageSelectorBloc: _chatMessageSelectorBloc,
|
||||||
notifier: notifier,
|
notifier: notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -71,6 +82,7 @@ class AIChatPagePlugin extends Plugin {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_viewInfoBloc.close();
|
_viewInfoBloc.close();
|
||||||
|
_chatMessageSelectorBloc.close();
|
||||||
notifier.dispose();
|
notifier.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -78,11 +90,13 @@ class AIChatPagePlugin extends Plugin {
|
|||||||
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||||
with NavigationItem {
|
with NavigationItem {
|
||||||
AIChatPagePluginWidgetBuilder({
|
AIChatPagePluginWidgetBuilder({
|
||||||
required this.bloc,
|
required this.viewInfoBloc,
|
||||||
|
required this.chatMessageSelectorBloc,
|
||||||
required this.notifier,
|
required this.notifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
final ViewInfoBloc bloc;
|
final ViewInfoBloc viewInfoBloc;
|
||||||
|
final ChatSelectMessageBloc chatMessageSelectorBloc;
|
||||||
final ViewPluginNotifier notifier;
|
final ViewPluginNotifier notifier;
|
||||||
int? deletedViewIndex;
|
int? deletedViewIndex;
|
||||||
|
|
||||||
@ -110,8 +124,11 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
return BlocProvider.value(
|
return MultiBlocProvider(
|
||||||
value: bloc,
|
providers: [
|
||||||
|
BlocProvider.value(value: chatMessageSelectorBloc),
|
||||||
|
BlocProvider.value(value: viewInfoBloc),
|
||||||
|
],
|
||||||
child: AIChatPage(
|
child: AIChatPage(
|
||||||
userProfile: context.userProfile!,
|
userProfile: context.userProfile!,
|
||||||
key: ValueKey(notifier.view.id),
|
key: ValueKey(notifier.view.id),
|
||||||
@ -134,4 +151,51 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? get rightBarItem => MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider.value(value: viewInfoBloc),
|
||||||
|
BlocProvider.value(value: chatMessageSelectorBloc),
|
||||||
|
],
|
||||||
|
child: BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isSelectingMessages) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ViewFavoriteButton(
|
||||||
|
key: ValueKey('favorite_button_${notifier.view.id}'),
|
||||||
|
view: notifier.view,
|
||||||
|
),
|
||||||
|
const HSpace(4),
|
||||||
|
MoreViewActions(
|
||||||
|
key: ValueKey(notifier.view.id),
|
||||||
|
view: notifier.view,
|
||||||
|
customActions: [
|
||||||
|
CustomViewAction(
|
||||||
|
view: notifier.view,
|
||||||
|
leftIcon: FlowySvgs.download_s,
|
||||||
|
label: LocaleKeys.moreAction_saveAsNewPage.tr(),
|
||||||
|
onTap: () {
|
||||||
|
chatMessageSelectorBloc.add(
|
||||||
|
const ChatSelectMessageEvent
|
||||||
|
.toggleSelectingMessages(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ViewAction(
|
||||||
|
type: ViewMoreActionType.divider,
|
||||||
|
view: notifier.view,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
@ -22,6 +23,7 @@ import 'application/ai_prompt_input_bloc.dart';
|
|||||||
import 'application/chat_bloc.dart';
|
import 'application/chat_bloc.dart';
|
||||||
import 'application/chat_entity.dart';
|
import 'application/chat_entity.dart';
|
||||||
import 'application/chat_member_bloc.dart';
|
import 'application/chat_member_bloc.dart';
|
||||||
|
import 'application/chat_select_message_bloc.dart';
|
||||||
import 'application/chat_message_stream.dart';
|
import 'application/chat_message_stream.dart';
|
||||||
import 'presentation/animated_chat_list.dart';
|
import 'presentation/animated_chat_list.dart';
|
||||||
import 'presentation/chat_input/desktop_chat_input.dart';
|
import 'presentation/chat_input/desktop_chat_input.dart';
|
||||||
@ -106,20 +108,22 @@ class _ChatContentPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Center(
|
return BlocBuilder<ChatBloc, ChatState>(
|
||||||
child: Container(
|
builder: (context, state) {
|
||||||
constraints: const BoxConstraints(maxWidth: 784),
|
return switch (state.loadingState) {
|
||||||
margin: UniversalPlatform.isDesktop
|
LoadChatMessageStatus.ready => Column(
|
||||||
? const EdgeInsets.symmetric(horizontal: 60.0)
|
children: [
|
||||||
: null,
|
ChatMessageSelectorBanner(
|
||||||
child: ScrollConfiguration(
|
view: view,
|
||||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
allMessages: context.read<ChatBloc>().chatController.messages,
|
||||||
child: BlocBuilder<ChatBloc, ChatState>(
|
),
|
||||||
builder: (context, state) {
|
Expanded(
|
||||||
return switch (state.loadingState) {
|
child: Align(
|
||||||
LoadChatMessageStatus.ready => Column(
|
alignment: Alignment.topCenter,
|
||||||
children: [
|
child: _wrapConstraints(
|
||||||
Expanded(
|
ScrollConfiguration(
|
||||||
|
behavior: ScrollConfiguration.of(context)
|
||||||
|
.copyWith(scrollbars: false),
|
||||||
child: Chat(
|
child: Chat(
|
||||||
chatController:
|
chatController:
|
||||||
context.read<ChatBloc>().chatController,
|
context.read<ChatBloc>().chatController,
|
||||||
@ -135,15 +139,27 @@ class _ChatContentPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildInput(context),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
),
|
||||||
};
|
_wrapConstraints(
|
||||||
},
|
_builtInput(context),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
|
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _wrapConstraints(Widget child) {
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 784),
|
||||||
|
margin: UniversalPlatform.isDesktop
|
||||||
|
? const EdgeInsets.symmetric(horizontal: 60.0)
|
||||||
|
: null,
|
||||||
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,31 +209,38 @@ class _ChatContentPage extends StatelessWidget {
|
|||||||
final refSourceJsonString =
|
final refSourceJsonString =
|
||||||
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
||||||
|
|
||||||
return BlocBuilder<ChatBloc, ChatState>(
|
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||||
builder: (context, state) {
|
selector: (state) => state.isSelectingMessages,
|
||||||
final chatController = context.read<ChatBloc>().chatController;
|
builder: (context, isSelectingMessages) {
|
||||||
final messages = chatController.messages
|
return BlocBuilder<ChatBloc, ChatState>(
|
||||||
.where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);
|
builder: (context, state) {
|
||||||
final isLastMessage =
|
final chatController = context.read<ChatBloc>().chatController;
|
||||||
messages.isEmpty ? false : messages.last.id == message.id;
|
final messages = chatController.messages
|
||||||
return ChatAIMessageWidget(
|
.where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);
|
||||||
user: message.author,
|
final isLastMessage =
|
||||||
messageUserId: message.id,
|
messages.isEmpty ? false : messages.last.id == message.id;
|
||||||
message: message,
|
return ChatAIMessageWidget(
|
||||||
stream: stream is AnswerStream ? stream : null,
|
user: message.author,
|
||||||
questionId: questionId,
|
messageUserId: message.id,
|
||||||
chatId: view.id,
|
message: message,
|
||||||
refSourceJsonString: refSourceJsonString,
|
stream: stream is AnswerStream ? stream : null,
|
||||||
isStreaming: state.promptResponseState != PromptResponseState.ready,
|
questionId: questionId,
|
||||||
isLastMessage: isLastMessage,
|
chatId: view.id,
|
||||||
onSelectedMetadata: (metadata) =>
|
refSourceJsonString: refSourceJsonString,
|
||||||
_onSelectMetadata(context, metadata),
|
isStreaming:
|
||||||
onRegenerate: () => context
|
state.promptResponseState != PromptResponseState.ready,
|
||||||
.read<ChatBloc>()
|
isLastMessage: isLastMessage,
|
||||||
.add(ChatEvent.regenerateAnswer(message.id, null)),
|
isSelectingMessages: isSelectingMessages,
|
||||||
onChangeFormat: (format) => context
|
onSelectedMetadata: (metadata) =>
|
||||||
.read<ChatBloc>()
|
_onSelectMetadata(context, metadata),
|
||||||
.add(ChatEvent.regenerateAnswer(message.id, format)),
|
onRegenerate: () => context
|
||||||
|
.read<ChatBloc>()
|
||||||
|
.add(ChatEvent.regenerateAnswer(message.id, null)),
|
||||||
|
onChangeFormat: (format) => context
|
||||||
|
.read<ChatBloc>()
|
||||||
|
.add(ChatEvent.regenerateAnswer(message.id, format)),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -265,74 +288,100 @@ class _ChatContentPage extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChatAnimatedListReversed(
|
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||||
scrollController: scrollController,
|
selector: (state) => state.isSelectingMessages,
|
||||||
itemBuilder: itemBuilder,
|
builder: (context, isSelectingMessages) {
|
||||||
onLoadPreviousMessages: () {
|
return ChatAnimatedListReversed(
|
||||||
bloc.add(const ChatEvent.loadPreviousMessages());
|
scrollController: scrollController,
|
||||||
|
itemBuilder: itemBuilder,
|
||||||
|
bottomPadding: isSelectingMessages
|
||||||
|
? 48.0 + DesktopAIChatSizes.messageActionBarIconSize
|
||||||
|
: 8.0,
|
||||||
|
onLoadPreviousMessages: () {
|
||||||
|
bloc.add(const ChatEvent.loadPreviousMessages());
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInput(BuildContext context) {
|
Widget _builtInput(BuildContext context) {
|
||||||
return Padding(
|
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||||
padding: AIChatUILayout.safeAreaInsets(context),
|
selector: (state) => state.isSelectingMessages,
|
||||||
child: BlocSelector<ChatBloc, ChatState, bool>(
|
builder: (context, isSelectingMessages) {
|
||||||
selector: (state) {
|
return AnimatedSwitcher(
|
||||||
return state.promptResponseState == PromptResponseState.ready;
|
duration: const Duration(milliseconds: 150),
|
||||||
},
|
transitionBuilder: (child, animation) {
|
||||||
builder: (context, canSendMessage) {
|
return SizeTransition(
|
||||||
final chatBloc = context.read<ChatBloc>();
|
sizeFactor: animation,
|
||||||
|
axisAlignment: -1,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: isSelectingMessages
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: Padding(
|
||||||
|
padding: AIChatUILayout.safeAreaInsets(context),
|
||||||
|
child: BlocSelector<ChatBloc, ChatState, bool>(
|
||||||
|
selector: (state) {
|
||||||
|
return state.promptResponseState ==
|
||||||
|
PromptResponseState.ready;
|
||||||
|
},
|
||||||
|
builder: (context, canSendMessage) {
|
||||||
|
final chatBloc = context.read<ChatBloc>();
|
||||||
|
|
||||||
return UniversalPlatform.isDesktop
|
return UniversalPlatform.isDesktop
|
||||||
? DesktopChatInput(
|
? DesktopChatInput(
|
||||||
chatId: view.id,
|
chatId: view.id,
|
||||||
isStreaming: !canSendMessage,
|
isStreaming: !canSendMessage,
|
||||||
onStopStreaming: () {
|
onStopStreaming: () {
|
||||||
chatBloc.add(const ChatEvent.stopStream());
|
chatBloc.add(const ChatEvent.stopStream());
|
||||||
},
|
},
|
||||||
onSubmitted: (text, format, metadata) {
|
onSubmitted: (text, format, metadata) {
|
||||||
chatBloc.add(
|
chatBloc.add(
|
||||||
ChatEvent.sendMessage(
|
ChatEvent.sendMessage(
|
||||||
message: text,
|
message: text,
|
||||||
format: format,
|
format: format,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onUpdateSelectedSources: (ids) {
|
onUpdateSelectedSources: (ids) {
|
||||||
chatBloc.add(
|
chatBloc.add(
|
||||||
ChatEvent.updateSelectedSources(
|
ChatEvent.updateSelectedSources(
|
||||||
selectedSourcesIds: ids,
|
selectedSourcesIds: ids,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: MobileChatInput(
|
: MobileChatInput(
|
||||||
chatId: view.id,
|
chatId: view.id,
|
||||||
isStreaming: !canSendMessage,
|
isStreaming: !canSendMessage,
|
||||||
onStopStreaming: () {
|
onStopStreaming: () {
|
||||||
chatBloc.add(const ChatEvent.stopStream());
|
chatBloc.add(const ChatEvent.stopStream());
|
||||||
},
|
},
|
||||||
onSubmitted: (text, format, metadata) {
|
onSubmitted: (text, format, metadata) {
|
||||||
chatBloc.add(
|
chatBloc.add(
|
||||||
ChatEvent.sendMessage(
|
ChatEvent.sendMessage(
|
||||||
message: text,
|
message: text,
|
||||||
format: format,
|
format: format,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onUpdateSelectedSources: (ids) {
|
onUpdateSelectedSources: (ids) {
|
||||||
chatBloc.add(
|
chatBloc.add(
|
||||||
ChatEvent.updateSelectedSources(
|
ChatEvent.updateSelectedSources(
|
||||||
selectedSourcesIds: ids,
|
selectedSourcesIds: ids,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,7 @@ import 'package:string_validator/string_validator.dart';
|
|||||||
import 'layout_define.dart';
|
import 'layout_define.dart';
|
||||||
|
|
||||||
class ChatAIAvatar extends StatelessWidget {
|
class ChatAIAvatar extends StatelessWidget {
|
||||||
const ChatAIAvatar({
|
const ChatAIAvatar({super.key});
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|||||||
@ -118,7 +118,6 @@ class _DesktopChatInputState extends State<DesktopChatInput> {
|
|||||||
? Theme.of(context).colorScheme.primary
|
? Theme.of(context).colorScheme.primary
|
||||||
: Theme.of(context).colorScheme.outline,
|
: Theme.of(context).colorScheme.outline,
|
||||||
width: focusNode.hasFocus ? 1.5 : 1.0,
|
width: focusNode.hasFocus ? 1.5 : 1.0,
|
||||||
strokeAlign: BorderSide.strokeAlignOutside,
|
|
||||||
),
|
),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -0,0 +1,314 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_select_sources_cubit.dart';
|
||||||
|
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/application/user/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||||
|
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||||
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
|
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||||
|
import 'package:appflowy_result/appflowy_result.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_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
|
|
||||||
|
import 'message/ai_message_action_bar.dart';
|
||||||
|
import 'message/message_util.dart';
|
||||||
|
|
||||||
|
class ChatMessageSelectorBanner extends StatelessWidget {
|
||||||
|
const ChatMessageSelectorBanner({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
this.allMessages = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final List<Message> allMessages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (!state.isSelectingMessages) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedAmount = state.selectedMessages.length;
|
||||||
|
final totalAmount = allMessages.length;
|
||||||
|
final allSelected = selectedAmount == totalAmount;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 48,
|
||||||
|
color: const Color(0xFF00BCF0),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (selectedAmount > 0) {
|
||||||
|
_unselectAllMessages(context);
|
||||||
|
} else {
|
||||||
|
_selectAllMessages(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: FlowySvg(
|
||||||
|
allSelected
|
||||||
|
? FlowySvgs.checkbox_ai_selected_s
|
||||||
|
: selectedAmount > 0
|
||||||
|
? FlowySvgs.checkbox_ai_minus_s
|
||||||
|
: FlowySvgs.checkbox_ai_empty_s,
|
||||||
|
blendMode: BlendMode.dstIn,
|
||||||
|
size: const Size.square(18),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
Expanded(
|
||||||
|
child: FlowyText.semibold(
|
||||||
|
allSelected
|
||||||
|
? LocaleKeys.chat_selectBanner_allSelected.tr()
|
||||||
|
: selectedAmount > 0
|
||||||
|
? LocaleKeys.chat_selectBanner_nSelected
|
||||||
|
.tr(args: [selectedAmount.toString()])
|
||||||
|
: LocaleKeys.chat_selectBanner_selectMessages.tr(),
|
||||||
|
figmaLineHeight: 16,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SaveToPageButton(
|
||||||
|
view: view,
|
||||||
|
),
|
||||||
|
const HSpace(8),
|
||||||
|
MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => context.read<ChatSelectMessageBloc>().add(
|
||||||
|
const ChatSelectMessageEvent.toggleSelectingMessages(),
|
||||||
|
),
|
||||||
|
child: const FlowySvg(
|
||||||
|
FlowySvgs.close_m,
|
||||||
|
color: Colors.white,
|
||||||
|
size: Size.square(24),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectAllMessages(BuildContext context) => context
|
||||||
|
.read<ChatSelectMessageBloc>()
|
||||||
|
.add(ChatSelectMessageEvent.selectAllMessages(allMessages));
|
||||||
|
|
||||||
|
void _unselectAllMessages(BuildContext context) => context
|
||||||
|
.read<ChatSelectMessageBloc>()
|
||||||
|
.add(const ChatSelectMessageEvent.unselectAllMessages());
|
||||||
|
}
|
||||||
|
|
||||||
|
class SaveToPageButton extends StatefulWidget {
|
||||||
|
const SaveToPageButton({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SaveToPageButton> createState() => _SaveToPageButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaveToPageButtonState extends State<SaveToPageButton> {
|
||||||
|
final popoverController = PopoverController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final userWorkspaceBloc = context.read<UserWorkspaceBloc>();
|
||||||
|
final userProfile = userWorkspaceBloc.userProfile;
|
||||||
|
final workspaceId =
|
||||||
|
userWorkspaceBloc.state.currentWorkspace?.workspaceId ?? '';
|
||||||
|
|
||||||
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => SpaceBloc(
|
||||||
|
userProfile: userProfile,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
)..add(const SpaceEvent.initial(openFirstPage: false)),
|
||||||
|
),
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) => ChatSettingsCubit(hideDisabled: true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: BlocSelector<SpaceBloc, SpaceState, ViewPB?>(
|
||||||
|
selector: (state) => state.currentSpace,
|
||||||
|
builder: (context, spaceView) {
|
||||||
|
return AppFlowyPopover(
|
||||||
|
controller: popoverController,
|
||||||
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
offset: const Offset(0, 18),
|
||||||
|
direction: PopoverDirection.bottomWithRightAligned,
|
||||||
|
constraints: const BoxConstraints.tightFor(width: 300, height: 400),
|
||||||
|
child: buildButton(context, spaceView),
|
||||||
|
popupBuilder: (_) => buildPopover(context),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildButton(BuildContext context, ViewPB? spaceView) {
|
||||||
|
return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final selectedAmount = state.selectedMessages.length;
|
||||||
|
|
||||||
|
return Opacity(
|
||||||
|
opacity: selectedAmount == 0 ? 0.5 : 1,
|
||||||
|
child: FlowyTextButton(
|
||||||
|
LocaleKeys.chat_selectBanner_saveButton.tr(),
|
||||||
|
onPressed: selectedAmount == 0
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final documentId = getOpenedDocumentId();
|
||||||
|
if (documentId != null) {
|
||||||
|
await onAddToExistingPage(context, documentId);
|
||||||
|
await forceReload(documentId);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
await updateSelection(documentId);
|
||||||
|
} else {
|
||||||
|
if (spaceView != null) {
|
||||||
|
context
|
||||||
|
.read<ChatSettingsCubit>()
|
||||||
|
.refreshSources([spaceView], spaceView);
|
||||||
|
}
|
||||||
|
popoverController.show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontColor: Colors.white,
|
||||||
|
borderColor: Colors.white,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12.0,
|
||||||
|
vertical: 6.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPopover(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: context.read<ChatSettingsCubit>(),
|
||||||
|
child: SaveToPagePopoverContent(
|
||||||
|
onAddToNewPage: (parentViewId) async {
|
||||||
|
await addMessageToNewPage(context, parentViewId);
|
||||||
|
popoverController.close();
|
||||||
|
},
|
||||||
|
onAddToExistingPage: (documentId) async {
|
||||||
|
final view = await onAddToExistingPage(context, documentId);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
openPageFromMessage(context, view);
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
await updateSelection(documentId);
|
||||||
|
popoverController.close();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ViewPB?> onAddToExistingPage(
|
||||||
|
BuildContext context,
|
||||||
|
String documentId,
|
||||||
|
) async {
|
||||||
|
final bloc = context.read<ChatSelectMessageBloc>();
|
||||||
|
|
||||||
|
final selectedMessages = [
|
||||||
|
...bloc.state.selectedMessages.whereType<TextMessage>(),
|
||||||
|
]..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
|
|
||||||
|
await ChatEditDocumentService.addMessagesToPage(
|
||||||
|
documentId,
|
||||||
|
selectedMessages,
|
||||||
|
);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
final view = await ViewBackendService.getView(documentId).toNullable();
|
||||||
|
if (context.mounted) {
|
||||||
|
showSaveMessageSuccessToast(context, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
bloc.add(const ChatSelectMessageEvent.saveAsPage());
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addMessageToNewPage(
|
||||||
|
BuildContext context,
|
||||||
|
String parentViewId,
|
||||||
|
) async {
|
||||||
|
final bloc = context.read<ChatSelectMessageBloc>();
|
||||||
|
|
||||||
|
final selectedMessages = [
|
||||||
|
...bloc.state.selectedMessages.whereType<TextMessage>(),
|
||||||
|
]..sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
|
|
||||||
|
final newView = await ChatEditDocumentService.saveMessagesToNewPage(
|
||||||
|
widget.view.nameOrDefault,
|
||||||
|
parentViewId,
|
||||||
|
selectedMessages,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showSaveMessageSuccessToast(context, newView);
|
||||||
|
openPageFromMessage(context, newView);
|
||||||
|
}
|
||||||
|
bloc.add(const ChatSelectMessageEvent.saveAsPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> forceReload(String documentId) async {
|
||||||
|
final bloc = DocumentBloc.findOpen(documentId);
|
||||||
|
if (bloc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bloc.forceReloadDocumentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateSelection(String documentId) async {
|
||||||
|
final bloc = DocumentBloc.findOpen(documentId);
|
||||||
|
if (bloc == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bloc.forceReloadDocumentState();
|
||||||
|
final editorState = bloc.state.editorState;
|
||||||
|
final lastNodePath = editorState?.getLastSelectable()?.$1.path;
|
||||||
|
if (editorState == null || lastNodePath == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(
|
||||||
|
editorState.updateSelectionWithReason(
|
||||||
|
Selection.collapsed(Position(path: lastNodePath)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getOpenedDocumentId() {
|
||||||
|
final pageManager = getIt<TabsBloc>().state.currentPageManager;
|
||||||
|
if (!pageManager.showSecondaryPluginNotifier.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pageManager.secondaryNotifier.plugin.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -488,7 +488,7 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||||||
Widget buildPopover(BuildContext context) {
|
Widget buildPopover(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: context.read<ChatSettingsCubit>(),
|
value: context.read<ChatSettingsCubit>(),
|
||||||
child: _SaveToPagePopoverContent(
|
child: SaveToPagePopoverContent(
|
||||||
onAddToNewPage: (parentViewId) {
|
onAddToNewPage: (parentViewId) {
|
||||||
addMessageToNewPage(context, parentViewId);
|
addMessageToNewPage(context, parentViewId);
|
||||||
popoverController.close();
|
popoverController.close();
|
||||||
@ -511,9 +511,9 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
String documentId,
|
String documentId,
|
||||||
) async {
|
) async {
|
||||||
await ChatEditDocumentService.addMessageToPage(
|
await ChatEditDocumentService.addMessagesToPage(
|
||||||
documentId,
|
documentId,
|
||||||
widget.textMessage,
|
[widget.textMessage],
|
||||||
);
|
);
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
final view = await ViewBackendService.getView(documentId).toNullable();
|
final view = await ViewBackendService.getView(documentId).toNullable();
|
||||||
@ -541,35 +541,6 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
|
||||||
if (view == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showToastNotification(
|
|
||||||
context,
|
|
||||||
richMessage: TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: const Color(0xFFFFFFFF),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const TextSpan(
|
|
||||||
text: ' ',
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: view.nameOrDefault,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: const Color(0xFFFFFFFF),
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> forceReload(String documentId) async {
|
Future<void> forceReload(String documentId) async {
|
||||||
final bloc = DocumentBloc.findOpen(documentId);
|
final bloc = DocumentBloc.findOpen(documentId);
|
||||||
if (bloc == null) {
|
if (bloc == null) {
|
||||||
@ -605,8 +576,9 @@ class _SaveToPageButtonState extends State<SaveToPageButton> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SaveToPagePopoverContent extends StatelessWidget {
|
class SaveToPagePopoverContent extends StatelessWidget {
|
||||||
const _SaveToPagePopoverContent({
|
const SaveToPagePopoverContent({
|
||||||
|
super.key,
|
||||||
required this.onAddToNewPage,
|
required this.onAddToNewPage,
|
||||||
required this.onAddToExistingPage,
|
required this.onAddToExistingPage,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
|||||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_edit_document_service.dart';
|
||||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||||
|
import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';
|
||||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.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/shared/markdown_to_document.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
@ -15,6 +16,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
@ -37,6 +39,7 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
required this.child,
|
required this.child,
|
||||||
required this.showActions,
|
required this.showActions,
|
||||||
this.isLastMessage = false,
|
this.isLastMessage = false,
|
||||||
|
this.isSelectingMessages = false,
|
||||||
this.onRegenerate,
|
this.onRegenerate,
|
||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
});
|
});
|
||||||
@ -45,27 +48,25 @@ class ChatAIMessageBubble extends StatelessWidget {
|
|||||||
final Widget child;
|
final Widget child;
|
||||||
final bool showActions;
|
final bool showActions;
|
||||||
final bool isLastMessage;
|
final bool isLastMessage;
|
||||||
|
final bool isSelectingMessages;
|
||||||
final void Function()? onRegenerate;
|
final void Function()? onRegenerate;
|
||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final avatarAndMessage = Row(
|
final messageWidget = _WrapIsSelectingMessage(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
isSelectingMessages: isSelectingMessages,
|
||||||
children: [
|
message: message,
|
||||||
const ChatAIAvatar(),
|
child: child,
|
||||||
const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),
|
|
||||||
Expanded(child: child),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return showActions
|
return !isSelectingMessages && showActions
|
||||||
? UniversalPlatform.isMobile
|
? UniversalPlatform.isMobile
|
||||||
? _wrapPopMenu(avatarAndMessage)
|
? _wrapPopMenu(messageWidget)
|
||||||
: isLastMessage
|
: isLastMessage
|
||||||
? _wrapBottomActions(avatarAndMessage)
|
? _wrapBottomActions(messageWidget)
|
||||||
: _wrapHover(avatarAndMessage)
|
: _wrapHover(messageWidget)
|
||||||
: avatarAndMessage;
|
: messageWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _wrapBottomActions(Widget child) {
|
Widget _wrapBottomActions(Widget child) {
|
||||||
@ -413,9 +414,9 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ChatEditDocumentService.addMessageToPage(
|
await ChatEditDocumentService.addMessagesToPage(
|
||||||
selectedView.id,
|
selectedView.id,
|
||||||
message as TextMessage,
|
[message as TextMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
@ -429,3 +430,85 @@ class ChatAIMessagePopup extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _WrapIsSelectingMessage extends StatelessWidget {
|
||||||
|
const _WrapIsSelectingMessage({
|
||||||
|
required this.message,
|
||||||
|
required this.child,
|
||||||
|
this.isSelectingMessages = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Message message;
|
||||||
|
final Widget child;
|
||||||
|
final bool isSelectingMessages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final isSelected =
|
||||||
|
context.read<ChatSelectMessageBloc>().isMessageSelected(message.id);
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (isSelectingMessages) {
|
||||||
|
context
|
||||||
|
.read<ChatSelectMessageBloc>()
|
||||||
|
.add(ChatSelectMessageEvent.toggleSelectMessage(message));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
behavior: isSelectingMessages ? HitTestBehavior.opaque : null,
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? Theme.of(context).colorScheme.tertiaryContainer
|
||||||
|
: null,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (isSelectingMessages)
|
||||||
|
ChatSelectMessageIndicator(isSelected: isSelected)
|
||||||
|
else
|
||||||
|
const ChatAIAvatar(),
|
||||||
|
const HSpace(DesktopAIChatSizes.avatarAndChatBubbleSpacing),
|
||||||
|
Expanded(
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: isSelectingMessages,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatSelectMessageIndicator extends StatelessWidget {
|
||||||
|
const ChatSelectMessageIndicator({
|
||||||
|
super.key,
|
||||||
|
required this.isSelected,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isSelected;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: DesktopAIChatSizes.avatarSize,
|
||||||
|
child: Center(
|
||||||
|
child: FlowySvg(
|
||||||
|
isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||||
|
blendMode: BlendMode.dst,
|
||||||
|
size: const Size.square(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
this.onChangeFormat,
|
this.onChangeFormat,
|
||||||
this.isLastMessage = false,
|
this.isLastMessage = false,
|
||||||
this.isStreaming = false,
|
this.isStreaming = false,
|
||||||
|
this.isSelectingMessages = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final User user;
|
final User user;
|
||||||
@ -53,6 +54,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
final void Function(PredefinedFormat)? onChangeFormat;
|
final void Function(PredefinedFormat)? onChangeFormat;
|
||||||
final bool isStreaming;
|
final bool isStreaming;
|
||||||
final bool isLastMessage;
|
final bool isLastMessage;
|
||||||
|
final bool isSelectingMessages;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -98,6 +100,7 @@ class ChatAIMessageWidget extends StatelessWidget {
|
|||||||
showActions: stream == null &&
|
showActions: stream == null &&
|
||||||
state.text.isNotEmpty &&
|
state.text.isNotEmpty &&
|
||||||
!isStreaming,
|
!isStreaming,
|
||||||
|
isSelectingMessages: isSelectingMessages,
|
||||||
onRegenerate: onRegenerate,
|
onRegenerate: onRegenerate,
|
||||||
onChangeFormat: onChangeFormat,
|
onChangeFormat: onChangeFormat,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
|||||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:universal_platform/universal_platform.dart';
|
import 'package:universal_platform/universal_platform.dart';
|
||||||
|
|
||||||
/// Opens a message in the right hand sidebar on desktop, and push the page
|
/// Opens a message in the right hand sidebar on desktop, and push the page
|
||||||
@ -30,3 +30,32 @@ void openPageFromMessage(BuildContext context, ViewPB? view) {
|
|||||||
context.pushView(view);
|
context.pushView(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showSaveMessageSuccessToast(BuildContext context, ViewPB? view) {
|
||||||
|
if (view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToastNotification(
|
||||||
|
context,
|
||||||
|
richMessage: TextSpan(
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: LocaleKeys.chat_addToNewPageSuccessToast.tr(),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text: ' ',
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: view.nameOrDefault,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: const Color(0xFFFFFFFF),
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -288,7 +288,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
|||||||
const HSpace(10),
|
const HSpace(10),
|
||||||
ViewFavoriteButton(view: view),
|
ViewFavoriteButton(view: view),
|
||||||
const HSpace(4),
|
const HSpace(4),
|
||||||
MoreViewActions(view: view, isDocument: false),
|
MoreViewActions(view: view),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -70,6 +70,8 @@ extension ViewExtension on ViewPB {
|
|||||||
String get nameOrDefault =>
|
String get nameOrDefault =>
|
||||||
name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name;
|
name.isEmpty ? LocaleKeys.menuAppHeader_defaultNewPageName.tr() : name;
|
||||||
|
|
||||||
|
bool get isDocument => pluginType == PluginType.document;
|
||||||
|
|
||||||
Widget defaultIcon({Size? size}) => FlowySvg(
|
Widget defaultIcon({Size? size}) => FlowySvg(
|
||||||
switch (layout) {
|
switch (layout) {
|
||||||
ViewLayoutPB.Board => FlowySvgs.icon_board_s,
|
ViewLayoutPB.Board => FlowySvgs.icon_board_s,
|
||||||
|
|||||||
@ -294,7 +294,7 @@ class _SecondaryViewState extends State<SecondaryView>
|
|||||||
return CompositedTransformFollower(
|
return CompositedTransformFollower(
|
||||||
link: layerLink,
|
link: layerLink,
|
||||||
followerAnchor: Alignment.topRight,
|
followerAnchor: Alignment.topRight,
|
||||||
offset: const Offset(0.0, 80.0),
|
offset: const Offset(0.0, 120.0),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
@ -964,13 +964,16 @@ class NonClippingSizeTransition extends AnimatedWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final AlignmentDirectional alignment;
|
final AlignmentDirectional alignment;
|
||||||
|
final Edge edge;
|
||||||
if (axis == Axis.vertical) {
|
if (axis == Axis.vertical) {
|
||||||
alignment = AlignmentDirectional(-1.0, axisAlignment);
|
alignment = AlignmentDirectional(-1.0, axisAlignment);
|
||||||
|
edge = switch (axisAlignment) { -1.0 => Edge.bottom, _ => Edge.top };
|
||||||
} else {
|
} else {
|
||||||
alignment = AlignmentDirectional(axisAlignment, -1.0);
|
alignment = AlignmentDirectional(axisAlignment, -1.0);
|
||||||
|
edge = switch (axisAlignment) { -1.0 => Edge.right, _ => Edge.left };
|
||||||
}
|
}
|
||||||
return ClipRect(
|
return ClipRect(
|
||||||
clipper: const EdgeRectClipper(edge: Edge.right, margin: 20),
|
clipper: EdgeRectClipper(edge: edge, margin: 20),
|
||||||
child: Align(
|
child: Align(
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
heightFactor: axis == Axis.vertical
|
heightFactor: axis == Axis.vertical
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
|
|||||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
import 'package:appflowy/workspace/application/view/view_bloc.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/application/view_info/view_info_bloc.dart';
|
||||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||||
@ -21,14 +22,16 @@ class MoreViewActions extends StatefulWidget {
|
|||||||
const MoreViewActions({
|
const MoreViewActions({
|
||||||
super.key,
|
super.key,
|
||||||
required this.view,
|
required this.view,
|
||||||
this.isDocument = true,
|
this.customActions = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The view to show the actions for.
|
/// The view to show the actions for.
|
||||||
|
///
|
||||||
final ViewPB view;
|
final ViewPB view;
|
||||||
|
|
||||||
/// If false the view is a Database, otherwise it is a Document.
|
/// Custom actions to show in the popover, will be laid out at the top.
|
||||||
final bool isDocument;
|
///
|
||||||
|
final List<Widget> customActions;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<MoreViewActions> createState() => _MoreViewActionsState();
|
State<MoreViewActions> createState() => _MoreViewActionsState();
|
||||||
@ -49,8 +52,9 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
mutex: popoverMutex,
|
mutex: popoverMutex,
|
||||||
constraints: const BoxConstraints(maxWidth: 220),
|
constraints: const BoxConstraints(maxWidth: 245),
|
||||||
offset: const Offset(0, 42),
|
direction: PopoverDirection.bottomWithRightAligned,
|
||||||
|
offset: const Offset(0, 12),
|
||||||
popupBuilder: (_) => _buildPopup(state),
|
popupBuilder: (_) => _buildPopup(state),
|
||||||
child: const _ThreeDots(),
|
child: const _ThreeDots(),
|
||||||
);
|
);
|
||||||
@ -106,16 +110,21 @@ class _MoreViewActionsState extends State<MoreViewActions> {
|
|||||||
final timeFormat = appearanceSettings.timeFormat;
|
final timeFormat = appearanceSettings.timeFormat;
|
||||||
|
|
||||||
final viewMoreActionTypes = [
|
final viewMoreActionTypes = [
|
||||||
if (widget.isDocument) ViewMoreActionType.divider,
|
if (widget.view.layout != ViewLayoutPB.Chat) ViewMoreActionType.duplicate,
|
||||||
ViewMoreActionType.duplicate,
|
|
||||||
ViewMoreActionType.moveTo,
|
ViewMoreActionType.moveTo,
|
||||||
ViewMoreActionType.delete,
|
ViewMoreActionType.delete,
|
||||||
ViewMoreActionType.divider,
|
ViewMoreActionType.divider,
|
||||||
];
|
];
|
||||||
|
|
||||||
final actions = [
|
final actions = [
|
||||||
if (widget.isDocument) ...[
|
...widget.customActions,
|
||||||
|
if (widget.view.isDocument) ...[
|
||||||
const FontSizeAction(),
|
const FontSizeAction(),
|
||||||
|
ViewAction(
|
||||||
|
type: ViewMoreActionType.divider,
|
||||||
|
view: widget.view,
|
||||||
|
mutex: popoverMutex,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
...viewMoreActionTypes.map(
|
...viewMoreActionTypes.map(
|
||||||
(type) => ViewAction(
|
(type) => ViewAction(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||||
@ -11,6 +12,8 @@ import 'package:appflowy_backend/log.dart';
|
|||||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||||
|
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@ -97,3 +100,38 @@ class ViewAction extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CustomViewAction extends StatelessWidget {
|
||||||
|
const CustomViewAction({
|
||||||
|
super.key,
|
||||||
|
required this.view,
|
||||||
|
required this.leftIcon,
|
||||||
|
required this.label,
|
||||||
|
this.onTap,
|
||||||
|
this.mutex,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ViewPB view;
|
||||||
|
final FlowySvgData leftIcon;
|
||||||
|
final String label;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final PopoverMutex? mutex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
height: 34,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||||
|
child: FlowyIconTextButton(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||||
|
onTap: onTap,
|
||||||
|
leftIconBuilder: (onHover) => FlowySvg(leftIcon),
|
||||||
|
iconPadding: 10.0,
|
||||||
|
textBuilder: (onHover) => FlowyText(
|
||||||
|
label,
|
||||||
|
figmaLineHeight: 18.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
3
frontend/resources/flowy_icons/16x/checkbox_ai_empty.svg
Normal file
3
frontend/resources/flowy_icons/16x/checkbox_ai_empty.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.8125" y="3.73499" width="12.375" height="12.375" rx="3.9375" fill="white" stroke="#BDBDBD" stroke-width="1.125"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 228 B |
4
frontend/resources/flowy_icons/16x/checkbox_ai_minus.svg
Normal file
4
frontend/resources/flowy_icons/16x/checkbox_ai_minus.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.8125" y="3.64246" width="12.375" height="12.375" rx="3.9375" fill="white" stroke="#BDBDBD" stroke-width="1.125"/>
|
||||||
|
<path d="M6 9.82996H12" stroke="#00BCF0" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 311 B |
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect x="2.815" y="2.815" width="12.37" height="12.37" rx="3.935" fill="white" stroke="#BDBDBD" stroke-width="1.13"/>
|
||||||
|
<path d="M6.75 9L8.56731 10.6875L11.8125 7.3125" stroke="#00BCF0" stroke-width="1.125" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 357 B |
@ -150,7 +150,8 @@
|
|||||||
"wordCountLabel": "Word count: ",
|
"wordCountLabel": "Word count: ",
|
||||||
"charCountLabel": "Character count: ",
|
"charCountLabel": "Character count: ",
|
||||||
"createdAtLabel": "Created: ",
|
"createdAtLabel": "Created: ",
|
||||||
"syncedAtLabel": "Synced: "
|
"syncedAtLabel": "Synced: ",
|
||||||
|
"saveAsNewPage": "Save messages to page"
|
||||||
},
|
},
|
||||||
"importPanel": {
|
"importPanel": {
|
||||||
"textAndMarkdown": "Text & Markdown",
|
"textAndMarkdown": "Text & Markdown",
|
||||||
@ -238,6 +239,12 @@
|
|||||||
"numberWithImageDescription": "@:chat.changeFormat.number with image",
|
"numberWithImageDescription": "@:chat.changeFormat.number with image",
|
||||||
"bulletWithImageDescription": "@:chat.changeFormat.bullet with image",
|
"bulletWithImageDescription": "@:chat.changeFormat.bullet with image",
|
||||||
"tableWithImageDescription": "@:chat.changeFormat.table with image"
|
"tableWithImageDescription": "@:chat.changeFormat.table with image"
|
||||||
|
},
|
||||||
|
"selectBanner": {
|
||||||
|
"saveButton": "Add to...",
|
||||||
|
"selectMessages": "Select messages",
|
||||||
|
"nSelected": "{} selected",
|
||||||
|
"allSelected": "All selected"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"trash": {
|
"trash": {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user