From dd40676b3603df4714bb5930654d13d6704f96b6 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 25 Jul 2025 22:18:08 +0200 Subject: [PATCH] local ai killswitch --- .../widgets/sheets/view_tabs.dart | 64 ++++++++++++------- .../domain/repositories/gecko_inference.dart | 17 +++++ .../repositories/gecko_inference.g.dart | 2 +- .../screens/general_settings.dart | 16 +++++ .../user/data/models/general_settings.dart | 7 +- .../user/data/models/general_settings.g.dart | 15 +++++ .../domain/repositories/general_settings.dart | 4 ++ .../repositories/general_settings.g.dart | 2 +- 8 files changed, 100 insertions(+), 27 deletions(-) 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 a86e5ad6..bfb821fc 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 @@ -25,6 +25,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/ta import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart'; import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/listenable_callback.dart'; import 'package:weblibre/presentation/widgets/speech_to_text_button.dart'; @@ -108,6 +109,12 @@ class _TabSheetHeader extends HookConsumerWidget { () => searchTextController.text.isNotEmpty, ); + final enableAiFeatures = ref.watch( + generalSettingsRepositoryProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + ); + useListenableCallback(searchTextController, () async { await ref .read( @@ -160,27 +167,29 @@ class _TabSheetHeader extends HookConsumerWidget { .toggle(); }, ), - Consumer( - builder: (context, ref, child) { - final tabSuggestionsEnabled = ref.watch( - tabSuggestionsControllerProvider, - ); + if (enableAiFeatures) + Consumer( + builder: (context, ref, child) { + final tabSuggestionsEnabled = ref.watch( + tabSuggestionsControllerProvider, + ); - return IconButton.filledTonal( - icon: const Icon(MdiIcons.imageAutoAdjust), - isSelected: tabSuggestionsEnabled, - iconSize: 18, - padding: EdgeInsets.zero, - onPressed: () { - ref - .read( - tabSuggestionsControllerProvider.notifier, - ) - .toggle(); - }, - ); - }, - ), + return IconButton.filledTonal( + icon: const Icon(MdiIcons.imageAutoAdjust), + isSelected: tabSuggestionsEnabled, + iconSize: 18, + padding: EdgeInsets.zero, + onPressed: () { + ref + .read( + tabSuggestionsControllerProvider + .notifier, + ) + .toggle(); + }, + ); + }, + ), ], ), TextButton.icon( @@ -215,9 +224,9 @@ class _TabSheetHeader extends HookConsumerWidget { ); if (result == true) { - await ref - .read(tabDataRepositoryProvider.notifier) - .closeAllTabsByContainer(container); + await ref + .read(tabDataRepositoryProvider.notifier) + .closeAllTabsByContainer(container); } }, icon: const Icon(MdiIcons.closeBoxMultiple), @@ -380,9 +389,16 @@ class ViewTabsSheetWidget extends HookConsumerWidget { final tabSuggestionsEnabled = ref.watch( tabSuggestionsControllerProvider, ); + final enableAiFeatures = ref.watch( + generalSettingsRepositoryProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + ); final suggestedTabEntities = ref.watch( suggestedTabEntitiesProvider( - tabSuggestionsEnabled ? containerId : null, + (enableAiFeatures && tabSuggestionsEnabled) + ? containerId + : null, ), ); diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart index ab399ba3..bd039561 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart @@ -10,6 +10,7 @@ import 'package:synchronized/synchronized.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/embedding_text_processing.dart'; import 'package:weblibre/features/geckoview/features/tabs/utils/nearest_neighbor.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/lru_cache.dart'; part 'gecko_inference.g.dart'; @@ -41,6 +42,14 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository { } Future predictDocumentTopic(Set titles) async { + if (!ref.read( + generalSettingsRepositoryProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + )) { + return null; + } + if (titles.isNotEmpty) { if (_topicCache.get(titles) case final String title) { return title; @@ -69,6 +78,14 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository { required List assignedDocumentsInput, required List unassignedDocumentsInput, }) async { + if (!ref.read( + generalSettingsRepositoryProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + )) { + return null; + } + final processedDocuments = {}; final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) { final processed = preprocessText(doc); diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.g.dart index dcae4e89..133a424c 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/gecko_inference.g.dart @@ -276,7 +276,7 @@ class _ContainerTabSuggestionsProviderElement } String _$geckoInferenceRepositoryHash() => - r'bdae6dab14c33c6503f37f2e17b8d50fb820ac26'; + r'170bd0b67e5f1edc43ed97d4535f41353fb8796e'; /// See also [GeckoInferenceRepository]. @ProviderFor(GeckoInferenceRepository) diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart index bf3830cd..3468232f 100644 --- a/app/lib/features/settings/presentation/screens/general_settings.dart +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -156,6 +156,22 @@ class GeneralSettingsScreen extends HookConsumerWidget { ], ), ), + SwitchListTile.adaptive( + title: const Text('On Device AI'), + subtitle: const Text( + 'Local on-device features including container topic and tab suggestions', + ), + secondary: const Icon(MdiIcons.creation), + value: generalSettings.enableLocalAiFeatures, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .enableLocalAiFeatures(value), + ); + }, + ), SwitchListTile.adaptive( title: const Text('Enable Reader Mode'), subtitle: const Text( diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 7339709a..faccac28 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -34,6 +34,7 @@ class GeneralSettings with FastEquatable { final SearchSuggestionProviders defaultSearchSuggestionsProvider; final bool createChildTabsOption; final bool showExtensionShortcut; + final bool enableLocalAiFeatures; final bool proxyPrivateTabsTor; @@ -47,6 +48,7 @@ class GeneralSettings with FastEquatable { required this.createChildTabsOption, required this.proxyPrivateTabsTor, required this.showExtensionShortcut, + required this.enableLocalAiFeatures, }); GeneralSettings.withDefaults({ @@ -59,6 +61,7 @@ class GeneralSettings with FastEquatable { bool? createChildTabsOption, bool? proxyPrivateTabsTor, bool? showExtensionShortcut, + bool? enableLocalAiFeatures, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -67,7 +70,8 @@ class GeneralSettings with FastEquatable { defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider, createChildTabsOption = createChildTabsOption ?? false, proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, - showExtensionShortcut = showExtensionShortcut ?? false; + showExtensionShortcut = showExtensionShortcut ?? false, + enableLocalAiFeatures = enableLocalAiFeatures ?? true; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -85,5 +89,6 @@ class GeneralSettings with FastEquatable { createChildTabsOption, showExtensionShortcut, proxyPrivateTabsTor, + enableLocalAiFeatures, ]; } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index d05e1ecb..f0aeb8d0 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -29,6 +29,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings showExtensionShortcut(bool showExtensionShortcut); + GeneralSettings enableLocalAiFeatures(bool enableLocalAiFeatures); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -45,6 +47,7 @@ abstract class _$GeneralSettingsCWProxy { bool createChildTabsOption, bool proxyPrivateTabsTor, bool showExtensionShortcut, + bool enableLocalAiFeatures, }); } @@ -91,6 +94,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings showExtensionShortcut(bool showExtensionShortcut) => this(showExtensionShortcut: showExtensionShortcut); + @override + GeneralSettings enableLocalAiFeatures(bool enableLocalAiFeatures) => + this(enableLocalAiFeatures: enableLocalAiFeatures); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -108,6 +115,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? createChildTabsOption = const $CopyWithPlaceholder(), Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), Object? showExtensionShortcut = const $CopyWithPlaceholder(), + Object? enableLocalAiFeatures = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() @@ -151,6 +159,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.showExtensionShortcut // ignore: cast_nullable_to_non_nullable : showExtensionShortcut as bool, + enableLocalAiFeatures: + enableLocalAiFeatures == const $CopyWithPlaceholder() + ? _value.enableLocalAiFeatures + // ignore: cast_nullable_to_non_nullable + : enableLocalAiFeatures as bool, ); } } @@ -182,6 +195,7 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => createChildTabsOption: json['createChildTabsOption'] as bool?, proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, showExtensionShortcut: json['showExtensionShortcut'] as bool?, + enableLocalAiFeatures: json['enableLocalAiFeatures'] as bool?, ); Map _$GeneralSettingsToJson(GeneralSettings instance) => @@ -198,6 +212,7 @@ Map _$GeneralSettingsToJson(GeneralSettings instance) => .defaultSearchSuggestionsProvider]!, 'createChildTabsOption': instance.createChildTabsOption, 'showExtensionShortcut': instance.showExtensionShortcut, + 'enableLocalAiFeatures': instance.enableLocalAiFeatures, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, }; diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index 06c7e6d9..d345d664 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -60,6 +60,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'enableLocalAiFeatures': settings['enableLocalAiFeatures']?.readAs( + DriftSqlType.bool, + db.typeMapping, + ), }); } diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index c1d85d8c..89a05795 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -7,7 +7,7 @@ part of 'general_settings.dart'; // ************************************************************************** String _$generalSettingsRepositoryHash() => - r'eb5f27e0bd8f5def833e1c703cb42181acc2440f'; + r'b3c60b14cdfba7e0bcf4817addc8295b5281a9b5'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository)