mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-28 05:34:41 +00:00
chore: adjust database custom prompt ui (#7902)
This commit is contained in:
parent
6e850ab8b2
commit
30f5692818
@ -80,7 +80,7 @@ class AiPromptSelectorCubit extends Cubit<AiPromptSelectorState> {
|
||||
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<AiPromptSelectorState> {
|
||||
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,
|
||||
),
|
||||
|
||||
@ -148,12 +148,8 @@ class AiPromptCustomPromptSection extends StatelessWidget {
|
||||
return state.maybeMap(
|
||||
ready: (readyState) {
|
||||
final isSelected = readyState.isCustomPromptSectionSelected;
|
||||
final isDisabled = context
|
||||
.read<AiPromptSelectorCubit>()
|
||||
.availablePrompts
|
||||
.every((prompt) => !prompt.isCustom);
|
||||
|
||||
return AFBaseButton(
|
||||
disabled: isDisabled,
|
||||
onTap: () {
|
||||
if (!isSelected) {
|
||||
context.read<AiPromptSelectorCubit>().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;
|
||||
}
|
||||
|
||||
@ -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<CustomPromptDatabaseSelector> 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<ViewSelectorCubit>()
|
||||
@ -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<AiPromptDatabaseSelectorButton> createState() =>
|
||||
_AiPromptDatabaseSelectorButtonState();
|
||||
}
|
||||
|
||||
class _AiPromptDatabaseSelectorButtonState
|
||||
extends State<AiPromptDatabaseSelectorButton> {
|
||||
late Future<ViewPB?> 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<ViewPB?>(
|
||||
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<ViewPB?> 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<ViewSelectorCubit>().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,
|
||||
),
|
||||
|
||||
@ -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<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
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<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
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<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
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<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
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) {},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<AiPromptVisibleList> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spacing = AppFlowyTheme.of(context).spacing;
|
||||
final theme = AppFlowyTheme.of(context);
|
||||
|
||||
return BlocListener<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
listener: (context, state) {
|
||||
state.maybeMap(
|
||||
@ -66,43 +69,77 @@ class _AiPromptVisibleListState extends State<AiPromptVisibleList> {
|
||||
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<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
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<AiPromptSelectorCubit,
|
||||
AiPromptSelectorState>(
|
||||
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<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
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<AiPromptVisibleList> {
|
||||
);
|
||||
}
|
||||
|
||||
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<AiPromptSelectorCubit, AiPromptSelectorState>(
|
||||
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<AiPrompt> 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,
|
||||
|
||||
@ -43,9 +43,10 @@ Future<void> 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<void> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user