From e46f84d055c37159891c94b73590b72306c8fa57 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Thu, 2 Oct 2025 15:47:25 +0200 Subject: [PATCH] suggest title based on selection --- .../domain/repositories/gecko_inference.dart | 28 +++ .../repositories/gecko_inference.g.dart | 162 +++++++++++++++++- .../screens/container_draft_suggestions.dart | 51 +++++- 3 files changed, 236 insertions(+), 5 deletions(-) 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 81d635ee..58de744d 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 @@ -27,6 +27,7 @@ import 'package:nullability/nullability.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:synchronized/synchronized.dart'; +import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/utils/lru_cache.dart'; @@ -251,6 +252,33 @@ Future containerTopic(Ref ref, String containerId) async { if (!ref.mounted) return null; + final topic = await ref.watch(topicSuggestionProvider(titles).future); + + if (ref.mounted && topic.isNotEmpty) { + ref.keepAlive(); + } + + return topic; +} + +@Riverpod() +AsyncValue tabsTopic(Ref ref, EquatableValue> tabIds) { + final tabTitles = ref.watch( + tabStatesProvider.select((states) { + return EquatableValue( + tabIds.value.map((tabId) => states[tabId]?.title).nonNulls.toSet(), + ); + }), + ); + + return ref.watch(topicSuggestionProvider(tabTitles)); +} + +@Riverpod() +Future topicSuggestion( + Ref ref, + EquatableValue> titles, +) async { final topic = await ref .read(geckoInferenceRepositoryProvider.notifier) .predictDocumentTopic(titles.value); 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 97704b85..43c691a1 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 @@ -112,7 +112,7 @@ final class ContainerTopicProvider } } -String _$containerTopicHash() => r'ddb306605a0e38b1d750bf4412cbd595f8f264cc'; +String _$containerTopicHash() => r'c6e4cc316d3f05d0316909ec0bb58a95ad2fd57c'; final class ContainerTopicFamily extends $Family with $FunctionalFamilyOverride, String> { @@ -132,6 +132,166 @@ final class ContainerTopicFamily extends $Family String toString() => r'containerTopicProvider'; } +@ProviderFor(tabsTopic) +const tabsTopicProvider = TabsTopicFamily._(); + +final class TabsTopicProvider + extends + $FunctionalProvider< + AsyncValue, + AsyncValue, + AsyncValue + > + with $Provider> { + const TabsTopicProvider._({ + required TabsTopicFamily super.from, + required EquatableValue> super.argument, + }) : super( + retry: null, + name: r'tabsTopicProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$tabsTopicHash(); + + @override + String toString() { + return r'tabsTopicProvider' + '' + '($argument)'; + } + + @$internal + @override + $ProviderElement> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AsyncValue create(Ref ref) { + final argument = this.argument as EquatableValue>; + return tabsTopic(ref, argument); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AsyncValue value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } + + @override + bool operator ==(Object other) { + return other is TabsTopicProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$tabsTopicHash() => r'b144f86782510333a48e92c4a2c70ac285da6bcb'; + +final class TabsTopicFamily extends $Family + with + $FunctionalFamilyOverride< + AsyncValue, + EquatableValue> + > { + const TabsTopicFamily._() + : super( + retry: null, + name: r'tabsTopicProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + TabsTopicProvider call(EquatableValue> tabIds) => + TabsTopicProvider._(argument: tabIds, from: this); + + @override + String toString() => r'tabsTopicProvider'; +} + +@ProviderFor(topicSuggestion) +const topicSuggestionProvider = TopicSuggestionFamily._(); + +final class TopicSuggestionProvider + extends $FunctionalProvider, String?, FutureOr> + with $FutureModifier, $FutureProvider { + const TopicSuggestionProvider._({ + required TopicSuggestionFamily super.from, + required EquatableValue> super.argument, + }) : super( + retry: null, + name: r'topicSuggestionProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$topicSuggestionHash(); + + @override + String toString() { + return r'topicSuggestionProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as EquatableValue>; + return topicSuggestion(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is TopicSuggestionProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$topicSuggestionHash() => r'5401b1cd91e53be6ad1b4c7a39a5253d380e05d4'; + +final class TopicSuggestionFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + EquatableValue> + > { + const TopicSuggestionFamily._() + : super( + retry: null, + name: r'topicSuggestionProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + TopicSuggestionProvider call(EquatableValue> titles) => + TopicSuggestionProvider._(argument: titles, from: this); + + @override + String toString() => r'topicSuggestionProvider'; +} + @ProviderFor(suggestClusters) const suggestClustersProvider = SuggestClustersProvider._(); diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart index 754f545a..835f677c 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart @@ -1,9 +1,13 @@ import 'dart:math' as math; +import 'package:fast_equatable/fast_equatable.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:nullability/nullability.dart'; +import 'package:skeletonizer/skeletonizer.dart'; +import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/routing/routes.dart'; import 'package:weblibre/core/uuid.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart'; @@ -64,8 +68,35 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget { itemId: (container) => container, itemAvatar: (container) => const Icon(MdiIcons.creation, size: 20), - itemLabel: (container) => - Text(container.topic ?? 'Untitled'), + itemLabel: (container) => HookConsumer( + builder: (context, ref, child) { + final items = useListenableSelector( + selectedContainerTabs, + () => selectedContainerTabs.value[container], + ); + + final topic = items.isNotEmpty + ? ref.watch( + tabsTopicProvider(EquatableValue(items!)), + ) + : AsyncValue.data(container.topic); + + return topic.when( + data: (topic) => Text(topic ?? 'Untitled'), + error: (error, stackTrace) { + logger.e( + 'Failed predicting selected tabs topic', + error: error, + stackTrace: stackTrace, + ); + + return Text(container.topic ?? 'Untitled'); + }, + loading: () => + const Skeletonizer(child: Text('Untitled')), + ); + }, + ), itemBadgeCount: (container) => container.tabIds.length, availableItems: suggestions!, selectedItem: selectedContainer.value, @@ -150,7 +181,19 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget { final initialContainer = ContainerData( id: uuid.v7(), color: initialColor, - name: selectedContainer.value?.topic, + name: + (ref.exists( + tabsTopicProvider(EquatableValue(selectedTabs!)), + )) + ? ref + .read( + tabsTopicProvider( + EquatableValue(selectedTabs), + ), + ) + .value ?? + selectedContainer.value?.topic + : selectedContainer.value?.topic, ); if (context.mounted) { @@ -159,7 +202,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget { ).push(context); if (result != null) { - for (final tab in selectedTabs!) { + for (final tab in selectedTabs) { await ref .read(tabDataRepositoryProvider.notifier) .assignContainer(tab, result);