local ai killswitch

This commit is contained in:
Fabian Freund
2025-07-25 22:18:08 +02:00
parent 9ae21190f4
commit dd40676b36
8 changed files with 100 additions and 27 deletions
@@ -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/domain/repositories/tab_search.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.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/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/hooks/listenable_callback.dart';
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart'; import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
@@ -108,6 +109,12 @@ class _TabSheetHeader extends HookConsumerWidget {
() => searchTextController.text.isNotEmpty, () => searchTextController.text.isNotEmpty,
); );
final enableAiFeatures = ref.watch(
generalSettingsRepositoryProvider.select(
(settings) => settings.enableLocalAiFeatures,
),
);
useListenableCallback(searchTextController, () async { useListenableCallback(searchTextController, () async {
await ref await ref
.read( .read(
@@ -160,27 +167,29 @@ class _TabSheetHeader extends HookConsumerWidget {
.toggle(); .toggle();
}, },
), ),
Consumer( if (enableAiFeatures)
builder: (context, ref, child) { Consumer(
final tabSuggestionsEnabled = ref.watch( builder: (context, ref, child) {
tabSuggestionsControllerProvider, final tabSuggestionsEnabled = ref.watch(
); tabSuggestionsControllerProvider,
);
return IconButton.filledTonal( return IconButton.filledTonal(
icon: const Icon(MdiIcons.imageAutoAdjust), icon: const Icon(MdiIcons.imageAutoAdjust),
isSelected: tabSuggestionsEnabled, isSelected: tabSuggestionsEnabled,
iconSize: 18, iconSize: 18,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () { onPressed: () {
ref ref
.read( .read(
tabSuggestionsControllerProvider.notifier, tabSuggestionsControllerProvider
) .notifier,
.toggle(); )
}, .toggle();
); },
}, );
), },
),
], ],
), ),
TextButton.icon( TextButton.icon(
@@ -215,9 +224,9 @@ class _TabSheetHeader extends HookConsumerWidget {
); );
if (result == true) { if (result == true) {
await ref await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.closeAllTabsByContainer(container); .closeAllTabsByContainer(container);
} }
}, },
icon: const Icon(MdiIcons.closeBoxMultiple), icon: const Icon(MdiIcons.closeBoxMultiple),
@@ -380,9 +389,16 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
final tabSuggestionsEnabled = ref.watch( final tabSuggestionsEnabled = ref.watch(
tabSuggestionsControllerProvider, tabSuggestionsControllerProvider,
); );
final enableAiFeatures = ref.watch(
generalSettingsRepositoryProvider.select(
(settings) => settings.enableLocalAiFeatures,
),
);
final suggestedTabEntities = ref.watch( final suggestedTabEntities = ref.watch(
suggestedTabEntitiesProvider( suggestedTabEntitiesProvider(
tabSuggestionsEnabled ? containerId : null, (enableAiFeatures && tabSuggestionsEnabled)
? containerId
: null,
), ),
); );
@@ -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/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/utils/embedding_text_processing.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/geckoview/features/tabs/utils/nearest_neighbor.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/utils/lru_cache.dart'; import 'package:weblibre/utils/lru_cache.dart';
part 'gecko_inference.g.dart'; part 'gecko_inference.g.dart';
@@ -41,6 +42,14 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
} }
Future<String?> predictDocumentTopic(Set<String> titles) async { Future<String?> predictDocumentTopic(Set<String> titles) async {
if (!ref.read(
generalSettingsRepositoryProvider.select(
(settings) => settings.enableLocalAiFeatures,
),
)) {
return null;
}
if (titles.isNotEmpty) { if (titles.isNotEmpty) {
if (_topicCache.get(titles) case final String title) { if (_topicCache.get(titles) case final String title) {
return title; return title;
@@ -69,6 +78,14 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository {
required List<String> assignedDocumentsInput, required List<String> assignedDocumentsInput,
required List<String> unassignedDocumentsInput, required List<String> unassignedDocumentsInput,
}) async { }) async {
if (!ref.read(
generalSettingsRepositoryProvider.select(
(settings) => settings.enableLocalAiFeatures,
),
)) {
return null;
}
final processedDocuments = <String, String>{}; final processedDocuments = <String, String>{};
final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) { final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) {
final processed = preprocessText(doc); final processed = preprocessText(doc);
@@ -276,7 +276,7 @@ class _ContainerTabSuggestionsProviderElement
} }
String _$geckoInferenceRepositoryHash() => String _$geckoInferenceRepositoryHash() =>
r'bdae6dab14c33c6503f37f2e17b8d50fb820ac26'; r'170bd0b67e5f1edc43ed97d4535f41353fb8796e';
/// See also [GeckoInferenceRepository]. /// See also [GeckoInferenceRepository].
@ProviderFor(GeckoInferenceRepository) @ProviderFor(GeckoInferenceRepository)
@@ -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( SwitchListTile.adaptive(
title: const Text('Enable Reader Mode'), title: const Text('Enable Reader Mode'),
subtitle: const Text( subtitle: const Text(
@@ -34,6 +34,7 @@ class GeneralSettings with FastEquatable {
final SearchSuggestionProviders defaultSearchSuggestionsProvider; final SearchSuggestionProviders defaultSearchSuggestionsProvider;
final bool createChildTabsOption; final bool createChildTabsOption;
final bool showExtensionShortcut; final bool showExtensionShortcut;
final bool enableLocalAiFeatures;
final bool proxyPrivateTabsTor; final bool proxyPrivateTabsTor;
@@ -47,6 +48,7 @@ class GeneralSettings with FastEquatable {
required this.createChildTabsOption, required this.createChildTabsOption,
required this.proxyPrivateTabsTor, required this.proxyPrivateTabsTor,
required this.showExtensionShortcut, required this.showExtensionShortcut,
required this.enableLocalAiFeatures,
}); });
GeneralSettings.withDefaults({ GeneralSettings.withDefaults({
@@ -59,6 +61,7 @@ class GeneralSettings with FastEquatable {
bool? createChildTabsOption, bool? createChildTabsOption,
bool? proxyPrivateTabsTor, bool? proxyPrivateTabsTor,
bool? showExtensionShortcut, bool? showExtensionShortcut,
bool? enableLocalAiFeatures,
}) : themeMode = themeMode ?? ThemeMode.dark, }) : themeMode = themeMode ?? ThemeMode.dark,
enableReadability = enableReadability ?? true, enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false, enforceReadability = enforceReadability ?? false,
@@ -67,7 +70,8 @@ class GeneralSettings with FastEquatable {
defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider, defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider,
createChildTabsOption = createChildTabsOption ?? false, createChildTabsOption = createChildTabsOption ?? false,
proxyPrivateTabsTor = proxyPrivateTabsTor ?? false, proxyPrivateTabsTor = proxyPrivateTabsTor ?? false,
showExtensionShortcut = showExtensionShortcut ?? false; showExtensionShortcut = showExtensionShortcut ?? false,
enableLocalAiFeatures = enableLocalAiFeatures ?? true;
factory GeneralSettings.fromJson(Map<String, dynamic> json) => factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
_$GeneralSettingsFromJson(json); _$GeneralSettingsFromJson(json);
@@ -85,5 +89,6 @@ class GeneralSettings with FastEquatable {
createChildTabsOption, createChildTabsOption,
showExtensionShortcut, showExtensionShortcut,
proxyPrivateTabsTor, proxyPrivateTabsTor,
enableLocalAiFeatures,
]; ];
} }
@@ -29,6 +29,8 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings showExtensionShortcut(bool showExtensionShortcut); 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. /// 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 /// Usage
@@ -45,6 +47,7 @@ abstract class _$GeneralSettingsCWProxy {
bool createChildTabsOption, bool createChildTabsOption,
bool proxyPrivateTabsTor, bool proxyPrivateTabsTor,
bool showExtensionShortcut, bool showExtensionShortcut,
bool enableLocalAiFeatures,
}); });
} }
@@ -91,6 +94,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings showExtensionShortcut(bool showExtensionShortcut) => GeneralSettings showExtensionShortcut(bool showExtensionShortcut) =>
this(showExtensionShortcut: showExtensionShortcut); this(showExtensionShortcut: showExtensionShortcut);
@override
GeneralSettings enableLocalAiFeatures(bool enableLocalAiFeatures) =>
this(enableLocalAiFeatures: enableLocalAiFeatures);
@override @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. /// 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? createChildTabsOption = const $CopyWithPlaceholder(),
Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(), Object? proxyPrivateTabsTor = const $CopyWithPlaceholder(),
Object? showExtensionShortcut = const $CopyWithPlaceholder(), Object? showExtensionShortcut = const $CopyWithPlaceholder(),
Object? enableLocalAiFeatures = const $CopyWithPlaceholder(),
}) { }) {
return GeneralSettings( return GeneralSettings(
themeMode: themeMode == const $CopyWithPlaceholder() themeMode: themeMode == const $CopyWithPlaceholder()
@@ -151,6 +159,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.showExtensionShortcut ? _value.showExtensionShortcut
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: showExtensionShortcut as bool, : 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<String, dynamic> json) =>
createChildTabsOption: json['createChildTabsOption'] as bool?, createChildTabsOption: json['createChildTabsOption'] as bool?,
proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?, proxyPrivateTabsTor: json['proxyPrivateTabsTor'] as bool?,
showExtensionShortcut: json['showExtensionShortcut'] as bool?, showExtensionShortcut: json['showExtensionShortcut'] as bool?,
enableLocalAiFeatures: json['enableLocalAiFeatures'] as bool?,
); );
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) => Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
@@ -198,6 +212,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
.defaultSearchSuggestionsProvider]!, .defaultSearchSuggestionsProvider]!,
'createChildTabsOption': instance.createChildTabsOption, 'createChildTabsOption': instance.createChildTabsOption,
'showExtensionShortcut': instance.showExtensionShortcut, 'showExtensionShortcut': instance.showExtensionShortcut,
'enableLocalAiFeatures': instance.enableLocalAiFeatures,
'proxyPrivateTabsTor': instance.proxyPrivateTabsTor, 'proxyPrivateTabsTor': instance.proxyPrivateTabsTor,
}; };
@@ -60,6 +60,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool, DriftSqlType.bool,
db.typeMapping, db.typeMapping,
), ),
'enableLocalAiFeatures': settings['enableLocalAiFeatures']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
}); });
} }
@@ -7,7 +7,7 @@ part of 'general_settings.dart';
// ************************************************************************** // **************************************************************************
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'eb5f27e0bd8f5def833e1c703cb42181acc2440f'; r'b3c60b14cdfba7e0bcf4817addc8295b5281a9b5';
/// See also [GeneralSettingsRepository]. /// See also [GeneralSettingsRepository].
@ProviderFor(GeneralSettingsRepository) @ProviderFor(GeneralSettingsRepository)