feat: tab suggestions
This commit is contained in:
@@ -16,7 +16,7 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/repositories/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||
import 'package:weblibre/features/geckoview/utils/image_helper.dart';
|
||||
|
||||
part 'tab_state.g.dart';
|
||||
@@ -43,7 +43,7 @@ class TabStates extends _$TabStates {
|
||||
|
||||
if (newState.isFinishedLoading) {
|
||||
ref
|
||||
.read(containerTopicRepositoryProvider.notifier)
|
||||
.read(geckoInferenceRepositoryProvider.notifier)
|
||||
.markInitialLoadComplete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ final selectedTabContainerIdProvider = Provider<AsyncValue<String?>>.internal(
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
typedef SelectedTabContainerIdRef = ProviderRef<AsyncValue<String?>>;
|
||||
String _$tabStatesHash() => r'ec7e0905d77b2f82491c5b5af0d2ce1351a564f4';
|
||||
String _$tabStatesHash() => r'66cff6a7b36328ee23b89fdd53046987b346bfcd';
|
||||
|
||||
/// See also [TabStates].
|
||||
@ProviderFor(TabStates)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
@@ -11,6 +12,7 @@ import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.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/domain/repositories/tab_search.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
@@ -88,6 +90,37 @@ EquatableValue<Map<String, TabState>> availableTabStates(
|
||||
});
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
EquatableValue<List<TabEntity>> suggestedTabEntities(
|
||||
Ref ref,
|
||||
String? containerId,
|
||||
) {
|
||||
final excludedTabIds = ref.watch(
|
||||
containerTabIdsProvider(
|
||||
// ignore: provider_parameters
|
||||
ContainerFilterById(containerId: containerId),
|
||||
).select((value) => EquatableValue(value.valueOrNull)),
|
||||
);
|
||||
|
||||
final suggestions = ref.watch(
|
||||
containerTabSuggestionsProvider(containerId).select(
|
||||
(value) => EquatableValue(
|
||||
value.valueOrNull.mapNotNull(
|
||||
(result) => result
|
||||
.whereNot(
|
||||
(tabId) => excludedTabIds.value?.contains(tabId) ?? false,
|
||||
)
|
||||
.map((tabId) => DefaultTabEntity(tabId: tabId))
|
||||
.toList(),
|
||||
) ??
|
||||
const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
||||
Ref ref, {
|
||||
|
||||
@@ -282,6 +282,134 @@ class _AvailableTabStatesProviderElement
|
||||
(origin as AvailableTabStatesProvider).containerFilter;
|
||||
}
|
||||
|
||||
String _$suggestedTabEntitiesHash() =>
|
||||
r'f8c41ea3136ae0761e558323afb9d33bf5b8fd21';
|
||||
|
||||
/// See also [suggestedTabEntities].
|
||||
@ProviderFor(suggestedTabEntities)
|
||||
const suggestedTabEntitiesProvider = SuggestedTabEntitiesFamily();
|
||||
|
||||
/// See also [suggestedTabEntities].
|
||||
class SuggestedTabEntitiesFamily
|
||||
extends Family<EquatableValue<List<TabEntity>>> {
|
||||
/// See also [suggestedTabEntities].
|
||||
const SuggestedTabEntitiesFamily();
|
||||
|
||||
/// See also [suggestedTabEntities].
|
||||
SuggestedTabEntitiesProvider call(String? containerId) {
|
||||
return SuggestedTabEntitiesProvider(containerId);
|
||||
}
|
||||
|
||||
@override
|
||||
SuggestedTabEntitiesProvider getProviderOverride(
|
||||
covariant SuggestedTabEntitiesProvider provider,
|
||||
) {
|
||||
return call(provider.containerId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'suggestedTabEntitiesProvider';
|
||||
}
|
||||
|
||||
/// See also [suggestedTabEntities].
|
||||
class SuggestedTabEntitiesProvider
|
||||
extends AutoDisposeProvider<EquatableValue<List<TabEntity>>> {
|
||||
/// See also [suggestedTabEntities].
|
||||
SuggestedTabEntitiesProvider(String? containerId)
|
||||
: this._internal(
|
||||
(ref) =>
|
||||
suggestedTabEntities(ref as SuggestedTabEntitiesRef, containerId),
|
||||
from: suggestedTabEntitiesProvider,
|
||||
name: r'suggestedTabEntitiesProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$suggestedTabEntitiesHash,
|
||||
dependencies: SuggestedTabEntitiesFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SuggestedTabEntitiesFamily._allTransitiveDependencies,
|
||||
containerId: containerId,
|
||||
);
|
||||
|
||||
SuggestedTabEntitiesProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.containerId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? containerId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
EquatableValue<List<TabEntity>> Function(SuggestedTabEntitiesRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SuggestedTabEntitiesProvider._internal(
|
||||
(ref) => create(ref as SuggestedTabEntitiesRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
containerId: containerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<EquatableValue<List<TabEntity>>> createElement() {
|
||||
return _SuggestedTabEntitiesProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SuggestedTabEntitiesProvider &&
|
||||
other.containerId == containerId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin SuggestedTabEntitiesRef
|
||||
on AutoDisposeProviderRef<EquatableValue<List<TabEntity>>> {
|
||||
/// The parameter `containerId` of this provider.
|
||||
String? get containerId;
|
||||
}
|
||||
|
||||
class _SuggestedTabEntitiesProviderElement
|
||||
extends AutoDisposeProviderElement<EquatableValue<List<TabEntity>>>
|
||||
with SuggestedTabEntitiesRef {
|
||||
_SuggestedTabEntitiesProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get containerId =>
|
||||
(origin as SuggestedTabEntitiesProvider).containerId;
|
||||
}
|
||||
|
||||
String _$seamlessFilteredTabEntitiesHash() =>
|
||||
r'2cd698d4e7c9d9cda9e17abbf9a99e512425e4cd';
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_suggestions.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabSuggestionsController extends _$TabSuggestionsController {
|
||||
void toggle() {
|
||||
state = !state;
|
||||
}
|
||||
|
||||
void hide() {
|
||||
if (state) {
|
||||
state = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_suggestions.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabSuggestionsControllerHash() =>
|
||||
r'5ce7f385b8aba14d432912cf0acb06aad04433fc';
|
||||
|
||||
/// See also [TabSuggestionsController].
|
||||
@ProviderFor(TabSuggestionsController)
|
||||
final tabSuggestionsControllerProvider =
|
||||
NotifierProvider<TabSuggestionsController, bool>.internal(
|
||||
TabSuggestionsController.new,
|
||||
name: r'tabSuggestionsControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabSuggestionsControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TabSuggestionsController = Notifier<bool>;
|
||||
// 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
|
||||
+100
-48
@@ -13,6 +13,7 @@ import 'package:weblibre/data/models/drag_data.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
||||
@@ -29,9 +30,14 @@ import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||
|
||||
class _TabDraggable extends HookConsumerWidget {
|
||||
final TabEntity entity;
|
||||
final String? suggestedContainerId;
|
||||
final VoidCallback onClose;
|
||||
|
||||
const _TabDraggable({required this.entity, required this.onClose});
|
||||
const _TabDraggable({
|
||||
required this.entity,
|
||||
required this.onClose,
|
||||
this.suggestedContainerId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -49,16 +55,22 @@ class _TabDraggable extends HookConsumerWidget {
|
||||
}),
|
||||
);
|
||||
|
||||
final tab = SingleTabPreview(
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
sourceSearchQuery: switch (entity) {
|
||||
DefaultTabEntity _ => null,
|
||||
final SearchResultTabEntity entity => entity.searchQuery,
|
||||
TabTreeEntity _ => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
final tab = (suggestedContainerId != null)
|
||||
? SuggestedSingleTabPreview(
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
containerId: suggestedContainerId!,
|
||||
)
|
||||
: SingleTabPreview(
|
||||
tabId: entity.tabId,
|
||||
activeTabId: activeTab,
|
||||
onClose: onClose,
|
||||
sourceSearchQuery: switch (entity) {
|
||||
DefaultTabEntity _ => null,
|
||||
final SearchResultTabEntity entity => entity.searchQuery,
|
||||
TabTreeEntity _ => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
|
||||
return switch (dragData) {
|
||||
ContainerDropData() => Opacity(
|
||||
@@ -132,6 +144,10 @@ class _TabSheetHeader extends HookConsumerWidget {
|
||||
searchTextFocus.requestFocus();
|
||||
},
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
child: VerticalDivider(indent: 4, endIndent: 4),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(MdiIcons.familyTree),
|
||||
selectedIcon: const Icon(MdiIcons.table),
|
||||
@@ -144,6 +160,27 @@ class _TabSheetHeader extends HookConsumerWidget {
|
||||
.toggle();
|
||||
},
|
||||
),
|
||||
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();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
TextButton.icon(
|
||||
@@ -298,7 +335,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
builder: (context, ref, child) {
|
||||
final gridViewKey = useMemoized(() => GlobalKey());
|
||||
|
||||
final container = ref.watch(selectedContainerProvider);
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
seamlessFilteredTabEntitiesProvider(
|
||||
@@ -306,35 +343,36 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
// ignore: document_ignores using fast equatable
|
||||
// ignore: provider_parameters
|
||||
containerFilter: ContainerFilterById(
|
||||
containerId: container,
|
||||
containerId: containerId,
|
||||
),
|
||||
groupTrees: false,
|
||||
),
|
||||
);
|
||||
|
||||
final tabSuggestionsEnabled = ref.watch(
|
||||
tabSuggestionsControllerProvider,
|
||||
);
|
||||
final suggestedTabEntities = ref.watch(
|
||||
suggestedTabEntitiesProvider(
|
||||
tabSuggestionsEnabled ? containerId : null,
|
||||
),
|
||||
);
|
||||
|
||||
final itemCount =
|
||||
filteredTabEntities.value.length +
|
||||
suggestedTabEntities.value.length;
|
||||
|
||||
final activeTab = ref.watch(selectedTabProvider);
|
||||
|
||||
final crossAxisCount = useMemoized(
|
||||
() {
|
||||
final calculatedCount = _calculateCrossAxisItemCount(
|
||||
screenWidth: MediaQuery.of(context).size.width,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = _calculateCrossAxisItemCount(
|
||||
screenWidth: MediaQuery.of(context).size.width,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
filteredTabEntities.value.length,
|
||||
),
|
||||
2,
|
||||
);
|
||||
},
|
||||
[
|
||||
MediaQuery.of(context).size.width,
|
||||
filteredTabEntities.value.length,
|
||||
],
|
||||
);
|
||||
return math.max(math.min(calculatedCount, itemCount), 2);
|
||||
}, [MediaQuery.of(context).size.width, itemCount]);
|
||||
|
||||
final itemHeight = useMemoized(
|
||||
() => _calculateItemHeight(
|
||||
@@ -390,20 +428,29 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final tabs = useMemoized(() {
|
||||
return filteredTabEntities.value
|
||||
.where((entity) => entity is! TabTreeEntity)
|
||||
.map(
|
||||
(entity) => CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
),
|
||||
return [
|
||||
...filteredTabEntities.value.map(
|
||||
(entity) => CustomDraggable(
|
||||
key: Key(entity.tabId),
|
||||
data: TabDragData(entity.tabId),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}, [filteredTabEntities]);
|
||||
),
|
||||
),
|
||||
...suggestedTabEntities.value.map(
|
||||
(entity) => CustomDraggable(
|
||||
key: Key('suggested_${entity.tabId}'),
|
||||
child: _TabDraggable(
|
||||
entity: entity,
|
||||
onClose: onClose,
|
||||
suggestedContainerId: containerId,
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}, [filteredTabEntities, suggestedTabEntities]);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
@@ -415,7 +462,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
//Rebuild when cross axis count changes
|
||||
key: ValueKey(crossAxisCount),
|
||||
scrollController: controller,
|
||||
itemCount: tabs.length,
|
||||
itemCount: itemCount,
|
||||
onDragStarted: (index) {
|
||||
ref.read(willAcceptDropProvider.notifier).clear();
|
||||
},
|
||||
@@ -432,6 +479,11 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
containerRepositoryProvider.notifier,
|
||||
);
|
||||
|
||||
//Suggestions are at the end and not reorderable, so skip
|
||||
if (oldIndex >= filteredTabEntities.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
final tabId =
|
||||
filteredTabEntities.value[oldIndex].tabId;
|
||||
final containerId = await ref
|
||||
@@ -482,7 +534,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: tabs.length,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) =>
|
||||
itemBuilder(tabs[index], index),
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
@@ -14,15 +15,21 @@ import 'package:weblibre/features/geckoview/features/find_in_page/domain/entitie
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
|
||||
class _TabBox extends StatelessWidget {
|
||||
class TabContainer extends StatelessWidget {
|
||||
final bool isActive;
|
||||
final bool isPrivate;
|
||||
final Widget? child;
|
||||
|
||||
const _TabBox({required this.isActive, required this.isPrivate, this.child});
|
||||
const TabContainer({
|
||||
required this.isActive,
|
||||
required this.isPrivate,
|
||||
this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -47,6 +54,37 @@ class _TabBox extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class TabMiniPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
|
||||
const TabMiniPreview({super.key, required this.tabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final thumbnail = ref.watch(
|
||||
tabStateProvider(tabId).select((value) => value?.thumbnail),
|
||||
);
|
||||
|
||||
return TabContainer(
|
||||
isActive: false,
|
||||
isPrivate: false,
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
||||
child: Skeleton.replace(
|
||||
replace: thumbnail == null,
|
||||
replacement: const Bone.square(size: double.infinity),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: RepaintBoundary(
|
||||
child: RawImage(image: thumbnail?.value, fit: BoxFit.fitWidth),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabPreview extends HookWidget {
|
||||
final TabState tab;
|
||||
final bool isActive;
|
||||
@@ -57,6 +95,8 @@ class TabPreview extends HookWidget {
|
||||
final VoidCallback? onDelete;
|
||||
final void Function(String host)? onDeleteAll;
|
||||
|
||||
final Widget? trailingChild;
|
||||
|
||||
const TabPreview({
|
||||
required this.tab,
|
||||
required this.isActive,
|
||||
@@ -65,13 +105,14 @@ class TabPreview extends HookWidget {
|
||||
this.onLongPress,
|
||||
this.onDelete,
|
||||
this.onDeleteAll,
|
||||
this.trailingChild,
|
||||
super.key,
|
||||
});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final extendedDeleteMenuController = useMenuController();
|
||||
|
||||
return _TabBox(
|
||||
return TabContainer(
|
||||
isActive: isActive,
|
||||
isPrivate: tab.isPrivate,
|
||||
child: InkWell(
|
||||
@@ -130,6 +171,7 @@ class TabPreview extends HookWidget {
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
?trailingChild,
|
||||
],
|
||||
),
|
||||
Row(
|
||||
@@ -259,6 +301,51 @@ class SingleTabPreview extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SuggestedSingleTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final String containerId;
|
||||
|
||||
final String? activeTabId;
|
||||
|
||||
SuggestedSingleTabPreview({
|
||||
required this.tabId,
|
||||
required this.containerId,
|
||||
required this.activeTabId,
|
||||
}) : super(key: ValueKey(tabId));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tab = ref.watch(tabStateProvider(tabId));
|
||||
|
||||
if (tab == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Opacity(
|
||||
opacity: 0.5,
|
||||
child: TabPreview(
|
||||
tab: tab,
|
||||
isActive: tabId == activeTabId,
|
||||
onTap: () async {
|
||||
final containerData = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.getContainerData(containerId);
|
||||
|
||||
if (containerData != null) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(tabId, containerData);
|
||||
}
|
||||
},
|
||||
trailingChild: const IconButton(
|
||||
icon: Icon(MdiIcons.creation),
|
||||
onPressed: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabTreePreview extends HookConsumerWidget {
|
||||
final TabTreeEntity entity;
|
||||
final String? activeTabId;
|
||||
@@ -303,7 +390,7 @@ class TabTreePreview extends HookConsumerWidget {
|
||||
_addPadding(
|
||||
index,
|
||||
stackCount,
|
||||
_TabBox(
|
||||
TabContainer(
|
||||
isActive: entity.tabId == activeTabId,
|
||||
isPrivate: tab.isPrivate,
|
||||
),
|
||||
|
||||
@@ -73,11 +73,17 @@ Stream<Map<String, String?>> tabDescendants(Ref ref, String tabId) {
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<TabData>> containerTabsData(Ref ref, String containerId) {
|
||||
Stream<List<TabData>> containerTabsData(Ref ref, String? containerId) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
return db.containerDao.getContainerTabsData(containerId).watch();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<ContainerData?> containerData(Ref ref, String containerId) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
return db.containerDao.getContainerData(containerId).watchSingleOrNull();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
||||
return ref
|
||||
|
||||
@@ -565,7 +565,7 @@ class _TabDescendantsProviderElement
|
||||
String get tabId => (origin as TabDescendantsProvider).tabId;
|
||||
}
|
||||
|
||||
String _$containerTabsDataHash() => r'1987b2d69f2ba663a7343e93572aad3c31a29be3';
|
||||
String _$containerTabsDataHash() => r'2961c89fc9e16c8e6005342356ecf677fb2ff63c';
|
||||
|
||||
/// See also [containerTabsData].
|
||||
@ProviderFor(containerTabsData)
|
||||
@@ -577,7 +577,7 @@ class ContainerTabsDataFamily extends Family<AsyncValue<List<TabData>>> {
|
||||
const ContainerTabsDataFamily();
|
||||
|
||||
/// See also [containerTabsData].
|
||||
ContainerTabsDataProvider call(String containerId) {
|
||||
ContainerTabsDataProvider call(String? containerId) {
|
||||
return ContainerTabsDataProvider(containerId);
|
||||
}
|
||||
|
||||
@@ -607,7 +607,7 @@ class ContainerTabsDataFamily extends Family<AsyncValue<List<TabData>>> {
|
||||
class ContainerTabsDataProvider
|
||||
extends AutoDisposeStreamProvider<List<TabData>> {
|
||||
/// See also [containerTabsData].
|
||||
ContainerTabsDataProvider(String containerId)
|
||||
ContainerTabsDataProvider(String? containerId)
|
||||
: this._internal(
|
||||
(ref) => containerTabsData(ref as ContainerTabsDataRef, containerId),
|
||||
from: containerTabsDataProvider,
|
||||
@@ -631,7 +631,7 @@ class ContainerTabsDataProvider
|
||||
required this.containerId,
|
||||
}) : super.internal();
|
||||
|
||||
final String containerId;
|
||||
final String? containerId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
@@ -675,7 +675,7 @@ class ContainerTabsDataProvider
|
||||
// ignore: unused_element
|
||||
mixin ContainerTabsDataRef on AutoDisposeStreamProviderRef<List<TabData>> {
|
||||
/// The parameter `containerId` of this provider.
|
||||
String get containerId;
|
||||
String? get containerId;
|
||||
}
|
||||
|
||||
class _ContainerTabsDataProviderElement
|
||||
@@ -684,7 +684,127 @@ class _ContainerTabsDataProviderElement
|
||||
_ContainerTabsDataProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get containerId => (origin as ContainerTabsDataProvider).containerId;
|
||||
String? get containerId => (origin as ContainerTabsDataProvider).containerId;
|
||||
}
|
||||
|
||||
String _$containerDataHash() => r'6adbec128876069189954832af35109b0779148c';
|
||||
|
||||
/// See also [containerData].
|
||||
@ProviderFor(containerData)
|
||||
const containerDataProvider = ContainerDataFamily();
|
||||
|
||||
/// See also [containerData].
|
||||
class ContainerDataFamily extends Family<AsyncValue<ContainerData?>> {
|
||||
/// See also [containerData].
|
||||
const ContainerDataFamily();
|
||||
|
||||
/// See also [containerData].
|
||||
ContainerDataProvider call(String containerId) {
|
||||
return ContainerDataProvider(containerId);
|
||||
}
|
||||
|
||||
@override
|
||||
ContainerDataProvider getProviderOverride(
|
||||
covariant ContainerDataProvider provider,
|
||||
) {
|
||||
return call(provider.containerId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'containerDataProvider';
|
||||
}
|
||||
|
||||
/// See also [containerData].
|
||||
class ContainerDataProvider extends AutoDisposeStreamProvider<ContainerData?> {
|
||||
/// See also [containerData].
|
||||
ContainerDataProvider(String containerId)
|
||||
: this._internal(
|
||||
(ref) => containerData(ref as ContainerDataRef, containerId),
|
||||
from: containerDataProvider,
|
||||
name: r'containerDataProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$containerDataHash,
|
||||
dependencies: ContainerDataFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ContainerDataFamily._allTransitiveDependencies,
|
||||
containerId: containerId,
|
||||
);
|
||||
|
||||
ContainerDataProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.containerId,
|
||||
}) : super.internal();
|
||||
|
||||
final String containerId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
Stream<ContainerData?> Function(ContainerDataRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ContainerDataProvider._internal(
|
||||
(ref) => create(ref as ContainerDataRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
containerId: containerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamProviderElement<ContainerData?> createElement() {
|
||||
return _ContainerDataProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ContainerDataProvider && other.containerId == containerId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ContainerDataRef on AutoDisposeStreamProviderRef<ContainerData?> {
|
||||
/// The parameter `containerId` of this provider.
|
||||
String get containerId;
|
||||
}
|
||||
|
||||
class _ContainerDataProviderElement
|
||||
extends AutoDisposeStreamProviderElement<ContainerData?>
|
||||
with ContainerDataRef {
|
||||
_ContainerDataProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get containerId => (origin as ContainerDataProvider).containerId;
|
||||
}
|
||||
|
||||
String _$watchContainerTabIdHash() =>
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod/riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/utils/lru_cache.dart';
|
||||
|
||||
part 'container_topic.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class ContainerTopicRepository extends _$ContainerTopicRepository {
|
||||
final _service = GeckoMlService();
|
||||
|
||||
//Wait for first complete page laod of any website after startup to ensure everything is ready
|
||||
final _initialLoadComplete = Completer();
|
||||
final _lock = Lock();
|
||||
|
||||
final _cache = LRUCache<Set<String>, String>(
|
||||
50,
|
||||
equals: (a, b) {
|
||||
return const DeepCollectionEquality.unordered().equals(a, b);
|
||||
},
|
||||
hashCode: (key) {
|
||||
return const DeepCollectionEquality.unordered().hash(key);
|
||||
},
|
||||
);
|
||||
|
||||
void markInitialLoadComplete() {
|
||||
if (!_initialLoadComplete.isCompleted) {
|
||||
_initialLoadComplete.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getContainerTopic(Set<String> titles) async {
|
||||
if (titles.isNotEmpty) {
|
||||
if (_cache.get(titles) case final String title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
try {
|
||||
final title = await _lock.synchronized(() async {
|
||||
await _initialLoadComplete.future;
|
||||
|
||||
final title = await _service.getContainerTopic(titles);
|
||||
|
||||
return _cache.set(titles, title);
|
||||
}, timeout: const Duration(seconds: 120));
|
||||
|
||||
return title;
|
||||
} on TimeoutException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<String?> containerTopic(Ref ref, String containerId) async {
|
||||
final titles = await ref.watch(
|
||||
containerTabsDataProvider(containerId).selectAsync(
|
||||
(tabData) =>
|
||||
EquatableValue(tabData.map((tab) => tab.title).nonNulls.toSet()),
|
||||
),
|
||||
);
|
||||
|
||||
final topic = await ref
|
||||
.read(containerTopicRepositoryProvider.notifier)
|
||||
.getContainerTopic(titles.value);
|
||||
|
||||
return topic;
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
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/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/utils/lru_cache.dart';
|
||||
|
||||
part 'gecko_inference.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class GeckoInferenceRepository extends _$GeckoInferenceRepository {
|
||||
final _service = GeckoMlService();
|
||||
|
||||
//Wait for first complete page laod of any website after startup to ensure everything is ready
|
||||
final _initialLoadComplete = Completer();
|
||||
final _engineLock = Lock();
|
||||
|
||||
final _topicCache = LRUCache<Set<String>, String>(
|
||||
50,
|
||||
equals: (a, b) {
|
||||
return const DeepCollectionEquality.unordered().equals(a, b);
|
||||
},
|
||||
hashCode: (key) {
|
||||
return const DeepCollectionEquality.unordered().hash(key);
|
||||
},
|
||||
);
|
||||
|
||||
final _embeddingCache = LRUCache<String, List<double>>(100);
|
||||
|
||||
void markInitialLoadComplete() {
|
||||
if (!_initialLoadComplete.isCompleted) {
|
||||
_initialLoadComplete.complete();
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> predictDocumentTopic(Set<String> titles) async {
|
||||
if (titles.isNotEmpty) {
|
||||
if (_topicCache.get(titles) case final String title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
try {
|
||||
final title = await _engineLock.synchronized(() async {
|
||||
await _initialLoadComplete.future;
|
||||
|
||||
final title = await _service.predictDocumentTopic(titles);
|
||||
|
||||
return _topicCache.set(titles, title);
|
||||
}, timeout: const Duration(seconds: 120));
|
||||
|
||||
return title;
|
||||
} on TimeoutException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<String>?> suggestDocuments({
|
||||
required String topic,
|
||||
required List<String> assignedDocumentsInput,
|
||||
required List<String> unassignedDocumentsInput,
|
||||
}) async {
|
||||
final processedDocuments = <String, String>{};
|
||||
final unassignedDocumentsProcessed = unassignedDocumentsInput.map((doc) {
|
||||
final processed = preprocessText(doc);
|
||||
if (processed != doc) {
|
||||
processedDocuments[processed] = doc;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}).toList();
|
||||
|
||||
final assignedDocumentsProcessed = assignedDocumentsInput
|
||||
.map((doc) => '$topic. ${preprocessText(doc)}')
|
||||
.toList();
|
||||
|
||||
final embeddings = await generateDocumentEmbeddings([
|
||||
...unassignedDocumentsProcessed,
|
||||
...assignedDocumentsProcessed,
|
||||
]);
|
||||
|
||||
final neighbors = embeddings.mapNotNull(
|
||||
(embeddings) => findNearestNeighborsRecursive(
|
||||
embeddings: embeddings,
|
||||
assignedDocuments: assignedDocumentsProcessed,
|
||||
unassignedDocuments: unassignedDocumentsProcessed,
|
||||
).map((neighbor) => processedDocuments[neighbor] ?? neighbor).toList(),
|
||||
);
|
||||
|
||||
return neighbors;
|
||||
}
|
||||
|
||||
Future<Map<String, List<double>>?> generateDocumentEmbeddings(
|
||||
List<String> documents,
|
||||
) async {
|
||||
try {
|
||||
final embeddings = Map.fromEntries(
|
||||
documents.map((doc) => MapEntry(doc, _embeddingCache.get(doc))),
|
||||
);
|
||||
|
||||
final embeddingsToGenerate = embeddings.entries
|
||||
.where((e) => e.value == null)
|
||||
.map((e) => e.key)
|
||||
.toList();
|
||||
|
||||
if (embeddingsToGenerate.isNotEmpty) {
|
||||
final generatedEmbeddings = await _engineLock.synchronized(() async {
|
||||
await _initialLoadComplete.future;
|
||||
|
||||
final embeddings = await _service.generateDocumentEmbeddings(
|
||||
documents,
|
||||
);
|
||||
|
||||
return embeddings;
|
||||
}, timeout: const Duration(seconds: 120));
|
||||
|
||||
for (var i = 0; i < embeddingsToGenerate.length; i++) {
|
||||
embeddings[embeddingsToGenerate[i]] = generatedEmbeddings[i];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
for (final MapEntry(:key, :value) in embeddings.entries)
|
||||
if (value != null) key: value,
|
||||
};
|
||||
} on TimeoutException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Future<String?> containerTopic(Ref ref, String containerId) async {
|
||||
final titles = await ref.watch(
|
||||
containerTabsDataProvider(containerId).selectAsync(
|
||||
(tabData) =>
|
||||
EquatableValue(tabData.map((tab) => tab.title).nonNulls.toSet()),
|
||||
),
|
||||
);
|
||||
|
||||
final topic = await ref
|
||||
.read(geckoInferenceRepositoryProvider.notifier)
|
||||
.predictDocumentTopic(titles.value);
|
||||
|
||||
return topic;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<List<String>?> containerTabSuggestions(
|
||||
Ref ref,
|
||||
String? containerId,
|
||||
) async {
|
||||
if (containerId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final container = await ref.watch(containerDataProvider(containerId).future);
|
||||
|
||||
final assignedTitles = await ref.watch(
|
||||
containerTabsDataProvider(containerId).selectAsync(
|
||||
(tabData) => EquatableValue(
|
||||
tabData
|
||||
.where((tab) => tab.title.isNotEmpty)
|
||||
.map((tab) => (tab.id, tab.title!))
|
||||
.toSet(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final unassignedTitles = await ref.watch(
|
||||
containerTabsDataProvider(null).selectAsync(
|
||||
(tabData) => EquatableValue(
|
||||
tabData
|
||||
.where((tab) => tab.title.isNotEmpty)
|
||||
.map((tab) => (tab.id, tab.title!))
|
||||
.toSet(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (assignedTitles.value.isNotEmpty && unassignedTitles.value.isNotEmpty) {
|
||||
final topic =
|
||||
container?.name ??
|
||||
await ref
|
||||
.read(geckoInferenceRepositoryProvider.notifier)
|
||||
.predictDocumentTopic(
|
||||
assignedTitles.value.map((tab) => tab.$2).toSet(),
|
||||
);
|
||||
|
||||
if (topic != null) {
|
||||
final suggestedTitles = await ref
|
||||
.read(geckoInferenceRepositoryProvider.notifier)
|
||||
.suggestDocuments(
|
||||
topic: topic,
|
||||
assignedDocumentsInput: assignedTitles.value
|
||||
.map((tab) => tab.$2)
|
||||
.toList(),
|
||||
unassignedDocumentsInput: unassignedTitles.value
|
||||
.map((tab) => tab.$2)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
return suggestedTitles.mapNotNull(
|
||||
(titles) => titles
|
||||
.map(
|
||||
(title) => unassignedTitles.value
|
||||
.firstWhere((tab) => tab.$2 == title)
|
||||
.$1,
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
+141
-12
@@ -1,12 +1,12 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'container_topic.dart';
|
||||
part of 'gecko_inference.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$containerTopicHash() => r'aa3fd27f26bb94b6c9e794d8470537240b8d2713';
|
||||
String _$containerTopicHash() => r'b680381eab3dded9ca6f174fbcc5da5c59d7e9f0';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
@@ -146,22 +146,151 @@ class _ContainerTopicProviderElement extends FutureProviderElement<String?>
|
||||
String get containerId => (origin as ContainerTopicProvider).containerId;
|
||||
}
|
||||
|
||||
String _$containerTopicRepositoryHash() =>
|
||||
r'8dfc0bc8b4953b5230a1faa24c8b2f0a94ab1f04';
|
||||
String _$containerTabSuggestionsHash() =>
|
||||
r'ab8e596f0c7e8562bf998c701145e48eb1e520d5';
|
||||
|
||||
/// See also [ContainerTopicRepository].
|
||||
@ProviderFor(ContainerTopicRepository)
|
||||
final containerTopicRepositoryProvider =
|
||||
NotifierProvider<ContainerTopicRepository, void>.internal(
|
||||
ContainerTopicRepository.new,
|
||||
name: r'containerTopicRepositoryProvider',
|
||||
/// See also [containerTabSuggestions].
|
||||
@ProviderFor(containerTabSuggestions)
|
||||
const containerTabSuggestionsProvider = ContainerTabSuggestionsFamily();
|
||||
|
||||
/// See also [containerTabSuggestions].
|
||||
class ContainerTabSuggestionsFamily extends Family<AsyncValue<List<String>?>> {
|
||||
/// See also [containerTabSuggestions].
|
||||
const ContainerTabSuggestionsFamily();
|
||||
|
||||
/// See also [containerTabSuggestions].
|
||||
ContainerTabSuggestionsProvider call(String? containerId) {
|
||||
return ContainerTabSuggestionsProvider(containerId);
|
||||
}
|
||||
|
||||
@override
|
||||
ContainerTabSuggestionsProvider getProviderOverride(
|
||||
covariant ContainerTabSuggestionsProvider provider,
|
||||
) {
|
||||
return call(provider.containerId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'containerTabSuggestionsProvider';
|
||||
}
|
||||
|
||||
/// See also [containerTabSuggestions].
|
||||
class ContainerTabSuggestionsProvider
|
||||
extends AutoDisposeFutureProvider<List<String>?> {
|
||||
/// See also [containerTabSuggestions].
|
||||
ContainerTabSuggestionsProvider(String? containerId)
|
||||
: this._internal(
|
||||
(ref) => containerTabSuggestions(
|
||||
ref as ContainerTabSuggestionsRef,
|
||||
containerId,
|
||||
),
|
||||
from: containerTabSuggestionsProvider,
|
||||
name: r'containerTabSuggestionsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$containerTabSuggestionsHash,
|
||||
dependencies: ContainerTabSuggestionsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ContainerTabSuggestionsFamily._allTransitiveDependencies,
|
||||
containerId: containerId,
|
||||
);
|
||||
|
||||
ContainerTabSuggestionsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.containerId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? containerId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<String>?> Function(ContainerTabSuggestionsRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ContainerTabSuggestionsProvider._internal(
|
||||
(ref) => create(ref as ContainerTabSuggestionsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
containerId: containerId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<String>?> createElement() {
|
||||
return _ContainerTabSuggestionsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ContainerTabSuggestionsProvider &&
|
||||
other.containerId == containerId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, containerId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ContainerTabSuggestionsRef
|
||||
on AutoDisposeFutureProviderRef<List<String>?> {
|
||||
/// The parameter `containerId` of this provider.
|
||||
String? get containerId;
|
||||
}
|
||||
|
||||
class _ContainerTabSuggestionsProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<String>?>
|
||||
with ContainerTabSuggestionsRef {
|
||||
_ContainerTabSuggestionsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get containerId =>
|
||||
(origin as ContainerTabSuggestionsProvider).containerId;
|
||||
}
|
||||
|
||||
String _$geckoInferenceRepositoryHash() =>
|
||||
r'bdae6dab14c33c6503f37f2e17b8d50fb820ac26';
|
||||
|
||||
/// See also [GeckoInferenceRepository].
|
||||
@ProviderFor(GeckoInferenceRepository)
|
||||
final geckoInferenceRepositoryProvider =
|
||||
NotifierProvider<GeckoInferenceRepository, void>.internal(
|
||||
GeckoInferenceRepository.new,
|
||||
name: r'geckoInferenceRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$containerTopicRepositoryHash,
|
||||
: _$geckoInferenceRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ContainerTopicRepository = Notifier<void>;
|
||||
typedef _$GeckoInferenceRepository = Notifier<void>;
|
||||
// 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
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||
|
||||
part 'container_topic.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class ContainerTopicController extends _$ContainerTopicController {
|
||||
Future<String?> getContainerTopic(String containerId) async {
|
||||
Future<String?> predictDocumentTopic(String containerId) async {
|
||||
state = const AsyncLoading();
|
||||
|
||||
final result = await AsyncValue.guard(() async {
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ part of 'container_topic.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$containerTopicControllerHash() =>
|
||||
r'229709202edecb21b20432f8e4d0d7aee293f623';
|
||||
r'fa38ebd3c1352b471e58444b04d1cb0528aae1dd';
|
||||
|
||||
/// See also [ContainerTopicController].
|
||||
@ProviderFor(ContainerTopicController)
|
||||
|
||||
@@ -164,7 +164,7 @@ class ContainerEditScreen extends HookConsumerWidget {
|
||||
containerTopicControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.getContainerTopic(
|
||||
.predictDocumentTopic(
|
||||
initialContainer.id,
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:weblibre/core/logger.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/container_topic.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||
|
||||
class ContainerTitle extends HookConsumerWidget {
|
||||
final ContainerData container;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// Calculates cosine similarity between two lists of floats
|
||||
/// The lists don't need to be normalized
|
||||
///
|
||||
/// [a] first list
|
||||
/// [b] second list
|
||||
/// Returns cosine similarity value
|
||||
double cosSim(List<double> a, List<double> b) {
|
||||
if (a.length != b.length) {
|
||||
throw ArgumentError("Lists should have same lengths");
|
||||
}
|
||||
if (a.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
double dotProduct = 0;
|
||||
double mA = 0;
|
||||
double mB = 0;
|
||||
|
||||
for (int i = 0; i < a.length; i++) {
|
||||
dotProduct += a[i] * b[i];
|
||||
mA += a[i] * a[i];
|
||||
mB += b[i] * b[i];
|
||||
}
|
||||
|
||||
mA = sqrt(mA);
|
||||
mB = sqrt(mB);
|
||||
|
||||
return mA == 0 || mB == 0 ? 0 : dotProduct / (mA * mB);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/// Removes trailing domain-related text such as '... - Mail' or '... | News'
|
||||
/// If there's not enough information remaining after, we keep the text as is
|
||||
/// [text] tab title with potential domain information
|
||||
/// Returns the processed string
|
||||
String preprocessText(String text) {
|
||||
// Matches 'xyz - Domain' or 'xyz | Domain'
|
||||
// with a space before and after delimiter
|
||||
// or if there are multiple delimiters next to each other
|
||||
final delimiters = RegExp(r'(?<=\s)[|–-]+(?=\s)');
|
||||
final splitText = text.split(delimiters);
|
||||
|
||||
// ensure there's enough info without the last element
|
||||
final hasEnoughInfo =
|
||||
splitText.isNotEmpty &&
|
||||
splitText.sublist(0, splitText.length - 1).join(' ').length > 5;
|
||||
|
||||
// domain related texts are usually shorter, this takes care of the most common cases
|
||||
final isPotentialDomainInfo =
|
||||
splitText.length > 1 && splitText.last.length < 20;
|
||||
|
||||
// If both conditions are met, remove the last chunk, filter out empty strings,
|
||||
// join on space, trim, and lowercase
|
||||
if (hasEnoughInfo && isPotentialDomainInfo) {
|
||||
return splitText
|
||||
.sublist(0, splitText.length - 1) // everything except the last element
|
||||
.map((t) => t.trim())
|
||||
.where((t) => t.isNotEmpty) // remove empty strings
|
||||
.join(' ') // join with spaces
|
||||
.trim(); // remove leading/trailing spaces
|
||||
}
|
||||
|
||||
// Otherwise, just return the text
|
||||
return text;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/cosine_similarity.dart';
|
||||
|
||||
List<String> findNearestNeighborsRecursive({
|
||||
required Map<String, List<double>> embeddings,
|
||||
required List<String> assignedDocuments,
|
||||
required List<String> unassignedDocuments,
|
||||
int thresholdMills = 275,
|
||||
int maxAssignedCount = 4,
|
||||
int depth = 0,
|
||||
}) {
|
||||
final closestTabs = <(String, double)>[];
|
||||
final similarTabsIndices = <String>[];
|
||||
|
||||
for (final unassigned in unassignedDocuments) {
|
||||
double? closestScore;
|
||||
for (final assigned in assignedDocuments.take(maxAssignedCount)) {
|
||||
final cosineSim = cosSim(embeddings[unassigned]!, embeddings[assigned]!);
|
||||
|
||||
if (closestScore == null || cosineSim > closestScore) {
|
||||
closestScore = cosineSim;
|
||||
}
|
||||
}
|
||||
|
||||
// threshold could also be set via a nimbus experiment, in which case
|
||||
// it will be an int <= 1000
|
||||
if (closestScore != null && closestScore > thresholdMills / 1000) {
|
||||
closestTabs.add((unassigned, closestScore));
|
||||
similarTabsIndices.add(unassigned);
|
||||
}
|
||||
}
|
||||
|
||||
closestTabs.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
final result = closestTabs.map((t) => t.$1).toList();
|
||||
|
||||
// recurse once if the initial call only had a single tab
|
||||
// and we found at least 1 similar tab - this improves recall
|
||||
if (assignedDocuments.length == 1 && closestTabs.isNotEmpty && depth == 1) {
|
||||
final recurseSimilarTabs = findNearestNeighborsRecursive(
|
||||
unassignedDocuments: unassignedDocuments
|
||||
.whereNot(similarTabsIndices.contains)
|
||||
.toList(),
|
||||
assignedDocuments: similarTabsIndices,
|
||||
thresholdMills: thresholdMills,
|
||||
embeddings: embeddings,
|
||||
depth: depth - 1,
|
||||
);
|
||||
|
||||
result.addAll(recurseSimilarTabs);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -96,6 +96,8 @@ dev_dependencies:
|
||||
drift_dev: ^2.28.0
|
||||
fast_equatable_lint: ^0.3.2
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
go_router_builder: ^3.0.1
|
||||
json_serializable: ^6.9.5
|
||||
lint: ^2.8.0
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/utils/embedding_text_processing.dart';
|
||||
|
||||
void main() {
|
||||
group('Text processing basic cases', () {
|
||||
test('trailing domain-like text should be removed', () {
|
||||
expect(
|
||||
preprocessText("Some Title - Random Mail"),
|
||||
equals("Some Title"),
|
||||
reason: "Should remove '- Random Mail' suffix",
|
||||
);
|
||||
});
|
||||
|
||||
test('trailing domain-like text with |', () {
|
||||
expect(
|
||||
preprocessText("Another Title | Some Video Website"),
|
||||
equals("Another Title"),
|
||||
reason: "Should remove '| Some Video Website' suffix",
|
||||
);
|
||||
});
|
||||
|
||||
test('no delimiter', () {
|
||||
expect(
|
||||
preprocessText("Simple Title"),
|
||||
equals("Simple Title"),
|
||||
reason: "Should remain unchanged since there's no recognized delimiter",
|
||||
);
|
||||
});
|
||||
|
||||
test('not enough info in first part', () {
|
||||
expect(
|
||||
preprocessText("AB - Mail"),
|
||||
equals("AB - Mail"),
|
||||
reason:
|
||||
"Should not remove '- Mail' because the first part is too short",
|
||||
);
|
||||
});
|
||||
|
||||
test('should not match for texts such as check-in', () {
|
||||
expect(
|
||||
preprocessText("Check-in for flight"),
|
||||
equals("Check-in for flight"),
|
||||
reason: "Should not remove '-in'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Text processing edge cases', () {
|
||||
test('empty string', () {
|
||||
expect(
|
||||
preprocessText(""),
|
||||
equals(""),
|
||||
reason: "Empty string returns empty string",
|
||||
);
|
||||
});
|
||||
|
||||
test('exactly 20 chars', () {
|
||||
const domain20Chars = "12345678901234567890"; // 20 characters
|
||||
expect(
|
||||
preprocessText("My Title - $domain20Chars"),
|
||||
equals("My Title - $domain20Chars"),
|
||||
reason:
|
||||
"Should not remove suffix because it's exactly 20 chars long, not < 20",
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple delimiters, remove last only', () {
|
||||
expect(
|
||||
preprocessText("Complex - Title - SomethingSmall"),
|
||||
equals("Complex Title"),
|
||||
reason:
|
||||
"Should remove only the last '- SomethingSmall', ignoring earlier delimiters",
|
||||
);
|
||||
});
|
||||
|
||||
test('repeated delimiters', () {
|
||||
expect(
|
||||
preprocessText("Title --- Domain"),
|
||||
equals("Title"),
|
||||
reason: "Should remove the last chunk and filter out empty strings",
|
||||
);
|
||||
|
||||
expect(
|
||||
preprocessText("Title || Domain"),
|
||||
equals("Title"),
|
||||
reason: "Should remove the last chunk with double pipe delimiters too",
|
||||
);
|
||||
});
|
||||
|
||||
test('long trailing text', () {
|
||||
const longDomain = "Useful information is present";
|
||||
expect(
|
||||
preprocessText("Some Title - $longDomain"),
|
||||
equals("Some Title - $longDomain"),
|
||||
reason: "Should not remove suffix if it's >= 20 characters",
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+25
-4
@@ -46,9 +46,9 @@ const SMART_TAB_GROUPING_CONFIG = {
|
||||
*/
|
||||
function createModelInput(keywords, documents) {
|
||||
if (!keywords || keywords.length === 0) {
|
||||
return `Topic from keywords: titles: \n${documents.join(" \n")}`;
|
||||
return `Topic from keywords: titles: \n${documents.slice(0, 3).join(" \n")}`;
|
||||
}
|
||||
return `Topic from keywords: ${keywords.join(", ")}. titles: \n${documents.join(" \n")}`;
|
||||
return `Topic from keywords: ${keywords.join(", ")}. titles: \n${documents.slice(0, 3).join(" \n")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,6 @@ function cutAtDuplicateWords(phrase) {
|
||||
return phrase; // return original phrase
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {MLEngine} engine the engine to check
|
||||
@@ -96,7 +95,29 @@ this.ml = class extends ExtensionAPI {
|
||||
return {
|
||||
experiments: {
|
||||
ml: {
|
||||
async containerTopic(keywords, documents) {
|
||||
async generateEmbeddings(textToEmbedList) {
|
||||
const inputData = {
|
||||
inputArgs: textToEmbedList,
|
||||
runOptions: {
|
||||
pooling: "mean",
|
||||
normalize: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (isEngineClosed(this.embeddingEngine)) {
|
||||
this.embeddingEngine = await createEngine(SMART_TAB_GROUPING_CONFIG.embedding);
|
||||
}
|
||||
|
||||
const request = {
|
||||
args: [inputData.inputArgs],
|
||||
options: inputData.runOptions,
|
||||
};
|
||||
|
||||
const generated = await this.embeddingEngine.run(request);
|
||||
|
||||
return JSON.stringify(generated);
|
||||
},
|
||||
async predictTopic(keywords, documents) {
|
||||
if (isEngineClosed(this.topicEngine)) {
|
||||
const {
|
||||
featureId,
|
||||
|
||||
+14
-4
@@ -26,15 +26,25 @@ function sendErrorForRequest(id) {
|
||||
port.onMessage.addListener(async (message) => {
|
||||
let requestId = message["id"]
|
||||
switch (message["action"]) {
|
||||
case "getContainerTopic":
|
||||
case "predictDocumentTopic": {
|
||||
const documents = message["args"];
|
||||
const keywords = (documents.length > 1)
|
||||
? await browser.experiments.nlp.extractKeywords([documents.slice(0, 3).join(" ")])
|
||||
: [[]];
|
||||
|
||||
browser.experiments.ml.containerTopic(keywords[0], documents)
|
||||
browser.experiments.ml.predictTopic(keywords[0], documents)
|
||||
.then(sendJsonResultForRequest(requestId))
|
||||
.catch(sendErrorForRequest(requestId))
|
||||
break
|
||||
.catch(sendErrorForRequest(requestId));
|
||||
|
||||
break;
|
||||
}
|
||||
case "generateDocumentEmbeddings": {
|
||||
const documents = message["args"];
|
||||
await browser.experiments.ml.generateEmbeddings(documents)
|
||||
.then(sendJsonResultForRequest(requestId))
|
||||
.catch(sendErrorForRequest(requestId));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+17
-1
@@ -33,7 +33,23 @@
|
||||
"description": "Machine Learning utilities",
|
||||
"functions": [
|
||||
{
|
||||
"name": "containerTopic",
|
||||
"name": "generateEmbeddings",
|
||||
"type": "function",
|
||||
"description": "Generate embeddings for a list of text strings using ML engine",
|
||||
"async": true,
|
||||
"parameters": [
|
||||
{
|
||||
"name": "textToEmbedList",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Array of text strings to generate embeddings for"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "predictTopic",
|
||||
"type": "function",
|
||||
"description": "Generate topic from keywords and documents using ML engine",
|
||||
"async": true,
|
||||
|
||||
+36
-2
@@ -13,8 +13,8 @@ class GeckoMlApiImpl : GeckoMlApi {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getContainerTopic(titles: List<String>, callback: (Result<String>) -> Unit) {
|
||||
MLEngineFeature.scheduleRequest("getContainerTopic", titles.toJson(), object : ResultConsumer<JSONObject> {
|
||||
override fun predictDocumentTopic(documents: List<String>, callback: (Result<String>) -> Unit) {
|
||||
MLEngineFeature.scheduleRequest("predictDocumentTopic", documents.toJson(), object : ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
callback(Result.success(result.getString("result")))
|
||||
}
|
||||
@@ -24,4 +24,38 @@ class GeckoMlApiImpl : GeckoMlApi {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun generateDocumentEmbeddings(
|
||||
documents: List<String>,
|
||||
callback: (Result<List<Any?>>) -> Unit
|
||||
) {
|
||||
MLEngineFeature.scheduleRequest("generateDocumentEmbeddings", documents.toJson(), object : ResultConsumer<JSONObject> {
|
||||
override fun success(result: JSONObject) {
|
||||
try {
|
||||
val encodedResult = result.getString("result")
|
||||
val decodedJsonArray = JSONArray(encodedResult)
|
||||
val embeddings = mutableListOf<List<Double>>()
|
||||
|
||||
for (i in 0 until decodedJsonArray.length()) {
|
||||
val embeddingArray = decodedJsonArray.getJSONArray(i)
|
||||
val embedding = mutableListOf<Double>()
|
||||
|
||||
for (j in 0 until embeddingArray.length()) {
|
||||
embedding.add(embeddingArray.getDouble(j))
|
||||
}
|
||||
embeddings.add(embedding)
|
||||
}
|
||||
|
||||
callback(Result.success(embeddings))
|
||||
} catch (e: Exception) {
|
||||
callback(Result.failure(e))
|
||||
}
|
||||
}
|
||||
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
callback(Result.failure(Exception("$errorCode $errorMessage $errorDetails")))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+25
-4
@@ -3425,7 +3425,8 @@ interface GeckoPrefApi {
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface GeckoMlApi {
|
||||
fun getContainerTopic(titles: List<String>, callback: (Result<String>) -> Unit)
|
||||
fun predictDocumentTopic(documents: List<String>, callback: (Result<String>) -> Unit)
|
||||
fun generateDocumentEmbeddings(documents: List<String>, callback: (Result<List<Any?>>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoMlApi. */
|
||||
@@ -3437,12 +3438,32 @@ interface GeckoMlApi {
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: GeckoMlApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.getContainerTopic$separatedMessageChannelSuffix", codec)
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val titlesArg = args[0] as List<String>
|
||||
api.getContainerTopic(titlesArg) { result: Result<String> ->
|
||||
val documentsArg = args[0] as List<String>
|
||||
api.predictDocumentTopic(documentsArg) { result: Result<String> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(GeckoPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(GeckoPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val documentsArg = args[0] as List<String>
|
||||
api.generateDocumentEmbeddings(documentsArg) { result: Result<List<Any?>> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(GeckoPigeonUtils.wrapError(error))
|
||||
|
||||
@@ -9,13 +9,23 @@ import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart';
|
||||
final _apiInstance = GeckoMlApi();
|
||||
|
||||
class GeckoMlService {
|
||||
Future<String> getContainerTopic(Set<String> titles, {int maxCount = 8}) {
|
||||
Future<String> predictDocumentTopic(Set<String> titles, {int maxCount = 10}) {
|
||||
var selectedTitles = titles.toList();
|
||||
if (selectedTitles.length > maxCount) {
|
||||
//TODO: Randomize for now, maybe use clusters later
|
||||
selectedTitles = (selectedTitles..shuffle()).take(maxCount).toList();
|
||||
}
|
||||
|
||||
return _apiInstance.getContainerTopic(selectedTitles);
|
||||
return _apiInstance.predictDocumentTopic(selectedTitles);
|
||||
}
|
||||
|
||||
Future<List<List<double>>> generateDocumentEmbeddings(
|
||||
List<String> documents,
|
||||
) async {
|
||||
final embeddings = await _apiInstance.generateDocumentEmbeddings(documents);
|
||||
|
||||
return embeddings
|
||||
.map((values) => (values! as List).cast<double>())
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3968,14 +3968,14 @@ class GeckoMlApi {
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<String> getContainerTopic(List<String> titles) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.getContainerTopic$pigeonVar_messageChannelSuffix';
|
||||
Future<String> predictDocumentTopic(List<String> documents) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.predictDocumentTopic$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[titles]);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[documents]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
@@ -3995,6 +3995,34 @@ class GeckoMlApi {
|
||||
return (pigeonVar_replyList[0] as String?)!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Object?>> generateDocumentEmbeddings(List<String> documents) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoMlApi.generateDocumentEmbeddings$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[documents]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GeckoBrowserExtensionApi {
|
||||
|
||||
@@ -956,7 +956,9 @@ abstract class GeckoPrefApi {
|
||||
@HostApi()
|
||||
abstract class GeckoMlApi {
|
||||
@async
|
||||
String getContainerTopic(List<String> titles);
|
||||
String predictDocumentTopic(List<String> documents);
|
||||
@async
|
||||
List generateDocumentEmbeddings(List<String> documents);
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
|
||||
Reference in New Issue
Block a user