intermediate

This commit is contained in:
Fabian Freund
2025-01-23 14:03:55 +01:00
parent bd45ed64ee
commit 78c17a70c5
122 changed files with 8730 additions and 1662 deletions
@@ -1,225 +1,225 @@
import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/presentation/hooks/sync_page_tab.dart';
// import 'package:expandable_page_view/expandable_page_view.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_hooks/flutter_hooks.dart';
// import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
// import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
// import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
// import 'package:lensai/features/kagi/data/entities/modes.dart';
// import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
// import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
// import 'package:lensai/features/user/domain/repositories/settings.dart';
// import 'package:lensai/presentation/hooks/sync_page_tab.dart';
class AssistantTab extends HookConsumerWidget {
final SharedContent? sharedContent;
final OnSubmitUri onSubmit;
// class AssistantTab extends HookConsumerWidget {
// final SharedContent? sharedContent;
// final OnSubmitUri onSubmit;
const AssistantTab({
required this.sharedContent,
required this.onSubmit,
super.key,
});
// const AssistantTab({
// required this.sharedContent,
// required this.onSubmit,
// super.key,
// });
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final incognitoEnabled = ref.watch(
// settingsRepositoryProvider.select((value) => value.incognitoMode),
// );
final researchVariant = ref.watch(activeResearchVariantProvider);
final chatModel = ref.watch(activeChatModelProvider);
// final researchVariant = ref.watch(activeResearchVariantProvider);
// final chatModel = ref.watch(activeChatModelProvider);
useAutomaticKeepAlive();
// useAutomaticKeepAlive();
final formKey = useMemoized(() => GlobalKey<FormState>());
final textController =
useTextEditingController(text: sharedContent?.toString());
// final formKey = useMemoized(() => GlobalKey<FormState>());
// final textController =
// useTextEditingController(text: sharedContent?.toString());
final tabController = useTabController(
initialLength: AssistantMode.values.length,
initialIndex: ref.read(lastUsedAssistantModeProvider).index,
);
final pageController = usePageController(initialPage: tabController.index);
// final tabController = useTabController(
// initialLength: AssistantMode.values.length,
// initialIndex: ref.read(lastUsedAssistantModeProvider).index,
// );
// final pageController = usePageController(initialPage: tabController.index);
useSyncPageWithTab(
tabController,
pageController,
onIndexChanged: (index) {
ref
.read(lastUsedAssistantModeProvider.notifier)
.update(AssistantMode.values[index]);
},
);
// useSyncPageWithTab(
// tabController,
// pageController,
// onIndexChanged: (index) {
// ref
// .read(lastUsedAssistantModeProvider.notifier)
// .update(AssistantMode.values[index]);
// },
// );
return Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TabBar.secondary(
controller: tabController,
tabs: const [
Tab(
text: 'Research',
icon: Icon(MdiIcons.layersSearch),
),
Tab(
text: 'Code',
icon: Icon(MdiIcons.codeJson),
),
Tab(
text: 'Chat',
icon: Icon(MdiIcons.commentTextMultiple),
),
Tab(
text: 'Custom',
icon: Icon(MdiIcons.creation),
),
],
),
ExpandablePageView(
controller: pageController,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 12,
),
SegmentedButton<ResearchVariant>(
segments: const [
ButtonSegment(
value: ResearchVariant.expert,
icon: Icon(MdiIcons.textSearch),
label: Text('Research'),
),
ButtonSegment(
value: ResearchVariant.fast,
icon: Icon(MdiIcons.invoiceTextFast),
label: Text('Fast'),
),
],
selected: {researchVariant},
onSelectionChanged: (value) {
ref
.read(activeResearchVariantProvider.notifier)
.update(value.first);
},
),
const SizedBox(
height: 12,
),
_PromptField(textController, incognitoEnabled),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 12,
),
_PromptField(textController, incognitoEnabled),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 12,
),
DropdownMenu<ChatModel>(
initialSelection: chatModel,
expandedInsets: EdgeInsets.zero,
label: const Text('Model'),
inputDecorationTheme: const InputDecorationTheme(),
dropdownMenuEntries: ChatModel.values
.map(
(model) => DropdownMenuEntry(
value: model,
label: model.label,
),
)
.toList(),
onSelected: (value) {
ref.read(activeChatModelProvider.notifier).update(value!);
},
),
const SizedBox(
height: 12,
),
_PromptField(textController, incognitoEnabled),
],
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(
height: 12,
),
_PromptField(textController, incognitoEnabled),
],
),
],
),
const SizedBox(
height: 12,
),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () {
if (formKey.currentState?.validate() ?? false) {
onSubmit(
uri_builder.assistantUri(
prompt: textController.text,
assistantMode: AssistantMode.values[tabController.index],
researchVariant: researchVariant,
chatModel: chatModel,
),
);
}
},
label: const Text('Submit'),
icon: const Icon(MdiIcons.invoiceTextSend),
),
),
],
),
);
}
}
// return Form(
// key: formKey,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// TabBar.secondary(
// controller: tabController,
// tabs: const [
// Tab(
// text: 'Research',
// icon: Icon(MdiIcons.layersSearch),
// ),
// Tab(
// text: 'Code',
// icon: Icon(MdiIcons.codeJson),
// ),
// Tab(
// text: 'Chat',
// icon: Icon(MdiIcons.commentTextMultiple),
// ),
// Tab(
// text: 'Custom',
// icon: Icon(MdiIcons.creation),
// ),
// ],
// ),
// ExpandablePageView(
// controller: pageController,
// children: [
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const SizedBox(
// height: 12,
// ),
// SegmentedButton<ResearchVariant>(
// segments: const [
// ButtonSegment(
// value: ResearchVariant.expert,
// icon: Icon(MdiIcons.textSearch),
// label: Text('Research'),
// ),
// ButtonSegment(
// value: ResearchVariant.fast,
// icon: Icon(MdiIcons.invoiceTextFast),
// label: Text('Fast'),
// ),
// ],
// selected: {researchVariant},
// onSelectionChanged: (value) {
// ref
// .read(activeResearchVariantProvider.notifier)
// .update(value.first);
// },
// ),
// const SizedBox(
// height: 12,
// ),
// _PromptField(textController, incognitoEnabled),
// ],
// ),
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const SizedBox(
// height: 12,
// ),
// _PromptField(textController, incognitoEnabled),
// ],
// ),
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const SizedBox(
// height: 12,
// ),
// DropdownMenu<ChatModel>(
// initialSelection: chatModel,
// expandedInsets: EdgeInsets.zero,
// label: const Text('Model'),
// inputDecorationTheme: const InputDecorationTheme(),
// dropdownMenuEntries: ChatModel.values
// .map(
// (model) => DropdownMenuEntry(
// value: model,
// label: model.label,
// ),
// )
// .toList(),
// onSelected: (value) {
// ref.read(activeChatModelProvider.notifier).update(value!);
// },
// ),
// const SizedBox(
// height: 12,
// ),
// _PromptField(textController, incognitoEnabled),
// ],
// ),
// Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const SizedBox(
// height: 12,
// ),
// _PromptField(textController, incognitoEnabled),
// ],
// ),
// ],
// ),
// const SizedBox(
// height: 12,
// ),
// SizedBox(
// width: double.infinity,
// child: FilledButton.icon(
// onPressed: () {
// if (formKey.currentState?.validate() ?? false) {
// onSubmit(
// uri_builder.assistantUri(
// prompt: textController.text,
// assistantMode: AssistantMode.values[tabController.index],
// researchVariant: researchVariant,
// chatModel: chatModel,
// ),
// );
// }
// },
// label: const Text('Submit'),
// icon: const Icon(MdiIcons.invoiceTextSend),
// ),
// ),
// ],
// ),
// );
// }
// }
class _PromptField extends StatelessWidget {
final TextEditingController textController;
final bool incognitoEnabled;
// class _PromptField extends StatelessWidget {
// final TextEditingController textController;
// final bool incognitoEnabled;
const _PromptField(this.textController, this.incognitoEnabled);
// const _PromptField(this.textController, this.incognitoEnabled);
@override
Widget build(BuildContext context) {
return TextFormField(
controller: textController,
enableIMEPersonalizedLearning: !incognitoEnabled,
decoration: InputDecoration(
label: const Text('Prompt'),
hintText: 'Enter your prompt...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: SpeechToTextButton(
onTextReceived: (data) {
textController.text = data.toString();
},
),
),
maxLines: null,
validator: (value) {
if (value?.isEmpty ?? true) {
return '';
}
// @override
// Widget build(BuildContext context) {
// return TextFormField(
// controller: textController,
// enableIMEPersonalizedLearning: !incognitoEnabled,
// decoration: InputDecoration(
// label: const Text('Prompt'),
// hintText: 'Enter your prompt...',
// floatingLabelBehavior: FloatingLabelBehavior.always,
// suffixIcon: SpeechToTextButton(
// onTextReceived: (data) {
// textController.text = data.toString();
// },
// ),
// ),
// maxLines: null,
// validator: (value) {
// if (value?.isEmpty ?? true) {
// return '';
// }
return null;
},
);
}
}
// return null;
// },
// );
// }
// }
@@ -1,195 +1,195 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_hooks/flutter_hooks.dart';
// import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
// import 'package:go_router/go_router.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:lensai/core/routing/routes.dart';
// import 'package:lensai/features/bangs/domain/providers/bangs.dart';
// import 'package:lensai/features/bangs/domain/providers/search.dart';
// import 'package:lensai/features/bangs/domain/repositories/data.dart';
// import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
// import 'package:lensai/features/bangs/presentation/widgets/search_field.dart';
// import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
// import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
// import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
// import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
// import 'package:lensai/presentation/widgets/selectable_chips.dart';
class SearchTab extends HookConsumerWidget {
final SharedContent? sharedContent;
final OnSubmitUri onSubmit;
// class SearchTab extends HookConsumerWidget {
// final SharedContent? sharedContent;
// final OnSubmitUri onSubmit;
const SearchTab({
required this.sharedContent,
required this.onSubmit,
super.key,
});
// const SearchTab({
// required this.sharedContent,
// required this.onSubmit,
// super.key,
// });
@override
Widget build(BuildContext context, WidgetRef ref) {
useAutomaticKeepAlive();
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// useAutomaticKeepAlive();
final formKey = useMemoized(() => GlobalKey<FormState>());
// final formKey = useMemoized(() => GlobalKey<FormState>());
final selectedBang = ref
.watch(selectedBangDataProvider().select((value) => value.valueOrNull));
final defaultSearchBang = ref.watch(
defaultSearchBangDataProvider.select((value) => value.valueOrNull),
);
// final selectedBang = ref
// .watch(selectedBangDataProvider().select((value) => value.valueOrNull));
// final defaultSearchBang = ref.watch(
// defaultSearchBangDataProvider.select((value) => value.valueOrNull),
// );
final activeBang = selectedBang ?? defaultSearchBang;
// final activeBang = selectedBang ?? defaultSearchBang;
final textController =
useTextEditingController(text: sharedContent?.toString());
// final textController =
// useTextEditingController(text: sharedContent?.toString());
Future<void> submitSearch() async {
if (activeBang != null && (formKey.currentState?.validate() == true)) {
final searchUri = await ref.read(
triggerBangSearchProvider(activeBang, textController.text).future,
);
// Future<void> submitSearch() async {
// if (activeBang != null && (formKey.currentState?.validate() == true)) {
// final searchUri = await ref.read(
// triggerBangSearchProvider(activeBang, textController.text).future,
// );
onSubmit(searchUri);
}
}
// onSubmit(searchUri);
// }
// }
return Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 4),
Consumer(
builder: (context, ref, child) {
final frequentBangsAsync =
ref.watch(frequentBangDataListProvider);
// return Form(
// key: formKey,
// child: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const SizedBox(height: 4),
// Consumer(
// builder: (context, ref, child) {
// final frequentBangsAsync =
// ref.watch(frequentBangDataListProvider);
return frequentBangsAsync.when(
data: (availableBangs) => SizedBox(
height: 48,
child: Row(
children: [
if (selectedBang != null || availableBangs.isNotEmpty)
Expanded(
child: SelectableChips(
itemId: (bang) => bang.trigger,
itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
itemLabel: (bang) => Text(bang.websiteName),
availableItems: availableBangs,
selectedItem: selectedBang,
onSelected: (bang) {
ref
.read(selectedBangTriggerProvider().notifier)
.setTrigger(bang.trigger);
},
onDeleted: (bang) async {
if (ref.read(selectedBangTriggerProvider()) ==
bang.trigger) {
ref
.read(
selectedBangTriggerProvider().notifier,
)
.clearTrigger();
} else {
final dialogResult = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Reset usage frequency of !${bang.trigger}?',
),
content: const Text(
'This will remove the Bang from quick select.',
),
actions: <Widget>[
TextButton(
onPressed: () =>
Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.pop(context, true),
child: const Text('Reset'),
),
],
),
);
// return frequentBangsAsync.when(
// data: (availableBangs) => SizedBox(
// height: 48,
// child: Row(
// children: [
// if (selectedBang != null || availableBangs.isNotEmpty)
// Expanded(
// child: SelectableChips(
// itemId: (bang) => bang.trigger,
// itemAvatar: (bang) => BangIcon(bang, iconSize: 20),
// itemLabel: (bang) => Text(bang.websiteName),
// availableItems: availableBangs,
// selectedItem: selectedBang,
// onSelected: (bang) {
// ref
// .read(selectedBangTriggerProvider().notifier)
// .setTrigger(bang.trigger);
// },
// onDeleted: (bang) async {
// if (ref.read(selectedBangTriggerProvider()) ==
// bang.trigger) {
// ref
// .read(
// selectedBangTriggerProvider().notifier,
// )
// .clearTrigger();
// } else {
// final dialogResult = await showDialog<bool>(
// context: context,
// builder: (context) => AlertDialog(
// title: Text(
// 'Reset usage frequency of !${bang.trigger}?',
// ),
// content: const Text(
// 'This will remove the Bang from quick select.',
// ),
// actions: <Widget>[
// TextButton(
// onPressed: () =>
// Navigator.pop(context, false),
// child: const Text('Cancel'),
// ),
// TextButton(
// onPressed: () =>
// Navigator.pop(context, true),
// child: const Text('Reset'),
// ),
// ],
// ),
// );
if (dialogResult == true) {
await ref
.read(bangDataRepositoryProvider.notifier)
.resetFrequency(bang.trigger);
}
}
},
),
)
else
Expanded(
child: Text(
"Press '>' to search Bangs.",
style: TextStyle(
color: Theme.of(context).hintColor,
fontStyle: FontStyle.italic,
),
),
),
IconButton(
onPressed: () async {
await context.push(BangSearchRoute().location);
},
icon: const Icon(Icons.chevron_right),
),
],
),
),
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(
height: 48,
width: double.infinity,
),
);
},
),
Consumer(
builder: (context, ref, child) {
const maxOptionsHeight = SearchField.defaultMaxOptionsHeight;
// if (dialogResult == true) {
// await ref
// .read(bangDataRepositoryProvider.notifier)
// .resetFrequency(bang.trigger);
// }
// }
// },
// ),
// )
// else
// Expanded(
// child: Text(
// "Press '>' to search Bangs.",
// style: TextStyle(
// color: Theme.of(context).hintColor,
// fontStyle: FontStyle.italic,
// ),
// ),
// ),
// IconButton(
// onPressed: () async {
// await context.push(BangSearchRoute().location);
// },
// icon: const Icon(Icons.chevron_right),
// ),
// ],
// ),
// ),
// error: (error, stackTrace) => const SizedBox.shrink(),
// loading: () => const SizedBox(
// height: 48,
// width: double.infinity,
// ),
// );
// },
// ),
// Consumer(
// builder: (context, ref, child) {
// const maxOptionsHeight = SearchField.defaultMaxOptionsHeight;
final openDirection = ref.watch(
bottomSheetExtendProvider.select((value) {
final extend = value.valueOrNull;
if (extend == null ||
(MediaQuery.of(context).size.height * (1 - extend)) >
maxOptionsHeight) {
return OptionsViewOpenDirection.up;
} else {
return OptionsViewOpenDirection.down;
}
}),
);
// final openDirection = ref.watch(
// bottomSheetExtendProvider.select((value) {
// final extend = value.valueOrNull;
// if (extend == null ||
// (MediaQuery.of(context).size.height * (1 - extend)) >
// maxOptionsHeight) {
// return OptionsViewOpenDirection.up;
// } else {
// return OptionsViewOpenDirection.down;
// }
// }),
// );
return SearchField(
textController: textController,
activeBang: activeBang,
openDirection: openDirection,
onFieldSubmitted: (_) async {
await submitSearch();
},
);
},
),
const SizedBox(
height: 12,
),
SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: submitSearch,
label: const Text('Search'),
icon: const Icon(MdiIcons.cloudSearch),
),
),
],
),
);
}
}
// return SearchField(
// textController: textController,
// activeBang: activeBang,
// openDirection: openDirection,
// onFieldSubmitted: (_) async {
// await submitSearch();
// },
// );
// },
// ),
// const SizedBox(
// height: 12,
// ),
// SizedBox(
// width: double.infinity,
// child: FilledButton.icon(
// onPressed: submitSearch,
// label: const Text('Search'),
// icon: const Icon(MdiIcons.cloudSearch),
// ),
// ),
// ],
// ),
// );
// }
// }