diff --git a/app/lib/core/models.dart b/app/lib/core/models.dart new file mode 100644 index 00000000..4c9406a6 --- /dev/null +++ b/app/lib/core/models.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:langchain_openai/langchain_openai.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'models.g.dart'; + +// @Riverpod(keepAlive: true) +// ChatOpenAI summarizerModel(Ref ref) { +// final openaiApiKey = Platform.environment['OPENAI_API_KEY']; +// return ChatOpenAI( +// apiKey: openaiApiKey, +// baseUrl: 'https://api.together.xyz/v1', +// defaultOptions: const ChatOpenAIOptions(model: 'deepseek-ai/DeepSeek-V3'), +// ); +// } + +@Riverpod(keepAlive: true) +ChatOpenAI chatModel(Ref ref) { + final openaiApiKey = 'FfTBKW1t5dOOXVoiaF2hbXPAHiz3fvfy'; + return ChatOpenAI( + apiKey: openaiApiKey, + baseUrl: 'https://api.deepinfra.com/v1/openai', + defaultOptions: const ChatOpenAIOptions( + model: 'meta-llama/Llama-3.3-70B-Instruct-Turbo', + ), + ); +} + +@Riverpod(keepAlive: true) +OpenAIEmbeddings embeddingModel(Ref ref) { + final openaiApiKey = 'FfTBKW1t5dOOXVoiaF2hbXPAHiz3fvfy'; + return OpenAIEmbeddings( + apiKey: openaiApiKey, + baseUrl: 'https://api.deepinfra.com/v1/openai', + model: 'BAAI/bge-m3', + ); +} + +@Riverpod(keepAlive: true) +int embeddingDimensions(Ref ref) => 1024; diff --git a/app/lib/core/models.g.dart b/app/lib/core/models.g.dart new file mode 100644 index 00000000..49d9204a --- /dev/null +++ b/app/lib/core/models.g.dart @@ -0,0 +1,61 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'models.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatModelHash() => r'e7538b9f0dd43ea64953164c5402e76fde96f5e9'; + +/// See also [chatModel]. +@ProviderFor(chatModel) +final chatModelProvider = Provider.internal( + chatModel, + name: r'chatModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$chatModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ChatModelRef = ProviderRef; +String _$embeddingModelHash() => r'5d65c75df195a26adefac5a75f0e8f6b0f1b8590'; + +/// See also [embeddingModel]. +@ProviderFor(embeddingModel) +final embeddingModelProvider = Provider.internal( + embeddingModel, + name: r'embeddingModelProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$embeddingModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef EmbeddingModelRef = ProviderRef; +String _$embeddingDimensionsHash() => + r'e34f544a18a7e2bd6a479b21f2016b0c0e2ba96c'; + +/// See also [embeddingDimensions]. +@ProviderFor(embeddingDimensions) +final embeddingDimensionsProvider = Provider.internal( + embeddingDimensions, + name: r'embeddingDimensionsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$embeddingDimensionsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef EmbeddingDimensionsRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/core/providers.dart b/app/lib/core/providers.dart index 8d389292..9a70f4ff 100644 --- a/app/lib/core/providers.dart +++ b/app/lib/core/providers.dart @@ -6,7 +6,9 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @Riverpod(keepAlive: true) -GoRouter router(Ref ref) => GoRouter( - debugLogDiagnostics: true, - routes: $appRoutes, - ); +GoRouter router(Ref ref) { + return GoRouter( + debugLogDiagnostics: true, + routes: $appRoutes, + ); +} diff --git a/app/lib/core/providers.g.dart b/app/lib/core/providers.g.dart index 05734119..a00bc133 100644 --- a/app/lib/core/providers.g.dart +++ b/app/lib/core/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$routerHash() => r'dcafae2924a2ad3d5d836c6756d2e4a4ead36b4e'; +String _$routerHash() => r'25fc899b87c080600f6e1d87d510db333378bdfd'; /// See also [router]. @ProviderFor(router) diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index e2c537d0..846ad260 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:lensai/core/routing/dialog_page.dart'; +import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/features/about/presentation/screens/about.dart'; import 'package:lensai/features/bangs/presentation/screens/categories.dart'; import 'package:lensai/features/bangs/presentation/screens/list.dart'; @@ -8,10 +9,12 @@ import 'package:lensai/features/bangs/presentation/screens/search.dart'; import 'package:lensai/features/chat_archive/presentation/screens/detail.dart'; import 'package:lensai/features/chat_archive/presentation/screens/list.dart'; import 'package:lensai/features/chat_archive/presentation/screens/search.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart'; import 'package:lensai/features/search/presentation/screens/search.dart'; import 'package:lensai/features/settings/presentation/screens/settings.dart'; +import 'package:lensai/features/user/presentation/screens/auth.dart'; part 'routes.g.dart'; @@ -19,10 +22,18 @@ part 'routes.g.dart'; name: 'BrowserRoute', path: '/', routes: [ + TypedGoRoute( + name: 'WebPageRoute', + path: 'page/:url', + ), TypedGoRoute( name: 'AboutRoute', path: 'about', ), + TypedGoRoute( + name: 'UserAuthRoute', + path: 'userAuth', + ), TypedGoRoute( name: 'SearchRoute', path: 'search/:searchText', @@ -60,6 +71,22 @@ class BrowserRoute extends GoRouteData { } } +class WebPageRoute extends GoRouteData { + final String url; + + const WebPageRoute({required this.url}); + + @override + Page buildPage(BuildContext context, GoRouterState state) { + return DialogPage( + builder: (_) => WebPageDialog( + url: Uri.parse(url), + precachedInfo: state.extra as WebPageInfo?, + ), + ); + } +} + class AboutRoute extends GoRouteData { @override Page buildPage(BuildContext context, GoRouterState state) { @@ -67,6 +94,13 @@ class AboutRoute extends GoRouteData { } } +class UserAuthRoute extends GoRouteData { + @override + Page buildPage(BuildContext context, GoRouterState state) { + return DialogPage(builder: (_) => const UserAuthScreen()); + } +} + class SearchRoute extends GoRouteData { static const String emptySearchText = ' '; diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index b383aefc..1f6b417a 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -17,11 +17,21 @@ RouteBase get $browserRoute => GoRouteData.$route( name: 'BrowserRoute', factory: $BrowserRouteExtension._fromState, routes: [ + GoRouteData.$route( + path: 'page/:url', + name: 'WebPageRoute', + factory: $WebPageRouteExtension._fromState, + ), GoRouteData.$route( path: 'about', name: 'AboutRoute', factory: $AboutRouteExtension._fromState, ), + GoRouteData.$route( + path: 'userAuth', + name: 'UserAuthRoute', + factory: $UserAuthRouteExtension._fromState, + ), GoRouteData.$route( path: 'search/:searchText', name: 'SearchRoute', @@ -76,6 +86,25 @@ extension $BrowserRouteExtension on BrowserRoute { void replace(BuildContext context) => context.replace(location); } +extension $WebPageRouteExtension on WebPageRoute { + static WebPageRoute _fromState(GoRouterState state) => WebPageRoute( + url: state.pathParameters['url']!, + ); + + String get location => GoRouteData.$location( + '/page/${Uri.encodeComponent(url)}', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + extension $AboutRouteExtension on AboutRoute { static AboutRoute _fromState(GoRouterState state) => AboutRoute(); @@ -93,6 +122,23 @@ extension $AboutRouteExtension on AboutRoute { void replace(BuildContext context) => context.replace(location); } +extension $UserAuthRouteExtension on UserAuthRoute { + static UserAuthRoute _fromState(GoRouterState state) => UserAuthRoute(); + + String get location => GoRouteData.$location( + '/userAuth', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + extension $SearchRouteExtension on SearchRoute { static SearchRoute _fromState(GoRouterState state) => SearchRoute( searchText: diff --git a/app/lib/extensions/string.dart b/app/lib/extensions/string.dart new file mode 100644 index 00000000..688e6331 --- /dev/null +++ b/app/lib/extensions/string.dart @@ -0,0 +1,4 @@ +extension StringExtension on String { + String toCapitalized() => + isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}'; +} diff --git a/app/lib/features/bangs/data/database/daos/bang.dart b/app/lib/features/bangs/data/database/daos/bang.dart index 1b889a7b..a98b4c64 100644 --- a/app/lib/features/bangs/data/database/daos/bang.dart +++ b/app/lib/features/bangs/data/database/daos/bang.dart @@ -91,7 +91,17 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { } Selectable queryBangs(String searchString) { - return db.bangQuery(query: db.buildFtsQuery(searchString)); + final ftsQuery = db.buildFtsQuery(searchString); + + if (ftsQuery.isNotEmpty) { + return db.queryBangs( + query: ftsQuery, + ); + } else { + return db.queryBangsBasic( + query: db.buildLikeQuery(searchString), + ); + } } Future addSearchEntry(String trigger, String searchQuery) { diff --git a/app/lib/features/bangs/data/database/database.drift b/app/lib/features/bangs/data/database/database.drift index e491dd9e..836dce23 100644 --- a/app/lib/features/bangs/data/database/database.drift +++ b/app/lib/features/bangs/data/database/database.drift @@ -72,20 +72,56 @@ END; optimizeFtsIndex: INSERT INTO bang_fts(bang_fts) VALUES ('optimize'); -bangQuery WITH BangData: - SELECT +queryBangs WITH BangData: + WITH weights AS ( + SELECT + -- Customize these weights (higher = more important) + 10.0 AS "trigger", -- Title matches are most important + 5.0 AS website_name -- URL matches are quite important + ) + SELECT b.*, bf.frequency, - bf.last_used - FROM + bf.last_used, + bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank + FROM bang_fts(:query) fts INNER JOIN bang b ON b.rowid = fts.rowid LEFT JOIN bang_frequency bf ON b."trigger" = bf."trigger" - ORDER BY - RANK, - bf.frequency NULLS LAST; + CROSS JOIN + weights + ORDER BY + weighted_rank ASC, + bf.frequency NULLS LAST; + +queryBangsBasic WITH BangData: + WITH weights AS ( + SELECT + -- Customize these weights (higher = more important) + 10.0 AS "trigger", -- Title matches are most important + 5.0 AS website_name -- URL matches are quite important + ) + SELECT + b.*, + bf.frequency, + bf.last_used, + bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank + FROM + bang_fts fts + INNER JOIN + bang b ON b.rowid = fts.rowid + LEFT JOIN + bang_frequency bf ON b."trigger" = bf."trigger" + CROSS JOIN + weights + WHERE + fts."trigger" LIKE :query OR + fts.website_name LIKE :query + ORDER BY + weighted_rank ASC, + bf.frequency NULLS LAST; categoriesJson: WITH categories AS ( diff --git a/app/lib/features/bangs/data/database/database.g.dart b/app/lib/features/bangs/data/database/database.g.dart index f830e954..82d1beac 100644 --- a/app/lib/features/bangs/data/database/database.g.dart +++ b/app/lib/features/bangs/data/database/database.g.dart @@ -1095,16 +1095,40 @@ abstract class _$BangDatabase extends GeneratedDatabase { ); } - Selectable bangQuery({required String query}) { + Selectable queryBangs({required String query}) { return customSelect( - 'SELECT b.*, bf.frequency, bf.last_used FROM bang_fts(?1)AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" ORDER BY RANK, bf.frequency NULLS LAST', + 'WITH weights AS (SELECT 10.0 AS "trigger", 5.0 AS website_name) SELECT b.*, bf.frequency, bf.last_used, bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank FROM bang_fts(?1)AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" CROSS JOIN weights ORDER BY weighted_rank ASC, bf.frequency NULLS LAST', variables: [ Variable(query) ], readsFrom: { bangFrequency, - bang, bangFts, + bang, + }).map((QueryRow row) => BangData( + websiteName: row.read('website_name'), + domain: row.read('domain'), + trigger: row.read('trigger'), + urlTemplate: row.read('url_template'), + category: row.readNullable('category'), + subCategory: row.readNullable('sub_category'), + format: BangTable.$converterformat + .fromSql(row.readNullable('format')), + frequency: row.readNullable('frequency'), + lastUsed: row.readNullable('last_used'), + )); + } + + Selectable queryBangsBasic({required String query}) { + return customSelect( + 'WITH weights AS (SELECT 10.0 AS "trigger", 5.0 AS website_name) SELECT b.*, bf.frequency, bf.last_used, bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank FROM bang_fts AS fts INNER JOIN bang AS b ON b."rowid" = fts."rowid" LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" CROSS JOIN weights WHERE fts."trigger" LIKE ?1 OR fts.website_name LIKE ?1 ORDER BY weighted_rank ASC, bf.frequency NULLS LAST', + variables: [ + Variable(query) + ], + readsFrom: { + bangFrequency, + bangFts, + bang, }).map((QueryRow row) => BangData( websiteName: row.read('website_name'), domain: row.read('domain'), @@ -1236,8 +1260,8 @@ final class $BangTableReferences db.bang.trigger, db.bangFrequency.trigger)); $BangFrequencyProcessedTableManager get bangFrequencyRefs { - final manager = $BangFrequencyTableManager($_db, $_db.bangFrequency) - .filter((f) => f.trigger.trigger($_item.trigger)); + final manager = $BangFrequencyTableManager($_db, $_db.bangFrequency).filter( + (f) => f.trigger.trigger.sqlEquals($_itemColumn('trigger')!)); final cache = $_typedResult.readTableOrNull(_bangFrequencyRefsTable($_db)); return ProcessedTableManager( @@ -1251,8 +1275,8 @@ final class $BangTableReferences $_aliasNameGenerator(db.bang.trigger, db.bangHistory.trigger)); $BangHistoryProcessedTableManager get bangHistoryRefs { - final manager = $BangHistoryTableManager($_db, $_db.bangHistory) - .filter((f) => f.trigger.trigger($_item.trigger)); + final manager = $BangHistoryTableManager($_db, $_db.bangHistory).filter( + (f) => f.trigger.trigger.sqlEquals($_itemColumn('trigger')!)); final cache = $_typedResult.readTableOrNull(_bangHistoryRefsTable($_db)); return ProcessedTableManager( @@ -1704,8 +1728,10 @@ final class $BangFrequencyReferences $_aliasNameGenerator(db.bangFrequency.trigger, db.bang.trigger)); $BangTableProcessedTableManager get trigger { + final $_column = $_itemColumn('trigger')!; + final manager = $BangTableTableManager($_db, $_db.bang) - .filter((f) => f.trigger($_item.trigger!)); + .filter((f) => f.trigger.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_triggerTable($_db)); if (item == null) return manager; return ProcessedTableManager( @@ -1941,8 +1967,10 @@ final class $BangHistoryReferences $_aliasNameGenerator(db.bangHistory.trigger, db.bang.trigger)); $BangTableProcessedTableManager get trigger { + final $_column = $_itemColumn('trigger')!; + final manager = $BangTableTableManager($_db, $_db.bang) - .filter((f) => f.trigger($_item.trigger!)); + .filter((f) => f.trigger.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_triggerTable($_db)); if (item == null) return manager; return ProcessedTableManager( diff --git a/app/lib/features/bangs/data/providers.dart b/app/lib/features/bangs/data/providers.dart index 57189c22..c7cf98ee 100644 --- a/app/lib/features/bangs/data/providers.dart +++ b/app/lib/features/bangs/data/providers.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:lensai/features/bangs/data/database/database.dart'; @@ -13,7 +15,7 @@ part 'providers.g.dart'; @Riverpod(keepAlive: true) BangDatabase bangDatabase(Ref ref) { - return BangDatabase( + final db = BangDatabase( LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder // for your app. @@ -35,4 +37,10 @@ BangDatabase bangDatabase(Ref ref) { return NativeDatabase.createInBackground(file); }), ); + + ref.onDispose(() { + unawaited(db.close()); + }); + + return db; } diff --git a/app/lib/features/bangs/data/providers.g.dart b/app/lib/features/bangs/data/providers.g.dart index c469e88c..0ba9f339 100644 --- a/app/lib/features/bangs/data/providers.g.dart +++ b/app/lib/features/bangs/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$bangDatabaseHash() => r'd7964e9cdbb6e60fb6d2a78f07bfd4fa34bcb236'; +String _$bangDatabaseHash() => r'1ff3fb8bfa29269f6e515f3b22c216998769f578'; /// See also [bangDatabase]. @ProviderFor(bangDatabase) diff --git a/app/lib/features/bangs/presentation/screens/list.dart b/app/lib/features/bangs/presentation/screens/list.dart index 7c92a841..c8a63914 100644 --- a/app/lib/features/bangs/presentation/screens/list.dart +++ b/app/lib/features/bangs/presentation/screens/list.dart @@ -6,7 +6,7 @@ import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; +import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; diff --git a/app/lib/features/bangs/presentation/widgets/search_field.dart b/app/lib/features/bangs/presentation/widgets/search_field.dart deleted file mode 100644 index 2fefadb6..00000000 --- a/app/lib/features/bangs/presentation/widgets/search_field.dart +++ /dev/null @@ -1,205 +0,0 @@ -// The default Material-style Autocomplete options. -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:lensai/features/bangs/data/models/bang_data.dart'; -import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; -import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart'; -import 'package:lensai/features/search/domain/providers/search_suggestions.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; -import 'package:lensai/presentation/widgets/autocomplete.dart'; - -class SearchField extends HookConsumerWidget { - static const defaultMaxOptionsHeight = 158.0; - - final TextEditingController textController; - final BangData? activeBang; - final OptionsViewOpenDirection openDirection; - final double maxOptionsHeight; - final void Function(String)? onFieldSubmitted; - - const SearchField({ - required this.textController, - required this.activeBang, - this.openDirection = OptionsViewOpenDirection.up, - this.maxOptionsHeight = defaultMaxOptionsHeight, - this.onFieldSubmitted, - super.key, - }); - @override - Widget build(BuildContext context, WidgetRef ref) { - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select((value) => value.incognitoMode), - ); - - final optionsStream = ref.watch(searchSuggestionsProvider()); - - final focusNode = useFocusNode(); - - // final quickAnswer = useListenableSelector( - // textController, - // () => textController.text.endsWith('?'), - // ); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ExternalResultsAutocomplete( - textEditingController: textController, - optionsStream: optionsStream, - focusNode: focusNode, - optionsViewOpenDirection: openDirection, - displayStringForOption: - // ignore: avoid_redundant_argument_values - RawAutocomplete.defaultStringForOption, - optionsViewBuilder: (context, onSelected, options) { - return _AutocompleteOptions( - //Must match RawAutocomplete parent - displayStringForOption: RawAutocomplete.defaultStringForOption, - onSelected: onSelected, - options: options, - //Must match RawAutocomplete parent - openDirection: openDirection, - maxOptionsHeight: maxOptionsHeight, - ); - }, - onTextChanged: (textEditingValue) { - ref - .read(searchSuggestionsProvider().notifier) - .addQuery(textEditingValue.text); - }, - fieldViewBuilder: ( - context, - textEditingController, - focusNode, - onFieldSubmitted, - ) { - return TextFormField( - controller: textEditingController, - enableIMEPersonalizedLearning: !incognitoEnabled, - focusNode: focusNode, - decoration: InputDecoration( - prefixIcon: (activeBang != null) - ? Padding( - padding: const EdgeInsetsDirectional.all(12.0), - child: BangIcon(activeBang!, iconSize: 24.0), - ) - : null, - // label: const Text('Query'), - hintText: 'Ask anything...', - floatingLabelBehavior: FloatingLabelBehavior.always, - suffixIcon: SpeechToTextButton( - onTextReceived: (data) { - textEditingController.text = data.toString(); - }, - ), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (value) { - if (value?.isEmpty ?? true) { - return ''; - } - - return null; - }, - onTapOutside: (event) { - focusNode.unfocus(); - }, - onFieldSubmitted: this.onFieldSubmitted, - ); - }, - ), - // if (activeBang?.domain.endsWith('kagi.com') == true) - // SwitchListTile( - // value: quickAnswer, - // onChanged: (_) { - // if (textController.text.endsWith('?')) { - // textController.text = textController.text - // .substring(0, textController.text.length - 1); - // } else { - // textController.text = '${textController.text}?'; - // } - // }, - // contentPadding: EdgeInsets.zero, - // title: const Text('Quick Answer'), - // secondary: const Icon(MdiIcons.lightningBolt), - // ), - ], - ); - } -} - -class _AutocompleteOptions extends StatelessWidget { - const _AutocompleteOptions({ - super.key, - required this.displayStringForOption, - required this.onSelected, - required this.openDirection, - required this.options, - required this.maxOptionsHeight, - }); - - final AutocompleteOptionToString displayStringForOption; - - final AutocompleteOnSelected onSelected; - final OptionsViewOpenDirection openDirection; - - final Iterable options; - final double maxOptionsHeight; - - @override - Widget build(BuildContext context) { - final AlignmentDirectional optionsAlignment = switch (openDirection) { - OptionsViewOpenDirection.up => AlignmentDirectional.bottomStart, - OptionsViewOpenDirection.down => AlignmentDirectional.topStart, - }; - return Align( - alignment: optionsAlignment, - child: Material( - elevation: 4.0, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: maxOptionsHeight), - child: ListView.builder( - padding: EdgeInsets.zero, - shrinkWrap: true, - reverse: switch (openDirection) { - OptionsViewOpenDirection.up => true, - OptionsViewOpenDirection.down => false, - }, - itemCount: options.length, - itemBuilder: (BuildContext context, int index) { - final T option = options.elementAt(index); - return InkWell( - onTap: () { - onSelected(option); - }, - child: Builder( - builder: (BuildContext context) { - final bool highlight = - AutocompleteHighlightedOption.of(context) == index; - // if (highlight) { - // SchedulerBinding.instance.addPostFrameCallback( - // (Duration timeStamp) async { - // await Scrollable.ensureVisible( - // context, - // alignment: 0.5, - // ); - // }, - // debugLabel: 'AutocompleteOptions.ensureVisible', - // ); - // } - return Container( - color: highlight ? Theme.of(context).focusColor : null, - padding: const EdgeInsets.all(16.0), - child: Text(displayStringForOption(option)), - ); - }, - ), - ); - }, - ), - ), - ), - ); - } -} diff --git a/app/lib/features/bangs/presentation/widgets/site_search.dart b/app/lib/features/bangs/presentation/widgets/site_search.dart index 0e672030..740bdc4d 100644 --- a/app/lib/features/bangs/presentation/widgets/site_search.dart +++ b/app/lib/features/bangs/presentation/widgets/site_search.dart @@ -1,14 +1,18 @@ +import 'package:fading_scroll/fading_scroll.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/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers/search.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/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; +import 'package:lensai/features/search/domain/providers/search_suggestions.dart'; +import 'package:lensai/features/search/presentation/widgets/search_field.dart'; +import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart'; +import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; class SiteSearch extends HookConsumerWidget { @@ -25,6 +29,17 @@ class SiteSearch extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final formKey = useMemoized(() => GlobalKey()); + final searchTextController = useTextEditingController(); + + useListenableCallback( + searchTextController, + () async { + ref + .read(searchSuggestionsProvider().notifier) + .addQuery(searchTextController.text); + }, + ); + final selectedBang = ref.watch( selectedBangDataProvider(domain: domain) .select((value) => value.valueOrNull), @@ -32,17 +47,17 @@ class SiteSearch extends HookConsumerWidget { final activeBang = selectedBang ?? availableBangs.firstOrNull; - final textController = useTextEditingController(); - - Future submitSearch() async { + Future submitSearch(String query) async { if (activeBang != null && (formKey.currentState?.validate() == true)) { final searchUri = await ref.read( - triggerBangSearchProvider(activeBang, textController.text).future, + triggerBangSearchProvider(activeBang, query).future, ); await ref.read(tabRepositoryProvider.notifier).addTab(url: searchUri); - ref.read(overlayDialogControllerProvider.notifier).dismiss(); + if (context.mounted) { + context.pop(); + } } } @@ -82,21 +97,29 @@ class SiteSearch extends HookConsumerWidget { ), ), SearchField( - textController: textController, + textEditingController: searchTextController, activeBang: activeBang, onFieldSubmitted: (_) async { - await submitSearch(); + await submitSearch(searchTextController.text); }, ), - const SizedBox( - height: 12, - ), - SizedBox( - width: double.infinity, - child: FilledButton.icon( - onPressed: submitSearch, - label: const Text('Search on Site'), - icon: const Icon(MdiIcons.cloudSearch), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 150), + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return CustomScrollView( + shrinkWrap: true, + controller: controller, + slivers: [ + SearchSuggestionList( + searchTextController: searchTextController, + submitSearch: submitSearch, + showHistory: false, + ), + ], + ); + }, ), ), ], diff --git a/app/lib/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart b/app/lib/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart new file mode 100644 index 00000000..ed5ec894 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:drift/drift.dart'; + +class ChatMetadataConverter extends TypeConverter { + const ChatMetadataConverter(); + + @override + ChatMetadata fromSql(String fromDb) { + return ChatMetadata.fromJson(jsonDecode(fromDb) as Map); + } + + @override + String toSql(ChatMetadata value) { + return jsonEncode(value.toJson()); + } +} diff --git a/app/lib/features/chat/features/chat_store/data/database/daos/messages.dart b/app/lib/features/chat/features/chat_store/data/database/daos/messages.dart new file mode 100644 index 00000000..90ebf954 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/daos/messages.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:lensai/features/chat/features/chat_store/data/database/database.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:lexo_rank/lexo_rank.dart'; + +part 'messages.g.dart'; + +@DriftAccessor() +class MessagesDao extends DatabaseAccessor + with _$MessagesDaoMixin { + MessagesDao(super.db); + + Selectable messageCount({required String chatId}) { + return db.chatMessage.count( + where: (row) => row.chatId.equals(chatId), + ); + } + + Selectable chatMessages({required String chatId}) { + return db.chatMessage.select() + ..where((t) => t.chatId.equals(chatId)) + ..orderBy([(row) => OrderingTerm(expression: row.orderKey)]); + } + + Selectable messageExists(String messageId) { + final existsExpression = existsQuery( + db.chatMessage.select()..where((row) => row.messageId.equals(messageId)), + ); + + return db.selectExpressions([existsExpression]).map( + (row) => row.read(existsExpression)!, + ); + } + + SingleOrNullSelectable messageIndex({ + required String chatId, + required String messageId, + }) { + return db + .messageIndex(chatId: chatId, messageId: messageId) + .map((rowId) => rowId - 1); + } + + Future insertMessage({ + required String chatId, + required Message message, + int? index, + }) async { + if (await messageExists(message.id).getSingle()) { + return null; + } + + return db.transaction( + () async { + final String orderKey; + if (index != null) { + if (index <= 0) { + orderKey = + await db.leadingOrderKey(bucket: 0, chatId: chatId).getSingle(); + } else { + orderKey = await db + .orderKeyBeforeIndex(chatId: chatId, rowIdx: index + 1) + .getSingle(); + } + } else { + orderKey = + await db.trailingOrderKey(bucket: 0, chatId: chatId).getSingle(); + } + + await db.chatMessage.insertOne( + ChatMessageCompanion.insert( + messageId: message.id, + chatId: chatId, + orderKey: orderKey, + messageJson: jsonEncode(message.toJson()), + ), + ); + + final rowIdx = await db + .messageIndex(chatId: chatId, messageId: message.id) + .getSingle(); + + return rowIdx - 1; + }, + ); + } + + Future removeMessage({required String messageId}) { + return db.chatMessage.deleteWhere((row) => row.messageId.equals(messageId)); + } + + Future updateMessage({ + required String messageId, + required Message message, + }) { + return (db.chatMessage.update() + ..where((row) => row.messageId.equals(messageId))) + .write( + ChatMessageCompanion( + messageJson: Value(jsonEncode(message.toJson())), + ), + ); + } + + Future deleteChatMessages({required String chatId}) { + return (db.chatMessage.delete()..where((t) => t.chatId.equals(chatId))) + .go(); + } + + Future replaceChatMessages({ + required String chatId, + required List messages, + }) async { + return db.transaction(() async { + await deleteChatMessages(chatId: chatId); + + var currentOrderKey = + await db.leadingOrderKey(bucket: 0, chatId: chatId).getSingle(); + + await db.chatMessage.insertAll( + messages.map( + (message) { + final insertable = ChatMessageCompanion.insert( + messageId: message.id, + chatId: chatId, + messageJson: jsonEncode(message.toJson()), + orderKey: currentOrderKey, + // timestamp: DateTime.now(), + ); + + currentOrderKey = LexoRank.parse(currentOrderKey).genPrev().value; + + return insertable; + }, + ), + ); + }); + } +} diff --git a/app/lib/features/chat/features/chat_store/data/database/daos/messages.g.dart b/app/lib/features/chat/features/chat_store/data/database/daos/messages.g.dart new file mode 100644 index 00000000..8c7718c5 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/daos/messages.g.dart @@ -0,0 +1,6 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'messages.dart'; + +// ignore_for_file: type=lint +mixin _$MessagesDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/chat/features/chat_store/data/database/daos/metadata.dart b/app/lib/features/chat/features/chat_store/data/database/daos/metadata.dart new file mode 100644 index 00000000..4a98d51a --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/daos/metadata.dart @@ -0,0 +1,26 @@ +import 'package:lensai/features/chat/features/chat_store/data/database/database.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:drift/drift.dart'; + +part 'metadata.g.dart'; + +@DriftAccessor() +class MetadataDao extends DatabaseAccessor + with _$MetadataDaoMixin { + MetadataDao(super.db); + + SingleOrNullSelectable chatMetadata(String chatId) { + final statement = db.chatData.selectOnly() + ..addColumns([db.chatData.metadata]) + ..where(db.chatData.chatId.equals(chatId)); + + return statement.map((row) => row.readWithConverter(db.chatData.metadata)!); + } + + Future updateChatMetadata(String chatId, ChatMetadata metadata) { + return db.chatData.insertOne( + ChatDataCompanion.insert(chatId: chatId, metadata: metadata), + mode: InsertMode.insertOrReplace, + ); + } +} diff --git a/app/lib/features/chat/features/chat_store/data/database/daos/metadata.g.dart b/app/lib/features/chat/features/chat_store/data/database/daos/metadata.g.dart new file mode 100644 index 00000000..c5e5a202 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/daos/metadata.g.dart @@ -0,0 +1,6 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'metadata.dart'; + +// ignore_for_file: type=lint +mixin _$MetadataDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/chat/features/chat_store/data/database/database.dart b/app/lib/features/chat/features/chat_store/data/database/database.dart new file mode 100644 index 00000000..90ecc1df --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/database.dart @@ -0,0 +1,16 @@ +import 'package:lensai/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart'; +import 'package:lensai/features/chat/features/chat_store/data/database/daos/messages.dart'; +import 'package:lensai/features/chat/features/chat_store/data/database/daos/metadata.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; + +import 'package:drift/drift.dart'; + +part 'database.g.dart'; + +@DriftDatabase(include: {'database.drift'}, daos: [MessagesDao, MetadataDao]) +class ChatDatabase extends _$ChatDatabase { + ChatDatabase(super.e); + + @override + int get schemaVersion => 1; +} diff --git a/app/lib/features/chat/features/chat_store/data/database/database.drift b/app/lib/features/chat/features/chat_store/data/database/database.drift new file mode 100644 index 00000000..f14f299e --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/database.drift @@ -0,0 +1,65 @@ +import 'package:lensai/features/chat/features/chat_store/data/database/converters/chat_metadata_converter.dart'; + +CREATE TABLE chat_data ( + chat_id TEXT PRIMARY KEY NOT NULL, + metadata TEXT NOT NULL MAPPED BY `const ChatMetadataConverter()` +); + +CREATE TABLE chat_message ( + message_id TEXT PRIMARY KEY NOT NULL, + chat_id TEXT NOT NULL, + order_key TEXT NOT NULL, + message_json TEXT NOT NULL +); +CREATE UNIQUE INDEX group_order_idx on chat_message (chat_id, order_key); + +leadingOrderKey: + SELECT lexo_rank_previous( + :bucket, + ( + SELECT order_key + FROM chat_message + WHERE chat_id = :chat_id + ORDER BY order_key + LIMIT 1 + ) + ); + +trailingOrderKey: + SELECT lexo_rank_next( + :bucket, + ( + SELECT order_key + FROM chat_message + WHERE chat_id = :chat_id + ORDER BY order_key DESC + LIMIT 1 + ) + ); + +orderKeyBeforeIndex: + WITH ordered_table AS + ( + SELECT message_id, + order_key, + LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key, + ROW_NUMBER() OVER (ORDER BY order_key) AS row_idx, + COUNT(*) OVER () as total_rows + FROM chat_message + WHERE chat_id = :chat_id + ) + SELECT lexo_rank_reorder_after(order_key, next_order_key) + FROM ordered_table + WHERE row_idx = MIN(:row_idx, total_rows); + +messageIndex: + WITH ordered_table AS + ( + SELECT message_id, + ROW_NUMBER() OVER (ORDER BY order_key) AS row_idx + FROM chat_message + WHERE chat_id = :chat_id + ) + SELECT row_idx + FROM ordered_table + WHERE message_id = :message_id; \ No newline at end of file diff --git a/app/lib/features/chat/features/chat_store/data/database/database.g.dart b/app/lib/features/chat/features/chat_store/data/database/database.g.dart new file mode 100644 index 00000000..bbf9cdd0 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/database/database.g.dart @@ -0,0 +1,764 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class ChatData extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ChatData(this.attachedDatabase, [this._alias]); + late final GeneratedColumn chatId = GeneratedColumn( + 'chat_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL'); + late final GeneratedColumnWithTypeConverter metadata = + GeneratedColumn('metadata', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL') + .withConverter(ChatData.$convertermetadata); + @override + List get $columns => [chatId, metadata]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'chat_data'; + @override + Set get $primaryKey => {chatId}; + @override + ChatDataData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ChatDataData( + chatId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}chat_id'])!, + metadata: ChatData.$convertermetadata.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}metadata'])!), + ); + } + + @override + ChatData createAlias(String alias) { + return ChatData(attachedDatabase, alias); + } + + static TypeConverter $convertermetadata = + const ChatMetadataConverter(); + @override + bool get dontWriteConstraints => true; +} + +class ChatDataData extends DataClass implements Insertable { + final String chatId; + final ChatMetadata metadata; + const ChatDataData({required this.chatId, required this.metadata}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['chat_id'] = Variable(chatId); + { + map['metadata'] = + Variable(ChatData.$convertermetadata.toSql(metadata)); + } + return map; + } + + factory ChatDataData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ChatDataData( + chatId: serializer.fromJson(json['chat_id']), + metadata: serializer.fromJson(json['metadata']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'chat_id': serializer.toJson(chatId), + 'metadata': serializer.toJson(metadata), + }; + } + + ChatDataData copyWith({String? chatId, ChatMetadata? metadata}) => + ChatDataData( + chatId: chatId ?? this.chatId, + metadata: metadata ?? this.metadata, + ); + ChatDataData copyWithCompanion(ChatDataCompanion data) { + return ChatDataData( + chatId: data.chatId.present ? data.chatId.value : this.chatId, + metadata: data.metadata.present ? data.metadata.value : this.metadata, + ); + } + + @override + String toString() { + return (StringBuffer('ChatDataData(') + ..write('chatId: $chatId, ') + ..write('metadata: $metadata') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(chatId, metadata); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ChatDataData && + other.chatId == this.chatId && + other.metadata == this.metadata); +} + +class ChatDataCompanion extends UpdateCompanion { + final Value chatId; + final Value metadata; + final Value rowid; + const ChatDataCompanion({ + this.chatId = const Value.absent(), + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }); + ChatDataCompanion.insert({ + required String chatId, + required ChatMetadata metadata, + this.rowid = const Value.absent(), + }) : chatId = Value(chatId), + metadata = Value(metadata); + static Insertable custom({ + Expression? chatId, + Expression? metadata, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (chatId != null) 'chat_id': chatId, + if (metadata != null) 'metadata': metadata, + if (rowid != null) 'rowid': rowid, + }); + } + + ChatDataCompanion copyWith( + {Value? chatId, + Value? metadata, + Value? rowid}) { + return ChatDataCompanion( + chatId: chatId ?? this.chatId, + metadata: metadata ?? this.metadata, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (chatId.present) { + map['chat_id'] = Variable(chatId.value); + } + if (metadata.present) { + map['metadata'] = + Variable(ChatData.$convertermetadata.toSql(metadata.value)); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ChatDataCompanion(') + ..write('chatId: $chatId, ') + ..write('metadata: $metadata, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class ChatMessage extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + ChatMessage(this.attachedDatabase, [this._alias]); + late final GeneratedColumn messageId = GeneratedColumn( + 'message_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'PRIMARY KEY NOT NULL'); + late final GeneratedColumn chatId = GeneratedColumn( + 'chat_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn orderKey = GeneratedColumn( + 'order_key', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn messageJson = GeneratedColumn( + 'message_json', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => + [messageId, chatId, orderKey, messageJson]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'chat_message'; + @override + Set get $primaryKey => {messageId}; + @override + ChatMessageData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return ChatMessageData( + messageId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_id'])!, + chatId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}chat_id'])!, + orderKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}order_key'])!, + messageJson: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}message_json'])!, + ); + } + + @override + ChatMessage createAlias(String alias) { + return ChatMessage(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class ChatMessageData extends DataClass implements Insertable { + final String messageId; + final String chatId; + final String orderKey; + final String messageJson; + const ChatMessageData( + {required this.messageId, + required this.chatId, + required this.orderKey, + required this.messageJson}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['message_id'] = Variable(messageId); + map['chat_id'] = Variable(chatId); + map['order_key'] = Variable(orderKey); + map['message_json'] = Variable(messageJson); + return map; + } + + factory ChatMessageData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return ChatMessageData( + messageId: serializer.fromJson(json['message_id']), + chatId: serializer.fromJson(json['chat_id']), + orderKey: serializer.fromJson(json['order_key']), + messageJson: serializer.fromJson(json['message_json']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'message_id': serializer.toJson(messageId), + 'chat_id': serializer.toJson(chatId), + 'order_key': serializer.toJson(orderKey), + 'message_json': serializer.toJson(messageJson), + }; + } + + ChatMessageData copyWith( + {String? messageId, + String? chatId, + String? orderKey, + String? messageJson}) => + ChatMessageData( + messageId: messageId ?? this.messageId, + chatId: chatId ?? this.chatId, + orderKey: orderKey ?? this.orderKey, + messageJson: messageJson ?? this.messageJson, + ); + ChatMessageData copyWithCompanion(ChatMessageCompanion data) { + return ChatMessageData( + messageId: data.messageId.present ? data.messageId.value : this.messageId, + chatId: data.chatId.present ? data.chatId.value : this.chatId, + orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey, + messageJson: + data.messageJson.present ? data.messageJson.value : this.messageJson, + ); + } + + @override + String toString() { + return (StringBuffer('ChatMessageData(') + ..write('messageId: $messageId, ') + ..write('chatId: $chatId, ') + ..write('orderKey: $orderKey, ') + ..write('messageJson: $messageJson') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(messageId, chatId, orderKey, messageJson); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ChatMessageData && + other.messageId == this.messageId && + other.chatId == this.chatId && + other.orderKey == this.orderKey && + other.messageJson == this.messageJson); +} + +class ChatMessageCompanion extends UpdateCompanion { + final Value messageId; + final Value chatId; + final Value orderKey; + final Value messageJson; + final Value rowid; + const ChatMessageCompanion({ + this.messageId = const Value.absent(), + this.chatId = const Value.absent(), + this.orderKey = const Value.absent(), + this.messageJson = const Value.absent(), + this.rowid = const Value.absent(), + }); + ChatMessageCompanion.insert({ + required String messageId, + required String chatId, + required String orderKey, + required String messageJson, + this.rowid = const Value.absent(), + }) : messageId = Value(messageId), + chatId = Value(chatId), + orderKey = Value(orderKey), + messageJson = Value(messageJson); + static Insertable custom({ + Expression? messageId, + Expression? chatId, + Expression? orderKey, + Expression? messageJson, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (messageId != null) 'message_id': messageId, + if (chatId != null) 'chat_id': chatId, + if (orderKey != null) 'order_key': orderKey, + if (messageJson != null) 'message_json': messageJson, + if (rowid != null) 'rowid': rowid, + }); + } + + ChatMessageCompanion copyWith( + {Value? messageId, + Value? chatId, + Value? orderKey, + Value? messageJson, + Value? rowid}) { + return ChatMessageCompanion( + messageId: messageId ?? this.messageId, + chatId: chatId ?? this.chatId, + orderKey: orderKey ?? this.orderKey, + messageJson: messageJson ?? this.messageJson, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (messageId.present) { + map['message_id'] = Variable(messageId.value); + } + if (chatId.present) { + map['chat_id'] = Variable(chatId.value); + } + if (orderKey.present) { + map['order_key'] = Variable(orderKey.value); + } + if (messageJson.present) { + map['message_json'] = Variable(messageJson.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('ChatMessageCompanion(') + ..write('messageId: $messageId, ') + ..write('chatId: $chatId, ') + ..write('orderKey: $orderKey, ') + ..write('messageJson: $messageJson, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$ChatDatabase extends GeneratedDatabase { + _$ChatDatabase(QueryExecutor e) : super(e); + $ChatDatabaseManager get managers => $ChatDatabaseManager(this); + late final ChatData chatData = ChatData(this); + late final ChatMessage chatMessage = ChatMessage(this); + late final Index groupOrderIdx = Index('group_order_idx', + 'CREATE UNIQUE INDEX group_order_idx ON chat_message (chat_id, order_key)'); + late final MessagesDao messagesDao = MessagesDao(this as ChatDatabase); + late final MetadataDao metadataDao = MetadataDao(this as ChatDatabase); + Selectable leadingOrderKey( + {required int bucket, required String chatId}) { + return customSelect( + 'SELECT lexo_rank_previous(?1, (SELECT order_key FROM chat_message WHERE chat_id = ?2 ORDER BY order_key LIMIT 1)) AS _c0', + variables: [ + Variable(bucket), + Variable(chatId) + ], + readsFrom: { + chatMessage, + }).map((QueryRow row) => row.read('_c0')); + } + + Selectable trailingOrderKey( + {required int bucket, required String chatId}) { + return customSelect( + 'SELECT lexo_rank_next(?1, (SELECT order_key FROM chat_message WHERE chat_id = ?2 ORDER BY order_key DESC LIMIT 1)) AS _c0', + variables: [ + Variable(bucket), + Variable(chatId) + ], + readsFrom: { + chatMessage, + }).map((QueryRow row) => row.read('_c0')); + } + + Selectable orderKeyBeforeIndex( + {required String chatId, required int rowIdx}) { + return customSelect( + 'WITH ordered_table AS (SELECT message_id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key, ROW_NUMBER()OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS row_idx, COUNT(*)OVER (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS total_rows FROM chat_message WHERE chat_id = ?1) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE row_idx = MIN(?2, total_rows)', + variables: [ + Variable(chatId), + Variable(rowIdx) + ], + readsFrom: { + chatMessage, + }).map((QueryRow row) => row.read('_c0')); + } + + Selectable messageIndex( + {required String chatId, required String messageId}) { + return customSelect( + 'WITH ordered_table AS (SELECT message_id, ROW_NUMBER()OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS row_idx FROM chat_message WHERE chat_id = ?1) SELECT row_idx FROM ordered_table WHERE message_id = ?2', + variables: [ + Variable(chatId), + Variable(messageId) + ], + readsFrom: { + chatMessage, + }).map((QueryRow row) => row.read('row_idx')); + } + + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [chatData, chatMessage, groupOrderIdx]; +} + +typedef $ChatDataCreateCompanionBuilder = ChatDataCompanion Function({ + required String chatId, + required ChatMetadata metadata, + Value rowid, +}); +typedef $ChatDataUpdateCompanionBuilder = ChatDataCompanion Function({ + Value chatId, + Value metadata, + Value rowid, +}); + +class $ChatDataFilterComposer extends Composer<_$ChatDatabase, ChatData> { + $ChatDataFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get chatId => $composableBuilder( + column: $table.chatId, builder: (column) => ColumnFilters(column)); + + ColumnWithTypeConverterFilters + get metadata => $composableBuilder( + column: $table.metadata, + builder: (column) => ColumnWithTypeConverterFilters(column)); +} + +class $ChatDataOrderingComposer extends Composer<_$ChatDatabase, ChatData> { + $ChatDataOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get chatId => $composableBuilder( + column: $table.chatId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get metadata => $composableBuilder( + column: $table.metadata, builder: (column) => ColumnOrderings(column)); +} + +class $ChatDataAnnotationComposer extends Composer<_$ChatDatabase, ChatData> { + $ChatDataAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get chatId => + $composableBuilder(column: $table.chatId, builder: (column) => column); + + GeneratedColumnWithTypeConverter get metadata => + $composableBuilder(column: $table.metadata, builder: (column) => column); +} + +class $ChatDataTableManager extends RootTableManager< + _$ChatDatabase, + ChatData, + ChatDataData, + $ChatDataFilterComposer, + $ChatDataOrderingComposer, + $ChatDataAnnotationComposer, + $ChatDataCreateCompanionBuilder, + $ChatDataUpdateCompanionBuilder, + (ChatDataData, BaseReferences<_$ChatDatabase, ChatData, ChatDataData>), + ChatDataData, + PrefetchHooks Function()> { + $ChatDataTableManager(_$ChatDatabase db, ChatData table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $ChatDataFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $ChatDataOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $ChatDataAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value chatId = const Value.absent(), + Value metadata = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ChatDataCompanion( + chatId: chatId, + metadata: metadata, + rowid: rowid, + ), + createCompanionCallback: ({ + required String chatId, + required ChatMetadata metadata, + Value rowid = const Value.absent(), + }) => + ChatDataCompanion.insert( + chatId: chatId, + metadata: metadata, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $ChatDataProcessedTableManager = ProcessedTableManager< + _$ChatDatabase, + ChatData, + ChatDataData, + $ChatDataFilterComposer, + $ChatDataOrderingComposer, + $ChatDataAnnotationComposer, + $ChatDataCreateCompanionBuilder, + $ChatDataUpdateCompanionBuilder, + (ChatDataData, BaseReferences<_$ChatDatabase, ChatData, ChatDataData>), + ChatDataData, + PrefetchHooks Function()>; +typedef $ChatMessageCreateCompanionBuilder = ChatMessageCompanion Function({ + required String messageId, + required String chatId, + required String orderKey, + required String messageJson, + Value rowid, +}); +typedef $ChatMessageUpdateCompanionBuilder = ChatMessageCompanion Function({ + Value messageId, + Value chatId, + Value orderKey, + Value messageJson, + Value rowid, +}); + +class $ChatMessageFilterComposer extends Composer<_$ChatDatabase, ChatMessage> { + $ChatMessageFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get chatId => $composableBuilder( + column: $table.chatId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get orderKey => $composableBuilder( + column: $table.orderKey, builder: (column) => ColumnFilters(column)); + + ColumnFilters get messageJson => $composableBuilder( + column: $table.messageJson, builder: (column) => ColumnFilters(column)); +} + +class $ChatMessageOrderingComposer + extends Composer<_$ChatDatabase, ChatMessage> { + $ChatMessageOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get messageId => $composableBuilder( + column: $table.messageId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get chatId => $composableBuilder( + column: $table.chatId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get orderKey => $composableBuilder( + column: $table.orderKey, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get messageJson => $composableBuilder( + column: $table.messageJson, builder: (column) => ColumnOrderings(column)); +} + +class $ChatMessageAnnotationComposer + extends Composer<_$ChatDatabase, ChatMessage> { + $ChatMessageAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get messageId => + $composableBuilder(column: $table.messageId, builder: (column) => column); + + GeneratedColumn get chatId => + $composableBuilder(column: $table.chatId, builder: (column) => column); + + GeneratedColumn get orderKey => + $composableBuilder(column: $table.orderKey, builder: (column) => column); + + GeneratedColumn get messageJson => $composableBuilder( + column: $table.messageJson, builder: (column) => column); +} + +class $ChatMessageTableManager extends RootTableManager< + _$ChatDatabase, + ChatMessage, + ChatMessageData, + $ChatMessageFilterComposer, + $ChatMessageOrderingComposer, + $ChatMessageAnnotationComposer, + $ChatMessageCreateCompanionBuilder, + $ChatMessageUpdateCompanionBuilder, + ( + ChatMessageData, + BaseReferences<_$ChatDatabase, ChatMessage, ChatMessageData> + ), + ChatMessageData, + PrefetchHooks Function()> { + $ChatMessageTableManager(_$ChatDatabase db, ChatMessage table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $ChatMessageFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $ChatMessageOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $ChatMessageAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value messageId = const Value.absent(), + Value chatId = const Value.absent(), + Value orderKey = const Value.absent(), + Value messageJson = const Value.absent(), + Value rowid = const Value.absent(), + }) => + ChatMessageCompanion( + messageId: messageId, + chatId: chatId, + orderKey: orderKey, + messageJson: messageJson, + rowid: rowid, + ), + createCompanionCallback: ({ + required String messageId, + required String chatId, + required String orderKey, + required String messageJson, + Value rowid = const Value.absent(), + }) => + ChatMessageCompanion.insert( + messageId: messageId, + chatId: chatId, + orderKey: orderKey, + messageJson: messageJson, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $ChatMessageProcessedTableManager = ProcessedTableManager< + _$ChatDatabase, + ChatMessage, + ChatMessageData, + $ChatMessageFilterComposer, + $ChatMessageOrderingComposer, + $ChatMessageAnnotationComposer, + $ChatMessageCreateCompanionBuilder, + $ChatMessageUpdateCompanionBuilder, + ( + ChatMessageData, + BaseReferences<_$ChatDatabase, ChatMessage, ChatMessageData> + ), + ChatMessageData, + PrefetchHooks Function()>; + +class $ChatDatabaseManager { + final _$ChatDatabase _db; + $ChatDatabaseManager(this._db); + $ChatDataTableManager get chatData => + $ChatDataTableManager(_db, _db.chatData); + $ChatMessageTableManager get chatMessage => + $ChatMessageTableManager(_db, _db.chatMessage); +} diff --git a/app/lib/features/chat/features/chat_store/data/models/chat_metadata.dart b/app/lib/features/chat/features/chat_store/data/models/chat_metadata.dart new file mode 100644 index 00000000..9b06b04e --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/models/chat_metadata.dart @@ -0,0 +1,16 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'chat_metadata.g.dart'; + +@JsonSerializable() +class ChatMetadata { + final String? mainDocumentId; + final String? contextId; + + ChatMetadata({this.mainDocumentId, this.contextId}); + + factory ChatMetadata.fromJson(Map json) => + _$ChatMetadataFromJson(json); + + Map toJson() => _$ChatMetadataToJson(this); +} diff --git a/app/lib/features/chat/features/chat_store/data/models/chat_metadata.g.dart b/app/lib/features/chat/features/chat_store/data/models/chat_metadata.g.dart new file mode 100644 index 00000000..2f1b7598 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/models/chat_metadata.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_metadata.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ChatMetadata _$ChatMetadataFromJson(Map json) => ChatMetadata( + mainDocumentId: json['mainDocumentId'] as String?, + contextId: json['contextId'] as String?, + ); + +Map _$ChatMetadataToJson(ChatMetadata instance) => + { + 'mainDocumentId': instance.mainDocumentId, + 'contextId': instance.contextId, + }; diff --git a/app/lib/features/chat/features/chat_store/data/models/message_types.dart b/app/lib/features/chat/features/chat_store/data/models/message_types.dart new file mode 100644 index 00000000..789fe71c --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/models/message_types.dart @@ -0,0 +1,22 @@ +import 'package:flutter_chat_core/flutter_chat_core.dart'; + +enum MessageAuthor { + ///langchain SystemChatMessage + system(User(id: 'system')), + + ///langchain HumanChatMessage + human(User(id: 'human')), + + ///langchain AIChatMessage + ai(User(id: 'ai')), + + ///langchain ToolChatMessage + tool(User(id: 'tool')), + + ///langchain CustomChatMessage + custom(User(id: 'custom')); + + final User user; + + const MessageAuthor(this.user); +} diff --git a/app/lib/features/chat/features/chat_store/data/providers.dart b/app/lib/features/chat/features/chat_store/data/providers.dart new file mode 100644 index 00000000..9e3d1786 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/providers.dart @@ -0,0 +1,52 @@ +import 'dart:async'; + +import 'package:lensai/data/database/functions/lexo_rank_functions.dart'; +import 'package:lensai/features/chat/features/chat_store/data/database/database.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:sqlite3/sqlite3.dart'; +import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; +import 'package:universal_io/io.dart'; + +part 'providers.g.dart'; + +@Riverpod(keepAlive: true) +ChatDatabase chatDatabase(Ref ref) { + final db = ChatDatabase( + LazyDatabase(() async { + // put the database file, called db.sqlite here, into the documents folder + // for your app. + final dbFolder = await path_provider.getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'chat.db')); + + // Also work around limitations on old Android versions + if (Platform.isAndroid) { + await applyWorkaroundToOpenSqlite3OnOldAndroidVersions(); + } + + // Make sqlite3 pick a more suitable location for temporary files - the + // one from the system may be inaccessible due to sandboxing. + final cachebase = (await path_provider.getTemporaryDirectory()).path; + // We can't access /tmp on Android, which sqlite3 would try by default. + // Explicitly tell it about the correct temporary directory. + sqlite3.tempDirectory = cachebase; + + return NativeDatabase.createInBackground( + file, + setup: (database) { + registerLexorankFunctions(database); + }, + ); + }), + ); + + ref.onDispose(() { + unawaited(db.close()); + }); + + return db; +} diff --git a/app/lib/features/chat/features/chat_store/data/providers.g.dart b/app/lib/features/chat/features/chat_store/data/providers.g.dart new file mode 100644 index 00000000..70a8d3b1 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/data/providers.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatDatabaseHash() => r'b48955bc016f8c74684ed6e2c5cba99f5ab38dfe'; + +/// See also [chatDatabase]. +@ProviderFor(chatDatabase) +final chatDatabaseProvider = Provider.internal( + chatDatabase, + name: r'chatDatabaseProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$chatDatabaseHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef ChatDatabaseRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat/features/chat_store/domain/chat_memory.dart b/app/lib/features/chat/features/chat_store/domain/chat_memory.dart new file mode 100644 index 00000000..c7edc9dd --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/chat_memory.dart @@ -0,0 +1,152 @@ +// ignore_for_file: avoid_dynamic_calls + +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:langchain/langchain.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart'; + +class ChatMemory implements BaseMemory { + final ChatController _controller; + + /// Max number of tokens to use. + final int maxTokenLimit; + + /// Language model to use for counting tokens. + final BaseLanguageModel llm; + + /// The memory key to use for the chat history. + /// This will be passed as input variable to the prompt. + final String memoryKey; + + /// If true, when [loadMemoryVariables] is called, it will return + /// [ChatMessage] objects. If false, it will return a String representation + /// of the messages. + /// + /// Set this to true when you are using a Chat model like `ChatOpenAI`. + /// Set this to false when you are use a text LLM like `OpenAI`. + final bool returnMessages; + + /// The prefix to use for system messages if [returnMessages] is false. + final String systemPrefix; + + /// The prefix to use for human messages if [returnMessages] is false. + final String humanPrefix; + + /// The prefix to use for AI messages if [returnMessages] is false. + final String aiPrefix; + + /// The prefix to use for tool messages if [returnMessages] is false. + final String toolPrefix; + + ChatMemory( + this._controller, { + required this.returnMessages, + this.maxTokenLimit = 2000, + required this.llm, + this.memoryKey = BaseMemory.defaultMemoryKey, + this.systemPrefix = SystemChatMessage.defaultPrefix, + this.humanPrefix = HumanChatMessage.defaultPrefix, + this.aiPrefix = AIChatMessage.defaultPrefix, + this.toolPrefix = ToolChatMessage.defaultPrefix, + }); + + @override + Set get memoryKeys => {memoryKey}; + + @override + Future loadMemoryVariables([ + MemoryInputValues values = const {}, + ]) async { + final messages = _controller.messages + .whereNot( + (message) => message.metadata?['hideFromModelChatHistory'] == true, + ) + .map( + (message) => switch (message) { + TextMessage( + authorId: final authorId, + text: final text, + metadata: final metadata, + ) => + () { + if (authorId == MessageAuthor.system.user.id) { + return SystemChatMessage(content: text); + } + if (authorId == MessageAuthor.human.user.id) { + return HumanChatMessage( + content: ChatMessageContent.text(text), + ); + } + if (authorId == MessageAuthor.ai.user.id) { + final toolsRaw = metadata?['toolCalls']; + if (toolsRaw is String) { + return AIChatMessage( + content: text, + toolCalls: _deserializeToolCalls(toolsRaw), + ); + } + return AIChatMessage(content: text); + } + throw UnimplementedError(); + }(), + ImageMessage() => throw UnimplementedError(), + CustomMessage() => throw UnimplementedError(), + UnsupportedMessage() => throw UnimplementedError() + }, + ) + .toList(); + + int currentBufferLength = await llm.countTokens(PromptValue.chat(messages)); + // Prune buffer if it exceeds max token limit + if (currentBufferLength > maxTokenLimit) { + while (currentBufferLength > maxTokenLimit) { + //remove oldest entry + messages.removeAt(0); + currentBufferLength = await llm.countTokens(PromptValue.chat(messages)); + } + } + + if (returnMessages) { + return {memoryKey: messages}; + } + + return { + memoryKey: messages.toBufferString( + systemPrefix: systemPrefix, + humanPrefix: humanPrefix, + aiPrefix: aiPrefix, + toolPrefix: toolPrefix, + ), + }; + } + + List _deserializeToolCalls(String toolsRaw) { + final decoded = jsonDecode(toolsRaw) as List; + final tools = decoded + .map( + (tool) => AIChatMessageToolCall( + id: tool['id'] as String, + name: tool['name'] as String, + arguments: tool['arguments'] as Map, + argumentsRaw: tool['argumentsRaw'] as String, + ), + ) + .toList(); + return tools; + } + + @override + Future saveContext({ + required MemoryInputValues inputValues, + required MemoryOutputValues outputValues, + }) { + throw UnimplementedError('This is a read only memory'); + } + + @override + Future clear() { + throw UnimplementedError('This is a read only memory'); + } +} diff --git a/app/lib/features/chat/features/chat_store/domain/drift_chat_controller.dart b/app/lib/features/chat/features/chat_store/domain/drift_chat_controller.dart new file mode 100644 index 00000000..f021608d --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/drift_chat_controller.dart @@ -0,0 +1,169 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:lensai/features/chat/features/chat_store/data/database/daos/messages.dart'; +import 'package:rxdart/rxdart.dart'; + +class DriftChatController implements ChatController { + final MessagesDao _dao; + final String chatId; + + final _operationsController = StreamController.broadcast(); + + late final ValueStream> _mergedMessagesStream; + late final StreamSubscription> _mergedMessagesStreamSub; + late final BehaviorSubject> _intermediateMessages; + + DriftChatController(this._dao, {required this.chatId}) { + final chatMessagesStream = _dao + .chatMessages(chatId: chatId) + .map( + (data) => Message.fromJson( + jsonDecode(data.messageJson) as Map, + ), + ) + .watch(); + + _intermediateMessages = BehaviorSubject.seeded({}); + + _mergedMessagesStream = Rx.combineLatest2( + chatMessagesStream, + _intermediateMessages, + (a, b) { + if (a.isNotEmpty && b.isNotEmpty) { + return [ + for (final message in a) b[message.id] ?? message, + ]; + } + + return a; + }, + ).shareValue(); + + //Set initial messages + unawaited( + _mergedMessagesStream.first.then((messages) { + if (messages.isNotEmpty) { + _operationsController.add(ChatOperation.set()); + } + }), + ); + + _mergedMessagesStreamSub = _mergedMessagesStream.listen(null); + } + + @override + List get messages => _mergedMessagesStream.valueOrNull ?? []; + + @override + Future insert(Message message, {int? index}) async { + final insertedIndex = await _dao.insertMessage( + chatId: chatId, + message: message, + index: index, + ); + + if (insertedIndex != null) { + await _mergedMessagesStream + .firstWhere( + (messages) => + messages.length > insertedIndex && + messages[insertedIndex].id == message.id, + ) + .timeout(const Duration(milliseconds: 100)); + + _operationsController.add(ChatOperation.insert(message, insertedIndex)); + } + } + + @override + Future remove(Message message) async { + final index = await _dao + .messageIndex(chatId: chatId, messageId: message.id) + .getSingleOrNull(); + + if (index != null) { + await _dao.removeMessage(messageId: message.id); + await _mergedMessagesStream + .firstWhere( + (messages) => + !messages.any((dbMessage) => dbMessage.id == message.id), + ) + .timeout(const Duration(milliseconds: 100)); + + _operationsController.add(ChatOperation.remove(message, index)); + } + } + + @override + Future update( + Message oldMessage, + Message newMessage, { + bool persistMessage = true, + }) async { + assert(oldMessage.id == newMessage.id); + + if (persistMessage) { + final intermediateMessage = _intermediateMessages.value[newMessage.id]; + + if (intermediateMessage == null && oldMessage == newMessage) { + //nothing to update + return; + } + + final rows = await _dao.updateMessage( + messageId: oldMessage.id, + message: newMessage, + ); + + if (intermediateMessage != null) { + _intermediateMessages.add( + {..._intermediateMessages.value}..remove(newMessage.id), + ); + } + + if (rows <= 0 || intermediateMessage == newMessage) { + //Dont send event + return; + } + } else if (oldMessage != newMessage) { + _intermediateMessages.add({ + ..._intermediateMessages.value, + newMessage.id: newMessage, + }); + } + + await _mergedMessagesStream + .firstWhere( + (messages) => messages.any((message) => message == newMessage), + ) + .timeout(const Duration(milliseconds: 100)); + + _operationsController.add(ChatOperation.update(oldMessage, newMessage)); + } + + @override + Future set(List messages) async { + await _dao.replaceChatMessages(chatId: chatId, messages: messages); + await _mergedMessagesStream + .firstWhere( + (replacedMessages) => + const DeepCollectionEquality().equals(messages, replacedMessages), + ) + .timeout(const Duration(milliseconds: 100)); + + _operationsController.add(ChatOperation.set()); + } + + @override + Stream get operationsStream => _operationsController.stream; + + @override + void dispose() { + unawaited(_intermediateMessages.close()); + unawaited(_mergedMessagesStreamSub.cancel()); + unawaited(_operationsController.close()); + } +} diff --git a/app/lib/features/chat/features/chat_store/domain/providers.dart b/app/lib/features/chat/features/chat_store/domain/providers.dart new file mode 100644 index 00000000..e1824135 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/providers.dart @@ -0,0 +1,25 @@ +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:lensai/features/chat/features/chat_store/data/providers.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/drift_chat_controller.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'providers.g.dart'; + +@Riverpod() +DriftChatController chatController(Ref ref, String chatId) { + final db = ref.watch(chatDatabaseProvider); + final controller = DriftChatController(db.messagesDao, chatId: chatId); + + ref.onDispose(() { + controller.dispose(); + }); + + return controller; +} + +@Riverpod() +Stream chatMetadata(Ref ref, String chatId) { + final db = ref.watch(chatDatabaseProvider); + return db.metadataDao.chatMetadata(chatId).watchSingleOrNull(); +} diff --git a/app/lib/features/chat/features/chat_store/domain/providers.g.dart b/app/lib/features/chat/features/chat_store/domain/providers.g.dart new file mode 100644 index 00000000..4fac3ea2 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/providers.g.dart @@ -0,0 +1,292 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatControllerHash() => r'493264a4413ffc2d767d73a90634e1a1744cfcfd'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [chatController]. +@ProviderFor(chatController) +const chatControllerProvider = ChatControllerFamily(); + +/// See also [chatController]. +class ChatControllerFamily extends Family { + /// See also [chatController]. + const ChatControllerFamily(); + + /// See also [chatController]. + ChatControllerProvider call( + String chatId, + ) { + return ChatControllerProvider( + chatId, + ); + } + + @override + ChatControllerProvider getProviderOverride( + covariant ChatControllerProvider provider, + ) { + return call( + provider.chatId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatControllerProvider'; +} + +/// See also [chatController]. +class ChatControllerProvider extends AutoDisposeProvider { + /// See also [chatController]. + ChatControllerProvider( + String chatId, + ) : this._internal( + (ref) => chatController( + ref as ChatControllerRef, + chatId, + ), + from: chatControllerProvider, + name: r'chatControllerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatControllerHash, + dependencies: ChatControllerFamily._dependencies, + allTransitiveDependencies: + ChatControllerFamily._allTransitiveDependencies, + chatId: chatId, + ); + + ChatControllerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + }) : super.internal(); + + final String chatId; + + @override + Override overrideWith( + DriftChatController Function(ChatControllerRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ChatControllerProvider._internal( + (ref) => create(ref as ChatControllerRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _ChatControllerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatControllerProvider && other.chatId == chatId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatControllerRef on AutoDisposeProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; +} + +class _ChatControllerProviderElement + extends AutoDisposeProviderElement + with ChatControllerRef { + _ChatControllerProviderElement(super.provider); + + @override + String get chatId => (origin as ChatControllerProvider).chatId; +} + +String _$chatMetadataHash() => r'22252e4a4c4e9cb039c5eee0e0f6eb5b8bd03944'; + +/// See also [chatMetadata]. +@ProviderFor(chatMetadata) +const chatMetadataProvider = ChatMetadataFamily(); + +/// See also [chatMetadata]. +class ChatMetadataFamily extends Family> { + /// See also [chatMetadata]. + const ChatMetadataFamily(); + + /// See also [chatMetadata]. + ChatMetadataProvider call( + String chatId, + ) { + return ChatMetadataProvider( + chatId, + ); + } + + @override + ChatMetadataProvider getProviderOverride( + covariant ChatMetadataProvider provider, + ) { + return call( + provider.chatId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatMetadataProvider'; +} + +/// See also [chatMetadata]. +class ChatMetadataProvider extends AutoDisposeStreamProvider { + /// See also [chatMetadata]. + ChatMetadataProvider( + String chatId, + ) : this._internal( + (ref) => chatMetadata( + ref as ChatMetadataRef, + chatId, + ), + from: chatMetadataProvider, + name: r'chatMetadataProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatMetadataHash, + dependencies: ChatMetadataFamily._dependencies, + allTransitiveDependencies: + ChatMetadataFamily._allTransitiveDependencies, + chatId: chatId, + ); + + ChatMetadataProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + }) : super.internal(); + + final String chatId; + + @override + Override overrideWith( + Stream Function(ChatMetadataRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: ChatMetadataProvider._internal( + (ref) => create(ref as ChatMetadataRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _ChatMetadataProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatMetadataProvider && other.chatId == chatId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatMetadataRef on AutoDisposeStreamProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; +} + +class _ChatMetadataProviderElement + extends AutoDisposeStreamProviderElement + with ChatMetadataRef { + _ChatMetadataProviderElement(super.provider); + + @override + String get chatId => (origin as ChatMetadataProvider).chatId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart new file mode 100644 index 00000000..8d06a953 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.dart @@ -0,0 +1,87 @@ +import 'package:collection/collection.dart'; +import 'package:exceptions/exceptions.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:langchain/langchain.dart'; +import 'package:lensai/core/uuid.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/drift_chat_controller.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'chat_message.g.dart'; + +@Riverpod() +class ChatMessageRepository extends _$ChatMessageRepository { + late DriftChatController _controller; + + final _isTyping = {}; + + bool isTyping(MessageAuthor author) => _isTyping.contains(author); + + Future setTyping({ + required MessageAuthor author, + required bool typing, + }) async { + if (isTyping(author) == typing) { + return; + } + + if (typing) { + await _controller.insert( + CustomMessage( + id: uuid.v4(), + authorId: author.user.id, + createdAt: DateTime.now().toUtc(), + metadata: { + 'type': 'typing', + }, + ), + ); + + _isTyping.add(author); + } else { + final typingMessages = _controller.messages.where( + (message) => + message.authorId == author.user.id && + message.metadata?['type'] == 'typing', + ); + + await Future.wait( + typingMessages.map((message) => _controller.remove(message)), + ); + + _isTyping.remove(author); + } + } + + Future> insertTextMessage({ + required MessageAuthor author, + required String content, + List? toolCalls, + bool hideFromModelChatHistory = false, + }) async { + final message = TextMessage( + id: uuid.v4(), + authorId: author.user.id, + createdAt: DateTime.now().toUtc(), + text: content, + isOnlyEmoji: isOnlyEmoji(content), + metadata: (hideFromModelChatHistory || (toolCalls?.isNotEmpty ?? false)) + ? { + 'toolCalls': toolCalls?.map((tool) => tool.toMap()).toList(), + if (hideFromModelChatHistory) 'hideFromModelChatHistory': true, + } + : null, + ); + + return Result.fromAsync(() async { + await _controller.insert(message); + return message; + }); + } + + @override + void build(String chatId) { + _controller = ref.watch(chatControllerProvider(chatId)); + } +} diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.g.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.g.dart new file mode 100644 index 00000000..82a2cd20 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_message.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_message.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatMessageRepositoryHash() => + r'a4144618b339f7c0c329a01a294b6bebbb648bf6'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChatMessageRepository + extends BuildlessAutoDisposeNotifier { + late final String chatId; + + void build( + String chatId, + ); +} + +/// See also [ChatMessageRepository]. +@ProviderFor(ChatMessageRepository) +const chatMessageRepositoryProvider = ChatMessageRepositoryFamily(); + +/// See also [ChatMessageRepository]. +class ChatMessageRepositoryFamily extends Family { + /// See also [ChatMessageRepository]. + const ChatMessageRepositoryFamily(); + + /// See also [ChatMessageRepository]. + ChatMessageRepositoryProvider call( + String chatId, + ) { + return ChatMessageRepositoryProvider( + chatId, + ); + } + + @override + ChatMessageRepositoryProvider getProviderOverride( + covariant ChatMessageRepositoryProvider provider, + ) { + return call( + provider.chatId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatMessageRepositoryProvider'; +} + +/// See also [ChatMessageRepository]. +class ChatMessageRepositoryProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ChatMessageRepository]. + ChatMessageRepositoryProvider( + String chatId, + ) : this._internal( + () => ChatMessageRepository()..chatId = chatId, + from: chatMessageRepositoryProvider, + name: r'chatMessageRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatMessageRepositoryHash, + dependencies: ChatMessageRepositoryFamily._dependencies, + allTransitiveDependencies: + ChatMessageRepositoryFamily._allTransitiveDependencies, + chatId: chatId, + ); + + ChatMessageRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + }) : super.internal(); + + final String chatId; + + @override + void runNotifierBuild( + covariant ChatMessageRepository notifier, + ) { + return notifier.build( + chatId, + ); + } + + @override + Override overrideWith(ChatMessageRepository Function() create) { + return ProviderOverride( + origin: this, + override: ChatMessageRepositoryProvider._internal( + () => create()..chatId = chatId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _ChatMessageRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatMessageRepositoryProvider && other.chatId == chatId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatMessageRepositoryRef on AutoDisposeNotifierProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; +} + +class _ChatMessageRepositoryProviderElement + extends AutoDisposeNotifierProviderElement + with ChatMessageRepositoryRef { + _ChatMessageRepositoryProviderElement(super.provider); + + @override + String get chatId => (origin as ChatMessageRepositoryProvider).chatId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart new file mode 100644 index 00000000..6b44faf1 --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart @@ -0,0 +1,23 @@ +import 'package:exceptions/exceptions.dart'; +import 'package:lensai/features/chat/features/chat_store/data/database/database.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:lensai/features/chat/features/chat_store/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'chat_metadata.g.dart'; + +@Riverpod() +class ChatMetadataRepository extends _$ChatMetadataRepository { + late ChatDatabase _db; + + Future> updateMetadata(ChatMetadata metadata) { + return Result.fromAsync(() { + return _db.metadataDao.updateChatMetadata(chatId, metadata); + }); + } + + @override + void build(String chatId) { + _db = ref.watch(chatDatabaseProvider); + } +} diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart new file mode 100644 index 00000000..e7e58fcf --- /dev/null +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart @@ -0,0 +1,177 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_metadata.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatMetadataRepositoryHash() => + r'72a0226ed7e87f452f4dfe44594bb30663c2db45'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChatMetadataRepository + extends BuildlessAutoDisposeNotifier { + late final String chatId; + + void build( + String chatId, + ); +} + +/// See also [ChatMetadataRepository]. +@ProviderFor(ChatMetadataRepository) +const chatMetadataRepositoryProvider = ChatMetadataRepositoryFamily(); + +/// See also [ChatMetadataRepository]. +class ChatMetadataRepositoryFamily extends Family { + /// See also [ChatMetadataRepository]. + const ChatMetadataRepositoryFamily(); + + /// See also [ChatMetadataRepository]. + ChatMetadataRepositoryProvider call( + String chatId, + ) { + return ChatMetadataRepositoryProvider( + chatId, + ); + } + + @override + ChatMetadataRepositoryProvider getProviderOverride( + covariant ChatMetadataRepositoryProvider provider, + ) { + return call( + provider.chatId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatMetadataRepositoryProvider'; +} + +/// See also [ChatMetadataRepository]. +class ChatMetadataRepositoryProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ChatMetadataRepository]. + ChatMetadataRepositoryProvider( + String chatId, + ) : this._internal( + () => ChatMetadataRepository()..chatId = chatId, + from: chatMetadataRepositoryProvider, + name: r'chatMetadataRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatMetadataRepositoryHash, + dependencies: ChatMetadataRepositoryFamily._dependencies, + allTransitiveDependencies: + ChatMetadataRepositoryFamily._allTransitiveDependencies, + chatId: chatId, + ); + + ChatMetadataRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + }) : super.internal(); + + final String chatId; + + @override + void runNotifierBuild( + covariant ChatMetadataRepository notifier, + ) { + return notifier.build( + chatId, + ); + } + + @override + Override overrideWith(ChatMetadataRepository Function() create) { + return ProviderOverride( + origin: this, + override: ChatMetadataRepositoryProvider._internal( + () => create()..chatId = chatId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _ChatMetadataRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatMetadataRepositoryProvider && other.chatId == chatId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatMetadataRepositoryRef on AutoDisposeNotifierProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; +} + +class _ChatMetadataRepositoryProviderElement + extends AutoDisposeNotifierProviderElement + with ChatMetadataRepositoryRef { + _ChatMetadataRepositoryProviderElement(super.provider); + + @override + String get chatId => (origin as ChatMetadataRepositoryProvider).chatId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/chat_archive/data/database/database.g.dart b/app/lib/features/chat_archive/data/database/database.g.dart index 071f393e..f763219a 100644 --- a/app/lib/features/chat_archive/data/database/database.g.dart +++ b/app/lib/features/chat_archive/data/database/database.g.dart @@ -403,8 +403,8 @@ abstract class _$ChatSearchDatabase extends GeneratedDatabase { chatFts, }).map((QueryRow row) => ChatQueryResult( fileName: row.read('file_name'), - title: row.read('title'), - contentSnippet: row.read('content_snippet'), + title: row.readNullable('title'), + contentSnippet: row.readNullable('content_snippet'), )); } @@ -702,11 +702,11 @@ class $ChatSearchDatabaseManager { class ChatQueryResult { final String fileName; - final String title; - final String contentSnippet; + final String? title; + final String? contentSnippet; ChatQueryResult({ required this.fileName, - required this.title, - required this.contentSnippet, + this.title, + this.contentSnippet, }); } diff --git a/app/lib/features/chat_archive/data/providers.dart b/app/lib/features/chat_archive/data/providers.dart index a73c3677..a6ef8ab3 100644 --- a/app/lib/features/chat_archive/data/providers.dart +++ b/app/lib/features/chat_archive/data/providers.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:lensai/features/chat_archive/data/database/database.dart'; @@ -12,7 +14,7 @@ part 'providers.g.dart'; @Riverpod() ChatSearchDatabase chatSearchDatabase(Ref ref) { - return ChatSearchDatabase( + final db = ChatSearchDatabase( LazyDatabase(() async { // Also work around limitations on old Android versions if (Platform.isAndroid) { @@ -29,4 +31,10 @@ ChatSearchDatabase chatSearchDatabase(Ref ref) { return NativeDatabase.memory(); }), ); + + ref.onDispose(() { + unawaited(db.close()); + }); + + return db; } diff --git a/app/lib/features/chat_archive/data/providers.g.dart b/app/lib/features/chat_archive/data/providers.g.dart index 1c68568a..6d23314d 100644 --- a/app/lib/features/chat_archive/data/providers.g.dart +++ b/app/lib/features/chat_archive/data/providers.g.dart @@ -7,7 +7,7 @@ part of 'providers.dart'; // ************************************************************************** String _$chatSearchDatabaseHash() => - r'99700f1a2c25b24742c60fa8dd91fe744dcf21be'; + r'17c972b3da21b6a2365cc5fd56a4eade0ac0a3f3'; /// See also [chatSearchDatabase]. @ProviderFor(chatSearchDatabase) diff --git a/app/lib/features/chat_archive/presentation/screens/search.dart b/app/lib/features/chat_archive/presentation/screens/search.dart index c754763d..4305e3bc 100644 --- a/app/lib/features/chat_archive/presentation/screens/search.dart +++ b/app/lib/features/chat_archive/presentation/screens/search.dart @@ -91,9 +91,10 @@ class ChatArchiveSearchScreen extends HookConsumerWidget { chatEntity.dateTime!.formatWithMinutePrecision(), ), const SizedBox(height: 8.0), - MarkdownBody( - data: chat.contentSnippet, - ), + if (chat.contentSnippet != null) + MarkdownBody( + data: chat.contentSnippet!, + ), ], ), ), diff --git a/app/lib/features/geckoview/features/controllers/bottom_sheet.dart b/app/lib/features/geckoview/domain/controllers/bottom_sheet.dart similarity index 100% rename from app/lib/features/geckoview/features/controllers/bottom_sheet.dart rename to app/lib/features/geckoview/domain/controllers/bottom_sheet.dart diff --git a/app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart b/app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart similarity index 100% rename from app/lib/features/geckoview/features/controllers/bottom_sheet.g.dart rename to app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart diff --git a/app/lib/features/geckoview/features/controllers/overlay_dialog.dart b/app/lib/features/geckoview/domain/controllers/overlay_dialog.dart similarity index 100% rename from app/lib/features/geckoview/features/controllers/overlay_dialog.dart rename to app/lib/features/geckoview/domain/controllers/overlay_dialog.dart diff --git a/app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart b/app/lib/features/geckoview/domain/controllers/overlay_dialog.g.dart similarity index 100% rename from app/lib/features/geckoview/features/controllers/overlay_dialog.g.dart rename to app/lib/features/geckoview/domain/controllers/overlay_dialog.g.dart diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index de009978..520a6d15 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -78,13 +78,13 @@ class TabRepository extends _$TabRepository { final tabContentSub = tabContentService.tabContentStream.listen((content) async { - await _db.tabDao.updateTab( + await _db.tabDao.updateTabContent( content.tabId, - isProbablyReaderable: Value(content.isProbablyReaderable), - extractedContentMarkdown: Value(content.extractedContentMarkdown), - extractedContentPlain: Value(content.extractedContentPlain), - fullContentMarkdown: Value(content.fullContentMarkdown), - fullContentPlain: Value(content.fullContentPlain), + isProbablyReaderable: content.isProbablyReaderable, + extractedContentMarkdown: content.extractedContentMarkdown, + extractedContentPlain: content.extractedContentPlain, + fullContentMarkdown: content.fullContentMarkdown, + fullContentPlain: content.fullContentPlain, ); }); diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index fdc16a15..5f315035 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'4269add3c89250ab30835055e3567a1e6d5c185a'; +String _$tabRepositoryHash() => r'b9caf89fe372b79f37c4a3ebff800ccc2d9289d2'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart b/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart index 67241394..23fe91ba 100644 --- a/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart +++ b/app/lib/features/geckoview/features/browser/domain/entities/sheet.dart @@ -26,3 +26,15 @@ class ViewTabsSheet extends Sheet { @override List get hashParameters => []; } + +class TabQaChatSheet extends Sheet { + final String chatId; + + TabQaChatSheet({required this.chatId}); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [chatId]; +} diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart index fcb9b66a..fc71b993 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart @@ -1,16 +1,24 @@ import 'dart:ui' as ui; +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.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/data/models/web_page_info.dart'; +import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/presentation/widgets/site_search.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/repositories/chat_metadata.dart'; +import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; +import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.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'; @@ -24,12 +32,9 @@ class WebPageDialog extends HookConsumerWidget { final Uri url; final WebPageInfo? precachedInfo; - final void Function()? onDismiss; - const WebPageDialog({ required this.url, this.precachedInfo, - this.onDismiss, super.key, }); @@ -56,21 +61,19 @@ class WebPageDialog extends HookConsumerWidget { useTextEditingController(text: url.toString()); final addressTextFocusNode = useFocusNode(); - return Stack( - children: [ - ModalBarrier( - color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, - onDismiss: onDismiss, - ), - SimpleDialog( - titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), - contentPadding: EdgeInsets.zero, - insetPadding: const EdgeInsets.symmetric( - horizontal: 20.0, - vertical: 24.0, - ), - title: WebsiteTitleTile(url, precachedInfo: precachedInfo), + return MediaQuery.removeViewInsets( + context: context, + removeBottom: true, + child: Dialog( + insetPadding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 64.0), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), + child: WebsiteTitleTile(url, precachedInfo: precachedInfo), + ), SizedBox( //We need this to stretch the dialog, then padding from dialog is applied width: double.maxFinite, @@ -82,6 +85,7 @@ class WebPageDialog extends HookConsumerWidget { controller: addressTextController, focusNode: addressTextFocusNode, enableIMEPersonalizedLearning: !incognitoEnabled, + keyboardType: TextInputType.url, decoration: const InputDecoration( labelText: 'Address', ), @@ -106,136 +110,212 @@ class WebPageDialog extends HookConsumerWidget { .read(tabSessionProvider(tabId: null).notifier) .loadUrl(url: Uri.tryParse(value)!); - onDismiss?.call(); + if (context.mounted) { + context.pop(); + } }, ), ), ), ), + const SizedBox( + height: 4, + ), const Divider(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: availableBangsAsync.when( - data: (availableBangs) { - if (availableBangs.isEmpty) { - return const SizedBox.shrink(); - } + Flexible( + child: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return SingleChildScrollView( + controller: controller, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: availableBangsAsync.when( + data: (availableBangs) { + if (availableBangs.isEmpty) { + return const SizedBox.shrink(); + } - return SiteSearch( - domain: url.host, - availableBangs: availableBangs, + return SiteSearch( + domain: url.host, + availableBangs: availableBangs, + ); + }, + error: (error, stackTrace) => FailureWidget( + title: 'Could not load bangs', + exception: error, + ), + loading: () => SiteSearch( + domain: url.host, + availableBangs: [ + BangData( + websiteName: 'websiteName', + domain: 'domain', + trigger: 'trigger', + urlTemplate: 'urlTemplate', + ), + ], + ), + ), + ), + if (availableBangsAsync.isLoading || + availableBangCount == null || + availableBangCount > 0) + const Divider(), + ListTile( + leading: const Icon(MdiIcons.contentCopy), + title: const Text('Copy address'), + onTap: () async { + await Clipboard.setData( + ClipboardData(text: url.toString()), + ); + if (context.mounted) { + context.pop(); + } + }, + ), + ListTile( + onTap: () async { + await ui_helper.launchUrlFeedback(context, url); + }, + leading: const Icon(Icons.open_in_browser), + title: const Text('Launch External'), + ), + ListTile( + leading: const Icon(MdiIcons.tabPlus), + title: const Text('Clone tab'), + onTap: () async { + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: url); + + if (context.mounted) { + context.pop(); + } + }, + ), + ListTile( + leading: const Icon(Icons.share), + title: const Text('Share link'), + onTap: () async { + await Share.shareUri(url); + + if (context.mounted) { + context.pop(); + } + }, + ), + ListTile( + leading: const Icon(Icons.mobile_screen_share), + title: const Text('Share screenshot'), + onTap: () async { + final screenshot = await ref + .read(selectedTabSessionNotifierProvider) + .requestScreenshot(); + + if (screenshot != null) { + ui.decodeImageFromList( + screenshot, + (result) async { + final png = await result.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (png != null) { + final file = XFile.fromData( + png.buffer.asUint8List(), + mimeType: 'image/png', + ); + + await Share.shareXFiles( + [file], + subject: precachedInfo?.title, + ); + } + }, + ); + } + + if (context.mounted) { + context.pop(); + } + }, + ), + const Divider(), + ListTile( + leading: Icon(KagiTool.assistant.icon), + title: const Text('QA Chat'), + onTap: () async { + final selectedTabId = + ref.read(selectedTabStateProvider)?.id; + + if (selectedTabId != null) { + final updateResult = await ref + .read( + chatMetadataRepositoryProvider( + selectedTabId) + .notifier, + ) + .updateMetadata( + ChatMetadata(mainDocumentId: selectedTabId), + ); + + updateResult.onSuccess((_) { + ref + .read( + bottomSheetControllerProvider.notifier) + .show( + TabQaChatSheet(chatId: selectedTabId)); + }); + } + + if (context.mounted) { + context.pop(); + } + }, + ), + ListTile( + leading: Icon(KagiTool.summarizer.icon), + title: const Text('Summarize'), + onTap: () async { + final summarizerUrl = uri_builder.summarizerUri( + document: SharedUrl(url), + mode: SummarizerMode.keyMoments, + ); + + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: summarizerUrl); + + if (context.mounted) { + context.pop(); + } + }, + ), + ], + ), ); }, - error: (error, stackTrace) => FailureWidget( - title: 'Could not load bangs', - exception: error, - ), - loading: () => SiteSearch( - domain: url.host, - availableBangs: [ - // BangData( - // websiteName: 'websiteName', - // domain: 'domain', - // trigger: 'trigger', - // urlTemplate: 'urlTemplate', - // ), - ], + ), + ), + Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(right: 16.0, bottom: 16.0), + child: TextButton( + onPressed: () { + context.pop(); + }, + child: const Text('Close'), ), ), ), - if (availableBangsAsync.isLoading || - availableBangCount == null || - availableBangCount > 0) - const Divider(), - ListTile( - leading: const Icon(MdiIcons.contentCopy), - title: const Text('Copy address'), - onTap: () async { - await Clipboard.setData( - ClipboardData(text: url.toString()), - ); - onDismiss?.call(); - }, - ), - ListTile( - onTap: () async { - await ui_helper.launchUrlFeedback(context, url); - }, - leading: const Icon(Icons.open_in_browser), - title: const Text('Launch External'), - ), - ListTile( - leading: const Icon(MdiIcons.tabPlus), - title: const Text('Clone tab'), - onTap: () async { - await ref.read(tabRepositoryProvider.notifier).addTab(url: url); - - onDismiss?.call(); - }, - ), - ListTile( - leading: const Icon(Icons.share), - title: const Text('Share link'), - onTap: () async { - await Share.shareUri(url); - - onDismiss?.call(); - }, - ), - ListTile( - leading: const Icon(Icons.mobile_screen_share), - title: const Text('Share screenshot'), - onTap: () async { - final screenshot = await ref - .read(selectedTabSessionNotifierProvider) - .requestScreenshot(); - - if (screenshot != null) { - ui.decodeImageFromList( - screenshot, - (result) async { - final png = await result.toByteData( - format: ui.ImageByteFormat.png, - ); - - if (png != null) { - final file = XFile.fromData( - png.buffer.asUint8List(), - mimeType: 'image/png', - ); - - await Share.shareXFiles( - [file], - subject: precachedInfo?.title, - ); - } - }, - ); - } - - onDismiss?.call(); - }, - ), - const Divider(), - ListTile( - leading: Icon(KagiTool.summarizer.icon), - title: const Text('Summarize'), - onTap: () async { - final summarizerUrl = uri_builder.summarizerUri( - document: SharedUrl(url), - mode: SummarizerMode.keyMoments, - ); - - await ref - .read(tabRepositoryProvider.notifier) - .addTab(url: summarizerUrl); - - onDismiss?.call(); - }, - ), ], ), - ], + ), ); } } diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index fcc8dd5e..0a5682c8 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -5,6 +5,8 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.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/geckoview/domain/controllers/bottom_sheet.dart'; +import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; @@ -17,18 +19,20 @@ import 'package:lensai/features/geckoview/features/browser/domain/services/engin import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart'; -import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart'; -import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart'; import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart'; import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart'; import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/presentation/hooks/draggable_scrollable_controller.dart'; +import 'package:lensai/presentation/hooks/menu_controller.dart'; import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; import 'package:share_plus/share_plus.dart'; @@ -52,7 +56,8 @@ class BrowserScreen extends HookConsumerWidget { final selectedTabId = ref.watch(selectedTabStateProvider.select((value) => value?.id)); - final menuController = useMemoized(() => MenuController()); + final trippleDotMenuController = useMenuController(); + final tabMenuController = useMenuController(); final lastBackButtonPress = useRef(null); @@ -120,24 +125,13 @@ class BrowserScreen extends HookConsumerWidget { return (tabState != null) ? AppBarTitle( tab: tabState, - onTap: () { - ref - .read( - overlayDialogControllerProvider - .notifier, - ) - .show( - WebPageDialog( - url: tabState.url, - precachedInfo: tabState, - onDismiss: ref - .read( - overlayDialogControllerProvider - .notifier, - ) - .dismiss, - ), - ); + onTap: () async { + await context.push( + WebPageRoute( + url: tabState.url.toString(), + ).location, + extra: tabState, + ); }, ) : const SizedBox.shrink(); @@ -261,22 +255,58 @@ class BrowserScreen extends HookConsumerWidget { // ), // ), // ), - TabsActionButton( - isActive: displayedSheet is ViewTabsSheet, - onTap: () { - if (displayedSheet case ViewTabsSheet()) { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - } else { - ref - .read(bottomSheetControllerProvider.notifier) - .show(ViewTabsSheet()); - } + MenuAnchor( + controller: tabMenuController, + builder: (context, controller, child) { + return child!; }, + menuChildren: [ + if (selectedTabId != null) + MenuItemButton( + onPressed: () async { + await ref + .read( + tabRepositoryProvider.notifier, + ) + .closeTab(selectedTabId); + }, + leadingIcon: const Icon(Icons.close), + child: const Text('Close Tab'), + ), + MenuItemButton( + onPressed: () async { + await ref + .read(tabRepositoryProvider.notifier) + .addTab(url: Uri.https('kagi.com')); + }, + leadingIcon: const Icon(Icons.add), + child: const Text('Add Tab'), + ), + ], + child: TabsActionButton( + isActive: displayedSheet is ViewTabsSheet, + onTap: () { + if (displayedSheet case ViewTabsSheet()) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + } else { + ref + .read(bottomSheetControllerProvider.notifier) + .show(ViewTabsSheet()); + } + }, + onLongPress: () { + if (tabMenuController.isOpen) { + tabMenuController.close(); + } else { + tabMenuController.open(); + } + }, + ), ), MenuAnchor( - controller: menuController, + controller: trippleDotMenuController, builder: (context, controller, child) { return Padding( padding: const EdgeInsets.only(right: 4.0), @@ -487,7 +517,7 @@ class BrowserScreen extends HookConsumerWidget { ); await controller.reload(); - menuController.close(); + trippleDotMenuController.close(); }, leadingIcon: const Icon(Icons.refresh), child: const Text('Reload'), @@ -514,7 +544,7 @@ class BrowserScreen extends HookConsumerWidget { ); await controller.goBack(); - menuController.close(); + trippleDotMenuController.close(); }, icon: const Icon(Icons.arrow_back), ) @@ -526,7 +556,7 @@ class BrowserScreen extends HookConsumerWidget { .notifier, ) .closeTab(selectedTabId); - menuController.close(); + trippleDotMenuController.close(); }, icon: const Icon(Icons.close), ), @@ -546,7 +576,7 @@ class BrowserScreen extends HookConsumerWidget { ); await controller.goForward(); - menuController.close(); + trippleDotMenuController.close(); } : null, icon: const Icon(Icons.arrow_forward), @@ -556,6 +586,15 @@ class BrowserScreen extends HookConsumerWidget { ); }, ), + MenuItemButton( + onPressed: () async { + final x = + await context.push(UserAuthRoute().location); + print(x); + }, + leadingIcon: const Icon(Icons.info), + child: const Text('Auth'), + ), ], ), ], @@ -744,25 +783,36 @@ class BrowserScreen extends HookConsumerWidget { return false; }, child: switch (displayedSheet) { - ViewTabsSheet() => DraggableScrollableSheet( - key: ValueKey(displayedSheet), - expand: false, - minChildSize: 0.1, - maxChildSize: _realtiveSafeArea(context), - builder: (context, scrollController) { - return ClipRRect( - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(28), - topRight: Radius.circular(28), - ), - child: ViewTabsSheetWidget( - sheetScrollController: scrollController, - onClose: () { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - }, - ), + ViewTabsSheet() => HookBuilder( + builder: (localContext) { + final draggableScrollableController = + useDraggableScrollableController(); + + return DraggableScrollableSheet( + key: ValueKey(displayedSheet), + controller: draggableScrollableController, + expand: false, + minChildSize: 0.1, + maxChildSize: _realtiveSafeArea(context), + builder: (context, scrollController) { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + child: ViewTabsSheetWidget( + sheetScrollController: scrollController, + draggableScrollableController: + draggableScrollableController, + onClose: () { + ref + .read(bottomSheetControllerProvider + .notifier) + .dismiss(); + }, + ), + ); + }, ); }, ), @@ -794,6 +844,60 @@ class BrowserScreen extends HookConsumerWidget { ); }, ), + final TabQaChatSheet parameter => HookBuilder( + builder: (localContext) { + final draggableScrollableController = + useDraggableScrollableController(); + + return DraggableScrollableSheet( + key: ValueKey(displayedSheet), + controller: draggableScrollableController, + expand: false, + minChildSize: 0.1, + maxChildSize: _realtiveSafeArea(context), + builder: (context, scrollController) { + return ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(28), + topRight: Radius.circular(28), + ), + child: Column( + children: [ + DraggableScrollableHeader( + controller: draggableScrollableController, + child: Material( + child: SizedBox( + width: double.infinity, + child: Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.symmetric( + vertical: 16.0, + ), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: + BorderRadius.circular(2), + ), + ), + ), + ), + ), + ), + Expanded( + child: TabQaChat( + chatId: parameter.chatId, + scrollController: scrollController, + ), + ), + ], + ), + ); + }, + ); + }, + ), }, ) : null, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart index c8f7acc8..8f7c58dd 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart @@ -8,6 +8,7 @@ import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart'; class BrowserView extends StatefulHookConsumerWidget { @@ -87,6 +88,7 @@ class _BrowserViewState extends ConsumerState //Initialize and register dependencies ref.listenManual(tabRepositoryProvider, (previous, next) {}); + ref.listenManual(documentRepositoryProvider, (previous, next) {}); ref.listenManual( selectionActionServiceProvider, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart new file mode 100644 index 00000000..8b5a6113 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart @@ -0,0 +1,32 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class DraggableScrollableHeader extends StatelessWidget { + final DraggableScrollableController controller; + final Widget child; + + const DraggableScrollableHeader({ + super.key, + required this.controller, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: (details) { + // Use the DraggableScrollableSheet's controller + controller.jumpTo( + min( + 1, + controller.pixelsToSize( + controller.pixels - details.delta.dy, + ), + ), + ); + }, + child: child, + ); + } +} diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart index 864edb61..c5235032 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart @@ -79,10 +79,10 @@ class CreateTabSheetWidget extends HookConsumerWidget { TabBar( controller: tabController, tabs: [ - Tab( - icon: Icon(KagiTool.search.icon), - text: 'Search', - ), + // Tab( + // icon: Icon(KagiTool.search.icon), + // text: 'Search', + // ), Tab( icon: Icon(KagiTool.summarizer.icon), text: 'Summarize', @@ -99,10 +99,10 @@ class CreateTabSheetWidget extends HookConsumerWidget { child: ExpandablePageView( controller: pageController, children: [ - SearchTab( - sharedContent: sharedContent, - onSubmit: onSubmit, - ), + // SearchTab( + // sharedContent: sharedContent, + // onSubmit: onSubmit, + // ), SummarizeTab( sharedContent: sharedContent, onSubmit: onSubmit, diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index 3fcc68bb..adfb42a5 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -10,9 +10,10 @@ import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart'; -import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart'; +import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; +import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart'; -import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; +import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; @@ -22,6 +23,8 @@ import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:reorderable_grid/reorderable_grid.dart'; class _Tab extends HookConsumerWidget { + static const headerSize = 124.0; + final VoidCallback onClose; const _Tab({required this.onClose}); @@ -148,40 +151,15 @@ class _Tab extends HookConsumerWidget { } } -class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { - static const headerSize = 124.0; - - final VoidCallback onClose; - - _SliverHeaderDelagate({required this.onClose}); - - @override - Widget build( - BuildContext context, - double shrinkOffset, - bool overlapsContent, - ) { - return _Tab(onClose: onClose); - } - - @override - double get minExtent => headerSize; - - @override - double get maxExtent => headerSize; - - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => - false; -} - class ViewTabsSheetWidget extends HookConsumerWidget { final ScrollController sheetScrollController; + final DraggableScrollableController draggableScrollableController; final VoidCallback onClose; const ViewTabsSheetWidget({ required this.onClose, required this.sheetScrollController, + required this.draggableScrollableController, super.key, }); @@ -209,181 +187,191 @@ class ViewTabsSheetWidget extends HookConsumerWidget { return Stack( alignment: Alignment.bottomRight, children: [ - CustomScrollView( - controller: sheetScrollController, - slivers: [ - SliverPersistentHeader( - pinned: true, - delegate: _SliverHeaderDelagate(onClose: onClose), + Column( + children: [ + DraggableScrollableHeader( + controller: draggableScrollableController, + child: _Tab(onClose: onClose), ), - HookConsumer( - builder: (context, ref, child) { - final container = ref.watch(selectedContainerProvider); + Expanded( + child: CustomScrollView( + controller: sheetScrollController, + slivers: [ + HookConsumer( + builder: (context, ref, child) { + final container = ref.watch(selectedContainerProvider); - final filteredTabs = ref - .watch( - seamlessFilteredTabsProvider(container).select( - (value) => EquatableCollection(value, immutable: true), - ), - ) - .collection; + final filteredTabs = ref + .watch( + seamlessFilteredTabsProvider(container).select( + (value) => + EquatableCollection(value, immutable: true), + ), + ) + .collection; - final activeTab = ref.watch(selectedTabProvider); + final activeTab = ref.watch(selectedTabProvider); - final itemHeight = useMemoized( - () => _calculateItemHeight( - screenWidth: MediaQuery.of(context).size.width, - childAspectRatio: 0.75, - horizontalPadding: 4.0, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: 2, - ), - [MediaQuery.of(context).size.width], - ); + final itemHeight = useMemoized( + () => _calculateItemHeight( + screenWidth: MediaQuery.of(context).size.width, + childAspectRatio: 0.75, + horizontalPadding: 4.0, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: 2, + ), + [MediaQuery.of(context).size.width], + ); - useEffect( - () { - final index = filteredTabs - .indexWhere((webView) => webView == activeTab); + useEffect( + () { + final index = filteredTabs + .indexWhere((webView) => webView == activeTab); - if (index > -1) { - final offset = (index ~/ 2) * itemHeight; + if (index > -1) { + final offset = (index ~/ 2) * itemHeight; - if (offset != sheetScrollController.offset) { - unawaited( - sheetScrollController.animateTo( - offset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - ), - ); - } - } + if (offset != sheetScrollController.offset) { + unawaited( + sheetScrollController.animateTo( + offset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ), + ); + } + } - return null; - }, - [filteredTabs, activeTab], - ); + return null; + }, + [filteredTabs, activeTab], + ); - final tabs = useMemoized( - () { - return filteredTabs - .mapIndexed( - (index, tabId) => - ReorderableGridDelayedDragStartListener( - key: ValueKey(tabId), - index: index, - child: Consumer( - builder: (context, ref, child) { - final tab = ref.watch(tabStateProvider(tabId)); - return (tab != null) - ? TabPreview( - tab: tab, - isActive: tabId == activeTab, - onTap: () async { - if (tabId != activeTab) { - //Close first to avoid rebuilds - onClose(); - await ref - .read( - tabRepositoryProvider - .notifier, - ) - .selectTab(tab.id); - } else { - onClose(); - } - }, - onDoubleTap: () { - ref - .read( - overlayDialogControllerProvider - .notifier, - ) - .show( - TabActionDialog( - initialTab: tab, - onDismiss: ref + final tabs = useMemoized( + () { + return filteredTabs + .mapIndexed( + (index, tabId) => + ReorderableGridDelayedDragStartListener( + key: ValueKey(tabId), + index: index, + child: Consumer( + builder: (context, ref, child) { + final tab = + ref.watch(tabStateProvider(tabId)); + return (tab != null) + ? TabPreview( + tab: tab, + isActive: tabId == activeTab, + onTap: () async { + if (tabId != activeTab) { + //Close first to avoid rebuilds + onClose(); + await ref .read( - overlayDialogControllerProvider + tabRepositoryProvider .notifier, ) - .dismiss, - ), - ); - }, - onDelete: () async { - await ref - .read( - tabRepositoryProvider.notifier, - ) - .closeTab(tab.id); - }, - ) - : const SizedBox.shrink(); - }, - ), + .selectTab(tab.id); + } else { + onClose(); + } + }, + onDoubleTap: () { + ref + .read( + overlayDialogControllerProvider + .notifier, + ) + .show( + TabActionDialog( + initialTab: tab, + onDismiss: ref + .read( + overlayDialogControllerProvider + .notifier, + ) + .dismiss, + ), + ); + }, + onDelete: () async { + await ref + .read( + tabRepositoryProvider + .notifier, + ) + .closeTab(tab.id); + }, + ) + : const SizedBox.shrink(); + }, + ), + ), + ) + .toList(); + }, + [ + EquatableCollection(filteredTabs, immutable: true), + activeTab, + ], + ); + + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + sliver: SliverReorderableGrid( + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + //Sync values for itemHeight calculation _calculateItemHeight + childAspectRatio: 0.75, + mainAxisSpacing: 8.0, + crossAxisSpacing: 8.0, + crossAxisCount: 2, ), - ) - .toList(); - }, - [ - EquatableCollection(filteredTabs, immutable: true), - activeTab - ], - ); + itemCount: tabs.length, + itemBuilder: (context, index) => tabs[index], + onReorder: (oldIndex, newIndex) async { + final containerRepository = + ref.read(containerRepositoryProvider.notifier); - return SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - sliver: SliverReorderableGrid( - gridDelegate: - const SliverGridDelegateWithFixedCrossAxisCount( - //Sync values for itemHeight calculation _calculateItemHeight - childAspectRatio: 0.75, - mainAxisSpacing: 8.0, - crossAxisSpacing: 8.0, - crossAxisCount: 2, - ), - itemCount: tabs.length, - itemBuilder: (context, index) => tabs[index], - onReorder: (oldIndex, newIndex) async { - final containerRepository = - ref.read(containerRepositoryProvider.notifier); + final tabId = filteredTabs[oldIndex]; + final containerId = await ref + .read(tabDataRepositoryProvider.notifier) + .containerTabId(tabId); - final tabId = filteredTabs[oldIndex]; - final containerId = await ref - .read(tabDataRepositoryProvider.notifier) - .containerTabId(tabId); + final String key; + if (newIndex <= 0) { + key = await containerRepository + .getLeadingOrderKey(containerId); + } else if (newIndex >= filteredTabs.length - 1) { + key = await containerRepository + .getTrailingOrderKey(containerId); + } else { + final orderAfterIndex = newIndex; + key = + await containerRepository.getOrderKeyAfterTab( + filteredTabs[orderAfterIndex], + containerId, + ); + } - final String key; - if (newIndex <= 0) { - key = await containerRepository - .getLeadingOrderKey(containerId); - } else if (newIndex >= filteredTabs.length - 1) { - key = await containerRepository - .getTrailingOrderKey(containerId); - } else { - final orderAfterIndex = newIndex; - key = await containerRepository.getOrderKeyAfterTab( - filteredTabs[orderAfterIndex], - containerId, - ); - } - - await ref - .read(tabDataRepositoryProvider.notifier) - .assignOrderKey(tabId, key); + await ref + .read(tabDataRepositoryProvider.notifier) + .assignOrderKey(tabId, key); + }, + ), + ); }, ), - ); - }, + ], + ), ), ], ), Padding( padding: const EdgeInsets.only( - top: _SliverHeaderDelagate.headerSize + 4, + top: _Tab.headerSize + 4, right: 4, ), child: FloatingActionButton.small( diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart index 59431bec..94ea28e5 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart @@ -5,9 +5,11 @@ import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; class TabsActionButton extends HookConsumerWidget { final bool isActive; final VoidCallback onTap; + final VoidCallback onLongPress; const TabsActionButton({ required this.onTap, + required this.onLongPress, this.isActive = false, super.key, }); @@ -20,6 +22,7 @@ class TabsActionButton extends HookConsumerWidget { return InkWell( onTap: onTap, + onLongPress: onLongPress, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0, diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart index ef1e3744..c5587e4f 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; +import 'package:lensai/features/user/domain/repositories/settings.dart'; import 'package:lensai/presentation/widgets/animate_gradient_shader.dart'; class ReaderButton extends HookConsumerWidget { @@ -14,6 +15,10 @@ class ReaderButton extends HookConsumerWidget { final readerChanging = ref.watch(readerableScreenControllerProvider); + final enableReadability = ref.watch( + settingsRepositoryProvider.select((value) => value.enableReadability), + ); + final readerabilityState = ref.watch( selectedTabStateProvider.select( (state) => state?.readerableState ?? ReaderableState.$default(), @@ -34,7 +39,8 @@ class ReaderButton extends HookConsumerWidget { ); return Visibility( - visible: readerabilityState.readerable, + visible: readerabilityState.readerable && + (enableReadability || readerabilityState.active), child: Padding( padding: const EdgeInsets.symmetric( vertical: 15.0, diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart index fbe6922f..302dd869 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart @@ -110,38 +110,25 @@ class TabDao extends DatabaseAccessor with _$TabDaoMixin { ); } - Future updateTab( + Future updateTabContent( String id, { - Value url = const Value.absent(), - Value title = const Value.absent(), - Value isProbablyReaderable = const Value.absent(), - Value extractedContentMarkdown = const Value.absent(), - Value extractedContentPlain = const Value.absent(), - Value fullContentMarkdown = const Value.absent(), - Value fullContentPlain = const Value.absent(), + required bool isProbablyReaderable, + required String? extractedContentMarkdown, + required String? extractedContentPlain, + required String? fullContentMarkdown, + required String? fullContentPlain, }) async { - final doUpdate = url != const Value.absent() || - title != const Value.absent() || - isProbablyReaderable != const Value.absent() || - extractedContentMarkdown != const Value.absent() || - extractedContentPlain != const Value.absent() || - fullContentMarkdown != const Value.absent() || - fullContentPlain != const Value.absent(); + final statement = _updateByIdStatement(id); - if (doUpdate) { - final statement = _updateByIdStatement(id); - await statement.write( - TabCompanion( - url: url, - title: title, - isProbablyReaderable: isProbablyReaderable, - extractedContentMarkdown: extractedContentMarkdown, - extractedContentPlain: extractedContentPlain, - fullContentMarkdown: fullContentMarkdown, - fullContentPlain: fullContentPlain, - ), - ); - } + await statement.write( + TabCompanion( + isProbablyReaderable: Value(isProbablyReaderable), + extractedContentMarkdown: Value(extractedContentMarkdown), + extractedContentPlain: Value(extractedContentPlain), + fullContentMarkdown: Value(fullContentMarkdown), + fullContentPlain: Value(fullContentPlain), + ), + ); } Future updateTabs( @@ -209,7 +196,7 @@ class TabDao extends DatabaseAccessor with _$TabDaoMixin { if (ftsQuery.isNotEmpty) { return db.queryTabsFullContent( - query: db.buildFtsQuery(searchString), + query: ftsQuery, snippetLength: snippetLength, beforeMatch: matchPrefix, afterMatch: matchSuffix, diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.dart b/app/lib/features/geckoview/features/tabs/data/database/database.dart index b8a0a454..76486aff 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.dart @@ -1,5 +1,4 @@ import 'package:drift/drift.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart' show Color, IconData; import 'package:lensai/data/database/converters/color.dart'; import 'package:lensai/data/database/converters/icon_data.dart'; @@ -7,6 +6,9 @@ import 'package:lensai/features/geckoview/features/tabs/data/database/daos/conta import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart'; import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart'; import 'package:lensai/features/search/domain/fts_tokenizer.dart'; import 'package:lensai/features/search/domain/unix_tokenizer.dart'; @@ -15,9 +17,11 @@ part 'database.g.dart'; @DriftDatabase( include: {'database.drift'}, - daos: [ContainerDao, TabDao], + daos: [ContainerDao, TabDao, VectorDao], ) -class TabDatabase extends _$TabDatabase implements IQueryBuilder { +class TabDatabase extends _$TabDatabase with TrigramQueryBuilderMixin { + final int embeddingDimensions; + @override final int schemaVersion = 2; @@ -26,61 +30,25 @@ class TabDatabase extends _$TabDatabase implements IQueryBuilder { @override final int ftsMinTokenLength = 3; - @override - String buildFtsQuery(String input) { - final ftsQueryBuilder = TrigramQueryBuilder.tokenize( - input: input, - minTokenLength: ftsMinTokenLength, - tokenLimit: ftsTokenLimit, - ); - - if (ftsQueryBuilder.hasTokens) { - return ftsQueryBuilder.build(); - } - - return ''; - } - - String buildLikeQuery(String input) { - final likeQueryBuilder = UnixLikeQueryBuilder.tokenize( - input: input, - minTokenLength: 1, - tokenLimit: 5, - ); - - return likeQueryBuilder.build(); - } - @override MigrationStrategy get migration => MigrationStrategy( - onUpgrade: (m, from, to) async { - // disable foreign_keys before migrations - await customStatement('PRAGMA foreign_keys = OFF'); + onCreate: (m) async { + final migrator = + VectorDatabaseMigrator(dimensions: embeddingDimensions); - if (from < 2) { - await transaction(() async { - // await m.dropColumn(tabLink, 'url'); - // await m.dropColumn(tabLink, 'title'); - // await m.dropColumn(tabLink, 'screenshot'); + await m.database.customStatement(migrator.vectorTableDefinition); - // await m.alterTable(TableMigration(tab)); - }); - } - - // Assert that the schema is valid after migrations - if (kDebugMode) { - final wrongForeignKeys = - await customSelect('PRAGMA foreign_key_check').get(); - assert( - wrongForeignKeys.isEmpty, - '${wrongForeignKeys.map((e) => e.data)}', - ); + //instead of m.createAll(); we igoner vec0 table + for (final entity + in allSchemaEntities.where((entity) => entity is! DocumentVec)) { + await m.create(entity); } }, beforeOpen: (details) async { await customStatement('PRAGMA foreign_keys = ON;'); + await optimizeFtsIndex(); }, ); - TabDatabase(super.e); + TabDatabase(super.e, {required this.embeddingDimensions}); } diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.drift b/app/lib/features/geckoview/features/tabs/data/database/database.drift index 02d175ec..bf21dc6e 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.drift +++ b/app/lib/features/geckoview/features/tabs/data/database/database.drift @@ -25,6 +25,14 @@ CREATE TABLE tab ( timestamp DATETIME NOT NULL ); +import '../../features/vector_store/data/database/vector_store.drift'; + +-- automativcally remove documents and embeddings on delete +-- isnert and updates are managed code side +CREATE TRIGGER tab_document_delete AFTER DELETE ON tab BEGIN + DELETE FROM document WHERE main_document_id = old.id; +END; + CREATE VIRTUAL TABLE tab_fts USING fts5( title, @@ -55,6 +63,9 @@ CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN VALUES (new.rowid, new.title, new.url, new.extracted_content_plain, new.full_content_plain); END; +optimizeFtsIndex: + INSERT INTO tab_fts(tab_fts) VALUES ('optimize'); + containersWithCount WITH ContainerDataWithCount: SELECT container.*, diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart index 8357993c..b629bf0b 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart @@ -680,6 +680,304 @@ class TabCompanion extends UpdateCompanion { } } +class Document extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Document(this.attachedDatabase, [this._alias]); + late final GeneratedColumn documentId = GeneratedColumn( + 'document_id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY'); + late final GeneratedColumn mainDocumentId = GeneratedColumn( + 'main_document_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn contextId = GeneratedColumn( + 'context_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn content = GeneratedColumn( + 'content', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn contentHash = GeneratedColumn( + 'content_hash', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn metadata = GeneratedColumn( + 'metadata', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + @override + List get $columns => + [documentId, mainDocumentId, contextId, content, contentHash, metadata]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'document'; + @override + Set get $primaryKey => {documentId}; + @override + DocumentData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DocumentData( + documentId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}document_id'])!, + mainDocumentId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}main_document_id']), + contextId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}context_id']), + content: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content'])!, + contentHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content_hash'])!, + metadata: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}metadata']), + ); + } + + @override + Document createAlias(String alias) { + return Document(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class DocumentData extends DataClass implements Insertable { + final String documentId; + final String? mainDocumentId; + final String? contextId; + final String content; + final String contentHash; + final String? metadata; + const DocumentData( + {required this.documentId, + this.mainDocumentId, + this.contextId, + required this.content, + required this.contentHash, + this.metadata}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['document_id'] = Variable(documentId); + if (!nullToAbsent || mainDocumentId != null) { + map['main_document_id'] = Variable(mainDocumentId); + } + if (!nullToAbsent || contextId != null) { + map['context_id'] = Variable(contextId); + } + map['content'] = Variable(content); + map['content_hash'] = Variable(contentHash); + if (!nullToAbsent || metadata != null) { + map['metadata'] = Variable(metadata); + } + return map; + } + + factory DocumentData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DocumentData( + documentId: serializer.fromJson(json['document_id']), + mainDocumentId: serializer.fromJson(json['main_document_id']), + contextId: serializer.fromJson(json['context_id']), + content: serializer.fromJson(json['content']), + contentHash: serializer.fromJson(json['content_hash']), + metadata: serializer.fromJson(json['metadata']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'document_id': serializer.toJson(documentId), + 'main_document_id': serializer.toJson(mainDocumentId), + 'context_id': serializer.toJson(contextId), + 'content': serializer.toJson(content), + 'content_hash': serializer.toJson(contentHash), + 'metadata': serializer.toJson(metadata), + }; + } + + DocumentData copyWith( + {String? documentId, + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + String? content, + String? contentHash, + Value metadata = const Value.absent()}) => + DocumentData( + documentId: documentId ?? this.documentId, + mainDocumentId: + mainDocumentId.present ? mainDocumentId.value : this.mainDocumentId, + contextId: contextId.present ? contextId.value : this.contextId, + content: content ?? this.content, + contentHash: contentHash ?? this.contentHash, + metadata: metadata.present ? metadata.value : this.metadata, + ); + DocumentData copyWithCompanion(DocumentCompanion data) { + return DocumentData( + documentId: + data.documentId.present ? data.documentId.value : this.documentId, + mainDocumentId: data.mainDocumentId.present + ? data.mainDocumentId.value + : this.mainDocumentId, + contextId: data.contextId.present ? data.contextId.value : this.contextId, + content: data.content.present ? data.content.value : this.content, + contentHash: + data.contentHash.present ? data.contentHash.value : this.contentHash, + metadata: data.metadata.present ? data.metadata.value : this.metadata, + ); + } + + @override + String toString() { + return (StringBuffer('DocumentData(') + ..write('documentId: $documentId, ') + ..write('mainDocumentId: $mainDocumentId, ') + ..write('contextId: $contextId, ') + ..write('content: $content, ') + ..write('contentHash: $contentHash, ') + ..write('metadata: $metadata') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + documentId, mainDocumentId, contextId, content, contentHash, metadata); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DocumentData && + other.documentId == this.documentId && + other.mainDocumentId == this.mainDocumentId && + other.contextId == this.contextId && + other.content == this.content && + other.contentHash == this.contentHash && + other.metadata == this.metadata); +} + +class DocumentCompanion extends UpdateCompanion { + final Value documentId; + final Value mainDocumentId; + final Value contextId; + final Value content; + final Value contentHash; + final Value metadata; + final Value rowid; + const DocumentCompanion({ + this.documentId = const Value.absent(), + this.mainDocumentId = const Value.absent(), + this.contextId = const Value.absent(), + this.content = const Value.absent(), + this.contentHash = const Value.absent(), + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }); + DocumentCompanion.insert({ + required String documentId, + this.mainDocumentId = const Value.absent(), + this.contextId = const Value.absent(), + required String content, + required String contentHash, + this.metadata = const Value.absent(), + this.rowid = const Value.absent(), + }) : documentId = Value(documentId), + content = Value(content), + contentHash = Value(contentHash); + static Insertable custom({ + Expression? documentId, + Expression? mainDocumentId, + Expression? contextId, + Expression? content, + Expression? contentHash, + Expression? metadata, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (documentId != null) 'document_id': documentId, + if (mainDocumentId != null) 'main_document_id': mainDocumentId, + if (contextId != null) 'context_id': contextId, + if (content != null) 'content': content, + if (contentHash != null) 'content_hash': contentHash, + if (metadata != null) 'metadata': metadata, + if (rowid != null) 'rowid': rowid, + }); + } + + DocumentCompanion copyWith( + {Value? documentId, + Value? mainDocumentId, + Value? contextId, + Value? content, + Value? contentHash, + Value? metadata, + Value? rowid}) { + return DocumentCompanion( + documentId: documentId ?? this.documentId, + mainDocumentId: mainDocumentId ?? this.mainDocumentId, + contextId: contextId ?? this.contextId, + content: content ?? this.content, + contentHash: contentHash ?? this.contentHash, + metadata: metadata ?? this.metadata, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (documentId.present) { + map['document_id'] = Variable(documentId.value); + } + if (mainDocumentId.present) { + map['main_document_id'] = Variable(mainDocumentId.value); + } + if (contextId.present) { + map['context_id'] = Variable(contextId.value); + } + if (content.present) { + map['content'] = Variable(content.value); + } + if (contentHash.present) { + map['content_hash'] = Variable(contentHash.value); + } + if (metadata.present) { + map['metadata'] = Variable(metadata.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DocumentCompanion(') + ..write('documentId: $documentId, ') + ..write('mainDocumentId: $mainDocumentId, ') + ..write('contextId: $contextId, ') + ..write('content: $content, ') + ..write('contentHash: $contentHash, ') + ..write('metadata: $metadata, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + class TabFts extends Table with TableInfo, VirtualTableInfo { @override @@ -927,11 +1225,344 @@ class TabFtsCompanion extends UpdateCompanion { } } +class DocumentVec extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + DocumentVec(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL PRIMARY KEY'); + late final GeneratedColumn mainDocumentId = GeneratedColumn( + 'main_document_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn contextId = GeneratedColumn( + 'context_id', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn embedding = GeneratedColumn( + 'embedding', aliasedName, false, + type: DriftSqlType.blob, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn contentHash = GeneratedColumn( + 'content_hash', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + late final GeneratedColumn distance = GeneratedColumn( + 'distance', aliasedName, true, + type: DriftSqlType.double, + requiredDuringInsert: false, + $customConstraints: ''); + late final GeneratedColumn k = GeneratedColumn( + 'k', aliasedName, true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: ''); + @override + List get $columns => + [id, mainDocumentId, contextId, embedding, contentHash, distance, k]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'document_vec'; + @override + Set get $primaryKey => {id}; + @override + DocumentVecData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DocumentVecData( + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + mainDocumentId: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}main_document_id']), + contextId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}context_id']), + embedding: attachedDatabase.typeMapping + .read(DriftSqlType.blob, data['${effectivePrefix}embedding'])!, + contentHash: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}content_hash'])!, + distance: attachedDatabase.typeMapping + .read(DriftSqlType.double, data['${effectivePrefix}distance']), + k: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}k']), + ); + } + + @override + DocumentVec createAlias(String alias) { + return DocumentVec(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class DocumentVecData extends DataClass implements Insertable { + /// dummy definition of virtual table + /// keep in sync with defined table definition + final String id; + final String? mainDocumentId; + final String? contextId; + final Uint8List embedding; + final String contentHash; + final double? distance; + final int? k; + const DocumentVecData( + {required this.id, + this.mainDocumentId, + this.contextId, + required this.embedding, + required this.contentHash, + this.distance, + this.k}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + if (!nullToAbsent || mainDocumentId != null) { + map['main_document_id'] = Variable(mainDocumentId); + } + if (!nullToAbsent || contextId != null) { + map['context_id'] = Variable(contextId); + } + map['embedding'] = Variable(embedding); + map['content_hash'] = Variable(contentHash); + if (!nullToAbsent || distance != null) { + map['distance'] = Variable(distance); + } + if (!nullToAbsent || k != null) { + map['k'] = Variable(k); + } + return map; + } + + factory DocumentVecData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DocumentVecData( + id: serializer.fromJson(json['id']), + mainDocumentId: serializer.fromJson(json['main_document_id']), + contextId: serializer.fromJson(json['context_id']), + embedding: serializer.fromJson(json['embedding']), + contentHash: serializer.fromJson(json['content_hash']), + distance: serializer.fromJson(json['distance']), + k: serializer.fromJson(json['k']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'main_document_id': serializer.toJson(mainDocumentId), + 'context_id': serializer.toJson(contextId), + 'embedding': serializer.toJson(embedding), + 'content_hash': serializer.toJson(contentHash), + 'distance': serializer.toJson(distance), + 'k': serializer.toJson(k), + }; + } + + DocumentVecData copyWith( + {String? id, + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + Uint8List? embedding, + String? contentHash, + Value distance = const Value.absent(), + Value k = const Value.absent()}) => + DocumentVecData( + id: id ?? this.id, + mainDocumentId: + mainDocumentId.present ? mainDocumentId.value : this.mainDocumentId, + contextId: contextId.present ? contextId.value : this.contextId, + embedding: embedding ?? this.embedding, + contentHash: contentHash ?? this.contentHash, + distance: distance.present ? distance.value : this.distance, + k: k.present ? k.value : this.k, + ); + DocumentVecData copyWithCompanion(DocumentVecCompanion data) { + return DocumentVecData( + id: data.id.present ? data.id.value : this.id, + mainDocumentId: data.mainDocumentId.present + ? data.mainDocumentId.value + : this.mainDocumentId, + contextId: data.contextId.present ? data.contextId.value : this.contextId, + embedding: data.embedding.present ? data.embedding.value : this.embedding, + contentHash: + data.contentHash.present ? data.contentHash.value : this.contentHash, + distance: data.distance.present ? data.distance.value : this.distance, + k: data.k.present ? data.k.value : this.k, + ); + } + + @override + String toString() { + return (StringBuffer('DocumentVecData(') + ..write('id: $id, ') + ..write('mainDocumentId: $mainDocumentId, ') + ..write('contextId: $contextId, ') + ..write('embedding: $embedding, ') + ..write('contentHash: $contentHash, ') + ..write('distance: $distance, ') + ..write('k: $k') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, mainDocumentId, contextId, + $driftBlobEquality.hash(embedding), contentHash, distance, k); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DocumentVecData && + other.id == this.id && + other.mainDocumentId == this.mainDocumentId && + other.contextId == this.contextId && + $driftBlobEquality.equals(other.embedding, this.embedding) && + other.contentHash == this.contentHash && + other.distance == this.distance && + other.k == this.k); +} + +class DocumentVecCompanion extends UpdateCompanion { + final Value id; + final Value mainDocumentId; + final Value contextId; + final Value embedding; + final Value contentHash; + final Value distance; + final Value k; + final Value rowid; + const DocumentVecCompanion({ + this.id = const Value.absent(), + this.mainDocumentId = const Value.absent(), + this.contextId = const Value.absent(), + this.embedding = const Value.absent(), + this.contentHash = const Value.absent(), + this.distance = const Value.absent(), + this.k = const Value.absent(), + this.rowid = const Value.absent(), + }); + DocumentVecCompanion.insert({ + required String id, + this.mainDocumentId = const Value.absent(), + this.contextId = const Value.absent(), + required Uint8List embedding, + required String contentHash, + this.distance = const Value.absent(), + this.k = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + embedding = Value(embedding), + contentHash = Value(contentHash); + static Insertable custom({ + Expression? id, + Expression? mainDocumentId, + Expression? contextId, + Expression? embedding, + Expression? contentHash, + Expression? distance, + Expression? k, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (mainDocumentId != null) 'main_document_id': mainDocumentId, + if (contextId != null) 'context_id': contextId, + if (embedding != null) 'embedding': embedding, + if (contentHash != null) 'content_hash': contentHash, + if (distance != null) 'distance': distance, + if (k != null) 'k': k, + if (rowid != null) 'rowid': rowid, + }); + } + + DocumentVecCompanion copyWith( + {Value? id, + Value? mainDocumentId, + Value? contextId, + Value? embedding, + Value? contentHash, + Value? distance, + Value? k, + Value? rowid}) { + return DocumentVecCompanion( + id: id ?? this.id, + mainDocumentId: mainDocumentId ?? this.mainDocumentId, + contextId: contextId ?? this.contextId, + embedding: embedding ?? this.embedding, + contentHash: contentHash ?? this.contentHash, + distance: distance ?? this.distance, + k: k ?? this.k, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (mainDocumentId.present) { + map['main_document_id'] = Variable(mainDocumentId.value); + } + if (contextId.present) { + map['context_id'] = Variable(contextId.value); + } + if (embedding.present) { + map['embedding'] = Variable(embedding.value); + } + if (contentHash.present) { + map['content_hash'] = Variable(contentHash.value); + } + if (distance.present) { + map['distance'] = Variable(distance.value); + } + if (k.present) { + map['k'] = Variable(k.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DocumentVecCompanion(') + ..write('id: $id, ') + ..write('mainDocumentId: $mainDocumentId, ') + ..write('contextId: $contextId, ') + ..write('embedding: $embedding, ') + ..write('contentHash: $contentHash, ') + ..write('distance: $distance, ') + ..write('k: $k, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$TabDatabase extends GeneratedDatabase { _$TabDatabase(QueryExecutor e) : super(e); $TabDatabaseManager get managers => $TabDatabaseManager(this); late final Container container = Container(this); late final Tab tab = Tab(this); + late final Document document = Document(this); + late final Trigger tabDocumentDelete = Trigger( + 'CREATE TRIGGER tab_document_delete AFTER DELETE ON tab BEGIN DELETE FROM document WHERE main_document_id = old.id;END', + 'tab_document_delete'); late final TabFts tabFts = TabFts(this); late final Trigger tabAfterInsert = Trigger( 'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', @@ -942,8 +1573,28 @@ abstract class _$TabDatabase extends GeneratedDatabase { late final Trigger tabAfterUpdate = Trigger( 'CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END', 'tab_after_update'); + late final Index documentMainDocumentId = Index('document_main_document_id', + 'CREATE INDEX document_main_document_id ON document (main_document_id)'); + late final Index documentContextId = Index('document_context_id', + 'CREATE INDEX document_context_id ON document (context_id)'); + late final DocumentVec documentVec = DocumentVec(this); + late final Trigger documentDelete = Trigger( + 'CREATE TRIGGER document_delete AFTER DELETE ON document BEGIN DELETE FROM document_vec WHERE id = old.document_id;END', + 'document_delete'); + late final Trigger documentUpdateDelete = Trigger( + 'CREATE TRIGGER document_update_delete AFTER UPDATE ON document BEGIN DELETE FROM document_vec WHERE id = new.document_id AND content_hash != new.content_hash;END', + 'document_update_delete'); late final ContainerDao containerDao = ContainerDao(this as TabDatabase); late final TabDao tabDao = TabDao(this as TabDatabase); + late final VectorDao vectorDao = VectorDao(this as TabDatabase); + Future optimizeFtsIndex() { + return customInsert( + 'INSERT INTO tab_fts (tab_fts) VALUES (\'optimize\')', + variables: [], + updates: {tabFts}, + ); + } + Selectable containersWithCount() { return customSelect( 'SELECT container.*, tab_agg.tab_count FROM container LEFT JOIN (SELECT container_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY container_id) AS tab_agg ON container.id = tab_agg.container_id ORDER BY tab_agg.last_updated DESC NULLS FIRST', @@ -1017,8 +1668,8 @@ abstract class _$TabDatabase extends GeneratedDatabase { tabFts, }).map((QueryRow row) => TabQueryResult( id: row.read('id'), - title: row.read('title'), - url: row.read('url'), + title: row.readNullable('title'), + url: row.readNullable('url'), weightedRank: row.read('weighted_rank'), )); } @@ -1043,20 +1694,73 @@ abstract class _$TabDatabase extends GeneratedDatabase { tabFts, }).map((QueryRow row) => TabQueryResult( id: row.read('id'), - title: row.read('title'), - url: row.read('url'), - extractedContent: row.read('extracted_content'), - fullContent: row.read('full_content'), + title: row.readNullable('title'), + url: row.readNullable('url'), + extractedContent: row.readNullable('extracted_content'), + fullContent: row.readNullable('full_content'), weightedRank: row.read('weighted_rank'), )); } + Selectable missingDocumentEmbeddings( + {String? mainDocumentId, String? contextId}) { + return customSelect( + 'SELECT doc.* FROM document AS doc WHERE doc.main_document_id IS COALESCE(?1, doc.main_document_id) AND doc.context_id IS COALESCE(?2, doc.context_id) AND NOT EXISTS (SELECT 1 AS _c0 FROM document_vec AS vec WHERE vec.id = doc.document_id)', + variables: [ + Variable(mainDocumentId), + Variable(contextId) + ], + readsFrom: { + document, + documentVec, + }).asyncMap(document.mapFromRow); + } + + Selectable queryVectors( + {required Uint8List searchVectors, + int? k, + String? mainDocumentId, + String? contextId}) { + return customSelect( + 'SELECT vec.id, doc.main_document_id, doc.context_id, doc.content, doc.metadata, vec.distance FROM document_vec AS vec INNER JOIN document AS doc ON doc.document_id = vec.id WHERE vec.embedding MATCH ?1 AND vec.k = ?2 AND vec.main_document_id IS COALESCE(?3, vec.main_document_id) AND vec.context_id IS COALESCE(?4, vec.context_id) ORDER BY vec.distance', + variables: [ + Variable(searchVectors), + Variable(k), + Variable(mainDocumentId), + Variable(contextId) + ], + readsFrom: { + documentVec, + document, + }).map((QueryRow row) => VectorResult( + id: row.read('id'), + mainDocumentId: row.readNullable('main_document_id'), + contextId: row.readNullable('context_id'), + content: row.read('content'), + metadata: row.readNullable('metadata'), + distance: row.readNullable('distance'), + )); + } + @override Iterable> get allTables => allSchemaEntities.whereType>(); @override - List get allSchemaEntities => - [container, tab, tabFts, tabAfterInsert, tabAfterDelete, tabAfterUpdate]; + List get allSchemaEntities => [ + container, + tab, + document, + tabDocumentDelete, + tabFts, + tabAfterInsert, + tabAfterDelete, + tabAfterUpdate, + documentMainDocumentId, + documentContextId, + documentVec, + documentDelete, + documentUpdateDelete + ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( [ @@ -1067,6 +1771,13 @@ abstract class _$TabDatabase extends GeneratedDatabase { TableUpdate('tab', kind: UpdateKind.delete), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('tab', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('document', kind: UpdateKind.delete), + ], + ), WritePropagation( on: TableUpdateQuery.onTableName('tab', limitUpdateKind: UpdateKind.insert), @@ -1088,6 +1799,20 @@ abstract class _$TabDatabase extends GeneratedDatabase { TableUpdate('tab_fts', kind: UpdateKind.insert), ], ), + WritePropagation( + on: TableUpdateQuery.onTableName('document', + limitUpdateKind: UpdateKind.delete), + result: [ + TableUpdate('document_vec', kind: UpdateKind.delete), + ], + ), + WritePropagation( + on: TableUpdateQuery.onTableName('document', + limitUpdateKind: UpdateKind.update), + result: [ + TableUpdate('document_vec', kind: UpdateKind.delete), + ], + ), ], ); } @@ -1120,7 +1845,7 @@ final class $ContainerReferences $TabProcessedTableManager get tabRefs { final manager = $TabTableManager($_db, $_db.tab) - .filter((f) => f.containerId.id($_item.id)); + .filter((f) => f.containerId.id.sqlEquals($_itemColumn('id')!)); final cache = $_typedResult.readTableOrNull(_tabRefsTable($_db)); return ProcessedTableManager( @@ -1377,9 +2102,10 @@ final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> { .createAlias($_aliasNameGenerator(db.tab.containerId, db.container.id)); $ContainerProcessedTableManager? get containerId { - if ($_item.containerId == null) return null; + final $_column = $_itemColumn('container_id'); + if ($_column == null) return null; final manager = $ContainerTableManager($_db, $_db.container) - .filter((f) => f.id($_item.containerId!)); + .filter((f) => f.id.sqlEquals($_column)); final item = $_typedResult.readTableOrNull(_containerIdTable($_db)); if (item == null) return manager; return ProcessedTableManager( @@ -1703,6 +2429,185 @@ typedef $TabProcessedTableManager = ProcessedTableManager< (TabData, $TabReferences), TabData, PrefetchHooks Function({bool containerId})>; +typedef $DocumentCreateCompanionBuilder = DocumentCompanion Function({ + required String documentId, + Value mainDocumentId, + Value contextId, + required String content, + required String contentHash, + Value metadata, + Value rowid, +}); +typedef $DocumentUpdateCompanionBuilder = DocumentCompanion Function({ + Value documentId, + Value mainDocumentId, + Value contextId, + Value content, + Value contentHash, + Value metadata, + Value rowid, +}); + +class $DocumentFilterComposer extends Composer<_$TabDatabase, Document> { + $DocumentFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get documentId => $composableBuilder( + column: $table.documentId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get contextId => $composableBuilder( + column: $table.contextId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => ColumnFilters(column)); + + ColumnFilters get metadata => $composableBuilder( + column: $table.metadata, builder: (column) => ColumnFilters(column)); +} + +class $DocumentOrderingComposer extends Composer<_$TabDatabase, Document> { + $DocumentOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get documentId => $composableBuilder( + column: $table.documentId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contextId => $composableBuilder( + column: $table.contextId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get metadata => $composableBuilder( + column: $table.metadata, builder: (column) => ColumnOrderings(column)); +} + +class $DocumentAnnotationComposer extends Composer<_$TabDatabase, Document> { + $DocumentAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get documentId => $composableBuilder( + column: $table.documentId, builder: (column) => column); + + GeneratedColumn get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, builder: (column) => column); + + GeneratedColumn get contextId => + $composableBuilder(column: $table.contextId, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => column); + + GeneratedColumn get metadata => + $composableBuilder(column: $table.metadata, builder: (column) => column); +} + +class $DocumentTableManager extends RootTableManager< + _$TabDatabase, + Document, + DocumentData, + $DocumentFilterComposer, + $DocumentOrderingComposer, + $DocumentAnnotationComposer, + $DocumentCreateCompanionBuilder, + $DocumentUpdateCompanionBuilder, + (DocumentData, BaseReferences<_$TabDatabase, Document, DocumentData>), + DocumentData, + PrefetchHooks Function()> { + $DocumentTableManager(_$TabDatabase db, Document table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $DocumentFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $DocumentOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $DocumentAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value documentId = const Value.absent(), + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + Value content = const Value.absent(), + Value contentHash = const Value.absent(), + Value metadata = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DocumentCompanion( + documentId: documentId, + mainDocumentId: mainDocumentId, + contextId: contextId, + content: content, + contentHash: contentHash, + metadata: metadata, + rowid: rowid, + ), + createCompanionCallback: ({ + required String documentId, + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + required String content, + required String contentHash, + Value metadata = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DocumentCompanion.insert( + documentId: documentId, + mainDocumentId: mainDocumentId, + contextId: contextId, + content: content, + contentHash: contentHash, + metadata: metadata, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $DocumentProcessedTableManager = ProcessedTableManager< + _$TabDatabase, + Document, + DocumentData, + $DocumentFilterComposer, + $DocumentOrderingComposer, + $DocumentAnnotationComposer, + $DocumentCreateCompanionBuilder, + $DocumentUpdateCompanionBuilder, + (DocumentData, BaseReferences<_$TabDatabase, Document, DocumentData>), + DocumentData, + PrefetchHooks Function()>; typedef $TabFtsCreateCompanionBuilder = TabFtsCompanion Function({ required String title, required String url, @@ -1854,6 +2759,208 @@ typedef $TabFtsProcessedTableManager = ProcessedTableManager< (TabFt, BaseReferences<_$TabDatabase, TabFts, TabFt>), TabFt, PrefetchHooks Function()>; +typedef $DocumentVecCreateCompanionBuilder = DocumentVecCompanion Function({ + required String id, + Value mainDocumentId, + Value contextId, + required Uint8List embedding, + required String contentHash, + Value distance, + Value k, + Value rowid, +}); +typedef $DocumentVecUpdateCompanionBuilder = DocumentVecCompanion Function({ + Value id, + Value mainDocumentId, + Value contextId, + Value embedding, + Value contentHash, + Value distance, + Value k, + Value rowid, +}); + +class $DocumentVecFilterComposer extends Composer<_$TabDatabase, DocumentVec> { + $DocumentVecFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get contextId => $composableBuilder( + column: $table.contextId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get embedding => $composableBuilder( + column: $table.embedding, builder: (column) => ColumnFilters(column)); + + ColumnFilters get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => ColumnFilters(column)); + + ColumnFilters get distance => $composableBuilder( + column: $table.distance, builder: (column) => ColumnFilters(column)); + + ColumnFilters get k => $composableBuilder( + column: $table.k, builder: (column) => ColumnFilters(column)); +} + +class $DocumentVecOrderingComposer + extends Composer<_$TabDatabase, DocumentVec> { + $DocumentVecOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contextId => $composableBuilder( + column: $table.contextId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get embedding => $composableBuilder( + column: $table.embedding, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get distance => $composableBuilder( + column: $table.distance, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get k => $composableBuilder( + column: $table.k, builder: (column) => ColumnOrderings(column)); +} + +class $DocumentVecAnnotationComposer + extends Composer<_$TabDatabase, DocumentVec> { + $DocumentVecAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get mainDocumentId => $composableBuilder( + column: $table.mainDocumentId, builder: (column) => column); + + GeneratedColumn get contextId => + $composableBuilder(column: $table.contextId, builder: (column) => column); + + GeneratedColumn get embedding => + $composableBuilder(column: $table.embedding, builder: (column) => column); + + GeneratedColumn get contentHash => $composableBuilder( + column: $table.contentHash, builder: (column) => column); + + GeneratedColumn get distance => + $composableBuilder(column: $table.distance, builder: (column) => column); + + GeneratedColumn get k => + $composableBuilder(column: $table.k, builder: (column) => column); +} + +class $DocumentVecTableManager extends RootTableManager< + _$TabDatabase, + DocumentVec, + DocumentVecData, + $DocumentVecFilterComposer, + $DocumentVecOrderingComposer, + $DocumentVecAnnotationComposer, + $DocumentVecCreateCompanionBuilder, + $DocumentVecUpdateCompanionBuilder, + ( + DocumentVecData, + BaseReferences<_$TabDatabase, DocumentVec, DocumentVecData> + ), + DocumentVecData, + PrefetchHooks Function()> { + $DocumentVecTableManager(_$TabDatabase db, DocumentVec table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $DocumentVecFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $DocumentVecOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $DocumentVecAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + Value embedding = const Value.absent(), + Value contentHash = const Value.absent(), + Value distance = const Value.absent(), + Value k = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DocumentVecCompanion( + id: id, + mainDocumentId: mainDocumentId, + contextId: contextId, + embedding: embedding, + contentHash: contentHash, + distance: distance, + k: k, + rowid: rowid, + ), + createCompanionCallback: ({ + required String id, + Value mainDocumentId = const Value.absent(), + Value contextId = const Value.absent(), + required Uint8List embedding, + required String contentHash, + Value distance = const Value.absent(), + Value k = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DocumentVecCompanion.insert( + id: id, + mainDocumentId: mainDocumentId, + contextId: contextId, + embedding: embedding, + contentHash: contentHash, + distance: distance, + k: k, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $DocumentVecProcessedTableManager = ProcessedTableManager< + _$TabDatabase, + DocumentVec, + DocumentVecData, + $DocumentVecFilterComposer, + $DocumentVecOrderingComposer, + $DocumentVecAnnotationComposer, + $DocumentVecCreateCompanionBuilder, + $DocumentVecUpdateCompanionBuilder, + ( + DocumentVecData, + BaseReferences<_$TabDatabase, DocumentVec, DocumentVecData> + ), + DocumentVecData, + PrefetchHooks Function()>; class $TabDatabaseManager { final _$TabDatabase _db; @@ -1861,5 +2968,9 @@ class $TabDatabaseManager { $ContainerTableManager get container => $ContainerTableManager(_db, _db.container); $TabTableManager get tab => $TabTableManager(_db, _db.tab); + $DocumentTableManager get document => + $DocumentTableManager(_db, _db.document); $TabFtsTableManager get tabFts => $TabFtsTableManager(_db, _db.tabFts); + $DocumentVecTableManager get documentVec => + $DocumentVecTableManager(_db, _db.documentVec); } diff --git a/app/lib/features/geckoview/features/tabs/data/models/tab_query_result.dart b/app/lib/features/geckoview/features/tabs/data/models/tab_query_result.dart index a23dd50f..d874a1f0 100644 --- a/app/lib/features/geckoview/features/tabs/data/models/tab_query_result.dart +++ b/app/lib/features/geckoview/features/tabs/data/models/tab_query_result.dart @@ -1,8 +1,8 @@ class TabQueryResult { final String id; - final String title; - final String url; + final String? title; + final String? url; final String? extractedContent; final String? fullContent; diff --git a/app/lib/features/geckoview/features/tabs/data/providers.dart b/app/lib/features/geckoview/features/tabs/data/providers.dart index 23d9df83..6487da28 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; +import 'package:lensai/core/models.dart'; import 'package:lensai/data/database/functions/lexo_rank_functions.dart'; import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; import 'package:path/path.dart' as p; @@ -8,13 +11,16 @@ import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; +import 'package:sqlite3_vec/sqlite3_vec.dart'; import 'package:universal_io/io.dart'; part 'providers.g.dart'; @Riverpod(keepAlive: true) TabDatabase tabDatabase(Ref ref) { - return TabDatabase( + final dimensions = ref.watch(embeddingDimensionsProvider); + + final db = TabDatabase( LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder // for your app. @@ -33,6 +39,8 @@ TabDatabase tabDatabase(Ref ref) { // Explicitly tell it about the correct temporary directory. sqlite3.tempDirectory = cachebase; + Sqlite3Vec.ensureExtensionLoaded(); + return NativeDatabase.createInBackground( file, setup: (database) { @@ -40,5 +48,12 @@ TabDatabase tabDatabase(Ref ref) { }, ); }), + embeddingDimensions: dimensions, ); + + ref.onDispose(() { + unawaited(db.close()); + }); + + return db; } diff --git a/app/lib/features/geckoview/features/tabs/data/providers.g.dart b/app/lib/features/geckoview/features/tabs/data/providers.g.dart index 62adeebc..1e3c0309 100644 --- a/app/lib/features/geckoview/features/tabs/data/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDatabaseHash() => r'1379e222fe4d43c119a89ec89a779ef35fbe1563'; +String _$tabDatabaseHash() => r'940dd2a1f1df6f2a0b77a1a3e9b8318550b832d2'; /// See also [tabDatabase]. @ProviderFor(tabDatabase) diff --git a/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.dart b/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.dart new file mode 100644 index 00000000..bb9843a6 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.dart @@ -0,0 +1,84 @@ +import 'dart:async'; + +import 'package:exceptions/exceptions.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/providers.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/repositories/chat_message.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'chat_backend.g.dart'; + +@Riverpod() +class ChatBackend extends _$ChatBackend { + late ChatMetadata? _metadata; + + late ChatMessageRepository _chatRepository; + + late QAMemoryChain _qaMemoryChain; + + Future? _embeddingsUpdate; + Future prepareEmbeddings() { + if (_embeddingsUpdate != null) { + return _embeddingsUpdate!; + } + + _embeddingsUpdate = + ref.read(documentRepositoryProvider.notifier).updateEmbeddings( + mainDocumentId: _metadata?.mainDocumentId, + contextId: _metadata?.contextId, + ); + + _embeddingsUpdate!.whenComplete(() => _embeddingsUpdate = null); + + return _embeddingsUpdate!; + } + + Future> processQAMessage(String input) async { + //TODO: fix result mess + + final humanMessageResult = await _chatRepository.insertTextMessage( + author: MessageAuthor.human, + content: input, + ); + + await prepareEmbeddings(); + + return humanMessageResult.flatMapAsync((humanMessage) async { + await _chatRepository.setTyping( + author: MessageAuthor.ai, + typing: true, + ); + + final result = await _qaMemoryChain.processQuestion(humanMessage.text); + await _chatRepository.setTyping( + author: MessageAuthor.ai, + typing: false, + ); + + await _chatRepository.insertTextMessage( + author: MessageAuthor.ai, + content: result.answer, + ); + }); + } + + @override + void build(String chatId) { + _metadata = ref.watch( + chatMetadataProvider(chatId).select((value) => value.valueOrNull), + ); + + _chatRepository = ref.watch(chatMessageRepositoryProvider(chatId).notifier); + + _qaMemoryChain = ref.watch( + qAMemoryChainProvider( + chatId: chatId, + mainDocumentId: _metadata?.mainDocumentId, + contextId: _metadata?.contextId, + ).notifier, + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.g.dart b/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.g.dart new file mode 100644 index 00000000..e52ffd6c --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/domain/chat_backend.g.dart @@ -0,0 +1,174 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'chat_backend.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$chatBackendHash() => r'befdad9878bf0a1a8a196af5e3006978026eb8ba'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChatBackend extends BuildlessAutoDisposeNotifier { + late final String chatId; + + void build( + String chatId, + ); +} + +/// See also [ChatBackend]. +@ProviderFor(ChatBackend) +const chatBackendProvider = ChatBackendFamily(); + +/// See also [ChatBackend]. +class ChatBackendFamily extends Family { + /// See also [ChatBackend]. + const ChatBackendFamily(); + + /// See also [ChatBackend]. + ChatBackendProvider call( + String chatId, + ) { + return ChatBackendProvider( + chatId, + ); + } + + @override + ChatBackendProvider getProviderOverride( + covariant ChatBackendProvider provider, + ) { + return call( + provider.chatId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'chatBackendProvider'; +} + +/// See also [ChatBackend]. +class ChatBackendProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [ChatBackend]. + ChatBackendProvider( + String chatId, + ) : this._internal( + () => ChatBackend()..chatId = chatId, + from: chatBackendProvider, + name: r'chatBackendProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$chatBackendHash, + dependencies: ChatBackendFamily._dependencies, + allTransitiveDependencies: + ChatBackendFamily._allTransitiveDependencies, + chatId: chatId, + ); + + ChatBackendProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + }) : super.internal(); + + final String chatId; + + @override + void runNotifierBuild( + covariant ChatBackend notifier, + ) { + return notifier.build( + chatId, + ); + } + + @override + Override overrideWith(ChatBackend Function() create) { + return ProviderOverride( + origin: this, + override: ChatBackendProvider._internal( + () => create()..chatId = chatId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _ChatBackendProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is ChatBackendProvider && other.chatId == chatId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin ChatBackendRef on AutoDisposeNotifierProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; +} + +class _ChatBackendProviderElement + extends AutoDisposeNotifierProviderElement + with ChatBackendRef { + _ChatBackendProviderElement(super.provider); + + @override + String get chatId => (origin as ChatBackendProvider).chatId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/chat_text_message.dart b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/chat_text_message.dart new file mode 100644 index 00000000..83779728 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/chat_text_message.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:provider/provider.dart'; + +class ChatTextMessage extends StatelessWidget { + final TextMessage message; + final int index; + final EdgeInsetsGeometry? padding; + final BorderRadiusGeometry? borderRadius; + final double? onlyEmojiFontSize; + + const ChatTextMessage({ + super.key, + required this.message, + required this.index, + this.padding = const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + this.borderRadius = const BorderRadius.all(Radius.circular(12)), + this.onlyEmojiFontSize = 48, + }); + + @override + Widget build(BuildContext context) { + final textMessageTheme = + context.select((ChatTheme theme) => theme.textMessageTheme); + final isSentByMe = context.watch() == message.authorId; + final paragraphStyle = isSentByMe + ? textMessageTheme.sentTextStyle + : textMessageTheme.receivedTextStyle; + + return Container( + padding: padding, + decoration: message.isOnlyEmoji == true + ? null + : BoxDecoration( + color: isSentByMe + ? textMessageTheme.sentBackgroundColor + : textMessageTheme.receivedBackgroundColor, + borderRadius: borderRadius, + ), + child: MarkdownBody( + data: message.text, + selectable: true, + styleSheet: MarkdownStyleSheet( + p: message.isOnlyEmoji == true + ? paragraphStyle?.copyWith(fontSize: onlyEmojiFontSize) + : paragraphStyle, + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/qa_chat_input.dart b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/qa_chat_input.dart new file mode 100644 index 00000000..22c719bf --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/qa_chat_input.dart @@ -0,0 +1,183 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; +import 'package:provider/provider.dart'; + +typedef OnMessageTapCallback = void Function(Message message); +typedef OnMessageSendCallback = void Function(String text); +typedef OnAttachmentTapCallback = VoidCallback; + +class QaChatInput extends StatefulWidget { + final double? left; + final double? right; + final double? top; + final double? bottom; + final double? sigmaX; + final double? sigmaY; + final EdgeInsetsGeometry? padding; + final Widget? attachmentIcon; + final Widget? sendIcon; + final double? gap; + final InputBorder? inputBorder; + final bool? filled; + final Widget? topWidget; + final bool? handleSafeArea; + + const QaChatInput({ + super.key, + this.left = 0, + this.right = 0, + this.top, + this.bottom = 0, + this.sigmaX = 20, + this.sigmaY = 20, + this.padding = const EdgeInsets.all(8.0), + this.attachmentIcon = const Icon(Icons.attachment), + this.sendIcon = const Icon(Icons.send), + this.gap = 8, + this.inputBorder = const OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.all(Radius.circular(24)), + ), + this.filled = true, + this.topWidget, + this.handleSafeArea = true, + }); + + @override + State createState() => _QaChatInputState(); +} + +class _QaChatInputState extends State { + final GlobalKey _inputKey = GlobalKey(); + final TextEditingController _textController = TextEditingController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateInputHeight()); + } + + @override + void didUpdateWidget(covariant QaChatInput oldWidget) { + super.didUpdateWidget(oldWidget); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateInputHeight()); + } + + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final bottomSafeArea = widget.handleSafeArea == true + ? MediaQuery.of(context).padding.bottom + : 0.0; + final inputTheme = context.select((ChatTheme theme) => theme.inputTheme); + final onAttachmentTap = context.read(); + + return Positioned( + left: widget.left, + right: widget.right, + top: widget.top, + bottom: widget.bottom, + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + // TODO: remove backdrop filter if both are 0 + sigmaX: widget.sigmaX ?? 0, + sigmaY: widget.sigmaY ?? 0, + ), + child: Container( + key: _inputKey, + color: inputTheme.backgroundColor, + child: Column( + children: [ + if (widget.topWidget != null) widget.topWidget!, + Padding( + padding: widget.handleSafeArea == true + ? (widget.padding + ?.add(EdgeInsets.only(bottom: bottomSafeArea)) ?? + EdgeInsets.only(bottom: bottomSafeArea)) + : (widget.padding ?? EdgeInsets.zero), + child: Row( + children: [ + if (widget.attachmentIcon != null) + IconButton( + icon: widget.attachmentIcon!, + color: inputTheme.hintStyle?.color, + onPressed: onAttachmentTap, + ) + else + const SizedBox.shrink(), + SizedBox(width: widget.gap), + Expanded( + child: TextField( + controller: _textController, + decoration: InputDecoration( + hintText: 'Type a message', + hintStyle: inputTheme.hintStyle, + border: widget.inputBorder, + filled: widget.filled, + fillColor: inputTheme.textFieldColor, + hoverColor: Colors.transparent, + suffixIcon: SpeechToTextButton( + onTextReceived: (data) { + _textController.text = data.toString(); + }, + ), + ), + style: inputTheme.textStyle, + onSubmitted: _handleSubmitted, + textInputAction: TextInputAction.send, + ), + ), + SizedBox(width: widget.gap), + if (widget.sendIcon != null) + IconButton( + icon: widget.sendIcon!, + color: inputTheme.hintStyle?.color, + onPressed: () => + _handleSubmitted(_textController.text), + ) + else + const SizedBox.shrink(), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _updateInputHeight() { + if (!mounted) return; + + final renderBox = + _inputKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final height = renderBox.size.height; + final bottomSafeArea = MediaQuery.of(context).padding.bottom; + + context.read().updateHeight( + // only set real height of the input, ignoring safe area + widget.handleSafeArea == true ? height - bottomSafeArea : height, + ); + } + } + + void _handleSubmitted(String text) { + if (text.isNotEmpty) { + context.read()?.call(text); + _textController.clear(); + } + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart new file mode 100644 index 00000000..b35267f7 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart @@ -0,0 +1,75 @@ +import 'package:collection/collection.dart'; +import 'package:cross_cache/cross_cache.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_chat_core/flutter_chat_core.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/chat/features/chat_store/data/models/message_types.dart'; +import 'package:lensai/features/chat/features/chat_store/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/chat/domain/chat_backend.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/chat_text_message.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/qa_chat_input.dart'; +import 'package:lensai/presentation/hooks/on_initialization.dart'; + +class TabQaChat extends HookConsumerWidget { + final String chatId; + final ScrollController? scrollController; + + const TabQaChat({required this.chatId, this.scrollController, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final chatBackend = ref.watch(chatBackendProvider(chatId).notifier); + final chatController = ref.watch(chatControllerProvider(chatId)); + + final crossCache = useMemoized(() => CrossCache()); + final chatScrollController = scrollController ?? useScrollController(); + + useOnInitialization(() async { + await chatBackend.prepareEmbeddings(); + }); + + return Chat( + darkTheme: ChatTheme.dark( + backgroundColor: Theme.of(context).colorScheme.surface, + inputTheme: InputTheme( + backgroundColor: Theme.of(context).colorScheme.surface, + ), + ), + theme: ChatTheme.light( + backgroundColor: Theme.of(context).colorScheme.surface, + ), + builders: Builders( + textMessageBuilder: (context, message, index) => + ChatTextMessage(message: message, index: index), + customMessageBuilder: (context, message, index) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + decoration: const BoxDecoration( + color: Color(0xFFF0F0F0), + borderRadius: BorderRadius.all(Radius.circular(12)), + ), + child: const IsTypingIndicator(), + ), + inputBuilder: (context) => const QaChatInput( + attachmentIcon: null, + ), + ), + chatController: chatController, + crossCache: crossCache, + scrollController: chatScrollController, + onMessageSend: (text) async { + await chatBackend.processQAMessage(text); + }, + currentUserId: MessageAuthor.human.user.id, + resolveUser: (id) => Future.value( + MessageAuthor.values + .firstWhereOrNull((user) => user.user.id == id) + ?.user, + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart new file mode 100644 index 00000000..c02dee16 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.dart @@ -0,0 +1,59 @@ +import 'package:langchain/langchain.dart'; +import 'package:langchain_openai/langchain_openai.dart'; +import 'package:lensai/core/models.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/sqlite_vector_store.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'qa_memory_chain.g.dart'; + +typedef QAResult = ({String answer, List docs}); + +@Riverpod() +class QAMemoryChain extends _$QAMemoryChain { + late RetrievalQAChain _retrievalQA; + + @override + void build({ + required String chatId, + String? mainDocumentId, + String? contextId, + }) { + final chatModel = ref.watch(chatModelProvider); + final embeddingsModel = ref.watch(embeddingModelProvider); + final db = ref.watch(tabDatabaseProvider); + + final retriever = SqliteVectorStore( + db.vectorDao, + embeddings: embeddingsModel, + mainDocumentId: mainDocumentId, + contextId: contextId, + ).asRetriever( + defaultOptions: const VectorStoreRetrieverOptions( + searchType: VectorStoreSimilaritySearch(k: 3), + ), + ); + + final qaChain = OpenAIQAWithSourcesChain(llm: chatModel); + final docPrompt = PromptTemplate.fromTemplate( + 'Content: {page_content}\nSource: {source}', + ); + + final finalQAChain = StuffDocumentsChain( + llmChain: qaChain, + documentPrompt: docPrompt, + ); + + _retrievalQA = RetrievalQAChain( + retriever: retriever, + combineDocumentsChain: finalQAChain, + ); + } + + Future processQuestion(String input) async { + final result = await _retrievalQA(input); + final qaResult = result['result'] as QAWithSources; + + return (answer: qaResult.answer, docs: []); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart new file mode 100644 index 00000000..500c320e --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/chat/services/qa_memory_chain.g.dart @@ -0,0 +1,218 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'qa_memory_chain.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$qAMemoryChainHash() => r'd5efc3fc01a6a88c67164d7eefc06412c9ab7c5c'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$QAMemoryChain extends BuildlessAutoDisposeNotifier { + late final String chatId; + late final String? mainDocumentId; + late final String? contextId; + + void build({ + required String chatId, + String? mainDocumentId, + String? contextId, + }); +} + +/// See also [QAMemoryChain]. +@ProviderFor(QAMemoryChain) +const qAMemoryChainProvider = QAMemoryChainFamily(); + +/// See also [QAMemoryChain]. +class QAMemoryChainFamily extends Family { + /// See also [QAMemoryChain]. + const QAMemoryChainFamily(); + + /// See also [QAMemoryChain]. + QAMemoryChainProvider call({ + required String chatId, + String? mainDocumentId, + String? contextId, + }) { + return QAMemoryChainProvider( + chatId: chatId, + mainDocumentId: mainDocumentId, + contextId: contextId, + ); + } + + @override + QAMemoryChainProvider getProviderOverride( + covariant QAMemoryChainProvider provider, + ) { + return call( + chatId: provider.chatId, + mainDocumentId: provider.mainDocumentId, + contextId: provider.contextId, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'qAMemoryChainProvider'; +} + +/// See also [QAMemoryChain]. +class QAMemoryChainProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [QAMemoryChain]. + QAMemoryChainProvider({ + required String chatId, + String? mainDocumentId, + String? contextId, + }) : this._internal( + () => QAMemoryChain() + ..chatId = chatId + ..mainDocumentId = mainDocumentId + ..contextId = contextId, + from: qAMemoryChainProvider, + name: r'qAMemoryChainProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$qAMemoryChainHash, + dependencies: QAMemoryChainFamily._dependencies, + allTransitiveDependencies: + QAMemoryChainFamily._allTransitiveDependencies, + chatId: chatId, + mainDocumentId: mainDocumentId, + contextId: contextId, + ); + + QAMemoryChainProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.chatId, + required this.mainDocumentId, + required this.contextId, + }) : super.internal(); + + final String chatId; + final String? mainDocumentId; + final String? contextId; + + @override + void runNotifierBuild( + covariant QAMemoryChain notifier, + ) { + return notifier.build( + chatId: chatId, + mainDocumentId: mainDocumentId, + contextId: contextId, + ); + } + + @override + Override overrideWith(QAMemoryChain Function() create) { + return ProviderOverride( + origin: this, + override: QAMemoryChainProvider._internal( + () => create() + ..chatId = chatId + ..mainDocumentId = mainDocumentId + ..contextId = contextId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + chatId: chatId, + mainDocumentId: mainDocumentId, + contextId: contextId, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _QAMemoryChainProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is QAMemoryChainProvider && + other.chatId == chatId && + other.mainDocumentId == mainDocumentId && + other.contextId == contextId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, chatId.hashCode); + hash = _SystemHash.combine(hash, mainDocumentId.hashCode); + hash = _SystemHash.combine(hash, contextId.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin QAMemoryChainRef on AutoDisposeNotifierProviderRef { + /// The parameter `chatId` of this provider. + String get chatId; + + /// The parameter `mainDocumentId` of this provider. + String? get mainDocumentId; + + /// The parameter `contextId` of this provider. + String? get contextId; +} + +class _QAMemoryChainProviderElement + extends AutoDisposeNotifierProviderElement + with QAMemoryChainRef { + _QAMemoryChainProviderElement(super.provider); + + @override + String get chatId => (origin as QAMemoryChainProvider).chatId; + @override + String? get mainDocumentId => + (origin as QAMemoryChainProvider).mainDocumentId; + @override + String? get contextId => (origin as QAMemoryChainProvider).contextId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart new file mode 100644 index 00000000..771f7f3c --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:langchain/langchain.dart' as langchain; +import 'package:lensai/core/uuid.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/hash.dart'; +import 'package:lensai/utils/langchain_utils.dart'; + +part 'vector.g.dart'; + +@DriftAccessor() +class VectorDao extends DatabaseAccessor with _$VectorDaoMixin { + VectorDao(super.attachedDatabase); + + Future deleteDocuments(List ids) { + return db.document.deleteWhere((doc) => doc.documentId.isIn(ids)); + } + + Future deleteDocumentsByMainDocumentId(String mainDocumentId) { + return db.document + .deleteWhere((doc) => doc.mainDocumentId.equals(mainDocumentId)); + } + + SingleOrNullSelectable getDocumentById(String documentId) { + return db.document.select() + ..where((row) => row.documentId.equals(documentId)); + } + + Selectable getDocuments({ + String? mainDocumentId, + String? contextId, + }) { + final statement = db.document.select(); + + if (mainDocumentId != null) { + statement.where((row) => row.mainDocumentId.equals(mainDocumentId)); + } + + if (contextId != null) { + statement.where((row) => row.contextId.equals(contextId)); + } + + return statement; + } + + Selectable getDocumentsWithMissingEmbeddings({ + String? mainDocumentId, + String? contextId, + }) { + return db.missingDocumentEmbeddings( + mainDocumentId: mainDocumentId, + contextId: contextId, + ); + } + + Future insertDocuments( + List documents, { + String? mainDocumentId, + String? contextId, + }) { + return db.document.insertAll( + documents.map((document) { + final documentId = document.id ?? uuid.v4(); + final contentHash = sha2(document.pageContent); + + return DocumentCompanion.insert( + documentId: documentId, + mainDocumentId: Value.absentIfNull(mainDocumentId), + contextId: Value.absentIfNull(contextId), + content: document.pageContent, + metadata: (document.metadata.isNotEmpty) + ? Value(jsonEncode(document.metadata)) + : const Value.absent(), + contentHash: contentHash, + ); + }), + mode: InsertMode.insertOrReplace, + ); + } + + Future insertEmbeddings(List<(DocumentData, List)> documents) { + return db.documentVec.insertAll( + documents.map( + (doc) { + final (document, vector) = doc; + + return DocumentVecCompanion.insert( + id: document.documentId, + embedding: serializeVector(vector), + mainDocumentId: Value.absentIfNull(document.mainDocumentId), + contextId: Value.absentIfNull(document.contextId), + contentHash: document.contentHash, + ); + }, + ), + ); + } + + Future> insertDocumentsWithEmbedings( + List<(langchain.Document, List)> documents, { + String? mainDocumentId, + String? contextId, + }) async { + final insertedIds = []; + + await db.batch((b) async { + final insertableDocuments = >[]; + final insertableEmbeddings = >[]; + + for (final (document, vector) in documents) { + final documentId = document.id ?? uuid.v4(); + final contentHash = sha2(document.pageContent); + + insertedIds.add(documentId); + + insertableDocuments.add( + DocumentCompanion.insert( + documentId: documentId, + mainDocumentId: Value.absentIfNull(mainDocumentId), + contextId: Value.absentIfNull(contextId), + content: document.pageContent, + metadata: (document.metadata.isNotEmpty) + ? Value(jsonEncode(document.metadata)) + : const Value.absent(), + contentHash: contentHash, + ), + ); + + insertableEmbeddings.add( + DocumentVecCompanion.insert( + id: documentId, + embedding: serializeVector(vector), + mainDocumentId: Value.absentIfNull(mainDocumentId), + contextId: Value.absentIfNull(contextId), + contentHash: contentHash, + ), + ); + } + + await db.document.insertAll( + insertableDocuments, + mode: InsertMode.insertOrReplace, + ); + + await db.documentVec.insertAll(insertableEmbeddings); + }); + + return insertedIds; + } + + Selectable vectorSearch({ + required langchain.VectorStoreSimilaritySearch config, + required List searchVectors, + String? mainDocumentId, + String? contextId, + }) { + assert(config.filter?.isNotEmpty ?? true, 'Filters are unsupported'); + assert( + !(mainDocumentId != null && contextId != null), + 'Either filter by document or context, not both', + ); + + return db.queryVectors( + searchVectors: serializeVector(searchVectors), + k: config.k, + mainDocumentId: mainDocumentId, + contextId: contextId, + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.g.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.g.dart new file mode 100644 index 00000000..ae20acd0 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.g.dart @@ -0,0 +1,6 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'vector.dart'; + +// ignore_for_file: type=lint +mixin _$VectorDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/database.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/database.dart new file mode 100644 index 00000000..5d85a827 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/database.dart @@ -0,0 +1,15 @@ +class VectorDatabaseMigrator { + final int dimensions; + + String get vectorTableDefinition => ''' +CREATE VIRTUAL TABLE document_vec using vec0( + id TEXT PRIMARY KEY, + main_document_id TEXT, + context_id TEXT PARTITION KEY, + embedding float[$dimensions], + content_hash TEXT +); +'''; + + VectorDatabaseMigrator({required this.dimensions}); +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/vector_store.drift b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/vector_store.drift new file mode 100644 index 00000000..152c537a --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/data/database/vector_store.drift @@ -0,0 +1,72 @@ +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart'; + +CREATE TABLE document( + document_id TEXT NOT NULL PRIMARY KEY, + main_document_id TEXT, + context_id TEXT, + content TEXT NOT NULL, + content_hash TEXT NOT NULL, + metadata TEXT +); + +CREATE INDEX document_main_document_id ON document (main_document_id); +CREATE INDEX document_context_id ON document (context_id); + +CREATE TABLE document_vec( + -- dummy definition of virtual table + -- keep in sync with defined table definition + + id TEXT NOT NULL PRIMARY KEY, + main_document_id TEXT, + context_id TEXT, + embedding BLOB NOT NULL, + content_hash TEXT NOT NULL, + + distance REAL, + k INTEGER +); + +CREATE TRIGGER document_delete AFTER DELETE ON document BEGIN + DELETE FROM document_vec WHERE id = old.document_id; +END; + +CREATE TRIGGER document_update_delete AFTER UPDATE ON document BEGIN + DELETE FROM document_vec WHERE id = new.document_id AND content_hash != new.content_hash; +END; + +missingDocumentEmbeddings( + :main_document_id AS TEXT OR NULL, + :context_id AS TEXT OR NULL +): + SELECT + doc.* + FROM document doc + WHERE + doc.main_document_id IS COALESCE(:main_document_id, doc.main_document_id) AND + doc.context_id IS COALESCE(:context_id, doc.context_id) AND + NOT EXISTS ( + SELECT 1 + FROM document_vec vec + WHERE vec.id = doc.document_id + ); + +queryVectors( + :searchVectors AS BLOB, + :main_document_id AS TEXT OR NULL, + :context_id AS TEXT OR NULL +) WITH VectorResult: + SELECT + vec.id, + doc.main_document_id, + doc.context_id, + doc.content, + doc.metadata, + vec.distance + FROM document_vec vec + INNER JOIN document doc ON doc.document_id = vec.id + WHERE + vec.embedding MATCH :searchVectors AND + vec.k = :k AND + vec.main_document_id IS COALESCE(:main_document_id, vec.main_document_id) AND + vec.context_id IS COALESCE(:context_id, vec.context_id) + ORDER BY vec.distance; \ No newline at end of file diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart new file mode 100644 index 00000000..597ed2db --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart @@ -0,0 +1,32 @@ +import 'dart:convert'; + +import 'package:langchain/langchain.dart'; + +class VectorResult { + final String id; + final String? mainDocumentId; + final String? contextId; + final String content; + final Map metadata; + final double distance; + + VectorResult({ + required this.id, + required this.mainDocumentId, + required this.contextId, + required this.content, + required String? metadata, + required double? distance, + }) : metadata = (metadata != null) + ? jsonDecode(metadata) as Map + : const {}, + distance = distance!; + + Document toDocument() { + return Document( + id: id, + pageContent: content, + metadata: metadata, + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart new file mode 100644 index 00000000..d2181c4a --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart @@ -0,0 +1,116 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:exceptions/exceptions.dart'; +import 'package:langchain/langchain.dart' as langchain; +import 'package:langchain/langchain.dart'; +import 'package:langchain_openai/langchain_openai.dart'; +import 'package:lensai/core/models.dart'; +import 'package:lensai/features/geckoview/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/markdown_document_splitter.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'document.g.dart'; + +@Riverpod() +class DocumentRepository extends _$DocumentRepository { + late VectorDao _vectorDao; + late OpenAIEmbeddings _embeddings; + + Future _insertMarkdownDocumentsSplitted( + List originalDocuments, { + List<(String, String)> headersToSplitOn = const [ + ('#', 'h1'), + ('##', 'h2'), + ('###', 'h3'), + ('####', 'h4'), + ('#####', 'h5'), + ('######', 'h6'), + ], + int chunkSize = 748, + int chunkOverlap = 150, + }) { + return _vectorDao.transaction(() async { + for (final doc in originalDocuments) { + final splitted = splitMarkdownDocument( + doc, + headersToSplitOn: headersToSplitOn, + chunkSize: chunkSize, + chunkOverlap: chunkOverlap, + ); + + final splittedWithSource = (splitted != null) + ? ( + mainDocumentId: splitted.mainDocumentId, + parts: splitted.parts + .map( + (part) => part.copyWith( + metadata: { + ...part.metadata, + 'source': part.id, + }, + ), + ) + .toList() + ) + : null; + + await _vectorDao.insertDocuments( + splittedWithSource?.parts ?? [doc], + mainDocumentId: splittedWithSource?.mainDocumentId, + ); + } + }); + } + + Future updateEmbeddings({ + String? mainDocumentId, + String? contextId, + }) { + return Result.fromAsync(() async { + final missing = await _vectorDao + .getDocumentsWithMissingEmbeddings( + mainDocumentId: mainDocumentId, + contextId: contextId, + ) + .get(); + + final documentEmbeddings = await _embeddings.embedDocuments( + missing + .map((doc) => langchain.Document(pageContent: doc.content)) + .toList(), + ); + + return _vectorDao.insertEmbeddings( + missing.mapIndexed((i, doc) => (doc, documentEmbeddings[i])).toList(), + ); + }); + } + + @override + void build() { + _vectorDao = ref.watch(tabDatabaseProvider).vectorDao; + _embeddings = ref.watch(embeddingModelProvider); + + final tabContentService = ref.watch(tabContentServiceProvider); + + final tabContentSub = + tabContentService.tabContentStream.listen((content) async { + final bestContent = content.isProbablyReaderable + ? content.extractedContentMarkdown + : content.fullContentMarkdown; + + if (bestContent != null) { + await _insertMarkdownDocumentsSplitted( + [Document(id: content.tabId, pageContent: bestContent)], + ); + } + }); + + ref.onDispose(() { + unawaited(tabContentSub.cancel()); + }); + } +} diff --git a/app/lib/features/user/domain/providers/cache.g.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart similarity index 51% rename from app/lib/features/user/domain/providers/cache.g.dart rename to app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart index fe704035..d77dfe62 100644 --- a/app/lib/features/user/domain/providers/cache.g.dart +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart @@ -1,29 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'cache.dart'; +part of 'document.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$iconCacheSizeMegabytesHash() => - r'43162fce3ce8b04a5a55ed52021b274afbde2861'; +String _$documentRepositoryHash() => + r'0c93116e0f9887d452b7c493dd4da45e5bf0c2d4'; -/// See also [iconCacheSizeMegabytes]. -@ProviderFor(iconCacheSizeMegabytes) -final iconCacheSizeMegabytesProvider = - AutoDisposeStreamProvider.internal( - iconCacheSizeMegabytes, - name: r'iconCacheSizeMegabytesProvider', +/// See also [DocumentRepository]. +@ProviderFor(DocumentRepository) +final documentRepositoryProvider = + AutoDisposeNotifierProvider.internal( + DocumentRepository.new, + name: r'documentRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$iconCacheSizeMegabytesHash, + : _$documentRepositoryHash, dependencies: null, allTransitiveDependencies: null, ); -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef; +typedef _$DocumentRepository = AutoDisposeNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/sqlite_vector_store.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/sqlite_vector_store.dart new file mode 100644 index 00000000..c4f48ce2 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/sqlite_vector_store.dart @@ -0,0 +1,60 @@ +import 'package:collection/collection.dart'; +import 'package:langchain/langchain.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/models/vector_result.dart'; + +class SqliteVectorStore extends VectorStore { + final VectorDao _dao; + + final String? mainDocumentId; + final String? contextId; + + SqliteVectorStore( + this._dao, { + required super.embeddings, + this.mainDocumentId, + this.contextId, + }); + + @override + Future> addVectors({ + required List documents, + required List> vectors, + }) { + return _dao.insertDocumentsWithEmbedings( + documents.mapIndexed((i, doc) => (doc, vectors[i])).toList(), + contextId: contextId, + mainDocumentId: mainDocumentId, + ); + } + + @override + Future delete({required List ids}) { + return _dao.deleteDocuments(ids); + } + + @override + Future> similaritySearchByVectorWithScores({ + required List embedding, + VectorStoreSimilaritySearch config = const VectorStoreSimilaritySearch(), + }) async { + Iterable results = await _dao + .vectorSearch( + config: config, + searchVectors: embedding, + contextId: contextId, + mainDocumentId: mainDocumentId, + ) + .get(); + + if (config.scoreThreshold != null) { + results = results.where( + (result) => result.distance >= config.scoreThreshold!, + ); + } + + return results + .map((result) => (result.toDocument(), result.distance)) + .toList(); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/utils/document_uuid.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/document_uuid.dart new file mode 100644 index 00000000..f3a6e1bf --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/document_uuid.dart @@ -0,0 +1,26 @@ +import 'dart:typed_data'; + +import 'package:lensai/core/uuid.dart'; +import 'package:uuid/data.dart'; +import 'package:uuid/parsing.dart'; +import 'package:uuid/rng.dart'; + +final _rng = CryptoRNG(); + +class DocumentUuid { + final Uint8List _baseBytes; + + DocumentUuid([Uint8List? baseBytes]) + : _baseBytes = baseBytes ?? _rng.generate(); + + factory DocumentUuid.fromUuid(String uuid) { + return DocumentUuid(UuidParsing.parseAsByteList(uuid)); + } + + String getDocumentPartUuid(int sequence) { + final bytes = Uint8List.fromList(_baseBytes); + bytes.buffer.asByteData().setInt16(14, sequence); + + return uuid.v8g(config: V8GenericOptions(bytes)); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/utils/hash.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/hash.dart new file mode 100644 index 00000000..ce0d4878 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/hash.dart @@ -0,0 +1,17 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; +import 'package:flutter/foundation.dart'; + +String sha2(String input) { + return sha256.convert(utf8.encode(input)).toString(); +} + +Future sha2Isolated(String input) async { + if (input.length < 1048576) { + // Less than 1MB + return sha2(input); + } else { + return await compute(sha2, input); + } +} diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/utils/markdown_document_splitter.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/markdown_document_splitter.dart new file mode 100644 index 00000000..df199ff4 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/utils/markdown_document_splitter.dart @@ -0,0 +1,112 @@ +import 'package:collection/collection.dart'; +import 'package:langchain/langchain.dart'; +import 'package:lensai/core/uuid.dart'; +import 'package:lensai/features/geckoview/features/tabs/features/vector_store/utils/document_uuid.dart'; + +typedef DocumentParts = ({String mainDocumentId, List parts}); + +List mergeShortDocuments( + List documents, + int maxLength, { + String separator = '\n', + required Document Function(Document a, Document b) doMerge, + int Function(String) lengthFunction = TextSplitter.defaultLengthFunction, +}) { + final result = []; + + Document? current; + for (final doc in documents) { + if (current == null) { + current = doc; + } else if (lengthFunction(current.pageContent) + + lengthFunction(doc.pageContent) <= + maxLength) { + current = doMerge(current, doc); + } else { + result.add(current); + current = doc; + } + } + + if (current != null) { + result.add(current); + } + + return result; +} + +List _headerValueList(dynamic value) { + return switch (value) { + String _ => [value], + List _ => value, + _ => throw Exception('Unsupported type') + }; +} + +DocumentParts? splitMarkdownDocument( + Document originalDoc, { + required List<(String, String)> headersToSplitOn, + required int chunkSize, + required int chunkOverlap, +}) { + final markdownHeaderSplitter = MarkdownHeaderTextSplitter( + stripHeaders: false, + headersToSplitOn: headersToSplitOn, + ); + + final markdownTextSplitter = MarkdownTextSplitter( + chunkSize: chunkSize, + chunkOverlap: chunkOverlap, + ); + + final headerChunks = + markdownHeaderSplitter.splitText(originalDoc.pageContent); + final docChunks = markdownTextSplitter.splitDocuments(headerChunks); + + if (docChunks.length == 1) { + return null; + } + + final headerKeys = headersToSplitOn.map((header) => header.$2).toSet(); + final mergedDocChunks = mergeShortDocuments( + docChunks, + chunkSize, + doMerge: (a, b) { + //Disallow merging into headers of the same + final intersectingHeaders = a.metadata.keys + .toSet() + .intersection(b.metadata.keys.toSet()) + .intersection(headerKeys); + + return Document( + id: a.id ?? b.id, + pageContent: '${a.pageContent}\n${b.pageContent}', + metadata: { + ...a.metadata, + ...b.metadata, + for (final header in intersectingHeaders) + header: { + ..._headerValueList(a.metadata[header]), + ..._headerValueList(b.metadata[header]), + }.toList(), + }, + ); + }, + ); + + final mainDocumentId = originalDoc.id ?? uuid.v4(); + final documentIdFactory = DocumentUuid.fromUuid(mainDocumentId); + + return ( + mainDocumentId: mainDocumentId, + parts: mergedDocChunks + .mapIndexed( + (i, doc) => Document( + id: documentIdFactory.getDocumentPartUuid(i), + pageContent: doc.pageContent, + metadata: mergeMaps(originalDoc.metadata, doc.metadata), + ), + ) + .toList() + ); +} diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart index 63e956c6..aa80684b 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart @@ -122,30 +122,6 @@ class ContainerListScreen extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text('Containers'), - actions: [ - IconButton( - onPressed: () async { - final initialColor = - await ref.read(unusedRandomContainerColorProvider.future); - - if (context.mounted) { - final result = await showDialog( - context: context, - builder: (context) => ContainerDialog.create( - initialColor: initialColor, - ), - ); - - if (result != null) { - await ref - .read(containerRepositoryProvider.notifier) - .addContainer(name: result.name, color: result.color); - } - } - }, - icon: const Icon(Icons.add), - ), - ], ), body: HookConsumer( builder: (context, ref, child) { @@ -206,6 +182,28 @@ class ContainerListScreen extends HookConsumerWidget { ); }, ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + final initialColor = + await ref.read(unusedRandomContainerColorProvider.future); + + if (context.mounted) { + final result = await showDialog( + context: context, + builder: (context) => ContainerDialog.create( + initialColor: initialColor, + ), + ); + + if (result != null) { + await ref + .read(containerRepositoryProvider.notifier) + .addContainer(name: result.name, color: result.color); + } + } + }, + child: const Icon(Icons.add), + ), ); } } diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart new file mode 100644 index 00000000..e7b3fa86 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/reorderable_grid.dart @@ -0,0 +1,1215 @@ +import 'dart:math'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +/// {@template reorderable_grid_view.reorderable_grid} +/// A scrolling container that allows the user to interactively reorder the +/// grid items. +/// +/// This widget is similar to one created by [GridView.builder], and uses +/// an [IndexedWidgetBuilder] to create each item. +/// +/// It is up to the application to wrap each child (or an internal part of the +/// child such as a drag handle) with a drag listener that will recognize +/// the start of an item drag and then start the reorder by calling +/// [ReorderableGridState.startItemDragReorder]. This is most easily achieved +/// by wrapping each child in a [ReorderableGridDragStartListener] or a +/// [ReorderableGridDelayedDragStartListener]. These will take care of recognizing +/// the start of a drag gesture and call the grid state's +/// [ReorderableGridState.startItemDragReorder] method. +/// +/// This widget's [ReorderableGridState] can be used to manually start an item +/// reorder, or cancel a current drag. To refer to the +/// [ReorderableGridState] either provide a [GlobalKey] or use the static +/// [ReorderableGrid.of] method from an item's build method. +/// +/// See also: +/// +/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder +/// its items. +/// {@endtemplate} +class ReorderableGrid extends StatefulWidget { + /// {@macro reorderable_grid_view.reorderable_grid} + /// The [itemCount] must be greater than or equal to zero. + const ReorderableGrid({ + Key? key, + required this.itemBuilder, + required this.itemCount, + required this.onReorder, + required this.gridDelegate, + this.proxyDecorator, + this.padding, + this.scrollDirection = Axis.vertical, + this.reverse = false, + this.controller, + this.primary, + this.physics, + this.shrinkWrap = false, + this.anchor = 0.0, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + this.clipBehavior = Clip.hardEdge, + this.autoScroll, + this.onReorderStart, + }) : assert(itemCount >= 0), + super(key: key); + + /// Called, as needed, to build grid item widgets. + /// + /// List items are only built when they're scrolled into view. + /// + /// The [IndexedWidgetBuilder] index parameter indicates the item's + /// position in the grid. The value of the index parameter will be between + /// zero and one less than [itemCount]. All items in the grid must have a + /// unique [Key], and should have some kind of listener to start the drag + /// (usually a [ReorderableGridDragStartListener] or + /// [ReorderableGridDelayedDragStartListener]). + final IndexedWidgetBuilder itemBuilder; + + /// {@macro flutter.widgets.reorderable_list.itemCount} + final int itemCount; + + /// {@macro flutter.widgets.reorderable_list.onReorder} + final ReorderCallback onReorder; + + /// {@macro flutter.widgets.reorderable_list.onReorderStart} + final void Function(int index)? onReorderStart; + + /// {@macro flutter.widgets.reorderable_list.proxyDecorator} + final ReorderItemProxyDecorator? proxyDecorator; + + /// {@macro flutter.widgets.reorderable_list.padding} + final EdgeInsetsGeometry? padding; + + /// {@macro flutter.widgets.scroll_view.scrollDirection} + final Axis scrollDirection; + + /// {@macro flutter.widgets.scroll_view.reverse} + final bool reverse; + + /// {@macro flutter.widgets.scroll_view.controller} + final ScrollController? controller; + + /// {@macro flutter.widgets.scroll_view.primary} + final bool? primary; + + /// {@macro flutter.widgets.scroll_view.physics} + final ScrollPhysics? physics; + + /// {@macro flutter.widgets.scroll_view.shrinkWrap} + final bool shrinkWrap; + + /// {@macro flutter.widgets.scroll_view.anchor} + final double anchor; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@macro flutter.widgets.scroll_view.keyboardDismissBehavior} + /// + /// The default is [ScrollViewKeyboardDismissBehavior.manual] + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + final SliverGridDelegate gridDelegate; + + /// Overrides if autoscrolling is enabled. Defaults to false if `physics` is + /// [NeverScrollableScrollPhysics] + final bool? autoScroll; + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [ReorderableGrid] item widgets that + /// insert or remove items in response to user input. + /// + /// If no [ReorderableGrid] surrounds the given context, then this function + /// will assert in debug mode and throw an exception in release mode. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [maybeOf], a similar function that will return null if no + /// [ReorderableGrid] ancestor is found. + static ReorderableGridState of(BuildContext context) { + final ReorderableGridState? result = + context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError.fromParts([ + ErrorSummary( + 'ReorderableGrid.of() called with a context that does not contain a ReorderableGrid.'), + ErrorDescription( + 'No ReorderableGrid ancestor could be found starting from the context that was passed to ReorderableGrid.of().', + ), + ErrorHint( + 'This can happen when the context provided is from the same StatefulWidget that ' + 'built the ReorderableGrid. Please see the ReorderableGrid documentation for examples ' + 'of how to refer to an ReorderableGridState object:\n' + ' https://api.flutter.dev/flutter/widgets/ReorderableGridState-class.html', + ), + context.describeElement('The context used was'), + ]); + } + return true; + }()); + return result!; + } + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [ReorderableGrid] item widgets that insert + /// or remove items in response to user input. + /// + /// If no [ReorderableGrid] surrounds the context given, then this function will + /// return null. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [of], a similar function that will throw if no [ReorderableGrid] ancestor + /// is found. + static ReorderableGridState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + ReorderableGridState createState() => ReorderableGridState(); +} + +/// The state for a grid that allows the user to interactively reorder +/// the grid items. +/// +/// An app that needs to start a new item drag or cancel an existing one +/// can refer to the [ReorderableGrid]'s state with a global key: +/// +/// ```dart +/// GlobalKey gridKey = GlobalKey(); +/// ... +/// ReorderableGrid(key: gridKey, ...); +/// ... +/// gridKey.currentState.cancelReorder(); +/// ``` +class ReorderableGridState extends State { + final GlobalKey _sliverReorderableGridKey = + GlobalKey(); + + /// Initiate the dragging of the item at [index] that was started with + /// the pointer down [event]. + /// + /// The given [recognizer] will be used to recognize and start the drag + /// item tracking and lead to either an item reorder, or a cancelled drag. + /// The grid will take ownership of the returned recognizer and will dispose + /// it when it is no longer needed. + /// + /// Most applications will not use this directly, but will wrap the item + /// (or part of the item, like a drag handle) in either a + /// [ReorderableGridDragStartListener] or [ReorderableGridDelayedDragStartListener] + /// which call this for the application. + void startItemDragReorder({ + required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer, + }) { + _sliverReorderableGridKey.currentState!.startItemDragReorder( + index: index, event: event, recognizer: recognizer); + } + + /// Cancel any item drag in progress. + /// + /// This should be called before any major changes to the item grid + /// occur so that any item drags will not get confused by + /// changes to the underlying grid. + /// + /// If no drag is active, this will do nothing. + void cancelReorder() { + _sliverReorderableGridKey.currentState!.cancelReorder(); + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + scrollDirection: widget.scrollDirection, + reverse: widget.reverse, + controller: widget.controller, + primary: widget.primary, + physics: widget.physics, + shrinkWrap: widget.shrinkWrap, + anchor: widget.anchor, + cacheExtent: widget.cacheExtent, + dragStartBehavior: widget.dragStartBehavior, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + clipBehavior: widget.clipBehavior, + slivers: [ + SliverPadding( + padding: widget.padding ?? EdgeInsets.zero, + sliver: SliverReorderableGrid( + key: _sliverReorderableGridKey, + gridDelegate: widget.gridDelegate, + itemBuilder: widget.itemBuilder, + itemCount: widget.itemCount, + onReorder: widget.onReorder, + onReorderStart: widget.onReorderStart, + proxyDecorator: widget.proxyDecorator, + reverse: widget.reverse, + autoScroll: widget.autoScroll ?? + widget.physics is! NeverScrollableScrollPhysics, + scrollDirection: widget.scrollDirection, + ), + ), + ], + ); + } +} + +/// A sliver grid that allows the user to interactively reorder the grid items. +/// +/// It is up to the application to wrap each child (or an internal part of the +/// child) with a drag listener that will recognize the start of an item drag +/// and then start the reorder by calling +/// [SliverReorderableGridState.startItemDragReorder]. This is most easily +/// achieved by wrapping each child in a [ReorderableGridDragStartListener] or +/// a [ReorderableGridDelayedDragStartListener]. These will take care of +/// recognizing the start of a drag gesture and call the grid state's start +/// item drag method. +/// +/// This widget's [SliverReorderableGridState] can be used to manually start an item +/// reorder, or cancel a current drag that's already underway. To refer to the +/// [SliverReorderableGridState] either provide a [GlobalKey] or use the static +/// [SliverReorderableGrid.of] method from an item's build method. +/// +/// See also: +/// +/// * [ReorderableGrid], a regular widget grid that allows the user to reorder +/// its items. +class SliverReorderableGrid extends StatefulWidget { + /// Creates a sliver grid that allows the user to interactively reorder its + /// items. + /// + /// The [itemCount] must be greater than or equal to zero. + const SliverReorderableGrid({ + Key? key, + required this.itemBuilder, + required this.itemCount, + required this.onReorder, + required this.gridDelegate, + this.onReorderStart, + this.reverse = false, + this.proxyDecorator, + this.autoScroll = true, + this.scrollDirection = Axis.vertical, + }) : assert(itemCount >= 0), + super(key: key); + + /// {@macro flutter.widgets.reorderable_list.itemBuilder} + final IndexedWidgetBuilder itemBuilder; + + /// {@macro flutter.widgets.reorderable_list.itemCount} + final int itemCount; + + /// {@macro flutter.widgets.reorderable_list.onReorder} + final ReorderCallback onReorder; + + /// {@macro flutter.widgets.reorderable_list.onReorderStart} + final void Function(int index)? onReorderStart; + + /// {@macro flutter.widgets.reorderable_list.proxyDecorator} + final ReorderItemProxyDecorator? proxyDecorator; + + final SliverGridDelegate gridDelegate; + + /// If auto scrolling is enabled. Should be disabled if associated scroll + /// physics are [NeverScrollableScrollPhysics] + final bool autoScroll; + + /// {@macro flutter.widgets.scroll_view.reverse} + final bool reverse; + + /// {@macro flutter.widgets.scroll_view.scrollDirection} + final Axis scrollDirection; + + @override + SliverReorderableGridState createState() => SliverReorderableGridState(); + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [SliverReorderableGrid] item widgets to + /// start or cancel an item drag operation. + /// + /// If no [SliverReorderableGrid] surrounds the context given, this function + /// will assert in debug mode and throw an exception in release mode. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [maybeOf], a similar function that will return null if no + /// [SliverReorderableGrid] ancestor is found. + static SliverReorderableGridState of(BuildContext context) { + final SliverReorderableGridState? result = + context.findAncestorStateOfType(); + assert(() { + if (result == null) { + throw FlutterError.fromParts([ + ErrorSummary( + 'SliverReorderableGrid.of() called with a context that does not contain a SliverReorderableGrid.', + ), + ErrorDescription( + 'No SliverReorderableGrid ancestor could be found starting from the context that was passed to SliverReorderableGrid.of().', + ), + ErrorHint( + 'This can happen when the context provided is from the same StatefulWidget that ' + 'built the SliverReorderableGrid. Please see the SliverReorderableGrid documentation for examples'), + context.describeElement('The context used was'), + ]); + } + return true; + }()); + return result!; + } + + /// The state from the closest instance of this class that encloses the given + /// context. + /// + /// This method is typically used by [SliverReorderableGrid] item widgets that + /// insert or remove items in response to user input. + /// + /// If no [SliverReorderableGrid] surrounds the context given, this function + /// will return null. + /// + /// This method can be expensive (it walks the element tree). + /// + /// See also: + /// + /// * [of], a similar function that will throw if no [SliverReorderableGrid] + /// ancestor is found. + static SliverReorderableGridState? maybeOf(BuildContext context) { + return context.findAncestorStateOfType(); + } +} + +/// The state for a sliver grid that allows the user to interactively reorder +/// the grid items. +/// +/// An app that needs to start a new item drag or cancel an existing one +/// can refer to the [SliverReorderableGrid]'s state with a global key: +/// +/// ```dart +/// GlobalKey gridKey = GlobalKey(); +/// ... +/// SliverReorderableGrid(key: gridKey, ...); +/// ... +/// gridKey.currentState.cancelReorder(); +/// ``` +/// +/// [ReorderableGridDragStartListener] and [ReorderableGridDelayedDragStartListener] +/// refer to their [SliverReorderableGrid] with the static +/// [SliverReorderableGrid.of] method. +class SliverReorderableGridState extends State + with TickerProviderStateMixin { + // Map of index -> child state used manage where the dragging item will need + // to be inserted. + final Map _items = {}; + + OverlayEntry? _overlayEntry; + int? _dragIndex; + _DragInfo? _dragInfo; + int? _insertIndex; + Offset? _finalDropPosition; + MultiDragGestureRecognizer? _recognizer; + bool _autoScrolling = false; + + @override + void didUpdateWidget(covariant SliverReorderableGrid oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.itemCount != oldWidget.itemCount) { + cancelReorder(); + } + } + + @override + void dispose() { + _dragInfo?.dispose(); + super.dispose(); + } + + /// Initiate the dragging of the item at [index] that was started with + /// the pointer down [event]. + /// + /// The given [recognizer] will be used to recognize and start the drag + /// item tracking and lead to either an item reorder, or a cancelled drag. + /// + /// Most applications will not use this directly, but will wrap the item + /// (or part of the item, like a drag handle) in either a + /// [ReorderableGridDragStartListener] or [ReorderableGridDelayedDragStartListener] + /// which call this method when they detect the gesture that triggers a drag + /// start. + void startItemDragReorder({ + required int index, + required PointerDownEvent event, + required MultiDragGestureRecognizer recognizer, + }) { + assert(0 <= index && index < widget.itemCount); + setState(() { + if (_dragInfo != null) { + cancelReorder(); + } + if (_items.containsKey(index)) { + _dragIndex = index; + _recognizer = recognizer + ..onStart = _dragStart + ..addPointer(event); + } else { + throw Exception('Attempting to start a drag on a non-visible item'); + } + }); + } + + /// Cancel any item drag in progress. + /// + /// This should be called before any major changes to the item grid + /// occur so that any item drags will not get confused by + /// changes to the underlying grid. + /// + /// If a drag operation is in progress, this will immediately reset + /// the grid to back to its pre-drag state. + /// + /// If no drag is active, this will do nothing. + void cancelReorder() { + _dragReset(); + } + + void _registerItem(_ReorderableItemState item) { + _items[item.index] = item; + if (item.index == _dragInfo?.index) { + item.dragging = true; + item.rebuild(); + } + } + + void _unregisterItem(int index, _ReorderableItemState item) { + final _ReorderableItemState? currentItem = _items[index]; + if (currentItem == item) { + _items.remove(index); + } + } + + Drag? _dragStart(Offset position) { + assert(_dragInfo == null); + + // if any items are already being dragged, ignore this dragstart + if (_items.values.any((e) => e.dragging)) return null; + + final _ReorderableItemState item = _items[_dragIndex!]!; + item.dragging = true; + item.rebuild(); + widget.onReorderStart?.call(_dragIndex!); + + _insertIndex = item.index; + + _dragInfo = _DragInfo( + item: item, + initialPosition: position, + onUpdate: _dragUpdate, + onCancel: _dragCancel, + onEnd: _dragEnd, + onDropCompleted: _dropCompleted, + proxyDecorator: widget.proxyDecorator, + tickerProvider: this, + ); + _dragInfo!.startDrag(); + + final OverlayState overlay = Overlay.of(context); + assert(_overlayEntry == null); + _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy); + overlay.insert(_overlayEntry!); + + for (final _ReorderableItemState childItem in _items.values) { + if (childItem == item || !childItem.mounted) continue; + childItem.updateForGap(_insertIndex!, false); + } + return _dragInfo; + } + + void _dragUpdate(_DragInfo item, Offset position, Offset delta) { + setState(() { + _overlayEntry?.markNeedsBuild(); + _dragUpdateItems(); + _autoScrollIfNecessary(); + }); + } + + void _dragCancel(_DragInfo item) { + _dragReset(); + } + + void _dragEnd(_DragInfo item) { + setState(() => _finalDropPosition = _itemOffsetAt(_insertIndex!)); + } + + void _dropCompleted() { + final int fromIndex = _dragIndex!; + final int toIndex = _insertIndex!; + if (fromIndex != toIndex) { + widget.onReorder.call(fromIndex, toIndex); + } + _dragReset(); + } + + void _dragReset() { + setState(() { + if (_dragInfo != null) { + if (_dragIndex != null && _items.containsKey(_dragIndex)) { + final _ReorderableItemState dragItem = _items[_dragIndex!]!; + dragItem._dragging = false; + dragItem.rebuild(); + _dragIndex = null; + } + _dragInfo?.dispose(); + _dragInfo = null; + _resetItemGap(); + _recognizer?.dispose(); + _recognizer = null; + _overlayEntry?.remove(); + _overlayEntry = null; + _finalDropPosition = null; + } + }); + } + + void _resetItemGap() { + for (final _ReorderableItemState item in _items.values) { + item.resetGap(); + } + } + + void _dragUpdateItems() { + assert(_dragInfo != null); + + int newIndex = _insertIndex!; + + final dragCenter = _dragInfo!.itemSize + .center(_dragInfo!.dragPosition - _dragInfo!.dragOffset); + + for (final _ReorderableItemState item in _items.values) { + if (!item.mounted) continue; + + final Rect geometry = item.targetGeometryNonOffset(); + + if (geometry.contains(dragCenter)) { + newIndex = item.index; + break; + } + } + + if (newIndex == _insertIndex) return; + _insertIndex = newIndex; + + for (final _ReorderableItemState item in _items.values) { + item.updateForGap(_insertIndex!, true); + } + } + + Future _autoScrollIfNecessary() async { + if (_autoScrolling || + _dragInfo == null || + _dragInfo!.scrollable == null || + widget.autoScroll == false) { + return; + } + + final position = _dragInfo!.scrollable!.position; + double? newOffset; + + const duration = Duration(milliseconds: 14); + const step = 1.0; + const overDragMax = 20.0; + const overDragCoef = 10; + + final isVertical = widget.scrollDirection == Axis.vertical; + final isReversed = widget.reverse; + + /// get the scroll window position on the screen + final scrollRenderBox = + _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox; + final Offset scrollPosition = scrollRenderBox.localToGlobal(Offset.zero); + + /// calculate the start and end position for the scroll window + double scrollWindowStart = + isVertical ? scrollPosition.dy : scrollPosition.dx; + double scrollWindowEnd = scrollWindowStart + + (isVertical ? scrollRenderBox.size.height : scrollRenderBox.size.width); + + /// get the proxy (dragged) object's position on the screen + final proxyObjectPosition = _dragInfo!.dragPosition - _dragInfo!.dragOffset; + + /// calculate the start and end position for the proxy object + double proxyObjectStart = + isVertical ? proxyObjectPosition.dy : proxyObjectPosition.dx; + double proxyObjectEnd = proxyObjectStart + + (isVertical ? _dragInfo!.itemSize.height : _dragInfo!.itemSize.width); + + if (!isReversed) { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels > position.minScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels < position.maxScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + } else { + /// if start of proxy object is before scroll window + if (proxyObjectStart < scrollWindowStart && + position.pixels < position.maxScrollExtent) { + final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax); + newOffset = max(position.minScrollExtent, + position.pixels + step * overDrag / overDragCoef); + } + + /// if end of proxy object is after scroll window + else if (proxyObjectEnd > scrollWindowEnd && + position.pixels > position.minScrollExtent) { + final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax); + newOffset = min(position.maxScrollExtent, + position.pixels - step * overDrag / overDragCoef); + } + } + + if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) { + _autoScrolling = true; + await position.animateTo( + newOffset, + duration: duration, + curve: Curves.linear, + ); + _autoScrolling = false; + if (_dragInfo != null) { + _dragUpdateItems(); + _autoScrollIfNecessary(); + } + } + } + + Offset _calculateNextDragOffset(int index) { + int minPos = min(_dragIndex!, _insertIndex!); + int maxPos = max(_dragIndex!, _insertIndex!); + + if (index < minPos || index > maxPos) return Offset.zero; + + final int direction = _insertIndex! > _dragIndex! ? -1 : 1; + return _itemOffsetAt(index + direction) - _itemOffsetAt(index); + } + + Offset _itemOffsetAt(int index) { + final box = _items[index]?.context.findRenderObject() as RenderBox?; + if (box == null) return Offset.zero; + + return box.localToGlobal(Offset.zero); + } + + Widget _itemBuilder(BuildContext context, int index) { + if (_dragInfo != null && index >= widget.itemCount) { + return SizedBox.fromSize(size: _dragInfo!.itemSize); + } + + final Widget child = widget.itemBuilder(context, index); + assert(child.key != null, 'All grid items must have a key'); + + final OverlayState overlay = Overlay.of(context); + return _ReorderableItem( + key: _ReorderableItemGlobalKey(child.key!, index, this), + index: index, + capturedThemes: + InheritedTheme.capture(from: context, to: overlay.context), + child: child, + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasOverlay(context)); + final SliverChildBuilderDelegate childrenDelegate = + SliverChildBuilderDelegate( + _itemBuilder, + childCount: widget.itemCount, + ); + return SliverGrid( + delegate: childrenDelegate, + gridDelegate: widget.gridDelegate, + ); + } +} + +class _ReorderableItem extends StatefulWidget { + const _ReorderableItem({ + required Key key, + required this.index, + required this.child, + required this.capturedThemes, + }) : super(key: key); + + final int index; + final Widget child; + final CapturedThemes capturedThemes; + + @override + _ReorderableItemState createState() => _ReorderableItemState(); +} + +class _ReorderableItemState extends State<_ReorderableItem> { + late SliverReorderableGridState _listState; + + Offset _startOffset = Offset.zero; + Offset _targetOffset = Offset.zero; + AnimationController? _offsetAnimation; + + Key get key => widget.key!; + int get index => widget.index; + + bool get dragging => _dragging; + set dragging(bool dragging) { + if (mounted) { + setState(() { + _dragging = dragging; + }); + } + } + + bool _dragging = false; + + @override + void initState() { + _listState = SliverReorderableGrid.of(context); + _listState._registerItem(this); + super.initState(); + } + + @override + void dispose() { + _offsetAnimation?.dispose(); + _listState._unregisterItem(index, this); + super.dispose(); + } + + @override + void didUpdateWidget(covariant _ReorderableItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.index != widget.index) { + _listState._unregisterItem(oldWidget.index, this); + _listState._registerItem(this); + } + } + + @override + Widget build(BuildContext context) { + if (_dragging) { + return const SizedBox.shrink(); + } + _listState._registerItem(this); + return Transform( + transform: Matrix4.translationValues(offset.dx, offset.dy, 0.0), + child: widget.child, + ); + } + + @override + void deactivate() { + _listState._unregisterItem(index, this); + super.deactivate(); + } + + Offset get offset { + if (_offsetAnimation != null) { + final double animValue = + Curves.easeInOut.transform(_offsetAnimation!.value); + return Offset.lerp(_startOffset, _targetOffset, animValue)!; + } + return _targetOffset; + } + + void updateForGap(int gapIndex, bool animate) { + if (!mounted) return; + + final Offset newTargetOffset = _listState._calculateNextDragOffset(index); + + if (newTargetOffset == _targetOffset) return; + _targetOffset = newTargetOffset; + + if (animate) { + if (_offsetAnimation == null) { + _offsetAnimation = AnimationController( + vsync: _listState, + duration: const Duration(milliseconds: 250), + ) + ..addListener(rebuild) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + _startOffset = _targetOffset; + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + }) + ..forward(); + } else { + _startOffset = offset; + _offsetAnimation!.forward(from: 0.0); + } + } else { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = _targetOffset; + } + rebuild(); + } + + void resetGap() { + if (_offsetAnimation != null) { + _offsetAnimation!.dispose(); + _offsetAnimation = null; + } + _startOffset = Offset.zero; + _targetOffset = Offset.zero; + rebuild(); + } + + Rect targetGeometry() { + final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; + final Offset itemPosition = + itemRenderBox.localToGlobal(Offset.zero) + _targetOffset; + return itemPosition & itemRenderBox.size; + } + + Rect targetGeometryNonOffset() { + final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox; + final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero); + return itemPosition & itemRenderBox.size; + } + + void rebuild() { + if (mounted) { + setState(() {}); + } + } +} + +/// A wrapper widget that will recognize the start of a drag on the wrapped +/// widget by a [PointerDownEvent], and immediately initiate dragging the +/// wrapped item to a new location in a reorderable grid. +/// +/// See also: +/// +/// * [ReorderableGridDelayedDragStartListener], a similar wrapper that will +/// only recognize the start after a long press event. +/// * [ReorderableGrid], a widget grid that allows the user to reorder +/// its items. +/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder +/// its items. +/// * [ReorderableGridView], a material design grid that allows the user to +/// reorder its items. +class ReorderableGridDragStartListener extends StatelessWidget { + /// Creates a listener for a drag immediately following a pointer down + /// event over the given child widget. + /// + /// This is most commonly used to wrap part of a grid item like a drag + /// handle. + const ReorderableGridDragStartListener({ + Key? key, + required this.child, + required this.index, + this.enabled = true, + }) : super(key: key); + + /// The widget for which the application would like to respond to a tap and + /// drag gesture by starting a reordering drag on a reorderable grid. + final Widget child; + + /// The index of the associated item that will be dragged in the grid. + final int index; + + /// Whether the [child] item can be dragged and moved in the grid. + /// + /// If true, the item can be moved to another location in the grid when the + /// user taps on the child. If false, tapping on the child will be ignored. + final bool enabled; + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: enabled + ? (PointerDownEvent event) => _startDragging(context, event) + : null, + child: child, + ); + } + + /// Provides the gesture recognizer used to indicate the start of a reordering + /// drag operation. + /// + /// By default this returns an [ImmediateMultiDragGestureRecognizer] but + /// subclasses can use this to customize the drag start gesture. + @protected + MultiDragGestureRecognizer createRecognizer() { + return ImmediateMultiDragGestureRecognizer(debugOwner: this); + } + + void _startDragging(BuildContext context, PointerDownEvent event) { + final SliverReorderableGridState? list = + SliverReorderableGrid.maybeOf(context); + list?.startItemDragReorder( + index: index, + event: event, + recognizer: createRecognizer(), + ); + } +} + +/// A wrapper widget that will recognize the start of a drag operation by +/// looking for a long press event. Once it is recognized, it will start +/// a drag operation on the wrapped item in the reorderable grid. +/// +/// See also: +/// +/// * [ReorderableGridDragStartListener], a similar wrapper that will +/// recognize the start of the drag immediately after a pointer down event. +/// * [ReorderableGrid], a widget grid that allows the user to reorder +/// its items. +/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder +/// its items. +/// * [ReorderableGridView], a material design grid that allows the user to +/// reorder its items. +class ReorderableGridDelayedDragStartListener + extends ReorderableGridDragStartListener { + /// Creates a listener for an drag following a long press event over the + /// given child widget. + /// + /// This is most commonly used to wrap an entire grid item in a reorderable + /// grid. + const ReorderableGridDelayedDragStartListener({ + Key? key, + required Widget child, + required int index, + bool enabled = true, + }) : super(key: key, child: child, index: index, enabled: enabled); + + @override + MultiDragGestureRecognizer createRecognizer() { + return DelayedMultiDragGestureRecognizer(debugOwner: this); + } +} + +typedef _DragItemUpdate = void Function( + _DragInfo item, Offset position, Offset delta); +typedef _DragItemCallback = void Function(_DragInfo item); + +class _DragInfo extends Drag { + _DragInfo({ + required _ReorderableItemState item, + Offset initialPosition = Offset.zero, + this.onUpdate, + this.onEnd, + this.onCancel, + this.onDropCompleted, + this.proxyDecorator, + required this.tickerProvider, + }) { + final RenderBox itemRenderBox = + item.context.findRenderObject()! as RenderBox; + listState = item._listState; + index = item.index; + child = item.widget.child; + capturedThemes = item.widget.capturedThemes; + dragPosition = initialPosition; + dragOffset = itemRenderBox.globalToLocal(initialPosition); + itemSize = item.context.size!; + scrollable = Scrollable.of(item.context); + } + + final _DragItemUpdate? onUpdate; + final _DragItemCallback? onEnd; + final _DragItemCallback? onCancel; + final VoidCallback? onDropCompleted; + final ReorderItemProxyDecorator? proxyDecorator; + final TickerProvider tickerProvider; + + late SliverReorderableGridState listState; + late int index; + late Widget child; + late Offset dragPosition; + late Offset dragOffset; + late Size itemSize; + late CapturedThemes capturedThemes; + ScrollableState? scrollable; + AnimationController? _proxyAnimation; + + void dispose() { + _proxyAnimation?.dispose(); + } + + void startDrag() { + _proxyAnimation = AnimationController( + vsync: tickerProvider, + duration: const Duration(milliseconds: 250), + ) + ..addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.dismissed) { + _dropCompleted(); + } + }) + ..forward(); + } + + @override + void update(DragUpdateDetails details) { + dragPosition += details.delta; + onUpdate?.call(this, dragPosition, details.delta); + } + + @override + void end(DragEndDetails details) { + _proxyAnimation!.reverse(); + onEnd?.call(this); + } + + @override + void cancel() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onCancel?.call(this); + } + + void _dropCompleted() { + _proxyAnimation?.dispose(); + _proxyAnimation = null; + onDropCompleted?.call(); + } + + Widget createProxy(BuildContext context) { + return capturedThemes.wrap( + _DragItemProxy( + listState: listState, + index: index, + size: itemSize, + animation: _proxyAnimation!, + position: dragPosition - dragOffset - _overlayOrigin(context), + proxyDecorator: proxyDecorator, + child: child, + ), + ); + } +} + +Offset _overlayOrigin(BuildContext context) { + final OverlayState overlay = Overlay.of(context); + final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox; + return overlayBox.localToGlobal(Offset.zero); +} + +class _DragItemProxy extends StatelessWidget { + const _DragItemProxy({ + Key? key, + required this.listState, + required this.index, + required this.child, + required this.position, + required this.size, + required this.animation, + required this.proxyDecorator, + }) : super(key: key); + + final SliverReorderableGridState listState; + final int index; + final Widget child; + final Offset position; + final Size size; + final AnimationController animation; + final ReorderItemProxyDecorator? proxyDecorator; + + @override + Widget build(BuildContext context) { + final Widget proxyChild = + proxyDecorator?.call(child, index, animation.view) ?? child; + final Offset overlayOrigin = _overlayOrigin(context); + + return MediaQuery( + // Remove the top padding so that any nested grid views in the item + // won't pick up the scaffold's padding in the overlay. + data: MediaQuery.of(context).removePadding(removeTop: true), + child: AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + Offset effectivePosition = position; + final Offset? dropPosition = listState._finalDropPosition; + if (dropPosition != null) { + effectivePosition = Offset.lerp(dropPosition - overlayOrigin, + effectivePosition, Curves.easeOut.transform(animation.value))!; + } + return Positioned( + left: effectivePosition.dx, + top: effectivePosition.dy, + child: SizedBox( + width: size.width, + height: size.height, + child: child, + ), + ); + }, + child: proxyChild, + ), + ); + } +} + +// A global key that takes its identity from the object and uses a value of a +// particular type to identify itself. +// +// The difference with GlobalObjectKey is that it uses [==] instead of [identical] +// of the objects used to generate widgets. +@optionalTypeArgs +class _ReorderableItemGlobalKey extends GlobalObjectKey { + const _ReorderableItemGlobalKey(this.subKey, this.index, this.state) + : super(subKey); + + final Key subKey; + final int index; + final SliverReorderableGridState state; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + return other is _ReorderableItemGlobalKey && + other.subKey == subKey && + other.index == index && + other.state == state; + } + + @override + int get hashCode => Object.hash(subKey, index, state); +} diff --git a/app/lib/features/kagi/presentation/tabs/assistant_tab.dart b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart index adf5f1c5..10bc3045 100644 --- a/app/lib/features/kagi/presentation/tabs/assistant_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart @@ -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()); - final textController = - useTextEditingController(text: sharedContent?.toString()); +// final formKey = useMemoized(() => GlobalKey()); +// 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( - 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( - 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( +// 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( +// 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; +// }, +// ); +// } +// } diff --git a/app/lib/features/kagi/presentation/tabs/search_tab.dart b/app/lib/features/kagi/presentation/tabs/search_tab.dart index f1b3a76c..36f1cdd3 100644 --- a/app/lib/features/kagi/presentation/tabs/search_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/search_tab.dart @@ -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()); +// final formKey = useMemoized(() => GlobalKey()); - 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 submitSearch() async { - if (activeBang != null && (formKey.currentState?.validate() == true)) { - final searchUri = await ref.read( - triggerBangSearchProvider(activeBang, textController.text).future, - ); +// Future 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( - 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: [ - 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( +// 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: [ +// 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), +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/app/lib/features/search/domain/entities/abstract/i_query_builder.dart b/app/lib/features/search/domain/entities/abstract/i_query_builder.dart index a3a16f22..d3cac5b7 100644 --- a/app/lib/features/search/domain/entities/abstract/i_query_builder.dart +++ b/app/lib/features/search/domain/entities/abstract/i_query_builder.dart @@ -3,4 +3,5 @@ abstract interface class IQueryBuilder { int get ftsMinTokenLength; String buildFtsQuery(String input); + String buildLikeQuery(String input); } diff --git a/app/lib/features/search/domain/fts_tokenizer.dart b/app/lib/features/search/domain/fts_tokenizer.dart index cb4f7515..ee5f37d9 100644 --- a/app/lib/features/search/domain/fts_tokenizer.dart +++ b/app/lib/features/search/domain/fts_tokenizer.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:lensai/features/search/domain/entities/abstract/i_query_builder.dart'; import 'package:lensai/features/search/domain/entities/bareword.dart'; +import 'package:lensai/features/search/domain/unix_tokenizer.dart'; typedef _Phrase = List; @@ -228,7 +229,26 @@ mixin PrefixQueryBuilderMixin implements IQueryBuilder { tokenLimit: ftsTokenLimit, ); - return queryBuilder.build(); + if (queryBuilder.hasTokens) { + return queryBuilder.build(); + } + + return ''; + } + + @override + String buildLikeQuery(String input) { + final likeQueryBuilder = UnixLikeQueryBuilder.tokenize( + input: input, + minTokenLength: 1, + tokenLimit: 5, + ); + + if (likeQueryBuilder.hasTokens) { + return likeQueryBuilder.build(); + } + + return ''; } } @@ -241,6 +261,25 @@ mixin TrigramQueryBuilderMixin implements IQueryBuilder { tokenLimit: ftsTokenLimit, ); - return queryBuilder.build(); + if (queryBuilder.hasTokens) { + return queryBuilder.build(); + } + + return ''; + } + + @override + String buildLikeQuery(String input) { + final likeQueryBuilder = UnixLikeQueryBuilder.tokenize( + input: input, + minTokenLength: 1, + tokenLimit: 5, + ); + + if (likeQueryBuilder.hasTokens) { + return likeQueryBuilder.build(); + } + + return ''; } } diff --git a/app/lib/features/search/presentation/screens/search.dart b/app/lib/features/search/presentation/screens/search.dart index ca4d56a4..91569f24 100644 --- a/app/lib/features/search/presentation/screens/search.dart +++ b/app/lib/features/search/presentation/screens/search.dart @@ -19,6 +19,7 @@ import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/con import 'package:lensai/features/search/domain/providers/search_suggestions.dart'; import 'package:lensai/features/search/presentation/widgets/bang_chips.dart'; import 'package:lensai/features/search/presentation/widgets/search_field.dart'; +import 'package:lensai/features/search/presentation/widgets/search_suggestion_list.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -166,76 +167,9 @@ class SearchScreen extends HookConsumerWidget { ), ), ), - HookConsumer( - builder: (context, ref, child) { - useListenableSelector( - searchTextController, - () => searchTextController.text.isNotEmpty, - ); - - final suggestions = - useStream(ref.watch(searchSuggestionsProvider())); - - final searchHistory = ref.watch(searchHistoryProvider); - - final searchText = searchTextController.text; - - if ((!searchText.isNotEmpty || !suggestions.hasData) && - (searchHistory.value?.isNotEmpty ?? false)) { - final entries = searchHistory.value!; - - return SliverList.builder( - itemCount: entries.length, - itemBuilder: (context, index) { - final query = entries[index].searchQuery; - - return ListTile( - leading: const Icon(Icons.history), - title: Text(query), - onLongPress: () { - searchTextController.text = query; - }, - onTap: () async { - await submitSearch(query); - }, - trailing: IconButton( - onPressed: () async { - await ref - .read(bangDataRepositoryProvider.notifier) - .removeSearchEntry(query); - }, - icon: const Icon(Icons.close), - ), - ); - }, - ); - } - - final prioritizedSuggestions = [ - if (searchText.isNotEmpty) searchText, - if (suggestions.data != null) - ...suggestions.data! - .whereNot((suggestion) => suggestion == searchText), - ]; - - return SliverList.builder( - itemCount: prioritizedSuggestions.length, - itemBuilder: (context, index) { - final suggestion = prioritizedSuggestions[index]; - - return ListTile( - leading: const Icon(Icons.search), - title: Text(suggestion), - onLongPress: () { - searchTextController.text = suggestion; - }, - onTap: () async { - await submitSearch(suggestion); - }, - ); - }, - ); - }, + SearchSuggestionList( + searchTextController: searchTextController, + submitSearch: submitSearch, ), const SliverToBoxAdapter( child: Divider(), @@ -274,7 +208,9 @@ class SearchScreen extends HookConsumerWidget { child: tabs.when( data: (data) { if (data == null) { - return const SizedBox.shrink(); + return const SliverToBoxAdapter( + child: SizedBox.shrink(), + ); } return SliverList.builder( @@ -283,12 +219,14 @@ class SearchScreen extends HookConsumerWidget { final result = data[index]; final headHasMatch = - result.title.contains(_matchPrefix); + result.title?.contains(_matchPrefix) ?? false; final bodyResult = result.extractedContent ?? result.fullContent; return ListTile( - title: MarkdownBody(data: result.title), + title: (result.title != null) + ? MarkdownBody(data: result.title!) + : null, subtitle: (!headHasMatch && bodyResult != null) ? MarkdownBody(data: bodyResult) : null, diff --git a/app/lib/features/search/presentation/widgets/search_field.dart b/app/lib/features/search/presentation/widgets/search_field.dart index 9ae9975a..1a5cb23d 100644 --- a/app/lib/features/search/presentation/widgets/search_field.dart +++ b/app/lib/features/search/presentation/widgets/search_field.dart @@ -3,8 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; -import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart'; import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; class SearchField extends HookConsumerWidget { final TextEditingController textEditingController; @@ -64,14 +64,6 @@ class SearchField extends HookConsumerWidget { }, ), ), - // autovalidateMode: AutovalidateMode.onUserInteraction, - // validator: (value) { - // if (value?.isEmpty ?? true) { - // return ''; - // } - - // return null; - // }, onTapOutside: (focusNode != null) ? (event) { focusNode!.unfocus(); diff --git a/app/lib/features/search/presentation/widgets/search_suggestion_list.dart b/app/lib/features/search/presentation/widgets/search_suggestion_list.dart new file mode 100644 index 00000000..fd27868f --- /dev/null +++ b/app/lib/features/search/presentation/widgets/search_suggestion_list.dart @@ -0,0 +1,90 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/bangs/domain/providers/bangs.dart'; +import 'package:lensai/features/bangs/domain/repositories/data.dart'; +import 'package:lensai/features/search/domain/providers/search_suggestions.dart'; + +class SearchSuggestionList extends HookConsumerWidget { + final TextEditingController searchTextController; + final Future Function(String query) submitSearch; + final bool showHistory; + + const SearchSuggestionList({ + required this.searchTextController, + required this.submitSearch, + this.showHistory = true, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + useListenableSelector( + searchTextController, + () => searchTextController.text.isNotEmpty, + ); + + final suggestions = useStream(ref.watch(searchSuggestionsProvider())); + + final searchHistory = ref.watch(searchHistoryProvider); + + final searchText = searchTextController.text; + + if (showHistory && + (!searchText.isNotEmpty || !suggestions.hasData) && + (searchHistory.value?.isNotEmpty ?? false)) { + final entries = searchHistory.value!; + + return SliverList.builder( + itemCount: entries.length, + itemBuilder: (context, index) { + final query = entries[index].searchQuery; + + return ListTile( + leading: const Icon(Icons.history), + title: Text(query), + onLongPress: () { + searchTextController.text = query; + }, + onTap: () async { + await submitSearch(query); + }, + trailing: IconButton( + onPressed: () async { + await ref + .read(bangDataRepositoryProvider.notifier) + .removeSearchEntry(query); + }, + icon: const Icon(Icons.close), + ), + ); + }, + ); + } + + final prioritizedSuggestions = [ + if (searchText.isNotEmpty) searchText, + if (suggestions.data != null) + ...suggestions.data!.whereNot((suggestion) => suggestion == searchText), + ]; + + return SliverList.builder( + itemCount: prioritizedSuggestions.length, + itemBuilder: (context, index) { + final suggestion = prioritizedSuggestions[index]; + + return ListTile( + leading: const Icon(Icons.search), + title: Text(suggestion), + onLongPress: () { + searchTextController.text = suggestion; + }, + onTap: () async { + await submitSearch(suggestion); + }, + ); + }, + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index d455deab..726d1f65 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -8,7 +8,7 @@ import 'package:lensai/features/settings/presentation/controllers/save_settings. import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart'; import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart'; import 'package:lensai/features/user/data/models/settings.dart'; -import 'package:lensai/features/user/domain/providers/cache.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart'; import 'package:lensai/features/user/domain/repositories/settings.dart'; @@ -149,11 +149,6 @@ class SettingsScreen extends HookConsumerWidget { const SizedBox( height: 16, ), - _buildSection(theme, 'Content Blocking'), - _buildSubSection(theme, 'Lists'), - const SizedBox( - height: 16, - ), _buildSection(theme, 'Bangs'), CustomListTile( title: 'Bang Frequencies', diff --git a/app/lib/features/user/data/providers.dart b/app/lib/features/user/data/providers.dart index 35e2770c..f8cfffbe 100644 --- a/app/lib/features/user/data/providers.dart +++ b/app/lib/features/user/data/providers.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:lensai/features/user/data/database/database.dart'; @@ -13,7 +15,7 @@ part 'providers.g.dart'; @Riverpod(keepAlive: true) UserDatabase userDatabase(Ref ref) { - return UserDatabase( + final db = UserDatabase( LazyDatabase(() async { // put the database file, called db.sqlite here, into the documents folder // for your app. @@ -35,4 +37,10 @@ UserDatabase userDatabase(Ref ref) { return NativeDatabase.createInBackground(file); }), ); + + ref.onDispose(() { + unawaited(db.close()); + }); + + return db; } diff --git a/app/lib/features/user/data/providers.g.dart b/app/lib/features/user/data/providers.g.dart index 6609562a..a3c95d2a 100644 --- a/app/lib/features/user/data/providers.g.dart +++ b/app/lib/features/user/data/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$userDatabaseHash() => r'a84f951e1673e34b6857e1618cd7d80497c6f3a1'; +String _$userDatabaseHash() => r'717de293750f5da91dc7da3f964bd07f5dd61e10'; /// See also [userDatabase]. @ProviderFor(userDatabase) diff --git a/app/lib/features/user/domain/providers.dart b/app/lib/features/user/domain/providers.dart new file mode 100644 index 00000000..64fd490a --- /dev/null +++ b/app/lib/features/user/domain/providers.dart @@ -0,0 +1,49 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:lensai/features/user/data/providers.dart'; +import 'package:pocketbase/pocketbase.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'providers.g.dart'; + +const _authKey = 'pb_auth'; + +@Riverpod() +Stream iconCacheSizeMegabytes(Ref ref) { + final repository = ref.watch(userDatabaseProvider); + return repository.cacheDao.iconCacheSize().watchSingle(); +} + +@Riverpod() +Future _storedAuthData(Ref ref) { + const secureStorage = FlutterSecureStorage(); + return secureStorage.read(key: _authKey); +} + +@Riverpod(keepAlive: true) +AsyncAuthStore authStore(Ref ref) { + const secureStorage = FlutterSecureStorage(); + + final intial = + ref.watch(_storedAuthDataProvider.select((value) => value.valueOrNull)); + + return AsyncAuthStore( + initial: intial, + save: (data) async { + await secureStorage.write(key: _authKey, value: data); + }, + ); +} + +@Riverpod() +Stream authState(Ref ref) { + final authStore = ref.watch(authStoreProvider); + return authStore.onChange; +} + +@Riverpod(keepAlive: true) +PocketBase pocketBase(Ref ref) { + final authStore = ref.watch(authStoreProvider); + + return PocketBase('http://192.168.2.104:8090', authStore: authStore); +} diff --git a/app/lib/features/user/domain/providers.g.dart b/app/lib/features/user/domain/providers.g.dart new file mode 100644 index 00000000..c0d44ecf --- /dev/null +++ b/app/lib/features/user/domain/providers.g.dart @@ -0,0 +1,94 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$iconCacheSizeMegabytesHash() => + r'43162fce3ce8b04a5a55ed52021b274afbde2861'; + +/// See also [iconCacheSizeMegabytes]. +@ProviderFor(iconCacheSizeMegabytes) +final iconCacheSizeMegabytesProvider = + AutoDisposeStreamProvider.internal( + iconCacheSizeMegabytes, + name: r'iconCacheSizeMegabytesProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$iconCacheSizeMegabytesHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef IconCacheSizeMegabytesRef = AutoDisposeStreamProviderRef; +String _$storedAuthDataHash() => r'5f7e3ef6233a2036f7ce3728131901a46b1e548e'; + +/// See also [_storedAuthData]. +@ProviderFor(_storedAuthData) +final _storedAuthDataProvider = AutoDisposeFutureProvider.internal( + _storedAuthData, + name: r'_storedAuthDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$storedAuthDataHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef _StoredAuthDataRef = AutoDisposeFutureProviderRef; +String _$authStoreHash() => r'766c001bbb8d7dc4e148eff3816c414d78e77f26'; + +/// See also [authStore]. +@ProviderFor(authStore) +final authStoreProvider = Provider.internal( + authStore, + name: r'authStoreProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$authStoreHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AuthStoreRef = ProviderRef; +String _$authStateHash() => r'ffa411ae0abc5c7bb3e8a93e783bd8398ce7f666'; + +/// See also [authState]. +@ProviderFor(authState) +final authStateProvider = AutoDisposeStreamProvider.internal( + authState, + name: r'authStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$authStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef AuthStateRef = AutoDisposeStreamProviderRef; +String _$pocketBaseHash() => r'9e1dc8057e300bc7ca697da9f68ec61e70fdbaab'; + +/// See also [pocketBase]. +@ProviderFor(pocketBase) +final pocketBaseProvider = Provider.internal( + pocketBase, + name: r'pocketBaseProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$pocketBaseHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef PocketBaseRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/domain/providers/cache.dart b/app/lib/features/user/domain/providers/cache.dart deleted file mode 100644 index 94d88634..00000000 --- a/app/lib/features/user/domain/providers/cache.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:lensai/features/user/data/providers.dart'; -import 'package:riverpod/riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'cache.g.dart'; - -@Riverpod() -Stream iconCacheSizeMegabytes(Ref ref) { - final repository = ref.watch(userDatabaseProvider); - return repository.cacheDao.iconCacheSize().watchSingle(); -} diff --git a/app/lib/features/user/domain/repositories/auth.dart b/app/lib/features/user/domain/repositories/auth.dart new file mode 100644 index 00000000..ce98210b --- /dev/null +++ b/app/lib/features/user/domain/repositories/auth.dart @@ -0,0 +1,52 @@ +import 'package:lensai/features/user/domain/providers.dart'; +import 'package:lensai/features/user/extensions/client_exception.dart'; +import 'package:pocketbase/pocketbase.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'auth.g.dart'; + +class AuthException implements Exception { + final String message; + + AuthException(this.message); + + @override + String toString() { + return message; + } +} + +@Riverpod() +class AuthRepository extends _$AuthRepository { + late PocketBase _pb; + + Future authWithPassword(String user, String password) async { + try { + return await _pb.collection('users').authWithPassword(user, password); + } on ClientException catch (e) { + throw AuthException(e.errorMessage); + } + } + + Future createUserWithPassword( + String email, + String password, + ) async { + try { + return await _pb.collection('users').create( + body: { + "email": email, + "password": password, + "passwordConfirm": password, + }, + ); + } on ClientException catch (e) { + throw AuthException(e.errorMessage); + } + } + + @override + void build() { + _pb = ref.watch(pocketBaseProvider); + } +} diff --git a/app/lib/features/user/domain/repositories/auth.g.dart b/app/lib/features/user/domain/repositories/auth.g.dart new file mode 100644 index 00000000..1bdcd08a --- /dev/null +++ b/app/lib/features/user/domain/repositories/auth.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auth.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$authRepositoryHash() => r'90ea6f082ef968831763c7f7ee31863f1fe329fe'; + +/// See also [AuthRepository]. +@ProviderFor(AuthRepository) +final authRepositoryProvider = + AutoDisposeNotifierProvider.internal( + AuthRepository.new, + name: r'authRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$authRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AuthRepository = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/extensions/client_exception.dart b/app/lib/features/user/extensions/client_exception.dart new file mode 100644 index 00000000..cfd728d5 --- /dev/null +++ b/app/lib/features/user/extensions/client_exception.dart @@ -0,0 +1,24 @@ +import 'package:lensai/extensions/string.dart'; +import 'package:pocketbase/pocketbase.dart'; + +extension ClientExceptionExtension on ClientException { + String get errorMessage { + var message = "${response["message"] as String? ?? ""}\n\n"; + final data = response["data"] as Map; + try { + data.forEach((key, value) { + final detailedMessage = (value as Map)["message"]; + if (detailedMessage != null) { + message += '${key.toCapitalized()}: $detailedMessage\n'; + } + }); + } catch (_) {} + try { + if (originalError != null) { + message += ' $originalError'; + } + } catch (_) {} + + return message.trim(); + } +} diff --git a/app/lib/features/user/presentation/controllers/controllers.dart b/app/lib/features/user/presentation/controllers/controllers.dart new file mode 100644 index 00000000..085f1994 --- /dev/null +++ b/app/lib/features/user/presentation/controllers/controllers.dart @@ -0,0 +1,33 @@ +import 'package:lensai/features/user/domain/repositories/auth.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'controllers.g.dart'; + +@Riverpod() +class AuthController extends _$AuthController { + @override + FutureOr build() {} + + Future authWithPassword(String user, String password) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref + .read(authRepositoryProvider.notifier) + .authWithPassword(user, password), + ); + } + + Future registerWithPassword(String user, String password) async { + state = const AsyncLoading(); + state = await AsyncValue.guard(() { + final repo = ref.read(authRepositoryProvider.notifier); + return repo + .createUserWithPassword(user, password) + .then((_) => repo.authWithPassword(user, password)); + }); + } + + void clearState() { + state = const AsyncValue.data(null); + } +} diff --git a/app/lib/features/user/presentation/controllers/controllers.g.dart b/app/lib/features/user/presentation/controllers/controllers.g.dart new file mode 100644 index 00000000..cfbe9514 --- /dev/null +++ b/app/lib/features/user/presentation/controllers/controllers.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'controllers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$authControllerHash() => r'5b56651948683d669f29946fe02bdb004f436afb'; + +/// See also [AuthController]. +@ProviderFor(AuthController) +final authControllerProvider = + AutoDisposeAsyncNotifierProvider.internal( + AuthController.new, + name: r'authControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$authControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AuthController = AutoDisposeAsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/presentation/screens/auth.dart b/app/lib/features/user/presentation/screens/auth.dart new file mode 100644 index 00000000..fc848c0a --- /dev/null +++ b/app/lib/features/user/presentation/screens/auth.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/user/domain/providers.dart'; +import 'package:lensai/features/user/presentation/controllers/controllers.dart'; + +enum _AuthType { + login, + signup, +} + +class UserAuthScreen extends HookConsumerWidget { + const UserAuthScreen(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + + final authType = useState(_AuthType.login); + + final userTextController = useTextEditingController(); + final passwordTextController = useTextEditingController(); + final confirmPasswordTextController = useTextEditingController(); + + final authState = ref.watch(authControllerProvider); + + ref.listen( + authStateProvider.select((value) => value.valueOrNull), + (previous, next) { + if (next?.token.isNotEmpty ?? false) { + context.pop(true); + } + }, + ); + + return Dialog( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: userTextController, + decoration: const InputDecoration( + label: Text('Email'), + icon: Icon(Icons.account_circle), + ), + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Email must be provided'; + } + + return null; + }, + keyboardType: TextInputType.emailAddress, + ), + const SizedBox( + height: 8.0, + ), + HookBuilder( + builder: (context) { + final obscure = useState(true); + + return Column( + children: [ + TextFormField( + controller: passwordTextController, + decoration: InputDecoration( + label: const Text('Password'), + icon: const Icon(Icons.lock), + suffixIcon: IconButton( + onPressed: () { + obscure.value = !obscure.value; + }, + icon: Icon( + obscure.value + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + obscureText: obscure.value, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Password must be provided'; + } + + return null; + }, + ), + if (authType.value == _AuthType.signup) + TextFormField( + controller: confirmPasswordTextController, + decoration: InputDecoration( + label: const Text('Confirm Password'), + icon: const Icon(Icons.lock), + suffixIcon: IconButton( + onPressed: () { + obscure.value = !obscure.value; + }, + icon: Icon( + obscure.value + ? Icons.visibility + : Icons.visibility_off, + ), + ), + ), + obscureText: obscure.value, + validator: (value) { + if (value?.isEmpty ?? true) { + return 'Password must be provided'; + } + + if (value != passwordTextController.text) { + return 'Password not matching'; + } + + return null; + }, + ), + ], + ); + }, + ), + const SizedBox( + height: 16, + ), + if (authState.hasError && !authState.isLoading) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text( + authState.error.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.error, + ), + ), + ), + if (!authState.isLoading) + switch (authType.value) { + _AuthType.login => FilledButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + final controller = + ref.read(authControllerProvider.notifier); + + await controller.authWithPassword( + userTextController.text, + passwordTextController.text, + ); + } + }, + child: const Text('Login'), + ), + _AuthType.signup => FilledButton( + onPressed: () async { + if (formKey.currentState?.validate() ?? false) { + final controller = + ref.read(authControllerProvider.notifier); + + await controller.registerWithPassword( + userTextController.text, + passwordTextController.text, + ); + } + }, + child: const Text('Signup'), + ) + } + else + const CircularProgressIndicator(), + if (!authState.isLoading) + switch (authType.value) { + _AuthType.login => TextButton( + onPressed: () { + final controller = + ref.read(authControllerProvider.notifier); + + controller.clearState(); + authType.value = _AuthType.signup; + }, + child: const Text('Signup'), + ), + _AuthType.signup => TextButton( + onPressed: () { + final controller = + ref.read(authControllerProvider.notifier); + + controller.clearState(); + authType.value = _AuthType.login; + }, + child: const Text('Login'), + ), + }, + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/presentation/hooks/draggable_scrollable_controller.dart b/app/lib/presentation/hooks/draggable_scrollable_controller.dart new file mode 100644 index 00000000..427adcb6 --- /dev/null +++ b/app/lib/presentation/hooks/draggable_scrollable_controller.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +DraggableScrollableController useDraggableScrollableController() { + return use(const _DraggableScrollableControllerHook()); +} + +class _DraggableScrollableControllerHook + extends Hook { + const _DraggableScrollableControllerHook(); + + @override + HookState> + createState() { + return _DraggableScrollableControllerHookState(); + } +} + +class _DraggableScrollableControllerHookState extends HookState< + DraggableScrollableController, _DraggableScrollableControllerHook> { + late final _controller = DraggableScrollableController(); + + @override + DraggableScrollableController build(BuildContext context) => _controller; + + @override + void dispose() => _controller.dispose(); + + @override + String get debugLabel => 'useDraggableScrollableController'; +} diff --git a/app/lib/presentation/hooks/menu_controller.dart b/app/lib/presentation/hooks/menu_controller.dart new file mode 100644 index 00000000..87d7ad50 --- /dev/null +++ b/app/lib/presentation/hooks/menu_controller.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +MenuController useMenuController() { + return useMemoized(() => MenuController()); +} diff --git a/app/lib/presentation/hooks/overlay_portal_controller.dart b/app/lib/presentation/hooks/overlay_portal_controller.dart index c97e18c6..8bb5d6b9 100644 --- a/app/lib/presentation/hooks/overlay_portal_controller.dart +++ b/app/lib/presentation/hooks/overlay_portal_controller.dart @@ -17,10 +17,10 @@ class _OverlayPortalControllerHook extends Hook { class _OverlayPortalControllerHookState extends HookState { - late final controller = OverlayPortalController(); + late final _controller = OverlayPortalController(); @override - OverlayPortalController build(BuildContext context) => controller; + OverlayPortalController build(BuildContext context) => _controller; @override String get debugLabel => 'useOverlayPortalController'; diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart b/app/lib/presentation/widgets/speech_to_text_button.dart similarity index 100% rename from app/lib/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart rename to app/lib/presentation/widgets/speech_to_text_button.dart diff --git a/app/lib/utils/langchain_utils.dart b/app/lib/utils/langchain_utils.dart new file mode 100644 index 00000000..7dcc4e9c --- /dev/null +++ b/app/lib/utils/langchain_utils.dart @@ -0,0 +1,15 @@ +import 'dart:typed_data'; + +import 'package:langchain/langchain.dart'; + +String combineDocuments( + List documents, { + String separator = '\n\n', +}) { + return documents.map((d) => d.pageContent).join(separator); +} + +Uint8List serializeVector(List vector) { + final buffer = Float32List.fromList(vector).buffer; + return Uint8List.view(buffer); +} diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 70e43931..98d5e86e 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -8,8 +8,10 @@ environment: dependencies: collection: ^1.19.0 - copy_with_extension: ^6.0.0 - drift: ^2.23.0 + copy_with_extension: ^6.0.1 + cross_cache: ^0.0.4 + crypto: ^3.0.6 + drift: ^2.24.0 dynamic_color: ^1.7.0 exceptions: ^0.6.1 expandable_page_view: ^1.0.17 @@ -18,42 +20,53 @@ dependencies: file_picker: ^8.1.7 flutter: sdk: flutter + flutter_chat_core: + path: ../../flutter_chat_ui/packages/flutter_chat_core + flutter_chat_ui: + path: ../../flutter_chat_ui/packages/flutter_chat_ui flutter_hooks: ^0.20.5 - flutter_markdown: ^0.7.4+3 + flutter_markdown: ^0.7.5 flutter_material_design_icons: ^1.1.7447 flutter_mozilla_components: path: ../packages/flutter_mozilla_components flutter_pdf_text: ^0.8.1 - flutter_secure_storage: ^9.2.2 + flutter_secure_storage: ^10.0.0-beta.4 flutter_sharing_intent: ^1.1.1 - go_router: ^14.6.2 + go_router: ^14.6.3 google_fonts: ^6.2.1 home_widget: ^0.7.0 hooks_riverpod: ^2.6.1 html: ^0.15.5 - http: ^1.2.2 + http: ^1.3.0 json_annotation: ^4.9.0 + langchain: + path: ../../langchain_dart/packages/langchain + langchain_openai: + path: ../../langchain_dart/packages/langchain_openai lexo_rank: git: url: https://github.com/FaFre/lexo_rank.git logger: ^2.5.0 - markdown: ^7.2.2 + markdown: ^7.3.0 mime: ^2.0.0 - package_info_plus: ^8.1.2 + package_info_plus: ^8.1.3 path: ^1.9.0 path_provider: ^2.1.5 + pocketbase: ^0.21.0 + provider: ^6.1.2 reorderable_grid: ^1.0.10 riverpod: ^2.6.1 riverpod_annotation: ^2.6.1 rxdart: ^0.28.0 - share_plus: ^10.1.3 - shared_preferences: ^2.3.5 - skeletonizer: ^1.4.2 + share_plus: ^10.1.4 + skeletonizer: ^1.4.3 speech_to_text_google_dialog: git: url: https://github.com/FaFre/speech_to_text_google_dialog.git - sqlite3: ^2.5.0 - sqlite3_flutter_libs: ^0.5.28 + sqlite3: ^2.7.2 + sqlite3_flutter_libs: ^0.5.29 + sqlite3_vec: + path: ../packages/sqlite3_vec synchronized: ^3.3.0+3 text_scroll: ^0.2.0 timeago: ^3.7.0 @@ -66,15 +79,19 @@ dependencies: watcher: ^1.1.1 xxh3: ^1.2.0 +dependency_overrides: + flutter_chat_core: + path: ../../flutter_chat_ui/packages/flutter_chat_core + dev_dependencies: build_runner: ^2.4.14 copy_with_extension_gen: ^6.0.0 custom_lint: ^0.6.10 dependency_validator: ^4.1.2 - drift_dev: ^2.23.0 + drift_dev: ^2.24.0 fast_equatable_lint: ^0.3.0 - flutter_launcher_icons: ^0.14.2 - go_router_builder: ^2.7.1 + flutter_launcher_icons: ^0.14.3 + go_router_builder: ^2.7.3 json_serializable: ^6.9.0 lint: ^2.3.0 riverpod_generator: ^2.6.1 diff --git a/app/pubspec_overrides.yaml b/app/pubspec_overrides.yaml index d146a556..e3b18934 100644 --- a/app/pubspec_overrides.yaml +++ b/app/pubspec_overrides.yaml @@ -1,4 +1,8 @@ -# melos_managed_dependency_overrides: flutter_mozilla_components +# melos_managed_dependency_overrides: flutter_mozilla_components,sqlite3_vec,flutter_chat_core dependency_overrides: + flutter_chat_core: + path: ../../flutter_chat_ui/packages/flutter_chat_core flutter_mozilla_components: path: ../packages/flutter_mozilla_components + sqlite3_vec: + path: ../packages/sqlite3_vec diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt index b2567d8c..1aaf5d84 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.7.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") diff --git a/packages/flutter_mozilla_components/javascript/readability/dist/readability.min.js b/packages/flutter_mozilla_components/javascript/readability/dist/readability.min.js index 19b0a03b..4d1bab66 100644 --- a/packages/flutter_mozilla_components/javascript/readability/dist/readability.min.js +++ b/packages/flutter_mozilla_components/javascript/readability/dist/readability.min.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.ReadabilityBundle=t():e.ReadabilityBundle=t()}(this,(()=>(()=>{var e={804:e=>{var t={unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i};function i(e){return(!e.style||"none"!=e.style.display)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))}e.exports=function(e,n={}){"function"==typeof n&&(n={visibilityChecker:n});var r={minScore:20,minContentLength:140,visibilityChecker:i};n=Object.assign(r,n);var a=e.querySelectorAll("p, pre, article"),s=e.querySelectorAll("div > br");if(s.length){var o=new Set(a);[].forEach.call(s,(function(e){o.add(e.parentNode)})),a=Array.from(o)}var l=0;return[].some.call(a,(function(e){if(!n.visibilityChecker(e))return!1;var i=e.className+" "+e.id;if(t.unlikelyCandidates.test(i)&&!t.okMaybeItsACandidate.test(i))return!1;if(e.matches("li p"))return!1;var r=e.textContent.trim().length;return!(rn.minScore}))}},238:e=>{function t(e,t){if(t&&t.documentElement)e=t,t=arguments[2];else if(!e||!e.documentElement)throw new Error("First argument to Readability constructor should be a document object.");if(t=t||{},this._doc=e,this._docJSDOMParser=this._doc.firstChild.__JSDOMParser__,this._articleTitle=null,this._articleByline=null,this._articleDir=null,this._articleSiteName=null,this._attempts=[],this._debug=!!t.debug,this._maxElemsToParse=t.maxElemsToParse||this.DEFAULT_MAX_ELEMS_TO_PARSE,this._nbTopCandidates=t.nbTopCandidates||this.DEFAULT_N_TOP_CANDIDATES,this._charThreshold=t.charThreshold||this.DEFAULT_CHAR_THRESHOLD,this._classesToPreserve=this.CLASSES_TO_PRESERVE.concat(t.classesToPreserve||[]),this._keepClasses=!!t.keepClasses,this._serializer=t.serializer||function(e){return e.innerHTML},this._disableJSONLD=!!t.disableJSONLD,this._allowedVideoRegex=t.allowedVideoRegex||this.REGEXPS.videos,this._flags=this.FLAG_STRIP_UNLIKELYS|this.FLAG_WEIGHT_CLASSES|this.FLAG_CLEAN_CONDITIONALLY,this._debug){let e=function(e){if(e.nodeType==e.TEXT_NODE)return`${e.nodeName} ("${e.textContent}")`;let t=Array.from(e.attributes||[],(function(e){return`${e.name}="${e.value}"`})).join(" ");return`<${e.localName} ${t}>`};this.log=function(){if("undefined"!=typeof console){let t=Array.from(arguments,(t=>t&&t.nodeType==this.ELEMENT_NODE?e(t):t));t.unshift("Reader: (Readability)"),console.log.apply(console,t)}else if("undefined"!=typeof dump){var t=Array.prototype.map.call(arguments,(function(t){return t&&t.nodeName?e(t):t})).join(" ");dump("Reader: (Readability) "+t+"\n")}}}else this.log=function(){}}t.prototype={FLAG_STRIP_UNLIKELYS:1,FLAG_WEIGHT_CLASSES:2,FLAG_CLEAN_CONDITIONALLY:4,ELEMENT_NODE:1,TEXT_NODE:3,DEFAULT_MAX_ELEMS_TO_PARSE:0,DEFAULT_N_TOP_CANDIDATES:5,DEFAULT_TAGS_TO_SCORE:"section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),DEFAULT_CHAR_THRESHOLD:500,REGEXPS:{unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i,positive:/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,negative:/-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,extraneous:/print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,byline:/byline|author|dateline|writtenby|p-author/i,replaceFonts:/<(\/?)font[^>]*>/gi,normalize:/\s{2,}/g,videos:/\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,shareElements:/(\b|_)(share|sharedaddy)(\b|_)/i,nextLink:/(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,prevLink:/(prev|earl|old|new|<|«)/i,tokenize:/\W+/g,whitespace:/^\s*$/,hasContent:/\S$/,hashUrl:/^#.+/,srcsetUrl:/(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,b64DataUrl:/^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,commas:/\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,jsonLdArticleTypes:/^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/},UNLIKELY_ROLES:["menu","menubar","complementary","navigation","alert","alertdialog","dialog"],DIV_TO_P_ELEMS:new Set(["BLOCKQUOTE","DL","DIV","IMG","OL","P","PRE","TABLE","UL"]),ALTER_TO_DIV_EXCEPTIONS:["DIV","ARTICLE","SECTION","P"],PRESENTATIONAL_ATTRIBUTES:["align","background","bgcolor","border","cellpadding","cellspacing","frame","hspace","rules","style","valign","vspace"],DEPRECATED_SIZE_ATTRIBUTE_ELEMS:["TABLE","TH","TD","HR","PRE"],PHRASING_ELEMS:["ABBR","AUDIO","B","BDO","BR","BUTTON","CITE","CODE","DATA","DATALIST","DFN","EM","EMBED","I","IMG","INPUT","KBD","LABEL","MARK","MATH","METER","NOSCRIPT","OBJECT","OUTPUT","PROGRESS","Q","RUBY","SAMP","SCRIPT","SELECT","SMALL","SPAN","STRONG","SUB","SUP","TEXTAREA","TIME","VAR","WBR"],CLASSES_TO_PRESERVE:["page"],HTML_ESCAPE_MAP:{lt:"<",gt:">",amp:"&",quot:'"',apos:"'"},_postProcessContent:function(e){this._fixRelativeUris(e),this._simplifyNestedElements(e),this._keepClasses||this._cleanClasses(e)},_removeNodes:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _removeNodes");for(var i=e.length-1;i>=0;i--){var n=e[i],r=n.parentNode;r&&(t&&!t.call(this,n,i,e)||r.removeChild(n))}},_replaceNodeTags:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _replaceNodeTags");for(const i of e)this._setNodeTag(i,t)},_forEachNode:function(e,t){Array.prototype.forEach.call(e,t,this)},_findNode:function(e,t){return Array.prototype.find.call(e,t,this)},_someNode:function(e,t){return Array.prototype.some.call(e,t,this)},_everyNode:function(e,t){return Array.prototype.every.call(e,t,this)},_concatNodeLists:function(){var e=Array.prototype.slice,t=e.call(arguments).map((function(t){return e.call(t)}));return Array.prototype.concat.apply([],t)},_getAllNodesWithTag:function(e,t){return e.querySelectorAll?e.querySelectorAll(t.join(",")):[].concat.apply([],t.map((function(t){var i=e.getElementsByTagName(t);return Array.isArray(i)?i:Array.from(i)})))},_cleanClasses:function(e){var t=this._classesToPreserve,i=(e.getAttribute("class")||"").split(/\s+/).filter((function(e){return-1!=t.indexOf(e)})).join(" ");for(i?e.setAttribute("class",i):e.removeAttribute("class"),e=e.firstElementChild;e;e=e.nextElementSibling)this._cleanClasses(e)},_fixRelativeUris:function(e){var t=this._doc.baseURI,i=this._doc.documentURI;function n(e){if(t==i&&"#"==e.charAt(0))return e;try{return new URL(e,t).href}catch(e){}return e}var r=this._getAllNodesWithTag(e,["a"]);this._forEachNode(r,(function(e){var t=e.getAttribute("href");if(t)if(0===t.indexOf("javascript:"))if(1===e.childNodes.length&&e.childNodes[0].nodeType===this.TEXT_NODE){var i=this._doc.createTextNode(e.textContent);e.parentNode.replaceChild(i,e)}else{for(var r=this._doc.createElement("span");e.firstChild;)r.appendChild(e.firstChild);e.parentNode.replaceChild(r,e)}else e.setAttribute("href",n(t))}));var a=this._getAllNodesWithTag(e,["img","picture","figure","video","audio","source"]);this._forEachNode(a,(function(e){var t=e.getAttribute("src"),i=e.getAttribute("poster"),r=e.getAttribute("srcset");if(t&&e.setAttribute("src",n(t)),i&&e.setAttribute("poster",n(i)),r){var a=r.replace(this.REGEXPS.srcsetUrl,(function(e,t,i,r){return n(t)+(i||"")+r}));e.setAttribute("srcset",a)}}))},_simplifyNestedElements:function(e){for(var t=e;t;){if(t.parentNode&&["DIV","SECTION"].includes(t.tagName)&&(!t.id||!t.id.startsWith("readability"))){if(this._isElementWithoutContent(t)){t=this._removeAndGetNext(t);continue}if(this._hasSingleTagInsideElement(t,"DIV")||this._hasSingleTagInsideElement(t,"SECTION")){for(var i=t.children[0],n=0;n»] /.test(t))n=/ [\\\/>»] /.test(t),r(t=i.replace(/(.*)[\|\-\\\/>»] .*/gi,"$1"))<3&&(t=i.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi,"$1"));else if(-1!==t.indexOf(": ")){var a=this._concatNodeLists(e.getElementsByTagName("h1"),e.getElementsByTagName("h2")),s=t.trim();this._someNode(a,(function(e){return e.textContent.trim()===s}))||(r(t=i.substring(i.lastIndexOf(":")+1))<3?t=i.substring(i.indexOf(":")+1):r(i.substr(0,i.indexOf(":")))>5&&(t=i))}else if(t.length>150||t.length<15){var o=e.getElementsByTagName("h1");1===o.length&&(t=this._getInnerText(o[0]))}var l=r(t=t.trim().replace(this.REGEXPS.normalize," "));return l<=4&&(!n||l!=r(i.replace(/[\|\-\\\/>»]+/g,""))-1)&&(t=i),t},_prepDocument:function(){var e=this._doc;this._removeNodes(this._getAllNodesWithTag(e,["style"])),e.body&&this._replaceBrs(e.body),this._replaceNodeTags(this._getAllNodesWithTag(e,["font"]),"SPAN")},_nextNode:function(e){for(var t=e;t&&t.nodeType!=this.ELEMENT_NODE&&this.REGEXPS.whitespace.test(t.textContent);)t=t.nextSibling;return t},_replaceBrs:function(e){this._forEachNode(this._getAllNodesWithTag(e,["br"]),(function(e){for(var t=e.nextSibling,i=!1;(t=this._nextNode(t))&&"BR"==t.tagName;){i=!0;var n=t.nextSibling;t.parentNode.removeChild(t),t=n}if(i){var r=this._doc.createElement("p");for(e.parentNode.replaceChild(r,e),t=r.nextSibling;t;){if("BR"==t.tagName){var a=this._nextNode(t.nextSibling);if(a&&"BR"==a.tagName)break}if(!this._isPhrasingContent(t))break;var s=t.nextSibling;r.appendChild(t),t=s}for(;r.lastChild&&this._isWhitespace(r.lastChild);)r.removeChild(r.lastChild);"P"===r.parentNode.tagName&&this._setNodeTag(r.parentNode,"DIV")}}))},_setNodeTag:function(e,t){if(this.log("_setNodeTag",e,t),this._docJSDOMParser)return e.localName=t.toLowerCase(),e.tagName=t.toUpperCase(),e;for(var i=e.ownerDocument.createElement(t);e.firstChild;)i.appendChild(e.firstChild);e.parentNode.replaceChild(i,e),e.readability&&(i.readability=e.readability);for(var n=0;n!i.includes(e))).join(" ").length/n.join(" ").length:0},_checkByline:function(e,t){if(this._articleByline)return!1;if(void 0!==e.getAttribute)var i=e.getAttribute("rel"),n=e.getAttribute("itemprop");return!(!("author"===i||n&&-1!==n.indexOf("author")||this.REGEXPS.byline.test(t))||!this._isValidByline(e.textContent))&&(this._articleByline=e.textContent.trim(),!0)},_getNodeAncestors:function(e,t){t=t||0;for(var i=0,n=[];e.parentNode&&(n.push(e.parentNode),!t||++i!==t);)e=e.parentNode;return n},_grabArticle:function(e){this.log("**** grabArticle ****");var t=this._doc,i=null!==e;if(!(e=e||this._doc.body))return this.log("No body found in document. Abort."),null;for(var n=e.innerHTML;;){this.log("Starting grabArticle loop");var r=this._flagIsActive(this.FLAG_STRIP_UNLIKELYS),a=[],s=this._doc.documentElement;let V=!0;for(;s;){"HTML"===s.tagName&&(this._articleLang=s.getAttribute("lang"));var o=s.className+" "+s.id;if(this._isProbablyVisible(s))if("true"!=s.getAttribute("aria-modal")||"dialog"!=s.getAttribute("role"))if(this._checkByline(s,o))s=this._removeAndGetNext(s);else if(V&&this._headerDuplicatesTitle(s))this.log("Removing header: ",s.textContent.trim(),this._articleTitle.trim()),V=!1,s=this._removeAndGetNext(s);else{if(r){if(this.REGEXPS.unlikelyCandidates.test(o)&&!this.REGEXPS.okMaybeItsACandidate.test(o)&&!this._hasAncestorTag(s,"table")&&!this._hasAncestorTag(s,"code")&&"BODY"!==s.tagName&&"A"!==s.tagName){this.log("Removing unlikely candidate - "+o),s=this._removeAndGetNext(s);continue}if(this.UNLIKELY_ROLES.includes(s.getAttribute("role"))){this.log("Removing content with role "+s.getAttribute("role")+" - "+o),s=this._removeAndGetNext(s);continue}}if("DIV"!==s.tagName&&"SECTION"!==s.tagName&&"HEADER"!==s.tagName&&"H1"!==s.tagName&&"H2"!==s.tagName&&"H3"!==s.tagName&&"H4"!==s.tagName&&"H5"!==s.tagName&&"H6"!==s.tagName||!this._isElementWithoutContent(s)){if(-1!==this.DEFAULT_TAGS_TO_SCORE.indexOf(s.tagName)&&a.push(s),"DIV"===s.tagName){for(var l=null,c=s.firstChild;c;){var h=c.nextSibling;if(this._isPhrasingContent(c))null!==l?l.appendChild(c):this._isWhitespace(c)||(l=t.createElement("p"),s.replaceChild(l,c),l.appendChild(c));else if(null!==l){for(;l.lastChild&&this._isWhitespace(l.lastChild);)l.removeChild(l.lastChild);l=null}c=h}if(this._hasSingleTagInsideElement(s,"P")&&this._getLinkDensity(s)<.25){var d=s.children[0];s.parentNode.replaceChild(d,s),s=d,a.push(s)}else this._hasChildBlockElement(s)||(s=this._setNodeTag(s,"P"),a.push(s))}s=this._getNextNode(s)}else s=this._removeAndGetNext(s)}else s=this._removeAndGetNext(s);else this.log("Removing hidden node - "+o),s=this._removeAndGetNext(s)}var u=[];this._forEachNode(a,(function(e){if(e.parentNode&&void 0!==e.parentNode.tagName){var t=this._getInnerText(e);if(!(t.length<25)){var i=this._getNodeAncestors(e,5);if(0!==i.length){var n=0;n+=1,n+=t.split(this.REGEXPS.commas).length,n+=Math.min(Math.floor(t.length/100),3),this._forEachNode(i,(function(e,t){if(e.tagName&&e.parentNode&&void 0!==e.parentNode.tagName){if(void 0===e.readability&&(this._initializeNode(e),u.push(e)),0===t)var i=1;else i=1===t?2:3*t;e.readability.contentScore+=n/i}}))}}}}));for(var g=[],f=0,p=u.length;fN.readability.contentScore){g.splice(b,0,m),g.length>this._nbTopCandidates&&g.pop();break}}}var E,y=g[0]||null,T=!1;if(null===y||"BODY"===y.tagName){for(y=t.createElement("DIV"),T=!0;e.firstChild;)this.log("Moving child out:",e.firstChild),y.appendChild(e.firstChild);e.appendChild(y),this._initializeNode(y)}else if(y){for(var v=[],A=1;A=.75&&v.push(this._getNodeAncestors(g[A]));if(v.length>=3)for(E=y.parentNode;"BODY"!==E.tagName;){for(var S=0,C=0;C=3){y=E;break}E=E.parentNode}y.readability||this._initializeNode(y),E=y.parentNode;for(var L=y.readability.contentScore,x=L/3;"BODY"!==E.tagName;)if(E.readability){var R=E.readability.contentScore;if(RL){y=E;break}L=E.readability.contentScore,E=E.parentNode}else E=E.parentNode;for(E=y.parentNode;"BODY"!=E.tagName&&1==E.children.length;)E=(y=E).parentNode;y.readability||this._initializeNode(y)}var O=t.createElement("DIV");i&&(O.id="readability-content");for(var D=Math.max(10,.2*y.readability.contentScore),I=(E=y.parentNode).children,w=0,P=I.length;w=D)B=!0;else if("P"===k.nodeName){var H=this._getLinkDensity(k),G=this._getInnerText(k),U=G.length;(U>80&&H<.25||U<80&&U>0&&0===H&&-1!==G.search(/\.( |$)/))&&(B=!0)}}B&&(this.log("Appending node:",k),-1===this.ALTER_TO_DIV_EXCEPTIONS.indexOf(k.nodeName)&&(this.log("Altering sibling:",k,"to div."),k=this._setNodeTag(k,"DIV")),O.appendChild(k),I=E.children,w-=1,P-=1)}if(this._debug&&this.log("Article content pre-prep: "+O.innerHTML),this._prepArticle(O),this._debug&&this.log("Article content post-prep: "+O.innerHTML),T)y.id="readability-page-1",y.className="page";else{var $=t.createElement("DIV");for($.id="readability-page-1",$.className="page";O.firstChild;)$.appendChild(O.firstChild);O.appendChild($)}this._debug&&this.log("Article content after paging: "+O.innerHTML);var j=!0,F=this._getInnerText(O,!0).length;if(F0&&e.length<100)},_unescapeHtmlEntities:function(e){if(!e)return e;var t=this.HTML_ESCAPE_MAP;return e.replace(/&(quot|amp|apos|lt|gt);/g,(function(e,i){return t[i]})).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi,(function(e,t,i){var n=parseInt(t||i,t?16:10);return String.fromCharCode(n)}))},_getJSONLD:function(e){var t,i=this._getAllNodesWithTag(e,["script"]);return this._forEachNode(i,(function(e){if(!t&&"application/ld+json"===e.getAttribute("type"))try{var i=e.textContent.replace(/^\s*\s*$/g,""),n=JSON.parse(i);if(!n["@context"]||!n["@context"].match(/^https?\:\/\/schema\.org$/))return;if(!n["@type"]&&Array.isArray(n["@graph"])&&(n=n["@graph"].find((function(e){return(e["@type"]||"").match(this.REGEXPS.jsonLdArticleTypes)}))),!n||!n["@type"]||!n["@type"].match(this.REGEXPS.jsonLdArticleTypes))return;if(t={},"string"==typeof n.name&&"string"==typeof n.headline&&n.name!==n.headline){var r=this._getArticleTitle(),a=this._textSimilarity(n.name,r)>.75,s=this._textSimilarity(n.headline,r)>.75;t.title=s&&!a?n.headline:n.name}else"string"==typeof n.name?t.title=n.name.trim():"string"==typeof n.headline&&(t.title=n.headline.trim());return n.author&&("string"==typeof n.author.name?t.byline=n.author.name.trim():Array.isArray(n.author)&&n.author[0]&&"string"==typeof n.author[0].name&&(t.byline=n.author.filter((function(e){return e&&"string"==typeof e.name})).map((function(e){return e.name.trim()})).join(", "))),"string"==typeof n.description&&(t.excerpt=n.description.trim()),n.publisher&&"string"==typeof n.publisher.name&&(t.siteName=n.publisher.name.trim()),void("string"==typeof n.datePublished&&(t.datePublished=n.datePublished.trim()))}catch(e){this.log(e.message)}})),t||{}},_getArticleMetadata:function(e){var t={},i={},n=this._doc.getElementsByTagName("meta"),r=/\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi,a=/^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;return this._forEachNode(n,(function(e){var t=e.getAttribute("name"),n=e.getAttribute("property"),s=e.getAttribute("content");if(s){var o=null,l=null;n&&(o=n.match(r))&&(l=o[0].toLowerCase().replace(/\s/g,""),i[l]=s.trim()),!o&&t&&a.test(t)&&(l=t,s&&(l=l.toLowerCase().replace(/\s/g,"").replace(/\./g,":"),i[l]=s.trim()))}})),t.title=e.title||i["dc:title"]||i["dcterm:title"]||i["og:title"]||i["weibo:article:title"]||i["weibo:webpage:title"]||i.title||i["twitter:title"],t.title||(t.title=this._getArticleTitle()),t.byline=e.byline||i["dc:creator"]||i["dcterm:creator"]||i.author,t.excerpt=e.excerpt||i["dc:description"]||i["dcterm:description"]||i["og:description"]||i["weibo:article:description"]||i["weibo:webpage:description"]||i.description||i["twitter:description"],t.siteName=e.siteName||i["og:site_name"],t.publishedTime=e.datePublished||i["article:published_time"]||null,t.title=this._unescapeHtmlEntities(t.title),t.byline=this._unescapeHtmlEntities(t.byline),t.excerpt=this._unescapeHtmlEntities(t.excerpt),t.siteName=this._unescapeHtmlEntities(t.siteName),t.publishedTime=this._unescapeHtmlEntities(t.publishedTime),t},_isSingleImage:function(e){return"IMG"===e.tagName||1===e.children.length&&""===e.textContent.trim()&&this._isSingleImage(e.children[0])},_unwrapNoscriptImages:function(e){var t=Array.from(e.getElementsByTagName("img"));this._forEachNode(t,(function(e){for(var t=0;t0&&r>i)return!1;if(e.parentNode.tagName===t&&(!n||n(e.parentNode)))return!0;e=e.parentNode,r++}return!1},_getRowAndColumnCount:function(e){for(var t=0,i=0,n=e.getElementsByTagName("tr"),r=0;r0)n._readabilityDataTable=!0;else{if(["col","colgroup","tfoot","thead","th"].some((function(e){return!!n.getElementsByTagName(e)[0]})))this.log("Data table because found data-y descendant"),n._readabilityDataTable=!0;else if(n.getElementsByTagName("table")[0])n._readabilityDataTable=!1;else{var a=this._getRowAndColumnCount(n);a.rows>=10||a.columns>4?n._readabilityDataTable=!0:n._readabilityDataTable=a.rows*a.columns>10}}}else n._readabilityDataTable=!1;else n._readabilityDataTable=!1}},_fixLazyImages:function(e){this._forEachNode(this._getAllNodesWithTag(e,["img","picture","figure"]),(function(e){if(e.src&&this.REGEXPS.b64DataUrl.test(e.src)){if("image/svg+xml"===this.REGEXPS.b64DataUrl.exec(e.src)[1])return;for(var t=!1,i=0;in+=this._getInnerText(e,!0).length)),n/i},_cleanConditionally:function(e,t){this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)&&this._removeNodes(this._getAllNodesWithTag(e,[t]),(function(e){var i=function(e){return e._readabilityDataTable},n="ul"===t||"ol"===t;if(!n){var r=0,a=this._getAllNodesWithTag(e,["ul","ol"]);this._forEachNode(a,(e=>r+=this._getInnerText(e).length)),n=r/this._getInnerText(e).length>.9}if("table"===t&&i(e))return!1;if(this._hasAncestorTag(e,"table",-1,i))return!1;if(this._hasAncestorTag(e,"code"))return!1;var s=this._getClassWeight(e);this.log("Cleaning Conditionally",e);if(s+0<0)return!0;if(this._getCharCount(e,",")<10){for(var o=e.getElementsByTagName("p").length,l=e.getElementsByTagName("img").length,c=e.getElementsByTagName("li").length-100,h=e.getElementsByTagName("input").length,d=this._getTextDensity(e,["h1","h2","h3","h4","h5","h6"]),u=0,g=this._getAllNodesWithTag(e,["object","embed","iframe"]),f=0;f1&&o/l<.5&&!this._hasAncestorTag(e,"figure")||!n&&c>o||h>Math.floor(o/3)||!n&&d<.9&&_<25&&(0===l||l>2)&&!this._hasAncestorTag(e,"figure")||!n&&s<25&&m>.2||s>=25&&m>.5||1===u&&_<75||u>1;if(n&&b){for(var N=0;N1)return b}if(l==e.getElementsByTagName("li").length)return!1}return b}return!1}))},_cleanMatchedNodes:function(e,t){for(var i=this._getNextNode(e,!0),n=this._getNextNode(e);n&&n!=i;)n=t.call(this,n,n.className+" "+n.id)?this._removeAndGetNext(n):this._getNextNode(n)},_cleanHeaders:function(e){let t=this._getAllNodesWithTag(e,["h1","h2"]);this._removeNodes(t,(function(e){let t=this._getClassWeight(e)<0;return t&&this.log("Removing header with low class weight:",e),t}))},_headerDuplicatesTitle:function(e){if("H1"!=e.tagName&&"H2"!=e.tagName)return!1;var t=this._getInnerText(e,!1);return this.log("Evaluating similarity of header:",t,this._articleTitle),this._textSimilarity(this._articleTitle,t)>.75},_flagIsActive:function(e){return(this._flags&e)>0},_removeFlag:function(e){this._flags=this._flags&~e},_isProbablyVisible:function(e){return(!e.style||"none"!=e.style.display)&&(!e.style||"hidden"!=e.style.visibility)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))},parse:function(){if(this._maxElemsToParse>0){var e=this._doc.getElementsByTagName("*").length;if(e>this._maxElemsToParse)throw new Error("Aborting parsing document; "+e+" elements found")}this._unwrapNoscriptImages(this._doc);var t=this._disableJSONLD?{}:this._getJSONLD(this._doc);this._removeScripts(this._doc),this._prepDocument();var i=this._getArticleMetadata(t);this._articleTitle=i.title;var n=this._grabArticle();if(!n)return null;if(this.log("Grabbed: "+n.innerHTML),this._postProcessContent(n),!i.excerpt){var r=n.getElementsByTagName("p");r.length>0&&(i.excerpt=r[0].textContent.trim())}var a=n.textContent;return{title:this._articleTitle,byline:i.byline||this._articleByline,dir:this._articleDir,lang:this._articleLang,content:this._serializer(n),textContent:a,length:a.length,excerpt:i.excerpt,siteName:i.siteName||this._articleSiteName,publishedTime:i.publishedTime}}},e.exports=t},396:(e,t,i)=>{var n=i(238),r=i(804);e.exports={Readability:n,isProbablyReaderable:r}},481:e=>{e.exports=function(e,t){(t=t||{}).listUnicodeChar=!!t.hasOwnProperty("listUnicodeChar")&&t.listUnicodeChar,t.stripListLeaders=!t.hasOwnProperty("stripListLeaders")||t.stripListLeaders,t.gfm=!t.hasOwnProperty("gfm")||t.gfm,t.useImgAltText=!t.hasOwnProperty("useImgAltText")||t.useImgAltText,t.abbr=!!t.hasOwnProperty("abbr")&&t.abbr,t.replaceLinksWithURL=!!t.hasOwnProperty("replaceLinksWithURL")&&t.replaceLinksWithURL,t.htmlTagsToSkip=t.hasOwnProperty("htmlTagsToSkip")?t.htmlTagsToSkip:[],t.throwError=!!t.hasOwnProperty("throwError")&&t.throwError;var i=e||"";i=i.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*/gm,"");try{t.stripListLeaders&&(i=t.listUnicodeChar?i.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm,t.listUnicodeChar+" $1"):i.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm,"$1")),t.gfm&&(i=i.replace(/\n={2,}/g,"\n").replace(/~{3}.*\n/g,"").replace(/~~/g,"").replace(/`{3}.*\n/g,"")),t.abbr&&(i=i.replace(/\*\[.*\]:.*\n/,"")),i=i.replace(/<[^>]*>/g,"");var n=new RegExp("<[^>]*>","g");if(t.htmlTagsToSkip.length>0){var r="(?!"+t.htmlTagsToSkip.join("|")+")";n=new RegExp("<"+r+"[^>]*>","ig")}i=i.replace(n,"").replace(/^[=\-]{2,}\s*$/g,"").replace(/\[\^.+?\](\: .*?$)?/g,"").replace(/\s{0,2}\[.*?\]: .*?$/g,"").replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g,t.useImgAltText?"$1":"").replace(/\[([^\]]*?)\][\[\(].*?[\]\)]/g,t.replaceLinksWithURL?"$2":"$1").replace(/^(\n)?\s{0,3}>\s?/gm,"$1").replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g,"").replace(/^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm,"$1$3$4$6").replace(/([\*]+)(\S)(.*?\S)??\1/g,"$2$3").replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g,"$1$3$4$5").replace(/(`{3,})(.*?)\1/gm,"$2").replace(/`(.+?)`/g,"$1").replace(/~(.*?)~/g,"$1")}catch(i){if(t.throwError)throw i;return console.error("remove-markdown encountered error: %s",i),e}return i}}},t={};function i(n){var r=t[n];if(void 0!==r)return r.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,i),a.exports}i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};return(()=>{"use strict";i.r(n),i.d(n,{parseReaderable:()=>U});var e=i(396);function t(e,t){return Array(t+1).join(e)}var r=["ADDRESS","ARTICLE","ASIDE","AUDIO","BLOCKQUOTE","BODY","CANVAS","CENTER","DD","DIR","DIV","DL","DT","FIELDSET","FIGCAPTION","FIGURE","FOOTER","FORM","FRAMESET","H1","H2","H3","H4","H5","H6","HEADER","HGROUP","HR","HTML","ISINDEX","LI","MAIN","MENU","NAV","NOFRAMES","NOSCRIPT","OL","OUTPUT","P","PRE","SECTION","TABLE","TBODY","TD","TFOOT","TH","THEAD","TR","UL"];function a(e){return c(e,r)}var s=["AREA","BASE","BR","COL","COMMAND","EMBED","HR","IMG","INPUT","KEYGEN","LINK","META","PARAM","SOURCE","TRACK","WBR"];function o(e){return c(e,s)}var l=["A","TABLE","THEAD","TBODY","TFOOT","TH","TD","IFRAME","SCRIPT","AUDIO","VIDEO"];function c(e,t){return t.indexOf(e.nodeName)>=0}function h(e,t){return e.getElementsByTagName&&t.some((function(t){return e.getElementsByTagName(t).length}))}var d={};function u(e){return e?e.replace(/(\n+\s*)+/g,"\n"):""}function g(e){for(var t in this.options=e,this._keep=[],this._remove=[],this.blankRule={replacement:e.blankReplacement},this.keepReplacement=e.keepReplacement,this.defaultRule={replacement:e.defaultReplacement},this.array=[],e.rules)this.array.push(e.rules[t])}function f(e,t,i){for(var n=0;n-1)return!0}else{if("function"!=typeof n)throw new TypeError("`filter` needs to be a string, array, or function");if(n.call(e,t,i))return!0}}function m(e){var t=e.nextSibling||e.parentNode;return e.parentNode.removeChild(e),t}function _(e,t,i){return e&&e.parentNode===t||i(t)?t.nextSibling||t.parentNode:t.firstChild||t.nextSibling||t.parentNode}d.paragraph={filter:"p",replacement:function(e){return"\n\n"+e+"\n\n"}},d.lineBreak={filter:"br",replacement:function(e,t,i){return i.br+"\n"}},d.heading={filter:["h1","h2","h3","h4","h5","h6"],replacement:function(e,i,n){var r=Number(i.nodeName.charAt(1));return"setext"===n.headingStyle&&r<3?"\n\n"+e+"\n"+t(1===r?"=":"-",e.length)+"\n\n":"\n\n"+t("#",r)+" "+e+"\n\n"}},d.blockquote={filter:"blockquote",replacement:function(e){return"\n\n"+(e=(e=e.replace(/^\n+|\n+$/g,"")).replace(/^/gm,"> "))+"\n\n"}},d.list={filter:["ul","ol"],replacement:function(e,t){var i=t.parentNode;return"LI"===i.nodeName&&i.lastElementChild===t?"\n"+e:"\n\n"+e+"\n\n"}},d.listItem={filter:"li",replacement:function(e,t,i){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var n=i.bulletListMarker+" ",r=t.parentNode;if("OL"===r.nodeName){var a=r.getAttribute("start"),s=Array.prototype.indexOf.call(r.children,t);n=(a?Number(a)+s:s+1)+". "}return n+e+(t.nextSibling&&!/\n$/.test(e)?"\n":"")}},d.indentedCodeBlock={filter:function(e,t){return"indented"===t.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,t,i){return"\n\n "+t.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}},d.fencedCodeBlock={filter:function(e,t){return"fenced"===t.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,i,n){for(var r,a=((i.firstChild.getAttribute("class")||"").match(/language-(\S+)/)||[null,""])[1],s=i.firstChild.textContent,o=n.fence.charAt(0),l=3,c=new RegExp("^"+o+"{3,}","gm");r=c.exec(s);)r[0].length>=l&&(l=r[0].length+1);var h=t(o,l);return"\n\n"+h+a+"\n"+s.replace(/\n$/,"")+"\n"+h+"\n\n"}},d.horizontalRule={filter:"hr",replacement:function(e,t,i){return"\n\n"+i.hr+"\n\n"}},d.inlineLink={filter:function(e,t){return"inlined"===t.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,t){var i=t.getAttribute("href");i&&(i=i.replace(/([()])/g,"\\$1"));var n=u(t.getAttribute("title"));return n&&(n=' "'+n.replace(/"/g,'\\"')+'"'),"["+e+"]("+i+n+")"}},d.referenceLink={filter:function(e,t){return"referenced"===t.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,t,i){var n,r,a=t.getAttribute("href"),s=u(t.getAttribute("title"));switch(s&&(s=' "'+s+'"'),i.linkReferenceStyle){case"collapsed":n="["+e+"][]",r="["+e+"]: "+a+s;break;case"shortcut":n="["+e+"]",r="["+e+"]: "+a+s;break;default:var o=this.references.length+1;n="["+e+"]["+o+"]",r="["+o+"]: "+a+s}return this.references.push(r),n},references:[],append:function(e){var t="";return this.references.length&&(t="\n\n"+this.references.join("\n")+"\n\n",this.references=[]),t}},d.emphasis={filter:["em","i"],replacement:function(e,t,i){return e.trim()?i.emDelimiter+e+i.emDelimiter:""}},d.strong={filter:["strong","b"],replacement:function(e,t,i){return e.trim()?i.strongDelimiter+e+i.strongDelimiter:""}},d.code={filter:function(e){var t=e.previousSibling||e.nextSibling,i="PRE"===e.parentNode.nodeName&&!t;return"CODE"===e.nodeName&&!i},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");for(var t=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"",i="`",n=e.match(/`+/gm)||[];-1!==n.indexOf(i);)i+="`";return i+t+e+t+i}},d.image={filter:"img",replacement:function(e,t){var i=u(t.getAttribute("alt")),n=t.getAttribute("src")||"",r=u(t.getAttribute("title"));return n?"!["+i+"]("+n+(r?' "'+r+'"':"")+")":""}},g.prototype={add:function(e,t){this.array.unshift(t)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:(t=f(this.array,e,this.options))||(t=f(this._keep,e,this.options))||(t=f(this._remove,e,this.options))?t:this.defaultRule;var t},forEach:function(e){for(var t=0;t'+e+"","text/html").getElementById("turndown-root"):i=e.cloneNode(!0);return function(e){var t=e.element,i=e.isBlock,n=e.isVoid,r=e.isPre||function(e){return"PRE"===e.nodeName};if(t.firstChild&&!r(t)){for(var a=null,s=!1,o=null,l=_(o,t,r);l!==t;){if(3===l.nodeType||4===l.nodeType){var c=l.data.replace(/[ \r\n\t]+/g," ");if(a&&!/ $/.test(a.data)||s||" "!==c[0]||(c=c.substr(1)),!c){l=m(l);continue}l.data=c,a=l}else{if(1!==l.nodeType){l=m(l);continue}i(l)||"BR"===l.nodeName?(a&&(a.data=a.data.replace(/ $/,"")),a=null,s=!1):n(l)||r(l)?(a=null,s=!0):a&&(s=!1)}var h=_(o,l,r);o=l,l=h}a&&(a.data=a.data.replace(/ $/,""),a.data||m(a))}}({element:i,isBlock:a,isVoid:o,isPre:t.preformattedCode?v:null}),i}function v(e){return"PRE"===e.nodeName||"CODE"===e.nodeName}function A(e,t){return e.isBlock=a(e),e.isCode="CODE"===e.nodeName||e.parentNode.isCode,e.isBlank=function(e){return!o(e)&&!function(e){return c(e,l)}(e)&&/^\s*$/i.test(e.textContent)&&!function(e){return h(e,s)}(e)&&!function(e){return h(e,l)}(e)}(e),e.flankingWhitespace=function(e,t){if(e.isBlock||t.preformattedCode&&e.isCode)return{leading:"",trailing:""};var i=(n=e.textContent,r=n.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/),{leading:r[1],leadingAscii:r[2],leadingNonAscii:r[3],trailing:r[4],trailingNonAscii:r[5],trailingAscii:r[6]});var n,r;i.leadingAscii&&S("left",e,t)&&(i.leading=i.leadingNonAscii);i.trailingAscii&&S("right",e,t)&&(i.trailing=i.trailingNonAscii);return{leading:i.leading,trailing:i.trailing}}(e,t),e}function S(e,t,i){var n,r,s;return"left"===e?(n=t.previousSibling,r=/ $/):(n=t.nextSibling,r=/^ /),n&&(3===n.nodeType?s=r.test(n.nodeValue):i.preformattedCode&&"CODE"===n.nodeName?s=!1:1!==n.nodeType||a(n)||(s=r.test(n.textContent))),s}var C=Array.prototype.reduce,L=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function x(e){if(!(this instanceof x))return new x(e);var t={rules:d,headingStyle:"setext",hr:"* * *",bulletListMarker:"*",codeBlockStyle:"indented",fence:"```",emDelimiter:"_",strongDelimiter:"**",linkStyle:"inlined",linkReferenceStyle:"full",br:" ",preformattedCode:!1,blankReplacement:function(e,t){return t.isBlock?"\n\n":""},keepReplacement:function(e,t){return t.isBlock?"\n\n"+t.outerHTML+"\n\n":t.outerHTML},defaultReplacement:function(e,t){return t.isBlock?"\n\n"+e+"\n\n":e}};this.options=function(e){for(var t=1;t0&&"\n"===e[t-1];)t--;return e.substring(0,t)}(e),n=t.replace(/^\n*/,""),r=Math.max(e.length-i.length,t.length-n.length);return i+"\n\n".substring(0,r)+n}x.prototype={turndown:function(e){if(!function(e){return null!=e&&("string"==typeof e||e.nodeType&&(1===e.nodeType||9===e.nodeType||11===e.nodeType))}(e))throw new TypeError(e+" is not a string, or an element/document/fragment node.");if(""===e)return"";var t=R.call(this,new T(e,this.options));return O.call(this,t)},use:function(e){if(Array.isArray(e))for(var t=0;t(()=>{var e={804:e=>{var t={unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i};function i(e){return(!e.style||"none"!=e.style.display)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))}e.exports=function(e,n={}){"function"==typeof n&&(n={visibilityChecker:n});var r={minScore:20,minContentLength:140,visibilityChecker:i};n=Object.assign(r,n);var a=e.querySelectorAll("p, pre, article"),s=e.querySelectorAll("div > br");if(s.length){var o=new Set(a);[].forEach.call(s,(function(e){o.add(e.parentNode)})),a=Array.from(o)}var l=0;return[].some.call(a,(function(e){if(!n.visibilityChecker(e))return!1;var i=e.className+" "+e.id;if(t.unlikelyCandidates.test(i)&&!t.okMaybeItsACandidate.test(i))return!1;if(e.matches("li p"))return!1;var r=e.textContent.trim().length;return!(rn.minScore}))}},238:e=>{function t(e,t){if(t&&t.documentElement)e=t,t=arguments[2];else if(!e||!e.documentElement)throw new Error("First argument to Readability constructor should be a document object.");if(t=t||{},this._doc=e,this._docJSDOMParser=this._doc.firstChild.__JSDOMParser__,this._articleTitle=null,this._articleByline=null,this._articleDir=null,this._articleSiteName=null,this._attempts=[],this._debug=!!t.debug,this._maxElemsToParse=t.maxElemsToParse||this.DEFAULT_MAX_ELEMS_TO_PARSE,this._nbTopCandidates=t.nbTopCandidates||this.DEFAULT_N_TOP_CANDIDATES,this._charThreshold=t.charThreshold||this.DEFAULT_CHAR_THRESHOLD,this._classesToPreserve=this.CLASSES_TO_PRESERVE.concat(t.classesToPreserve||[]),this._keepClasses=!!t.keepClasses,this._serializer=t.serializer||function(e){return e.innerHTML},this._disableJSONLD=!!t.disableJSONLD,this._allowedVideoRegex=t.allowedVideoRegex||this.REGEXPS.videos,this._flags=this.FLAG_STRIP_UNLIKELYS|this.FLAG_WEIGHT_CLASSES|this.FLAG_CLEAN_CONDITIONALLY,this._debug){let e=function(e){if(e.nodeType==e.TEXT_NODE)return`${e.nodeName} ("${e.textContent}")`;let t=Array.from(e.attributes||[],(function(e){return`${e.name}="${e.value}"`})).join(" ");return`<${e.localName} ${t}>`};this.log=function(){if("undefined"!=typeof console){let t=Array.from(arguments,(t=>t&&t.nodeType==this.ELEMENT_NODE?e(t):t));t.unshift("Reader: (Readability)"),console.log.apply(console,t)}else if("undefined"!=typeof dump){var t=Array.prototype.map.call(arguments,(function(t){return t&&t.nodeName?e(t):t})).join(" ");dump("Reader: (Readability) "+t+"\n")}}}else this.log=function(){}}t.prototype={FLAG_STRIP_UNLIKELYS:1,FLAG_WEIGHT_CLASSES:2,FLAG_CLEAN_CONDITIONALLY:4,ELEMENT_NODE:1,TEXT_NODE:3,DEFAULT_MAX_ELEMS_TO_PARSE:0,DEFAULT_N_TOP_CANDIDATES:5,DEFAULT_TAGS_TO_SCORE:"section,h2,h3,h4,h5,h6,p,td,pre".toUpperCase().split(","),DEFAULT_CHAR_THRESHOLD:500,REGEXPS:{unlikelyCandidates:/-ad-|ai2html|banner|breadcrumbs|combx|comment|community|cover-wrap|disqus|extra|footer|gdpr|header|legends|menu|related|remark|replies|rss|shoutbox|sidebar|skyscraper|social|sponsor|supplemental|ad-break|agegate|pagination|pager|popup|yom-remote/i,okMaybeItsACandidate:/and|article|body|column|content|main|shadow/i,positive:/article|body|content|entry|hentry|h-entry|main|page|pagination|post|text|blog|story/i,negative:/-ad-|hidden|^hid$| hid$| hid |^hid |banner|combx|comment|com-|contact|foot|footer|footnote|gdpr|masthead|media|meta|outbrain|promo|related|scroll|share|shoutbox|sidebar|skyscraper|sponsor|shopping|tags|tool|widget/i,extraneous:/print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|utility/i,byline:/byline|author|dateline|writtenby|p-author/i,replaceFonts:/<(\/?)font[^>]*>/gi,normalize:/\s{2,}/g,videos:/\/\/(www\.)?((dailymotion|youtube|youtube-nocookie|player\.vimeo|v\.qq)\.com|(archive|upload\.wikimedia)\.org|player\.twitch\.tv)/i,shareElements:/(\b|_)(share|sharedaddy)(\b|_)/i,nextLink:/(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,prevLink:/(prev|earl|old|new|<|«)/i,tokenize:/\W+/g,whitespace:/^\s*$/,hasContent:/\S$/,hashUrl:/^#.+/,srcsetUrl:/(\S+)(\s+[\d.]+[xw])?(\s*(?:,|$))/g,b64DataUrl:/^data:\s*([^\s;,]+)\s*;\s*base64\s*,/i,commas:/\u002C|\u060C|\uFE50|\uFE10|\uFE11|\u2E41|\u2E34|\u2E32|\uFF0C/g,jsonLdArticleTypes:/^Article|AdvertiserContentArticle|NewsArticle|AnalysisNewsArticle|AskPublicNewsArticle|BackgroundNewsArticle|OpinionNewsArticle|ReportageNewsArticle|ReviewNewsArticle|Report|SatiricalArticle|ScholarlyArticle|MedicalScholarlyArticle|SocialMediaPosting|BlogPosting|LiveBlogPosting|DiscussionForumPosting|TechArticle|APIReference$/},UNLIKELY_ROLES:["menu","menubar","complementary","navigation","alert","alertdialog","dialog"],DIV_TO_P_ELEMS:new Set(["BLOCKQUOTE","DL","DIV","IMG","OL","P","PRE","TABLE","UL"]),ALTER_TO_DIV_EXCEPTIONS:["DIV","ARTICLE","SECTION","P"],PRESENTATIONAL_ATTRIBUTES:["align","background","bgcolor","border","cellpadding","cellspacing","frame","hspace","rules","style","valign","vspace"],DEPRECATED_SIZE_ATTRIBUTE_ELEMS:["TABLE","TH","TD","HR","PRE"],PHRASING_ELEMS:["ABBR","AUDIO","B","BDO","BR","BUTTON","CITE","CODE","DATA","DATALIST","DFN","EM","EMBED","I","IMG","INPUT","KBD","LABEL","MARK","MATH","METER","NOSCRIPT","OBJECT","OUTPUT","PROGRESS","Q","RUBY","SAMP","SCRIPT","SELECT","SMALL","SPAN","STRONG","SUB","SUP","TEXTAREA","TIME","VAR","WBR"],CLASSES_TO_PRESERVE:["page"],HTML_ESCAPE_MAP:{lt:"<",gt:">",amp:"&",quot:'"',apos:"'"},_postProcessContent:function(e){this._fixRelativeUris(e),this._simplifyNestedElements(e),this._keepClasses||this._cleanClasses(e)},_removeNodes:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _removeNodes");for(var i=e.length-1;i>=0;i--){var n=e[i],r=n.parentNode;r&&(t&&!t.call(this,n,i,e)||r.removeChild(n))}},_replaceNodeTags:function(e,t){if(this._docJSDOMParser&&e._isLiveNodeList)throw new Error("Do not pass live node lists to _replaceNodeTags");for(const i of e)this._setNodeTag(i,t)},_forEachNode:function(e,t){Array.prototype.forEach.call(e,t,this)},_findNode:function(e,t){return Array.prototype.find.call(e,t,this)},_someNode:function(e,t){return Array.prototype.some.call(e,t,this)},_everyNode:function(e,t){return Array.prototype.every.call(e,t,this)},_concatNodeLists:function(){var e=Array.prototype.slice,t=e.call(arguments).map((function(t){return e.call(t)}));return Array.prototype.concat.apply([],t)},_getAllNodesWithTag:function(e,t){return e.querySelectorAll?e.querySelectorAll(t.join(",")):[].concat.apply([],t.map((function(t){var i=e.getElementsByTagName(t);return Array.isArray(i)?i:Array.from(i)})))},_cleanClasses:function(e){var t=this._classesToPreserve,i=(e.getAttribute("class")||"").split(/\s+/).filter((function(e){return-1!=t.indexOf(e)})).join(" ");for(i?e.setAttribute("class",i):e.removeAttribute("class"),e=e.firstElementChild;e;e=e.nextElementSibling)this._cleanClasses(e)},_fixRelativeUris:function(e){var t=this._doc.baseURI,i=this._doc.documentURI;function n(e){if(t==i&&"#"==e.charAt(0))return e;try{return new URL(e,t).href}catch(e){}return e}var r=this._getAllNodesWithTag(e,["a"]);this._forEachNode(r,(function(e){var t=e.getAttribute("href");if(t)if(0===t.indexOf("javascript:"))if(1===e.childNodes.length&&e.childNodes[0].nodeType===this.TEXT_NODE){var i=this._doc.createTextNode(e.textContent);e.parentNode.replaceChild(i,e)}else{for(var r=this._doc.createElement("span");e.firstChild;)r.appendChild(e.firstChild);e.parentNode.replaceChild(r,e)}else e.setAttribute("href",n(t))}));var a=this._getAllNodesWithTag(e,["img","picture","figure","video","audio","source"]);this._forEachNode(a,(function(e){var t=e.getAttribute("src"),i=e.getAttribute("poster"),r=e.getAttribute("srcset");if(t&&e.setAttribute("src",n(t)),i&&e.setAttribute("poster",n(i)),r){var a=r.replace(this.REGEXPS.srcsetUrl,(function(e,t,i,r){return n(t)+(i||"")+r}));e.setAttribute("srcset",a)}}))},_simplifyNestedElements:function(e){for(var t=e;t;){if(t.parentNode&&["DIV","SECTION"].includes(t.tagName)&&(!t.id||!t.id.startsWith("readability"))){if(this._isElementWithoutContent(t)){t=this._removeAndGetNext(t);continue}if(this._hasSingleTagInsideElement(t,"DIV")||this._hasSingleTagInsideElement(t,"SECTION")){for(var i=t.children[0],n=0;n»] /.test(t))n=/ [\\\/>»] /.test(t),r(t=i.replace(/(.*)[\|\-\\\/>»] .*/gi,"$1"))<3&&(t=i.replace(/[^\|\-\\\/>»]*[\|\-\\\/>»](.*)/gi,"$1"));else if(-1!==t.indexOf(": ")){var a=this._concatNodeLists(e.getElementsByTagName("h1"),e.getElementsByTagName("h2")),s=t.trim();this._someNode(a,(function(e){return e.textContent.trim()===s}))||(r(t=i.substring(i.lastIndexOf(":")+1))<3?t=i.substring(i.indexOf(":")+1):r(i.substr(0,i.indexOf(":")))>5&&(t=i))}else if(t.length>150||t.length<15){var o=e.getElementsByTagName("h1");1===o.length&&(t=this._getInnerText(o[0]))}var l=r(t=t.trim().replace(this.REGEXPS.normalize," "));return l<=4&&(!n||l!=r(i.replace(/[\|\-\\\/>»]+/g,""))-1)&&(t=i),t},_prepDocument:function(){var e=this._doc;this._removeNodes(this._getAllNodesWithTag(e,["style"])),e.body&&this._replaceBrs(e.body),this._replaceNodeTags(this._getAllNodesWithTag(e,["font"]),"SPAN")},_nextNode:function(e){for(var t=e;t&&t.nodeType!=this.ELEMENT_NODE&&this.REGEXPS.whitespace.test(t.textContent);)t=t.nextSibling;return t},_replaceBrs:function(e){this._forEachNode(this._getAllNodesWithTag(e,["br"]),(function(e){for(var t=e.nextSibling,i=!1;(t=this._nextNode(t))&&"BR"==t.tagName;){i=!0;var n=t.nextSibling;t.parentNode.removeChild(t),t=n}if(i){var r=this._doc.createElement("p");for(e.parentNode.replaceChild(r,e),t=r.nextSibling;t;){if("BR"==t.tagName){var a=this._nextNode(t.nextSibling);if(a&&"BR"==a.tagName)break}if(!this._isPhrasingContent(t))break;var s=t.nextSibling;r.appendChild(t),t=s}for(;r.lastChild&&this._isWhitespace(r.lastChild);)r.removeChild(r.lastChild);"P"===r.parentNode.tagName&&this._setNodeTag(r.parentNode,"DIV")}}))},_setNodeTag:function(e,t){if(this.log("_setNodeTag",e,t),this._docJSDOMParser)return e.localName=t.toLowerCase(),e.tagName=t.toUpperCase(),e;for(var i=e.ownerDocument.createElement(t);e.firstChild;)i.appendChild(e.firstChild);e.parentNode.replaceChild(i,e),e.readability&&(i.readability=e.readability);for(var n=0;n!i.includes(e))).join(" ").length/n.join(" ").length:0},_checkByline:function(e,t){if(this._articleByline)return!1;if(void 0!==e.getAttribute)var i=e.getAttribute("rel"),n=e.getAttribute("itemprop");return!(!("author"===i||n&&-1!==n.indexOf("author")||this.REGEXPS.byline.test(t))||!this._isValidByline(e.textContent))&&(this._articleByline=e.textContent.trim(),!0)},_getNodeAncestors:function(e,t){t=t||0;for(var i=0,n=[];e.parentNode&&(n.push(e.parentNode),!t||++i!==t);)e=e.parentNode;return n},_grabArticle:function(e){this.log("**** grabArticle ****");var t=this._doc,i=null!==e;if(!(e=e||this._doc.body))return this.log("No body found in document. Abort."),null;for(var n=e.innerHTML;;){this.log("Starting grabArticle loop");var r=this._flagIsActive(this.FLAG_STRIP_UNLIKELYS),a=[],s=this._doc.documentElement;let V=!0;for(;s;){"HTML"===s.tagName&&(this._articleLang=s.getAttribute("lang"));var o=s.className+" "+s.id;if(this._isProbablyVisible(s))if("true"!=s.getAttribute("aria-modal")||"dialog"!=s.getAttribute("role"))if(this._checkByline(s,o))s=this._removeAndGetNext(s);else if(V&&this._headerDuplicatesTitle(s))this.log("Removing header: ",s.textContent.trim(),this._articleTitle.trim()),V=!1,s=this._removeAndGetNext(s);else{if(r){if(this.REGEXPS.unlikelyCandidates.test(o)&&!this.REGEXPS.okMaybeItsACandidate.test(o)&&!this._hasAncestorTag(s,"table")&&!this._hasAncestorTag(s,"code")&&"BODY"!==s.tagName&&"A"!==s.tagName){this.log("Removing unlikely candidate - "+o),s=this._removeAndGetNext(s);continue}if(this.UNLIKELY_ROLES.includes(s.getAttribute("role"))){this.log("Removing content with role "+s.getAttribute("role")+" - "+o),s=this._removeAndGetNext(s);continue}}if("DIV"!==s.tagName&&"SECTION"!==s.tagName&&"HEADER"!==s.tagName&&"H1"!==s.tagName&&"H2"!==s.tagName&&"H3"!==s.tagName&&"H4"!==s.tagName&&"H5"!==s.tagName&&"H6"!==s.tagName||!this._isElementWithoutContent(s)){if(-1!==this.DEFAULT_TAGS_TO_SCORE.indexOf(s.tagName)&&a.push(s),"DIV"===s.tagName){for(var l=null,c=s.firstChild;c;){var h=c.nextSibling;if(this._isPhrasingContent(c))null!==l?l.appendChild(c):this._isWhitespace(c)||(l=t.createElement("p"),s.replaceChild(l,c),l.appendChild(c));else if(null!==l){for(;l.lastChild&&this._isWhitespace(l.lastChild);)l.removeChild(l.lastChild);l=null}c=h}if(this._hasSingleTagInsideElement(s,"P")&&this._getLinkDensity(s)<.25){var d=s.children[0];s.parentNode.replaceChild(d,s),s=d,a.push(s)}else this._hasChildBlockElement(s)||(s=this._setNodeTag(s,"P"),a.push(s))}s=this._getNextNode(s)}else s=this._removeAndGetNext(s)}else s=this._removeAndGetNext(s);else this.log("Removing hidden node - "+o),s=this._removeAndGetNext(s)}var u=[];this._forEachNode(a,(function(e){if(e.parentNode&&void 0!==e.parentNode.tagName){var t=this._getInnerText(e);if(!(t.length<25)){var i=this._getNodeAncestors(e,5);if(0!==i.length){var n=0;n+=1,n+=t.split(this.REGEXPS.commas).length,n+=Math.min(Math.floor(t.length/100),3),this._forEachNode(i,(function(e,t){if(e.tagName&&e.parentNode&&void 0!==e.parentNode.tagName){if(void 0===e.readability&&(this._initializeNode(e),u.push(e)),0===t)var i=1;else i=1===t?2:3*t;e.readability.contentScore+=n/i}}))}}}}));for(var g=[],f=0,p=u.length;fN.readability.contentScore){g.splice(b,0,m),g.length>this._nbTopCandidates&&g.pop();break}}}var E,y=g[0]||null,v=!1;if(null===y||"BODY"===y.tagName){for(y=t.createElement("DIV"),v=!0;e.firstChild;)this.log("Moving child out:",e.firstChild),y.appendChild(e.firstChild);e.appendChild(y),this._initializeNode(y)}else if(y){for(var T=[],A=1;A=.75&&T.push(this._getNodeAncestors(g[A]));if(T.length>=3)for(E=y.parentNode;"BODY"!==E.tagName;){for(var S=0,C=0;C=3){y=E;break}E=E.parentNode}y.readability||this._initializeNode(y),E=y.parentNode;for(var L=y.readability.contentScore,x=L/3;"BODY"!==E.tagName;)if(E.readability){var R=E.readability.contentScore;if(RL){y=E;break}L=E.readability.contentScore,E=E.parentNode}else E=E.parentNode;for(E=y.parentNode;"BODY"!=E.tagName&&1==E.children.length;)E=(y=E).parentNode;y.readability||this._initializeNode(y)}var O=t.createElement("DIV");i&&(O.id="readability-content");for(var D=Math.max(10,.2*y.readability.contentScore),I=(E=y.parentNode).children,w=0,P=I.length;w=D)B=!0;else if("P"===k.nodeName){var H=this._getLinkDensity(k),G=this._getInnerText(k),U=G.length;(U>80&&H<.25||U<80&&U>0&&0===H&&-1!==G.search(/\.( |$)/))&&(B=!0)}}B&&(this.log("Appending node:",k),-1===this.ALTER_TO_DIV_EXCEPTIONS.indexOf(k.nodeName)&&(this.log("Altering sibling:",k,"to div."),k=this._setNodeTag(k,"DIV")),O.appendChild(k),I=E.children,w-=1,P-=1)}if(this._debug&&this.log("Article content pre-prep: "+O.innerHTML),this._prepArticle(O),this._debug&&this.log("Article content post-prep: "+O.innerHTML),v)y.id="readability-page-1",y.className="page";else{var $=t.createElement("DIV");for($.id="readability-page-1",$.className="page";O.firstChild;)$.appendChild(O.firstChild);O.appendChild($)}this._debug&&this.log("Article content after paging: "+O.innerHTML);var j=!0,F=this._getInnerText(O,!0).length;if(F0&&e.length<100)},_unescapeHtmlEntities:function(e){if(!e)return e;var t=this.HTML_ESCAPE_MAP;return e.replace(/&(quot|amp|apos|lt|gt);/g,(function(e,i){return t[i]})).replace(/&#(?:x([0-9a-z]{1,4})|([0-9]{1,4}));/gi,(function(e,t,i){var n=parseInt(t||i,t?16:10);return String.fromCharCode(n)}))},_getJSONLD:function(e){var t,i=this._getAllNodesWithTag(e,["script"]);return this._forEachNode(i,(function(e){if(!t&&"application/ld+json"===e.getAttribute("type"))try{var i=e.textContent.replace(/^\s*\s*$/g,""),n=JSON.parse(i);if(!n["@context"]||!n["@context"].match(/^https?\:\/\/schema\.org$/))return;if(!n["@type"]&&Array.isArray(n["@graph"])&&(n=n["@graph"].find((function(e){return(e["@type"]||"").match(this.REGEXPS.jsonLdArticleTypes)}))),!n||!n["@type"]||!n["@type"].match(this.REGEXPS.jsonLdArticleTypes))return;if(t={},"string"==typeof n.name&&"string"==typeof n.headline&&n.name!==n.headline){var r=this._getArticleTitle(),a=this._textSimilarity(n.name,r)>.75,s=this._textSimilarity(n.headline,r)>.75;t.title=s&&!a?n.headline:n.name}else"string"==typeof n.name?t.title=n.name.trim():"string"==typeof n.headline&&(t.title=n.headline.trim());return n.author&&("string"==typeof n.author.name?t.byline=n.author.name.trim():Array.isArray(n.author)&&n.author[0]&&"string"==typeof n.author[0].name&&(t.byline=n.author.filter((function(e){return e&&"string"==typeof e.name})).map((function(e){return e.name.trim()})).join(", "))),"string"==typeof n.description&&(t.excerpt=n.description.trim()),n.publisher&&"string"==typeof n.publisher.name&&(t.siteName=n.publisher.name.trim()),void("string"==typeof n.datePublished&&(t.datePublished=n.datePublished.trim()))}catch(e){this.log(e.message)}})),t||{}},_getArticleMetadata:function(e){var t={},i={},n=this._doc.getElementsByTagName("meta"),r=/\s*(article|dc|dcterm|og|twitter)\s*:\s*(author|creator|description|published_time|title|site_name)\s*/gi,a=/^\s*(?:(dc|dcterm|og|twitter|weibo:(article|webpage))\s*[\.:]\s*)?(author|creator|description|title|site_name)\s*$/i;return this._forEachNode(n,(function(e){var t=e.getAttribute("name"),n=e.getAttribute("property"),s=e.getAttribute("content");if(s){var o=null,l=null;n&&(o=n.match(r))&&(l=o[0].toLowerCase().replace(/\s/g,""),i[l]=s.trim()),!o&&t&&a.test(t)&&(l=t,s&&(l=l.toLowerCase().replace(/\s/g,"").replace(/\./g,":"),i[l]=s.trim()))}})),t.title=e.title||i["dc:title"]||i["dcterm:title"]||i["og:title"]||i["weibo:article:title"]||i["weibo:webpage:title"]||i.title||i["twitter:title"],t.title||(t.title=this._getArticleTitle()),t.byline=e.byline||i["dc:creator"]||i["dcterm:creator"]||i.author,t.excerpt=e.excerpt||i["dc:description"]||i["dcterm:description"]||i["og:description"]||i["weibo:article:description"]||i["weibo:webpage:description"]||i.description||i["twitter:description"],t.siteName=e.siteName||i["og:site_name"],t.publishedTime=e.datePublished||i["article:published_time"]||null,t.title=this._unescapeHtmlEntities(t.title),t.byline=this._unescapeHtmlEntities(t.byline),t.excerpt=this._unescapeHtmlEntities(t.excerpt),t.siteName=this._unescapeHtmlEntities(t.siteName),t.publishedTime=this._unescapeHtmlEntities(t.publishedTime),t},_isSingleImage:function(e){return"IMG"===e.tagName||1===e.children.length&&""===e.textContent.trim()&&this._isSingleImage(e.children[0])},_unwrapNoscriptImages:function(e){var t=Array.from(e.getElementsByTagName("img"));this._forEachNode(t,(function(e){for(var t=0;t0&&r>i)return!1;if(e.parentNode.tagName===t&&(!n||n(e.parentNode)))return!0;e=e.parentNode,r++}return!1},_getRowAndColumnCount:function(e){for(var t=0,i=0,n=e.getElementsByTagName("tr"),r=0;r0)n._readabilityDataTable=!0;else{if(["col","colgroup","tfoot","thead","th"].some((function(e){return!!n.getElementsByTagName(e)[0]})))this.log("Data table because found data-y descendant"),n._readabilityDataTable=!0;else if(n.getElementsByTagName("table")[0])n._readabilityDataTable=!1;else{var a=this._getRowAndColumnCount(n);a.rows>=10||a.columns>4?n._readabilityDataTable=!0:n._readabilityDataTable=a.rows*a.columns>10}}}else n._readabilityDataTable=!1;else n._readabilityDataTable=!1}},_fixLazyImages:function(e){this._forEachNode(this._getAllNodesWithTag(e,["img","picture","figure"]),(function(e){if(e.src&&this.REGEXPS.b64DataUrl.test(e.src)){if("image/svg+xml"===this.REGEXPS.b64DataUrl.exec(e.src)[1])return;for(var t=!1,i=0;in+=this._getInnerText(e,!0).length)),n/i},_cleanConditionally:function(e,t){this._flagIsActive(this.FLAG_CLEAN_CONDITIONALLY)&&this._removeNodes(this._getAllNodesWithTag(e,[t]),(function(e){var i=function(e){return e._readabilityDataTable},n="ul"===t||"ol"===t;if(!n){var r=0,a=this._getAllNodesWithTag(e,["ul","ol"]);this._forEachNode(a,(e=>r+=this._getInnerText(e).length)),n=r/this._getInnerText(e).length>.9}if("table"===t&&i(e))return!1;if(this._hasAncestorTag(e,"table",-1,i))return!1;if(this._hasAncestorTag(e,"code"))return!1;var s=this._getClassWeight(e);this.log("Cleaning Conditionally",e);if(s+0<0)return!0;if(this._getCharCount(e,",")<10){for(var o=e.getElementsByTagName("p").length,l=e.getElementsByTagName("img").length,c=e.getElementsByTagName("li").length-100,h=e.getElementsByTagName("input").length,d=this._getTextDensity(e,["h1","h2","h3","h4","h5","h6"]),u=0,g=this._getAllNodesWithTag(e,["object","embed","iframe"]),f=0;f1&&o/l<.5&&!this._hasAncestorTag(e,"figure")||!n&&c>o||h>Math.floor(o/3)||!n&&d<.9&&_<25&&(0===l||l>2)&&!this._hasAncestorTag(e,"figure")||!n&&s<25&&m>.2||s>=25&&m>.5||1===u&&_<75||u>1;if(n&&b){for(var N=0;N1)return b}if(l==e.getElementsByTagName("li").length)return!1}return b}return!1}))},_cleanMatchedNodes:function(e,t){for(var i=this._getNextNode(e,!0),n=this._getNextNode(e);n&&n!=i;)n=t.call(this,n,n.className+" "+n.id)?this._removeAndGetNext(n):this._getNextNode(n)},_cleanHeaders:function(e){let t=this._getAllNodesWithTag(e,["h1","h2"]);this._removeNodes(t,(function(e){let t=this._getClassWeight(e)<0;return t&&this.log("Removing header with low class weight:",e),t}))},_headerDuplicatesTitle:function(e){if("H1"!=e.tagName&&"H2"!=e.tagName)return!1;var t=this._getInnerText(e,!1);return this.log("Evaluating similarity of header:",t,this._articleTitle),this._textSimilarity(this._articleTitle,t)>.75},_flagIsActive:function(e){return(this._flags&e)>0},_removeFlag:function(e){this._flags=this._flags&~e},_isProbablyVisible:function(e){return(!e.style||"none"!=e.style.display)&&(!e.style||"hidden"!=e.style.visibility)&&!e.hasAttribute("hidden")&&(!e.hasAttribute("aria-hidden")||"true"!=e.getAttribute("aria-hidden")||e.className&&e.className.indexOf&&-1!==e.className.indexOf("fallback-image"))},parse:function(){if(this._maxElemsToParse>0){var e=this._doc.getElementsByTagName("*").length;if(e>this._maxElemsToParse)throw new Error("Aborting parsing document; "+e+" elements found")}this._unwrapNoscriptImages(this._doc);var t=this._disableJSONLD?{}:this._getJSONLD(this._doc);this._removeScripts(this._doc),this._prepDocument();var i=this._getArticleMetadata(t);this._articleTitle=i.title;var n=this._grabArticle();if(!n)return null;if(this.log("Grabbed: "+n.innerHTML),this._postProcessContent(n),!i.excerpt){var r=n.getElementsByTagName("p");r.length>0&&(i.excerpt=r[0].textContent.trim())}var a=n.textContent;return{title:this._articleTitle,byline:i.byline||this._articleByline,dir:this._articleDir,lang:this._articleLang,content:this._serializer(n),textContent:a,length:a.length,excerpt:i.excerpt,siteName:i.siteName||this._articleSiteName,publishedTime:i.publishedTime}}},e.exports=t},396:(e,t,i)=>{var n=i(238),r=i(804);e.exports={Readability:n,isProbablyReaderable:r}},481:e=>{e.exports=function(e,t){(t=t||{}).listUnicodeChar=!!t.hasOwnProperty("listUnicodeChar")&&t.listUnicodeChar,t.stripListLeaders=!t.hasOwnProperty("stripListLeaders")||t.stripListLeaders,t.gfm=!t.hasOwnProperty("gfm")||t.gfm,t.useImgAltText=!t.hasOwnProperty("useImgAltText")||t.useImgAltText,t.abbr=!!t.hasOwnProperty("abbr")&&t.abbr,t.replaceLinksWithURL=!!t.hasOwnProperty("replaceLinksWithURL")&&t.replaceLinksWithURL,t.htmlTagsToSkip=t.hasOwnProperty("htmlTagsToSkip")?t.htmlTagsToSkip:[],t.throwError=!!t.hasOwnProperty("throwError")&&t.throwError;var i=e||"";i=i.replace(/^(-\s*?|\*\s*?|_\s*?){3,}\s*/gm,"");try{t.stripListLeaders&&(i=t.listUnicodeChar?i.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm,t.listUnicodeChar+" $1"):i.replace(/^([\s\t]*)([\*\-\+]|\d+\.)\s+/gm,"$1")),t.gfm&&(i=i.replace(/\n={2,}/g,"\n").replace(/~{3}.*\n/g,"").replace(/~~/g,"").replace(/`{3}.*\n/g,"")),t.abbr&&(i=i.replace(/\*\[.*\]:.*\n/,"")),i=i.replace(/<[^>]*>/g,"");var n=new RegExp("<[^>]*>","g");if(t.htmlTagsToSkip.length>0){var r="(?!"+t.htmlTagsToSkip.join("|")+")";n=new RegExp("<"+r+"[^>]*>","ig")}i=i.replace(n,"").replace(/^[=\-]{2,}\s*$/g,"").replace(/\[\^.+?\](\: .*?$)?/g,"").replace(/\s{0,2}\[.*?\]: .*?$/g,"").replace(/\!\[(.*?)\][\[\(].*?[\]\)]/g,t.useImgAltText?"$1":"").replace(/\[([^\]]*?)\][\[\(].*?[\]\)]/g,t.replaceLinksWithURL?"$2":"$1").replace(/^(\n)?\s{0,3}>\s?/gm,"$1").replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g,"").replace(/^(\n)?\s{0,}#{1,6}\s*( (.+))? +#+$|^(\n)?\s{0,}#{1,6}\s*( (.+))?$/gm,"$1$3$4$6").replace(/([\*]+)(\S)(.*?\S)??\1/g,"$2$3").replace(/(^|\W)([_]+)(\S)(.*?\S)??\2($|\W)/g,"$1$3$4$5").replace(/(`{3,})(.*?)\1/gm,"$2").replace(/`(.+?)`/g,"$1").replace(/~(.*?)~/g,"$1")}catch(i){if(t.throwError)throw i;return console.error("remove-markdown encountered error: %s",i),e}return i}}},t={};function i(n){var r=t[n];if(void 0!==r)return r.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,i),a.exports}i.d=(e,t)=>{for(var n in t)i.o(t,n)&&!i.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),i.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};return(()=>{"use strict";i.r(n),i.d(n,{parseReaderable:()=>$});var e=i(396);function t(e,t){return Array(t+1).join(e)}var r=["ADDRESS","ARTICLE","ASIDE","AUDIO","BLOCKQUOTE","BODY","CANVAS","CENTER","DD","DIR","DIV","DL","DT","FIELDSET","FIGCAPTION","FIGURE","FOOTER","FORM","FRAMESET","H1","H2","H3","H4","H5","H6","HEADER","HGROUP","HR","HTML","ISINDEX","LI","MAIN","MENU","NAV","NOFRAMES","NOSCRIPT","OL","OUTPUT","P","PRE","SECTION","TABLE","TBODY","TD","TFOOT","TH","THEAD","TR","UL"];function a(e){return c(e,r)}var s=["AREA","BASE","BR","COL","COMMAND","EMBED","HR","IMG","INPUT","KEYGEN","LINK","META","PARAM","SOURCE","TRACK","WBR"];function o(e){return c(e,s)}var l=["A","TABLE","THEAD","TBODY","TFOOT","TH","TD","IFRAME","SCRIPT","AUDIO","VIDEO"];function c(e,t){return t.indexOf(e.nodeName)>=0}function h(e,t){return e.getElementsByTagName&&t.some((function(t){return e.getElementsByTagName(t).length}))}var d={};function u(e){return e?e.replace(/(\n+\s*)+/g,"\n"):""}function g(e){for(var t in this.options=e,this._keep=[],this._remove=[],this.blankRule={replacement:e.blankReplacement},this.keepReplacement=e.keepReplacement,this.defaultRule={replacement:e.defaultReplacement},this.array=[],e.rules)this.array.push(e.rules[t])}function f(e,t,i){for(var n=0;n-1)return!0}else{if("function"!=typeof n)throw new TypeError("`filter` needs to be a string, array, or function");if(n.call(e,t,i))return!0}}function m(e){var t=e.nextSibling||e.parentNode;return e.parentNode.removeChild(e),t}function _(e,t,i){return e&&e.parentNode===t||i(t)?t.nextSibling||t.parentNode:t.firstChild||t.nextSibling||t.parentNode}d.paragraph={filter:"p",replacement:function(e){return"\n\n"+e+"\n\n"}},d.lineBreak={filter:"br",replacement:function(e,t,i){return i.br+"\n"}},d.heading={filter:["h1","h2","h3","h4","h5","h6"],replacement:function(e,i,n){var r=Number(i.nodeName.charAt(1));return"setext"===n.headingStyle&&r<3?"\n\n"+e+"\n"+t(1===r?"=":"-",e.length)+"\n\n":"\n\n"+t("#",r)+" "+e+"\n\n"}},d.blockquote={filter:"blockquote",replacement:function(e){return"\n\n"+(e=(e=e.replace(/^\n+|\n+$/g,"")).replace(/^/gm,"> "))+"\n\n"}},d.list={filter:["ul","ol"],replacement:function(e,t){var i=t.parentNode;return"LI"===i.nodeName&&i.lastElementChild===t?"\n"+e:"\n\n"+e+"\n\n"}},d.listItem={filter:"li",replacement:function(e,t,i){e=e.replace(/^\n+/,"").replace(/\n+$/,"\n").replace(/\n/gm,"\n ");var n=i.bulletListMarker+" ",r=t.parentNode;if("OL"===r.nodeName){var a=r.getAttribute("start"),s=Array.prototype.indexOf.call(r.children,t);n=(a?Number(a)+s:s+1)+". "}return n+e+(t.nextSibling&&!/\n$/.test(e)?"\n":"")}},d.indentedCodeBlock={filter:function(e,t){return"indented"===t.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,t,i){return"\n\n "+t.firstChild.textContent.replace(/\n/g,"\n ")+"\n\n"}},d.fencedCodeBlock={filter:function(e,t){return"fenced"===t.codeBlockStyle&&"PRE"===e.nodeName&&e.firstChild&&"CODE"===e.firstChild.nodeName},replacement:function(e,i,n){for(var r,a=((i.firstChild.getAttribute("class")||"").match(/language-(\S+)/)||[null,""])[1],s=i.firstChild.textContent,o=n.fence.charAt(0),l=3,c=new RegExp("^"+o+"{3,}","gm");r=c.exec(s);)r[0].length>=l&&(l=r[0].length+1);var h=t(o,l);return"\n\n"+h+a+"\n"+s.replace(/\n$/,"")+"\n"+h+"\n\n"}},d.horizontalRule={filter:"hr",replacement:function(e,t,i){return"\n\n"+i.hr+"\n\n"}},d.inlineLink={filter:function(e,t){return"inlined"===t.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,t){var i=t.getAttribute("href");i&&(i=i.replace(/([()])/g,"\\$1"));var n=u(t.getAttribute("title"));return n&&(n=' "'+n.replace(/"/g,'\\"')+'"'),"["+e+"]("+i+n+")"}},d.referenceLink={filter:function(e,t){return"referenced"===t.linkStyle&&"A"===e.nodeName&&e.getAttribute("href")},replacement:function(e,t,i){var n,r,a=t.getAttribute("href"),s=u(t.getAttribute("title"));switch(s&&(s=' "'+s+'"'),i.linkReferenceStyle){case"collapsed":n="["+e+"][]",r="["+e+"]: "+a+s;break;case"shortcut":n="["+e+"]",r="["+e+"]: "+a+s;break;default:var o=this.references.length+1;n="["+e+"]["+o+"]",r="["+o+"]: "+a+s}return this.references.push(r),n},references:[],append:function(e){var t="";return this.references.length&&(t="\n\n"+this.references.join("\n")+"\n\n",this.references=[]),t}},d.emphasis={filter:["em","i"],replacement:function(e,t,i){return e.trim()?i.emDelimiter+e+i.emDelimiter:""}},d.strong={filter:["strong","b"],replacement:function(e,t,i){return e.trim()?i.strongDelimiter+e+i.strongDelimiter:""}},d.code={filter:function(e){var t=e.previousSibling||e.nextSibling,i="PRE"===e.parentNode.nodeName&&!t;return"CODE"===e.nodeName&&!i},replacement:function(e){if(!e)return"";e=e.replace(/\r?\n|\r/g," ");for(var t=/^`|^ .*?[^ ].* $|`$/.test(e)?" ":"",i="`",n=e.match(/`+/gm)||[];-1!==n.indexOf(i);)i+="`";return i+t+e+t+i}},d.image={filter:"img",replacement:function(e,t){var i=u(t.getAttribute("alt")),n=t.getAttribute("src")||"",r=u(t.getAttribute("title"));return n?"!["+i+"]("+n+(r?' "'+r+'"':"")+")":""}},g.prototype={add:function(e,t){this.array.unshift(t)},keep:function(e){this._keep.unshift({filter:e,replacement:this.keepReplacement})},remove:function(e){this._remove.unshift({filter:e,replacement:function(){return""}})},forNode:function(e){return e.isBlank?this.blankRule:(t=f(this.array,e,this.options))||(t=f(this._keep,e,this.options))||(t=f(this._remove,e,this.options))?t:this.defaultRule;var t},forEach:function(e){for(var t=0;t'+e+"","text/html").getElementById("turndown-root"):i=e.cloneNode(!0);return function(e){var t=e.element,i=e.isBlock,n=e.isVoid,r=e.isPre||function(e){return"PRE"===e.nodeName};if(t.firstChild&&!r(t)){for(var a=null,s=!1,o=null,l=_(o,t,r);l!==t;){if(3===l.nodeType||4===l.nodeType){var c=l.data.replace(/[ \r\n\t]+/g," ");if(a&&!/ $/.test(a.data)||s||" "!==c[0]||(c=c.substr(1)),!c){l=m(l);continue}l.data=c,a=l}else{if(1!==l.nodeType){l=m(l);continue}i(l)||"BR"===l.nodeName?(a&&(a.data=a.data.replace(/ $/,"")),a=null,s=!1):n(l)||r(l)?(a=null,s=!0):a&&(s=!1)}var h=_(o,l,r);o=l,l=h}a&&(a.data=a.data.replace(/ $/,""),a.data||m(a))}}({element:i,isBlock:a,isVoid:o,isPre:t.preformattedCode?T:null}),i}function T(e){return"PRE"===e.nodeName||"CODE"===e.nodeName}function A(e,t){return e.isBlock=a(e),e.isCode="CODE"===e.nodeName||e.parentNode.isCode,e.isBlank=function(e){return!o(e)&&!function(e){return c(e,l)}(e)&&/^\s*$/i.test(e.textContent)&&!function(e){return h(e,s)}(e)&&!function(e){return h(e,l)}(e)}(e),e.flankingWhitespace=function(e,t){if(e.isBlock||t.preformattedCode&&e.isCode)return{leading:"",trailing:""};var i=(n=e.textContent,r=n.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/),{leading:r[1],leadingAscii:r[2],leadingNonAscii:r[3],trailing:r[4],trailingNonAscii:r[5],trailingAscii:r[6]});var n,r;i.leadingAscii&&S("left",e,t)&&(i.leading=i.leadingNonAscii);i.trailingAscii&&S("right",e,t)&&(i.trailing=i.trailingNonAscii);return{leading:i.leading,trailing:i.trailing}}(e,t),e}function S(e,t,i){var n,r,s;return"left"===e?(n=t.previousSibling,r=/ $/):(n=t.nextSibling,r=/^ /),n&&(3===n.nodeType?s=r.test(n.nodeValue):i.preformattedCode&&"CODE"===n.nodeName?s=!1:1!==n.nodeType||a(n)||(s=r.test(n.textContent))),s}var C=Array.prototype.reduce,L=[[/\\/g,"\\\\"],[/\*/g,"\\*"],[/^-/g,"\\-"],[/^\+ /g,"\\+ "],[/^(=+)/g,"\\$1"],[/^(#{1,6}) /g,"\\$1 "],[/`/g,"\\`"],[/^~~~/g,"\\~~~"],[/\[/g,"\\["],[/\]/g,"\\]"],[/^>/g,"\\>"],[/_/g,"\\_"],[/^(\d+)\. /g,"$1\\. "]];function x(e){if(!(this instanceof x))return new x(e);var t={rules:d,headingStyle:"setext",hr:"* * *",bulletListMarker:"*",codeBlockStyle:"indented",fence:"```",emDelimiter:"_",strongDelimiter:"**",linkStyle:"inlined",linkReferenceStyle:"full",br:" ",preformattedCode:!1,blankReplacement:function(e,t){return t.isBlock?"\n\n":""},keepReplacement:function(e,t){return t.isBlock?"\n\n"+t.outerHTML+"\n\n":t.outerHTML},defaultReplacement:function(e,t){return t.isBlock?"\n\n"+e+"\n\n":e}};this.options=function(e){for(var t=1;t0&&"\n"===e[t-1];)t--;return e.substring(0,t)}(e),n=t.replace(/^\n*/,""),r=Math.max(e.length-i.length,t.length-n.length);return i+"\n\n".substring(0,r)+n}x.prototype={turndown:function(e){if(!function(e){return null!=e&&("string"==typeof e||e.nodeType&&(1===e.nodeType||9===e.nodeType||11===e.nodeType))}(e))throw new TypeError(e+" is not a string, or an element/document/fragment node.");if(""===e)return"";var t=R.call(this,new v(e,this.options));return O.call(this,t)},use:function(e){if(Array.isArray(e))for(var t=0;t - readability [8 Jan 2025 at 20:49] + readability [18 Jan 2025 at 20:19] diff --git a/packages/flutter_mozilla_components/javascript/readability/dist/stats.json b/packages/flutter_mozilla_components/javascript/readability/dist/stats.json index 78a2bed7..b928e6b2 100644 --- a/packages/flutter_mozilla_components/javascript/readability/dist/stats.json +++ b/packages/flutter_mozilla_components/javascript/readability/dist/stats.json @@ -1,8 +1,8 @@ { - "hash": "a0b270ca122ccf1c69b1", - "version": "5.96.1", - "time": 676, - "builtAt": 1736365755248, + "hash": "68fe8281f85d9e1fa8f9", + "version": "5.97.1", + "time": 560, + "builtAt": 1737227987116, "publicPath": "auto", "outputPath": "/home/fafre/development/repos/lensai/packages/flutter_mozilla_components/javascript/readability/dist", "assetsByChunkName": { @@ -14,14 +14,14 @@ { "type": "asset", "name": "readability.min.js", - "size": 48065, + "size": 49051, "emitted": true, "comparedForEmit": false, "cached": false, "info": { "javascriptModule": false, "minimized": true, - "size": 48065 + "size": 49051 }, "chunkNames": [ "main" @@ -43,9 +43,9 @@ "initial": true, "entry": true, "recorded": false, - "size": 120878, + "size": 122485, "sizes": { - "javascript": 120208, + "javascript": 121815, "runtime": 670 }, "names": [ @@ -59,7 +59,7 @@ "readability.min.js" ], "auxiliaryFiles": [], - "hash": "fd19b958f18c40df17b3", + "hash": "bf73e1848f9bdc88f1b3", "childrenByOrder": {}, "id": 792, "siblings": [], @@ -316,7 +316,7 @@ "active": true, "explanation": "", "userRequest": "@mozilla/readability", - "loc": "13:17-37", + "loc": "44:17-37", "moduleId": 922, "resolvedModuleId": null }, @@ -330,7 +330,7 @@ "active": true, "explanation": "", "userRequest": "@mozilla/readability", - "loc": "19:19-30", + "loc": "45:19-30", "moduleId": 922, "resolvedModuleId": null } @@ -428,9 +428,9 @@ "type": "module", "moduleType": "javascript/esm", "layer": null, - "size": 27726, + "size": 29333, "sizes": { - "javascript": 27726 + "javascript": 29333 }, "built": true, "codeGenerated": true, @@ -497,9 +497,9 @@ "type": "module", "moduleType": "javascript/auto", "layer": null, - "size": 2664, + "size": 4271, "sizes": { - "javascript": 2664 + "javascript": 4271 }, "built": true, "codeGenerated": false, @@ -743,9 +743,9 @@ "type": "module", "moduleType": "javascript/esm", "layer": null, - "size": 27726, + "size": 29333, "sizes": { - "javascript": 27726 + "javascript": 29333 }, "built": true, "codeGenerated": true, @@ -811,9 +811,9 @@ "type": "module", "moduleType": "javascript/auto", "layer": null, - "size": 2664, + "size": 4271, "sizes": { - "javascript": 2664 + "javascript": 4271 }, "built": true, "codeGenerated": false, @@ -1009,7 +1009,7 @@ "active": true, "explanation": "", "userRequest": "@mozilla/readability", - "loc": "13:17-37", + "loc": "44:17-37", "moduleId": 922, "resolvedModuleId": null }, @@ -1023,7 +1023,7 @@ "active": true, "explanation": "", "userRequest": "@mozilla/readability", - "loc": "19:19-30", + "loc": "45:19-30", "moduleId": 922, "resolvedModuleId": null } @@ -1478,11 +1478,11 @@ "assets": [ { "name": "readability.min.js", - "size": 48065 + "size": 49051 } ], "filteredAssets": 0, - "assetsSize": 48065, + "assetsSize": 49051, "auxiliaryAssets": [], "filteredAuxiliaryAssets": 0, "auxiliaryAssetsSize": 0, @@ -1500,11 +1500,11 @@ "assets": [ { "name": "readability.min.js", - "size": 48065 + "size": 49051 } ], "filteredAssets": 0, - "assetsSize": 48065, + "assetsSize": 49051, "auxiliaryAssets": [], "filteredAuxiliaryAssets": 0, "auxiliaryAssetsSize": 0, diff --git a/packages/flutter_mozilla_components/javascript/readability/package-lock.json b/packages/flutter_mozilla_components/javascript/readability/package-lock.json index 8952f9b0..a8b2cb04 100644 --- a/packages/flutter_mozilla_components/javascript/readability/package-lock.json +++ b/packages/flutter_mozilla_components/javascript/readability/package-lock.json @@ -54,9 +54,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", "dev": true, "license": "MIT", "engines": { @@ -95,14 +95,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -124,28 +124,14 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -178,14 +164,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -196,9 +182,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dev": true, "license": "MIT", "dependencies": { @@ -272,9 +258,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -300,15 +286,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -317,20 +303,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", @@ -405,13 +377,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -619,13 +591,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -805,13 +777,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { @@ -954,15 +925,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1041,13 +1011,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1533,17 +1503,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/types": "^7.26.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1552,9 +1522,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", "dev": true, "license": "MIT", "dependencies": { @@ -1576,9 +1546,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "license": "MIT", "dependencies": { @@ -1716,83 +1686,83 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", "dependencies": { @@ -1800,9 +1770,9 @@ } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1810,79 +1780,79 @@ } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -2063,14 +2033,14 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -2092,13 +2062,13 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -2123,9 +2093,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -2143,9 +2113,9 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -2163,9 +2133,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001677", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", - "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", "dev": true, "funding": [ { @@ -2260,13 +2230,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.3" }, "funding": { "type": "opencollective", @@ -2296,9 +2266,9 @@ "license": "MIT" }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "license": "MIT", "dependencies": { @@ -2340,16 +2310,16 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.52", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.52.tgz", - "integrity": "sha512-xtoijJTZ+qeucLBDNztDOuQBE1ksqjvNjvqFoST3nGC7fSpqJ+X6BdTBaY5BHG+IhWWmpc6b/KfpeuEDupEPOQ==", + "version": "1.5.83", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.83.tgz", + "integrity": "sha512-LcUDPqSt+V0QmI47XLzZrz5OqILSMGsPFkDYus22rIbgorSvBYEFqq854ltTmUdHkY92FSdAAvsh4jWEULMdfQ==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2374,9 +2344,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "dev": true, "license": "MIT" }, @@ -2485,10 +2455,20 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -2820,9 +2800,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, "license": "MIT", "dependencies": { @@ -2924,9 +2904,9 @@ "license": "MIT" }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -3084,9 +3064,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -3343,16 +3323,16 @@ } }, "node_modules/regexpu-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", - "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -3368,9 +3348,9 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", - "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3380,6 +3360,19 @@ "regjsparser": "bin/parser" } }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/remove-markdown": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.5.tgz", @@ -3397,19 +3390,22 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3473,9 +3469,9 @@ "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "dev": true, "license": "MIT", "dependencies": { @@ -3485,7 +3481,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -3624,9 +3620,9 @@ } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3643,17 +3639,17 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -3677,59 +3673,6 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -3750,9 +3693,9 @@ } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "dev": true, "license": "MIT" }, @@ -3801,9 +3744,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "dev": true, "funding": [ { @@ -3822,7 +3765,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -3856,17 +3799,17 @@ } }, "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", diff --git a/packages/flutter_mozilla_components/javascript/readability/src/index.js b/packages/flutter_mozilla_components/javascript/readability/src/index.js index 1f545cc3..12ed0f82 100644 --- a/packages/flutter_mozilla_components/javascript/readability/src/index.js +++ b/packages/flutter_mozilla_components/javascript/readability/src/index.js @@ -5,33 +5,142 @@ const removeMarkdown = require('remove-markdown'); const turndownService = new TurndownService(); -function parseReaderable(document, options) { - const clonedDoc = document.cloneNode(true); - const readable = isProbablyReaderable(clonedDoc); +function sanitizeHtmlTags(node, unwantedSelectors) { + unwantedSelectors.forEach(selector => { + node.querySelectorAll(selector).forEach(el => el.remove()); + }); + + return node; +} + +function parseFullMarkdown(node) { + const clonedDoc = node.cloneNode(true); + + sanitizeHtmlTags(clonedDoc, [ + /* Navigation and Header Elements */ + 'nav', + 'footer', + 'header', + 'aside', + 'menu', + 'toolbar', + + /* Interactive Elements */ + 'form', + 'button', + '[role="button"]', + '[type="button"]', + 'dialog', + 'modal', + + /* Hidden Elements */ + '[aria-hidden="true"]', + '[hidden]', + '[style*="display: none"]', + '[style*="visibility: hidden"]', + '.hidden', + '.invisible', + + /* Advertising and Marketing */ + '.ad', + '.advertisement', + '.banner', + '.sponsored', + '.promotion', + '.popup', + '.newsletter', + '.subscribe', + + /* Social Media */ + '.social-share', + '.social-media', + '.share-buttons', + '.follow-us', + '.likes', + '.comments', + + /* Common UI Components */ + '.cookie-notice', + '.cookie-banner', + '.modal', + '.overlay', + '.tooltip', + '.popup', + '.sidebar', + '.widget', + + /* Navigation Related */ + '.breadcrumb', + '.pagination', + '.menu', + '.navbar', + '.navigation', + + /* Related Content */ + '.related-posts', + '.recommended', + '.suggestions', + '.read-more', + + /* Interactive Features */ + '.search', + '.search-box', + '.login', + '.signup', + '.register', + + /* Print-specific */ + '.print-only', + '[media="print"]' + ]); const fullMarkdown = turndownService.turndown(clonedDoc.body.innerHTML); let response = { - fullContentMarkdown: fullMarkdown, - fullContentPlain: removeMarkdown(fullMarkdown), - } - - const reader = new Readability(clonedDoc, options); - const article = reader.parse(); - - if(article != null) { - const extractedMarkdown = turndownService.turndown(article.content); - - response = { - ...response, - isProbablyReaderable: readable ? 1 : 0, - extractedContentMarkdown: extractedMarkdown, - extractedContentPlain: removeMarkdown(extractedMarkdown) - }; - } + fullContentMarkdown: fullMarkdown, + fullContentPlain: removeMarkdown(fullMarkdown), + }; return response; } +function parseReaderable(document, options) { + const clonedDoc = document.cloneNode(true); + sanitizeHtmlTags(clonedDoc, [ + /* Technical Elements */ + 'script', + 'style', + 'noscript', + 'iframe', + 'svg', + 'img', + 'video', + 'audio', + 'canvas', + 'object', + 'embed', + ]); + + let response = parseFullMarkdown(clonedDoc); + + const readable = isProbablyReaderable(clonedDoc); + + const reader = new Readability(clonedDoc, options); + const article = reader.parse(); + + if (article != null) { + const extractedMarkdown = turndownService.turndown(article.content); + + response = { + ...response, + isProbablyReaderable: readable ? 1 : 0, + extractedContentMarkdown: extractedMarkdown, + extractedContentPlain: removeMarkdown(extractedMarkdown) + }; + } + + return response; +} + window.parseReaderable = parseReaderable; export { parseReaderable }; \ No newline at end of file diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index 25f90a02..161b6e2a 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v22.7.1), do not edit directly. +// Autogenerated from Pigeon (v22.7.2), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers diff --git a/packages/flutter_mozilla_components/pubspec.yaml b/packages/flutter_mozilla_components/pubspec.yaml index 52aa665d..73a02b97 100644 --- a/packages/flutter_mozilla_components/pubspec.yaml +++ b/packages/flutter_mozilla_components/pubspec.yaml @@ -17,7 +17,7 @@ dev_dependencies: flutter_test: sdk: flutter lint: ^2.3.0 - pigeon: ^22.7.1 + pigeon: ^22.7.3 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/packages/sqlite3_vec/android/.gitignore b/packages/sqlite3_vec/android/.gitignore index 55afd919..98c6943f 100644 --- a/packages/sqlite3_vec/android/.gitignore +++ b/packages/sqlite3_vec/android/.gitignore @@ -1,3 +1,5 @@ +*.iml +.gradle gradle-wrapper.jar /.gradle /captures/ @@ -5,9 +7,9 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/to/reference-keystore -key.properties -**/*.keystore -**/*.jks +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/sqlite3_vec/example/.flutter-plugins b/packages/sqlite3_vec/example/.flutter-plugins index f7e79017..aa393105 100644 --- a/packages/sqlite3_vec/example/.flutter-plugins +++ b/packages/sqlite3_vec/example/.flutter-plugins @@ -1,4 +1,4 @@ # This is a generated file; do not edit or check into version control. integration_test=/home/fafre/development/flutter/packages/integration_test/ -sqlite3_flutter_libs=/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/ +sqlite3_flutter_libs=/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/ sqlite3_vec=/home/fafre/development/repos/lensai/packages/sqlite3_vec/ diff --git a/packages/sqlite3_vec/example/.flutter-plugins-dependencies b/packages/sqlite3_vec/example/.flutter-plugins-dependencies index 6898a0c9..05cff735 100644 --- a/packages/sqlite3_vec/example/.flutter-plugins-dependencies +++ b/packages/sqlite3_vec/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-17 14:26:43.439781","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-22 22:25:30.421767","version":"3.27.1","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs b/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs index 5df7b5a0..0a4a00f2 120000 --- a/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs +++ b/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.28/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/ \ No newline at end of file diff --git a/packages/sqlite3_vec/example/pubspec.yaml b/packages/sqlite3_vec/example/pubspec.yaml index 6c466314..47522076 100644 --- a/packages/sqlite3_vec/example/pubspec.yaml +++ b/packages/sqlite3_vec/example/pubspec.yaml @@ -28,8 +28,8 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 - sqlite3: ^2.6.0 - sqlite3_flutter_libs: ^0.5.28 + sqlite3: ^2.7.2 + sqlite3_flutter_libs: ^0.5.29 dev_dependencies: integration_test: diff --git a/packages/sqlite3_vec/pubspec.yaml b/packages/sqlite3_vec/pubspec.yaml index 4ce9b7b5..8f47aafa 100644 --- a/packages/sqlite3_vec/pubspec.yaml +++ b/packages/sqlite3_vec/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - sqlite3: ^2.6.0 + sqlite3: ^2.7.2 dev_dependencies: flutter_test: