From e460120a1c904c992fe5ba1537d3cb540507b7ac Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Aug 2024 09:50:42 +0800 Subject: [PATCH] feat: add ai bubble button on mobile home page (#5992) * chore: skip check list test if the task is not found * feat: add ai bubble button in home page * feat: only show the ai bubble button for the cloud user * chore: add border color to ai bubble button * Revert "chore: skip check list test if the task is not found" This reverts commit 961f594a31906c52384c09915dce8f9db7fbd5bc. * fix: only display ai bubble button on home page --- .../presentation/home/mobile_home_page.dart | 149 ++++++++++-------- .../home/tab/ai_bubble_button.dart | 81 ++++++++++ .../home/tab/mobile_space_tab.dart | 41 ++++- .../presentation/chat_welcome_page.dart | 1 + 4 files changed, 197 insertions(+), 75 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index dd9512b0ef..4fe84524a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -134,75 +134,7 @@ class _MobileHomePageState extends State { value: getIt()..add(const ReminderEvent.started()), ), ], - child: BlocConsumer( - buildWhen: (previous, current) => - previous.currentWorkspace?.workspaceId != - current.currentWorkspace?.workspaceId, - listener: (context, state) { - getIt().reset(); - mCurrentWorkspace.value = state.currentWorkspace; - }, - builder: (context, state) { - if (state.currentWorkspace == null) { - return const SizedBox.shrink(); - } - - final workspaceId = state.currentWorkspace!.workspaceId; - - return Column( - children: [ - // Header - Padding( - padding: EdgeInsets.only( - left: HomeSpaceViewSizes.mHorizontalPadding, - right: 8.0, - top: Platform.isAndroid ? 8.0 : 0.0, - ), - child: MobileHomePageHeader( - userProfile: widget.userProfile, - ), - ), - - Expanded( - child: MultiBlocProvider( - providers: [ - BlocProvider( - create: (_) => SpaceOrderBloc() - ..add(const SpaceOrderEvent.initial()), - ), - BlocProvider( - create: (_) => SidebarSectionsBloc() - ..add( - SidebarSectionsEvent.initial( - widget.userProfile, - workspaceId, - ), - ), - ), - BlocProvider( - create: (_) => - FavoriteBloc()..add(const FavoriteEvent.initial()), - ), - BlocProvider( - create: (_) => SpaceBloc() - ..add( - SpaceEvent.initial( - widget.userProfile, - workspaceId, - openFirstPage: false, - ), - ), - ), - ], - child: MobileSpaceTab( - userProfile: widget.userProfile, - ), - ), - ), - ], - ); - }, - ), + child: _HomePage(userProfile: widget.userProfile), ); } @@ -214,3 +146,82 @@ class _MobileHomePageState extends State { await FolderEventSetLatestView(ViewIdPB(value: id)).send(); } } + +class _HomePage extends StatelessWidget { + const _HomePage({required this.userProfile}); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + listener: (context, state) { + getIt().reset(); + mCurrentWorkspace.value = state.currentWorkspace; + }, + builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } + + final workspaceId = state.currentWorkspace!.workspaceId; + + return Column( + children: [ + // Header + Padding( + padding: EdgeInsets.only( + left: HomeSpaceViewSizes.mHorizontalPadding, + right: 8.0, + top: Platform.isAndroid ? 8.0 : 0.0, + ), + child: MobileHomePageHeader( + userProfile: userProfile, + ), + ), + + Expanded( + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (_) => + SpaceOrderBloc()..add(const SpaceOrderEvent.initial()), + ), + BlocProvider( + create: (_) => SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + userProfile, + workspaceId, + ), + ), + ), + BlocProvider( + create: (_) => + FavoriteBloc()..add(const FavoriteEvent.initial()), + ), + BlocProvider( + create: (_) => SpaceBloc() + ..add( + SpaceEvent.initial( + userProfile, + workspaceId, + openFirstPage: false, + ), + ), + ), + ], + child: MobileSpaceTab( + userProfile: userProfile, + ), + ), + ), + ], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart new file mode 100644 index 0000000000..8ecd70f7e5 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart @@ -0,0 +1,81 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/base/gesture.dart'; +import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart'; +import 'package:appflowy/util/theme_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FloatingAIEntry extends StatelessWidget { + const FloatingAIEntry({super.key}); + + @override + Widget build(BuildContext context) { + return AnimatedGestureDetector( + scaleFactor: 0.99, + onTapUp: () => mobileCreateNewAIChatNotifier.value = + mobileCreateNewAIChatNotifier.value + 1, + child: DecoratedBox( + decoration: _buildShadowDecoration(context), + child: Container( + decoration: _buildWrapperDecoration(context), + height: 48, + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 18), + child: _buildHintText(context), + ), + ), + ), + ); + } + + BoxDecoration _buildShadowDecoration(BuildContext context) { + return BoxDecoration( + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + blurRadius: 20, + spreadRadius: 1, + offset: const Offset(0, 4), + color: Colors.black.withOpacity(0.05), + ), + ], + ); + } + + BoxDecoration _buildWrapperDecoration(BuildContext context) { + final outlineColor = Theme.of(context).colorScheme.outline; + final borderColor = Theme.of(context).isLightMode + ? outlineColor.withOpacity(0.7) + : outlineColor.withOpacity(0.3); + return BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).colorScheme.surface, + border: Border.fromBorderSide( + BorderSide( + color: borderColor, + ), + ), + ); + } + + Widget _buildHintText(BuildContext context) { + return Row( + children: [ + FlowySvg( + FlowySvgs.toolbar_item_ai_s, + size: const Size.square(16.0), + color: Theme.of(context).hintColor, + opacity: 0.7, + ), + const HSpace(8), + FlowyText( + LocaleKeys.chat_inputMessageHint.tr(), + color: Theme.of(context).hintColor, + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart index 77c26005c4..1c0f5933fb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart @@ -20,6 +20,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:provider/provider.dart'; +import 'ai_bubble_button.dart'; + +final ValueNotifier mobileCreateNewAIChatNotifier = ValueNotifier(0); + class MobileSpaceTab extends StatefulWidget { const MobileSpaceTab({ super.key, @@ -40,7 +44,8 @@ class _MobileSpaceTabState extends State void initState() { super.initState(); - mobileCreateNewPageNotifier.addListener(_createNewPage); + mobileCreateNewPageNotifier.addListener(_createNewDocument); + mobileCreateNewAIChatNotifier.addListener(_createNewAIChat); mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace); } @@ -48,7 +53,9 @@ class _MobileSpaceTabState extends State void dispose() { tabController?.removeListener(_onTabChange); tabController?.dispose(); - mobileCreateNewPageNotifier.removeListener(_createNewPage); + + mobileCreateNewPageNotifier.removeListener(_createNewDocument); + mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat); mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace); super.dispose(); @@ -145,7 +152,20 @@ class _MobileSpaceTabState extends State case MobileSpaceTabType.recent: return const MobileRecentSpace(); case MobileSpaceTabType.spaces: - return MobileHomeSpace(userProfile: widget.userProfile); + return Stack( + children: [ + MobileHomeSpace(userProfile: widget.userProfile), + // only show ai chat button for cloud user + if (widget.userProfile.authenticator == + AuthenticatorPB.AppFlowyCloud) + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 16, + left: 20, + right: 20, + child: const FloatingAIEntry(), + ), + ], + ); case MobileSpaceTabType.favorites: return MobileFavoriteSpace(userProfile: widget.userProfile); default: @@ -155,15 +175,24 @@ class _MobileSpaceTabState extends State } // quick create new page when clicking the add button in navigation bar - void _createNewPage() { + void _createNewDocument() { + _createNewPage(ViewLayoutPB.Document); + } + + void _createNewAIChat() { + _createNewPage(ViewLayoutPB.Chat); + } + + void _createNewPage(ViewLayoutPB layout) { if (context.read().state.spaces.isNotEmpty) { context.read().add( SpaceEvent.createPage( name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - layout: ViewLayoutPB.Document, + layout: layout, ), ); - } else { + } else if (layout == ViewLayoutPB.Document) { + // only support create document in section context.read().add( SidebarSectionsEvent.createRootViewInSection( name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart index f1ec5d2a7d..5524f1ffbe 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_welcome_page.dart @@ -73,6 +73,7 @@ class ChatWelcomePage extends StatelessWidget { const VSpace(8), Wrap( direction: Axis.vertical, + spacing: isMobile ? 12.0 : 0.0, children: items .map( (i) => WelcomeQuestionWidget(