From 5651b9255d9aa36bd9feb7612b281452b20d1104 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 1 Oct 2025 08:00:06 +0200 Subject: [PATCH] basic container sugegstion implementation --- .../widgets/sheets/view_tabs.dart | 1 + .../domain/repositories/gecko_inference.dart | 49 +++++++++++------ .../repositories/gecko_inference.g.dart | 21 ++++---- .../presentation/widgets/container_chips.dart | 53 +++++++++++++++++++ 4 files changed, 97 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 a59e4235..9ec20891 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 @@ -279,6 +279,7 @@ class _TabSheetHeader extends HookConsumerWidget { ); return ContainerChips( + showGroupSuggestions: true, selectedContainer: selectedContainer, onSelected: (container) async { if (container != 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 d2e80aac..ecc15c93 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 @@ -134,8 +134,8 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository { return neighbors; } - Future>?> suggestClusters({ - required List unassignedDocumentsInput, + Future tabIds})>?> suggestClusters({ + required Map unassignedDocumentsInput, }) async { if (!ref.read( generalSettingsWithDefaultsProvider.select( @@ -146,7 +146,9 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository { } final processedDocuments = {}; - final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) { + final unassignedDocumentsProcessed = unassignedDocumentsInput.values.map(( + doc, + ) { final processed = preprocessText(doc); if (processed != doc) { processedDocuments[processed] = doc; @@ -168,9 +170,25 @@ class GeckoInferenceRepository extends _$GeckoInferenceRepository { .toList(), ); - print(clusters); + final clusterResult = await clusters.mapNotNull( + (cluster) => Future.wait( + cluster.map((clusterTitles) async { + final topic = await predictDocumentTopic(clusterTitles.toSet()); - return null; + return ( + topic: topic, + tabIds: clusterTitles.map((title) { + final originalTitle = processedDocuments[title] ?? title; + return unassignedDocumentsInput.entries + .firstWhere((entry) => entry.value == originalTitle) + .key; + }).toList(), + ); + }), + ), + ); + + return clusterResult; } Future>?> generateDocumentEmbeddings( @@ -236,26 +254,25 @@ Future containerTopic(Ref ref, String containerId) async { } @Riverpod() -Future>?> suggestClusters(Ref ref) async { +Future tabIds, String? topic})>?> suggestClusters( + Ref ref, +) async { final unassignedTitles = await ref.watch( containerTabsDataProvider(null).selectAsync( (tabData) => EquatableValue( - tabData - .where((tab) => tab.title.isNotEmpty) - .map((tab) => (tab.id, tab.title!)) - .toSet(), + Map.fromEntries( + tabData + .where((tab) => tab.title.isNotEmpty) + .map((tab) => MapEntry(tab.id, tab.title!)), + ), ), ), ); if (unassignedTitles.value.isNotEmpty) { - await ref + return await ref .read(geckoInferenceRepositoryProvider.notifier) - .suggestClusters( - unassignedDocumentsInput: unassignedTitles.value - .map((tab) => tab.$2) - .toList(), - ); + .suggestClusters(unassignedDocumentsInput: unassignedTitles.value); } return null; 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 d2151b6d..30740dbb 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 @@ -42,7 +42,7 @@ final class GeckoInferenceRepositoryProvider } String _$geckoInferenceRepositoryHash() => - r'1f08047bee912a475bfd0851f8a9868a41bca836'; + r'eb174c9f80f3537bc075ebbffcef787ec55ad217'; abstract class _$GeckoInferenceRepository extends $Notifier { void build(); @@ -138,13 +138,13 @@ const suggestClustersProvider = SuggestClustersProvider._(); final class SuggestClustersProvider extends $FunctionalProvider< - AsyncValue>?>, - List>?, - FutureOr>?> + AsyncValue tabIds, String? topic})>?>, + List<({List tabIds, String? topic})>?, + FutureOr tabIds, String? topic})>?> > with - $FutureModifier>?>, - $FutureProvider>?> { + $FutureModifier tabIds, String? topic})>?>, + $FutureProvider tabIds, String? topic})>?> { const SuggestClustersProvider._() : super( from: null, @@ -161,17 +161,16 @@ final class SuggestClustersProvider @$internal @override - $FutureProviderElement>?> $createElement( - $ProviderPointer pointer, - ) => $FutureProviderElement(pointer); + $FutureProviderElement tabIds, String? topic})>?> + $createElement($ProviderPointer pointer) => $FutureProviderElement(pointer); @override - FutureOr>?> create(Ref ref) { + FutureOr tabIds, String? topic})>?> create(Ref ref) { return suggestClusters(ref); } } -String _$suggestClustersHash() => r'9d212158b80ba53afe305db938ab399b1e931d88'; +String _$suggestClustersHash() => r'592058ed092384c37a185dcd5657276a3dde5b4e'; @ProviderFor(containerTabSuggestions) const containerTabSuggestionsProvider = ContainerTabSuggestionsFamily._(); diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index b3f65e14..0da6bd09 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -23,16 +23,21 @@ 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:nullability/nullability.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart'; import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/tab_drag_container_target.dart'; +import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/widgets/selectable_chips.dart'; class ContainerChips extends HookConsumerWidget { final bool displayMenu; + final bool showGroupSuggestions; final ContainerData? selectedContainer; final bool Function(ContainerDataWithCount)? containerFilter; @@ -48,6 +53,7 @@ class ContainerChips extends HookConsumerWidget { this.containerFilter, this.searchTextListenable, this.displayMenu = true, + this.showGroupSuggestions = false, }); @override @@ -132,6 +138,53 @@ class ContainerChips extends HookConsumerWidget { }, ), ), + if (showGroupSuggestions) + Consumer( + builder: (context, ref, child) { + final tabSuggestionsEnabled = ref.watch( + tabSuggestionsControllerProvider, + ); + final enableAiFeatures = ref.watch( + generalSettingsWithDefaultsProvider.select( + (settings) => settings.enableLocalAiFeatures, + ), + ); + + if (!enableAiFeatures || !tabSuggestionsEnabled) { + return const SizedBox.shrink(); + } + + final suggestions = ref.watch( + suggestClustersProvider, + ); + + return suggestions.when( + data: (data) { + if (data.isEmpty) { + return const SizedBox.shrink(); + } + + return FilterChip( + avatar: const Icon(MdiIcons.autoFix), + label: Text(data!.length.toString()), + showCheckmark: false, + onSelected: (value) {}, + ); + }, + error: (error, stackTrace) { + logger.e( + 'Error suggesting containers', + error: error, + stackTrace: stackTrace, + ); + return const SizedBox.shrink(); + }, + loading: () { + return const SizedBox.shrink(); + }, + ); + }, + ), ], availableItems: availableContainers, selectedItem: selectedContainer,