From 30f56928180cc47dc596dd0be5ce90c0cf0eae15 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 11 May 2025 16:45:28 +0800 Subject: [PATCH] chore: adjust database custom prompt ui (#7902) --- .../ai/service/ai_prompt_selector_cubit.dart | 45 ++-- .../ai_prompt_category_list.dart | 13 +- .../ai_prompt_database_selector.dart | 219 ++++++++++-------- .../ai_prompt_modal/ai_prompt_modal.dart | 118 +++++----- .../ai_prompt_modal/ai_prompt_onboarding.dart | 53 +++++ .../ai_prompt_visible_list.dart | 140 ++++++++--- .../presentation/widgets/dialog_v2.dart | 10 +- frontend/resources/translations/en.json | 12 +- .../src/services/database/database_editor.rs | 4 + 9 files changed, 380 insertions(+), 234 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_onboarding.dart diff --git a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_selector_cubit.dart b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_selector_cubit.dart index 298b25e981..3bc874fd4b 100644 --- a/frontend/appflowy_flutter/lib/ai/service/ai_prompt_selector_cubit.dart +++ b/frontend/appflowy_flutter/lib/ai/service/ai_prompt_selector_cubit.dart @@ -80,7 +80,7 @@ class AiPromptSelectorCubit extends Cubit { final customPrompts = await _aiService.getDatabasePrompts(databaseViewId); - if (customPrompts == null || customPrompts.isEmpty) { + if (customPrompts == null) { final prompts = availablePrompts.where((prompt) => prompt.isFeatured); final visiblePrompts = _getFilteredPrompts(prompts); final selectedPromptId = _getVisibleSelectedPrompt( @@ -277,37 +277,18 @@ class AiPromptSelectorCubit extends Cubit { emit( state.maybeMap( ready: (readyState) { - if (customPrompts.isEmpty) { - final prompts = - availablePrompts.where((prompt) => prompt.isFeatured); - final visiblePrompts = _getFilteredPrompts(prompts); - final selectedPromptId = _getVisibleSelectedPrompt( - visiblePrompts, - readyState.selectedPromptId, - ); - return readyState.copyWith( - visiblePrompts: visiblePrompts.toList(), - selectedPromptId: selectedPromptId, - customPromptDatabaseViewId: viewId, - isLoadingCustomPrompts: false, - isFeaturedSectionSelected: true, - isCustomPromptSectionSelected: false, - selectedCategory: null, - ); - } else { - final prompts = _getPromptsByCategory(readyState); - final visiblePrompts = _getFilteredPrompts(prompts); - final selectedPromptId = _getVisibleSelectedPrompt( - visiblePrompts, - readyState.selectedPromptId, - ); - return readyState.copyWith( - visiblePrompts: visiblePrompts.toList(), - selectedPromptId: selectedPromptId, - customPromptDatabaseViewId: viewId, - isLoadingCustomPrompts: false, - ); - } + final prompts = _getPromptsByCategory(readyState); + final visiblePrompts = _getFilteredPrompts(prompts); + final selectedPromptId = _getVisibleSelectedPrompt( + visiblePrompts, + readyState.selectedPromptId, + ); + return readyState.copyWith( + visiblePrompts: visiblePrompts.toList(), + selectedPromptId: selectedPromptId, + customPromptDatabaseViewId: viewId, + isLoadingCustomPrompts: false, + ); }, orElse: () => state, ), diff --git a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_category_list.dart b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_category_list.dart index 70d4936954..2ef02e2a8e 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_category_list.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_category_list.dart @@ -148,12 +148,8 @@ class AiPromptCustomPromptSection extends StatelessWidget { return state.maybeMap( ready: (readyState) { final isSelected = readyState.isCustomPromptSectionSelected; - final isDisabled = context - .read() - .availablePrompts - .every((prompt) => !prompt.isCustom); + return AFBaseButton( - disabled: isDisabled, onTap: () { if (!isSelected) { context.read().selectCustomSection(); @@ -163,9 +159,7 @@ class AiPromptCustomPromptSection extends StatelessWidget { return Text( LocaleKeys.ai_customPrompt_custom.tr(), style: AppFlowyTheme.of(context).textStyle.body.standard( - color: disabled - ? theme.textColorScheme.tertiary - : theme.textColorScheme.primary, + color: theme.textColorScheme.primary, ), overflow: TextOverflow.ellipsis, ); @@ -178,9 +172,6 @@ class AiPromptCustomPromptSection extends StatelessWidget { borderColor: (context, isHovering, disabled, isFocused) => Colors.transparent, backgroundColor: (context, isHovering, disabled) { - if (disabled) { - return Colors.transparent; - } if (isSelected) { return theme.fillColorScheme.themeSelect; } diff --git a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_database_selector.dart b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_database_selector.dart index ead4b21394..33655e101d 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_database_selector.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_database_selector.dart @@ -1,5 +1,4 @@ import 'package:appflowy/ai/ai.dart'; -import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart'; @@ -18,12 +17,16 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class CustomPromptDatabaseSelector extends StatefulWidget { const CustomPromptDatabaseSelector({ super.key, - required this.databaseViewId, - required this.isLoading, + this.databaseViewId, + this.isLoading = false, + this.popoverDirection = PopoverDirection.bottomWithCenterAligned, + required this.childBuilder, }); final String? databaseViewId; final bool isLoading; + final PopoverDirection popoverDirection; + final Widget Function(VoidCallback) childBuilder; @override State createState() => @@ -40,7 +43,10 @@ class _CustomPromptDatabaseSelectorState viewSelectorCubit: BlocProvider( create: (context) => ViewSelectorCubit( getIgnoreViewType: (view) { - if (view.layout.isDatabaseView || view.layout.isDocumentView) { + if (view.layout.isDatabaseView) { + return IgnoreViewType.none; + } + if (view.layout.isDocumentView) { return IgnoreViewType.none; } return IgnoreViewType.hide; @@ -54,8 +60,8 @@ class _CustomPromptDatabaseSelectorState controller: popoverController, triggerActions: PopoverTriggerFlags.none, margin: EdgeInsets.zero, - offset: const Offset(0, 2), - direction: PopoverDirection.bottomWithRightAligned, + offset: const Offset(0, 4.0), + direction: widget.popoverDirection, constraints: const BoxConstraints.tightFor(width: 300, height: 400), popupBuilder: (_) { return BlocProvider.value( @@ -72,10 +78,8 @@ class _CustomPromptDatabaseSelectorState ), ); }, - child: _Button( - selectedViewId: widget.databaseViewId, - isLoading: widget.isLoading, - onTap: () { + child: widget.childBuilder( + () { if (!widget.isLoading) { context .read() @@ -91,8 +95,9 @@ class _CustomPromptDatabaseSelectorState } } -class _Button extends StatelessWidget { - const _Button({ +class AiPromptDatabaseSelectorButton extends StatefulWidget { + const AiPromptDatabaseSelectorButton({ + super.key, required this.selectedViewId, required this.isLoading, required this.onTap, @@ -102,93 +107,110 @@ class _Button extends StatelessWidget { final bool isLoading; final VoidCallback onTap; + @override + State createState() => + _AiPromptDatabaseSelectorButtonState(); +} + +class _AiPromptDatabaseSelectorButtonState + extends State { + late Future viewFuture; + + @override + void initState() { + super.initState(); + viewFuture = getView(); + } + + @override + void didUpdateWidget(covariant AiPromptDatabaseSelectorButton oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.selectedViewId != widget.selectedViewId) { + viewFuture = getView(); + } + } + @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); return FutureBuilder( - future: getView(), + future: viewFuture, builder: (context, snapshot) { - final data = snapshot.data; + String name = ""; - final String name; - final String? tooltip; - if (isLoading) { - tooltip = null; - name = LocaleKeys.ai_customPrompt_loading.tr(); - } else if (!snapshot.hasData || - snapshot.connectionState != ConnectionState.done || - data == null) { - name = LocaleKeys.ai_customPrompt_selectDatabase.tr(); - tooltip = LocaleKeys.ai_customPrompt_selectDatabase.tr(); - } else { - name = tooltip = data.nameOrDefault; + if (snapshot.hasData && + snapshot.connectionState == ConnectionState.done) { + name = snapshot.data!.nameOrDefault; } - return FlowyTooltip( - message: tooltip, - preferBelow: false, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 150, - ), - child: AFGhostButton.normal( - onTap: onTap, - padding: EdgeInsets.symmetric( - vertical: theme.spacing.xs, - horizontal: theme.spacing.m, + return Row( + spacing: theme.spacing.s, + children: [ + Expanded( + child: Text( + "${LocaleKeys.ai_customPrompt_promptDatabase.tr()}: $name", + maxLines: 1, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + overflow: TextOverflow.ellipsis, ), - builder: (context, isHovering, disabled) { - return Row( - spacing: theme.spacing.xs, - mainAxisSize: MainAxisSize.min, - children: [ - buildLoadingIndicator(theme), - Flexible( - child: Text( - name, - maxLines: 1, - style: theme.textStyle.body.standard( - color: theme.textColorScheme.secondary, - ), - overflow: TextOverflow.ellipsis, - ), - ), - if (!isLoading) - FlowySvg( - FlowySvgs.toolbar_arrow_down_m, - color: theme.iconColorScheme.secondary, - ), - ], - ); - }, ), - ), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 150, + ), + child: AFOutlinedButton.normal( + onTap: widget.onTap, + builder: (context, isHovering, disabled) { + return Row( + spacing: theme.spacing.s, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.isLoading) buildLoadingIndicator(theme), + Flexible( + child: Text( + widget.isLoading + ? LocaleKeys.ai_customPrompt_loading.tr() + : LocaleKeys.button_change.tr(), + maxLines: 1, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ), + ), + ], ); }, ); } Widget buildLoadingIndicator(AppFlowyThemeData theme) { - return isLoading - ? SizedBox.square( - dimension: 20, - child: Padding( - padding: EdgeInsets.all(2.5), - child: CircularProgressIndicator( - color: theme.iconColorScheme.tertiary, - strokeWidth: 2.0, - ), - ), - ) - : const SizedBox.shrink(); + return SizedBox.square( + dimension: 20, + child: Padding( + padding: EdgeInsets.all(2.5), + child: CircularProgressIndicator( + color: theme.iconColorScheme.tertiary, + strokeWidth: 2.0, + ), + ), + ); } Future getView() async { - if (selectedViewId == null) { + if (widget.selectedViewId == null) { return null; } - final view = await ViewBackendService.getView(selectedViewId!).toNullable(); + final view = + await ViewBackendService.getView(widget.selectedViewId!).toNullable(); if (view != null) { return view; @@ -196,7 +218,7 @@ class _Button extends StatelessWidget { final trashViews = await TrashService().readTrash().toNullable(); final trashedItem = trashViews?.items - .firstWhereOrNull((element) => element.id == selectedViewId); + .firstWhereOrNull((element) => element.id == widget.selectedViewId); if (trashedItem == null) { return null; @@ -208,11 +230,34 @@ class _Button extends StatelessWidget { } } -class _PopoverContent extends StatelessWidget { - const _PopoverContent({required this.onSelectView}); +class _PopoverContent extends StatefulWidget { + const _PopoverContent({ + required this.onSelectView, + }); final void Function(ViewPB view) onSelectView; + @override + State<_PopoverContent> createState() => _PopoverContentState(); +} + +class _PopoverContentState extends State<_PopoverContent> { + final focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + } + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); @@ -221,23 +266,13 @@ class _PopoverContent extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - VSpace(theme.spacing.m), - Padding( - padding: EdgeInsets.symmetric( - horizontal: theme.spacing.l, - ), - child: Text( - LocaleKeys.ai_customPrompt_loadDatabasePromptsFrom.tr(), - style: theme.textStyle.caption - .standard(color: theme.textColorScheme.secondary), - ), - ), VSpace(theme.spacing.m), Padding( padding: EdgeInsets.symmetric( horizontal: theme.spacing.m, ), child: AFTextField( + focusNode: focusNode, size: AFTextFieldSize.m, hintText: LocaleKeys.search_label.tr(), controller: context.read().filterTextController, @@ -275,7 +310,7 @@ class _PopoverContent extends StatelessWidget { isSelectedSection: false, showCheckbox: false, onSelected: (source) { - onSelectView(source.view); + widget.onSelectView(source.view); }, height: 30.0, ), diff --git a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_modal.dart b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_modal.dart index f609d142d0..aa97f446d8 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_modal.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_modal.dart @@ -2,7 +2,7 @@ import 'package:appflowy/ai/ai.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/user/prelude.dart'; -import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialog_v2.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -10,7 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'ai_prompt_category_list.dart'; -import 'ai_prompt_database_selector.dart'; +import 'ai_prompt_onboarding.dart'; import 'ai_prompt_preview.dart'; import 'ai_prompt_visible_list.dart'; @@ -53,11 +53,8 @@ class AiPromptModal extends StatelessWidget { child: BlocListener( listener: (context, state) { state.maybeMap( - invalidDatabase: (state) { - showToastNotification( - message: LocaleKeys.ai_customPrompt_invalidDatabase.tr(), - type: ToastificationType.error, - ); + invalidDatabase: (_) { + showLoadPromptFailedDialog(context); }, orElse: () {}, ); @@ -72,33 +69,9 @@ class AiPromptModal extends StatelessWidget { ), ), trailing: [ - BlocBuilder( - buildWhen: (p, c) { - return p.maybeMap( - ready: (pr) => c.maybeMap( - ready: (cr) => - pr.customPromptDatabaseViewId != - cr.customPromptDatabaseViewId || - pr.isLoadingCustomPrompts != - cr.isLoadingCustomPrompts, - orElse: () => false, - ), - orElse: () => true, - ); - }, - builder: (context, state) { - return state.maybeMap( - ready: (readyState) => CustomPromptDatabaseSelector( - databaseViewId: readyState.customPromptDatabaseViewId, - isLoading: readyState.isLoadingCustomPrompts, - ), - orElse: () => const SizedBox.shrink(), - ); - }, - ), AFGhostButton.normal( onTap: () => Navigator.of(context).pop(), - padding: EdgeInsets.all(theme.spacing.s), + padding: EdgeInsets.all(theme.spacing.xs), builder: (context, isHovering, disabled) { return Center( child: FlowySvg( @@ -121,41 +94,52 @@ class AiPromptModal extends StatelessWidget { child: CircularProgressIndicator(), ); }, - ready: (_) { + ready: (readyState) { return Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Expanded( child: AiPromptCategoryList(), ), - const Expanded( - flex: 2, - child: AiPromptVisibleList(), - ), - Expanded( - flex: 3, - child: BlocBuilder( - builder: (context, state) { - final selectedPrompt = state.maybeMap( - ready: (state) { - return state.visiblePrompts - .firstWhereOrNull( - (prompt) => - prompt.id == state.selectedPromptId, - ); - }, - orElse: () => null, - ); - if (selectedPrompt == null) { - return const SizedBox.shrink(); - } - return AiPromptPreview( - prompt: selectedPrompt, - ); - }, + if (readyState.isCustomPromptSectionSelected && + readyState.customPromptDatabaseViewId == null) + const Expanded( + flex: 5, + child: Center( + child: AiPromptOnboarding(), + ), + ) + else ...[ + const Expanded( + flex: 2, + child: AiPromptVisibleList(), ), - ), + Expanded( + flex: 3, + child: BlocBuilder( + builder: (context, state) { + final selectedPrompt = state.maybeMap( + ready: (state) { + return state.visiblePrompts + .firstWhereOrNull( + (prompt) => + prompt.id == + state.selectedPromptId, + ); + }, + orElse: () => null, + ); + if (selectedPrompt == null) { + return const SizedBox.shrink(); + } + return AiPromptPreview( + prompt: selectedPrompt, + ); + }, + ), + ), + ], ], ); }, @@ -171,3 +155,17 @@ class AiPromptModal extends StatelessWidget { ); } } + +void showLoadPromptFailedDialog( + BuildContext context, +) { + showSimpleAFDialog( + context: context, + title: LocaleKeys.ai_customPrompt_invalidDatabase.tr(), + content: LocaleKeys.ai_customPrompt_invalidDatabaseHelp.tr(), + primaryAction: ( + LocaleKeys.button_ok.tr(), + (context) {}, + ), + ); +} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_onboarding.dart b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_onboarding.dart new file mode 100644 index 0000000000..145ea0f914 --- /dev/null +++ b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_onboarding.dart @@ -0,0 +1,53 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_ui/appflowy_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/widgets.dart'; + +import 'ai_prompt_database_selector.dart'; + +class AiPromptOnboarding extends StatelessWidget { + const AiPromptOnboarding({super.key}); + + @override + Widget build(BuildContext context) { + final theme = AppFlowyTheme.of(context); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + LocaleKeys.ai_customPrompt_customPrompt.tr(), + style: theme.textStyle.heading3.standard( + color: theme.textColorScheme.primary, + ), + ), + VSpace( + theme.spacing.s, + ), + Text( + LocaleKeys.ai_customPrompt_databasePrompts.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.secondary, + ), + ), + VSpace( + theme.spacing.xxl, + ), + CustomPromptDatabaseSelector( + childBuilder: (onTap) => AFFilledButton.primary( + onTap: onTap, + builder: (context, isHovering, disabled) { + return Text( + LocaleKeys.ai_customPrompt_selectDatabase.tr(), + style: theme.textStyle.body.enhanced( + color: theme.textColorScheme.onFill, + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_visible_list.dart b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_visible_list.dart index 1c817eb228..3f5fba16b7 100644 --- a/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_visible_list.dart +++ b/frontend/appflowy_flutter/lib/ai/widgets/ai_prompt_modal/ai_prompt_visible_list.dart @@ -7,10 +7,12 @@ import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:diffutil_dart/diffutil.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'ai_prompt_database_selector.dart'; + const Duration _listItemAnimationDuration = Duration(milliseconds: 150); class AiPromptVisibleList extends StatefulWidget { @@ -53,7 +55,8 @@ class _AiPromptVisibleListState extends State { @override Widget build(BuildContext context) { - final spacing = AppFlowyTheme.of(context).spacing; + final theme = AppFlowyTheme.of(context); + return BlocListener( listener: (context, state) { state.maybeMap( @@ -66,43 +69,77 @@ class _AiPromptVisibleListState extends State { child: Column( children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: spacing.l), + padding: EdgeInsets.symmetric(horizontal: theme.spacing.l), child: buildSearchField(context), ), - VSpace(spacing.s), + VSpace( + theme.spacing.s, + ), + BlocBuilder( + buildWhen: (p, c) { + return p.maybeMap( + ready: (pr) => c.maybeMap( + ready: (cr) => + pr.customPromptDatabaseViewId != + cr.customPromptDatabaseViewId || + pr.isLoadingCustomPrompts != cr.isLoadingCustomPrompts || + pr.isCustomPromptSectionSelected != + cr.isCustomPromptSectionSelected, + orElse: () => false, + ), + orElse: () => true, + ); + }, + builder: (context, state) { + return state.maybeMap( + ready: (readyState) { + if (!readyState.isCustomPromptSectionSelected) { + return const SizedBox.shrink(); + } + return Padding( + padding: EdgeInsets.only( + left: theme.spacing.l, + top: theme.spacing.s, + right: theme.spacing.l, + ), + child: CustomPromptDatabaseSelector( + databaseViewId: readyState.customPromptDatabaseViewId, + isLoading: readyState.isLoadingCustomPrompts, + popoverDirection: PopoverDirection.bottomWithRightAligned, + childBuilder: (onTap) { + return AiPromptDatabaseSelectorButton( + selectedViewId: readyState.customPromptDatabaseViewId, + isLoading: readyState.isLoadingCustomPrompts, + onTap: onTap, + ); + }, + ), + ); + }, + orElse: () => const SizedBox.shrink(), + ); + }, + ), Expanded( child: TextFieldTapRegion( groupId: "ai_prompt_category_list", - child: AnimatedList( - controller: scrollController, - padding: EdgeInsets.all(spacing.l), - key: listKey, - initialItemCount: oldList.length, - itemBuilder: (context, index, animation) { - return BlocBuilder( - builder: (context, state) { - return state.maybeMap( - ready: (state) { - final prompt = state.visiblePrompts[index]; - - return Padding( - padding: EdgeInsets.only( - top: index == 0 ? 0 : spacing.s, - bottom: index == state.visiblePrompts.length - 1 - ? 0 - : spacing.s, + child: BlocBuilder( + builder: (context, state) { + return state.maybeMap( + ready: (readyState) { + if (readyState.visiblePrompts.isEmpty) { + return Center( + child: Text( + LocaleKeys.ai_customPrompt_noResults.tr(), + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, ), - child: _AiPromptListItem( - animation: animation, - prompt: prompt, - isSelected: state.selectedPromptId == prompt.id, - ), - ); - }, - orElse: () => const SizedBox.shrink(), - ); + ), + ); + } + return buildPromptList(); }, + orElse: () => const SizedBox.shrink(), ); }, ), @@ -148,6 +185,43 @@ class _AiPromptVisibleListState extends State { ); } + Widget buildPromptList() { + final theme = AppFlowyTheme.of(context); + + return AnimatedList( + controller: scrollController, + padding: EdgeInsets.all(theme.spacing.l), + key: listKey, + initialItemCount: oldList.length, + itemBuilder: (context, index, animation) { + return BlocBuilder( + builder: (context, state) { + return state.maybeMap( + ready: (state) { + final prompt = state.visiblePrompts[index]; + + return Padding( + padding: EdgeInsets.only( + top: index == 0 ? 0 : theme.spacing.s, + bottom: index == state.visiblePrompts.length - 1 + ? 0 + : theme.spacing.s, + ), + child: _AiPromptListItem( + animation: animation, + prompt: prompt, + isSelected: state.selectedPromptId == prompt.id, + ), + ); + }, + orElse: () => const SizedBox.shrink(), + ); + }, + ); + }, + ); + } + void handleVisiblePromptListChanged( List newList, ) { @@ -265,6 +339,7 @@ class _AiPromptListItemState extends State<_AiPromptListItem> { padding: EdgeInsets.all(theme.spacing.m), decoration: BoxDecoration( borderRadius: BorderRadius.circular(theme.borderRadius.m), + color: Colors.transparent, border: Border.all( color: widget.isSelected ? isHovering @@ -274,7 +349,6 @@ class _AiPromptListItemState extends State<_AiPromptListItem> { ? theme.borderColorScheme.greyTertiaryHover : theme.borderColorScheme.greyTertiary, ), - color: theme.surfaceColorScheme.primary, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart index 979e35fad8..329315b475 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialog_v2.dart @@ -43,9 +43,10 @@ Future showSimpleAFDialog({ trailing: [ AFGhostButton.normal( onTap: () => Navigator.of(context).pop(), + padding: EdgeInsets.all(theme.spacing.xs), builder: (context, isHovering, disabled) { return FlowySvg( - FlowySvgs.close_s, + FlowySvgs.toast_close_s, size: Size.square(20), ); }, @@ -57,7 +58,12 @@ Future showSimpleAFDialog({ // AFModalDimension.dialogHeight - header - footer constraints: BoxConstraints(minHeight: 108.0), child: AFModalBody( - child: Text(content), + child: Text( + content, + style: theme.textStyle.body.standard( + color: theme.textColorScheme.primary, + ), + ), ), ), ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 75ef45d7b1..fbba439872 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -466,6 +466,7 @@ "signIn": "Sign In", "signOut": "Sign Out", "complete": "Complete", + "change": "Change", "save": "Save", "generate": "Generate", "esc": "ESC", @@ -3331,11 +3332,14 @@ "usePrompt": "Use prompt", "featured": "Featured", "custom": "Custom", - "loadDatabasePrompts": "Custom prompts from your database", - "loadDatabasePromptsFrom": "Load custom prompts from...", + "customPrompt": "Custom Prompts", + "databasePrompts": "Load prompts from your own database", "selectDatabase": "Select database", - "loading": "Loading...", - "invalidDatabase": "Failed to load custom prompts from database", + "promptDatabase": "Prompt database", + "loading": "Loading", + "invalidDatabase": "Invalid Database", + "invalidDatabaseHelp": "Ensure that the database has at least two text properties:\n ◦ One used for the prompt name\n ◦ One used for the prompt content\nYou can also optionally add properties for the prompt example and category.", + "noResults": "No prompts found", "all": "All", "development": "Development", "writing": "Writing", diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 7d513274a9..cbff27d81b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1897,6 +1897,10 @@ impl DatabaseEditor { let content_cell = row.cells.get(&content_field.id); let content = content_cell.map(|cell| stringify_cell(cell, content_field))?; + if content.is_empty() { + return None; + } + let example = example_field .and_then(|field| extract_cell_value(&row, field)) .unwrap_or_default();