From 438c5ef1629523ad76b13264d5fd782ee4df35e0 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 10 Feb 2022 18:43:38 +0100 Subject: [PATCH 1/4] feat: #1060 - new dev mode lenient/strong matching switch "lenient" means @jasmeet0817's version, "strong" means @monsieurtanuki's. Why? Because for @jasmeet0817 it's impossible to be an "unknown" match. For @monsieurtanuki with at least one unknown attribute, you're at best "unknown". New file: * `smooth_matched_product.dart`: replacing off-dart's "MatchedProduct" Impacted files: * `personalized_ranking_page.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `product_compatibility_helper.dart`: not using off's `matched_product.dart` anymore * `smooth_it_model.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `smooth_product_card_found.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `summary_card.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `user_preferences_dev_mode.dart`: added a "lenient/strong" matching switch (aka "Swiss switch") --- .../smooth_product_card_found.dart | 6 +- .../lib/data_models/smooth_it_model.dart | 11 +- .../helpers/product_compatibility_helper.dart | 2 +- .../lib/helpers/smooth_matched_product.dart | 178 ++++++++++++++++++ .../lib/pages/personalized_ranking_page.dart | 9 +- .../lib/pages/product/summary_card.dart | 10 +- .../lib/pages/user_preferences_dev_mode.dart | 15 ++ 7 files changed, 220 insertions(+), 11 deletions(-) create mode 100644 packages/smooth_app/lib/helpers/smooth_matched_product.dart diff --git a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart index 1715d4ddfab..8f24d7cbc7a 100644 --- a/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart +++ b/packages/smooth_app/lib/cards/product_cards/smooth_product_card_found.dart @@ -2,13 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/model/Attribute.dart'; import 'package:openfoodfacts/model/Product.dart'; -import 'package:openfoodfacts/personalized_search/matched_product.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/cards/data_cards/svg_icon_chip.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; +import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_product_image.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/helpers/product_compatibility_helper.dart'; +import 'package:smooth_app/helpers/smooth_matched_product.dart'; import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/pages/product/new_product_page.dart'; @@ -47,9 +48,10 @@ class SmoothProductCardFound extends StatelessWidget { for (final Attribute attribute in attributes) { scores.add(SvgIconChip(attribute.iconUrl!, height: iconSize)); } - final MatchedProduct matchedProduct = MatchedProduct( + final MatchedProduct matchedProduct = MatchedProduct.getMatchedProduct( product, context.watch(), + context.watch(), ); final ProductCompatibilityHelper helper = ProductCompatibilityHelper(matchedProduct); diff --git a/packages/smooth_app/lib/data_models/smooth_it_model.dart b/packages/smooth_app/lib/data_models/smooth_it_model.dart index fd53f372035..c99ea876d8e 100644 --- a/packages/smooth_app/lib/data_models/smooth_it_model.dart +++ b/packages/smooth_app/lib/data_models/smooth_it_model.dart @@ -1,6 +1,7 @@ import 'package:openfoodfacts/model/Product.dart'; -import 'package:openfoodfacts/personalized_search/matched_product.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; +import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/helpers/smooth_matched_product.dart'; /// Tabs where ranked products are displayed enum MatchTab { @@ -17,9 +18,13 @@ class SmoothItModel { void refresh( final List products, final ProductPreferences productPreferences, + final UserPreferences userPreferences, ) { - final List allProducts = - MatchedProduct.sort(products, productPreferences); + final List allProducts = MatchedProduct.sort( + products, + productPreferences, + userPreferences, + ); _categorizedProducts.clear(); _categorizedProducts[MatchTab.ALL] = allProducts; for (final MatchedProduct matchedProduct in allProducts) { diff --git a/packages/smooth_app/lib/helpers/product_compatibility_helper.dart b/packages/smooth_app/lib/helpers/product_compatibility_helper.dart index bb7768e4b6d..829b695a636 100644 --- a/packages/smooth_app/lib/helpers/product_compatibility_helper.dart +++ b/packages/smooth_app/lib/helpers/product_compatibility_helper.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:openfoodfacts/personalized_search/matched_product.dart'; +import 'package:smooth_app/helpers/smooth_matched_product.dart'; class ProductCompatibilityHelper { const ProductCompatibilityHelper(this.matchedProduct); diff --git a/packages/smooth_app/lib/helpers/smooth_matched_product.dart b/packages/smooth_app/lib/helpers/smooth_matched_product.dart new file mode 100644 index 00000000000..6d8a7e346e7 --- /dev/null +++ b/packages/smooth_app/lib/helpers/smooth_matched_product.dart @@ -0,0 +1,178 @@ +import 'package:openfoodfacts/model/Attribute.dart'; +import 'package:openfoodfacts/model/AttributeGroup.dart'; +import 'package:openfoodfacts/model/Product.dart'; +import 'package:openfoodfacts/personalized_search/preference_importance.dart'; +import 'package:openfoodfacts/personalized_search/product_preferences_manager.dart'; +import 'package:smooth_app/data_models/user_preferences.dart'; +import 'package:smooth_app/pages/user_preferences_dev_mode.dart'; +import 'attributes_card_helper.dart'; + +/// Match and score of a [Product] vs. Preferences +/// +/// cf. https://github.com/openfoodfacts/smooth-app/issues/39 +/// Inspiration taken from off-dart's MatchedProduct.dart + +enum MatchedProductStatus { + YES, + NO, + UNKNOWN, +} + +abstract class MatchedProduct { + MatchedProduct(this.product); + + static MatchedProduct getMatchedProduct( + final Product product, + final ProductPreferencesManager productPreferencesManager, + final UserPreferences userPreferences, + ) => + userPreferences.getFlag( + UserPreferencesDevMode.userPreferencesFlagLenientMatching) ?? + false + ? _StrongMatchedProduct(product, productPreferencesManager) + : _LenientMatchedProduct(product, productPreferencesManager); + + final Product product; + double get score; + MatchedProductStatus? get status; + + static List sort( + final List products, + final ProductPreferencesManager productPreferencesManager, + final UserPreferences userPreferences, + ) { + final List result = []; + for (final Product product in products) { + final MatchedProduct matchedProduct = MatchedProduct.getMatchedProduct( + product, + productPreferencesManager, + userPreferences, + ); + result.add(matchedProduct); + } + result.sort( + (MatchedProduct a, MatchedProduct b) => b.score.compareTo(a.score)); + return result; + } +} + +/// Original version (monsieurtanuki) +class _StrongMatchedProduct extends MatchedProduct { + _StrongMatchedProduct( + final Product product, + final ProductPreferencesManager productPreferencesManager, + ) : super(product) { + final List? attributeGroups = product.attributeGroups; + if (attributeGroups == null) { + _status = null; + return; + } + _status = MatchedProductStatus.YES; + for (final AttributeGroup group in attributeGroups) { + if (group.attributes != null) { + for (final Attribute attribute in group.attributes!) { + final PreferenceImportance? preferenceImportance = + productPreferencesManager.getPreferenceImportanceFromImportanceId( + productPreferencesManager.getImportanceIdForAttributeId( + attribute.id!, + ), + ); + if (preferenceImportance != null) { + final String? importanceId = preferenceImportance.id; + final int factor = preferenceImportance.factor ?? 0; + final int? minimalMatch = preferenceImportance.minimalMatch; + if (importanceId == null || factor == 0) { + _debug += '${attribute.id} $importanceId\n'; + } else { + if (attribute.status == Attribute.STATUS_UNKNOWN) { + if (_status == MatchedProductStatus.YES) { + _status = MatchedProductStatus.UNKNOWN; + } + } else { + _debug += + '${attribute.id} $importanceId - match: ${attribute.match}\n'; + _score += (attribute.match ?? 0) * factor; + if (minimalMatch != null && + (attribute.match ?? 0) <= minimalMatch) { + _status = MatchedProductStatus.NO; + } + } + } + } + } + } + } + } + + double _score = 0; + MatchedProductStatus? _status; + String _debug = ''; + + @override + double get score => _score; + @override + MatchedProductStatus? get status => _status; + String get debug => _debug; +} + +const Map _attributeImportanceWeight = { + PreferenceImportance.ID_MANDATORY: 4, + PreferenceImportance.ID_VERY_IMPORTANT: 1, // same as important from now on + PreferenceImportance.ID_IMPORTANT: 1, + PreferenceImportance.ID_NOT_IMPORTANT: 0, +}; + +/// Lenient version (monsieurtanuki) (found back in #1046) +class _LenientMatchedProduct extends MatchedProduct { + _LenientMatchedProduct( + final Product product, + final ProductPreferencesManager productPreferencesManager, + ) : super(product) { + _score = 0.0; + int numAttributesComputed = 0; + if (product.attributeGroups != null) { + for (final AttributeGroup group in product.attributeGroups!) { + if (group.attributes != null) { + for (final Attribute attribute in group.attributes!) { + final String importanceLevel = productPreferencesManager + .getImportanceIdForAttributeId(attribute.id!); + // Check whether any mandatory attribute is incompatible + if (importanceLevel == PreferenceImportance.ID_MANDATORY && + getAttributeEvaluation(attribute) == + AttributeEvaluation.VERY_BAD) { + _status = MatchedProductStatus.NO; + return; + } + if (!_attributeImportanceWeight.containsKey(importanceLevel)) { + // Unknown attribute importance level. (This should ideally never happen). + continue; + } + if (_attributeImportanceWeight[importanceLevel] == 0.0) { + // Skip attributes that are not important + continue; + } + if (!isMatchAvailable(attribute)) { + continue; + } + _score += + attribute.match! * _attributeImportanceWeight[importanceLevel]!; + numAttributesComputed++; + } + } + } + } + if (numAttributesComputed == 0) { + _status = MatchedProductStatus.NO; + return; + } + _status = MatchedProductStatus.YES; + } + + late double _score; + MatchedProductStatus? _status; + + @override + double get score => _score; + @override + MatchedProductStatus? get status => _status; +} diff --git a/packages/smooth_app/lib/pages/personalized_ranking_page.dart b/packages/smooth_app/lib/pages/personalized_ranking_page.dart index da95b2cb46c..1e6afaf9e1c 100644 --- a/packages/smooth_app/lib/pages/personalized_ranking_page.dart +++ b/packages/smooth_app/lib/pages/personalized_ranking_page.dart @@ -1,14 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:openfoodfacts/personalized_search/matched_product.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/cards/product_cards/smooth_product_card_found.dart'; import 'package:smooth_app/data_models/product_list.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; import 'package:smooth_app/data_models/smooth_it_model.dart'; +import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/database/dao_product_list.dart'; import 'package:smooth_app/database/local_database.dart'; +import 'package:smooth_app/helpers/smooth_matched_product.dart'; class PersonalizedRankingPage extends StatefulWidget { PersonalizedRankingPage({ @@ -52,7 +53,11 @@ class _PersonalizedRankingPageState extends State { context.watch(); final LocalDatabase localDatabase = context.watch(); final DaoProductList daoProductList = DaoProductList(localDatabase); - _model.refresh(widget.products, productPreferences); + _model.refresh( + widget.products, + productPreferences, + context.watch(), + ); final AppLocalizations appLocalizations = AppLocalizations.of(context)!; final List colors = []; final List titles = []; diff --git a/packages/smooth_app/lib/pages/product/summary_card.dart b/packages/smooth_app/lib/pages/product/summary_card.dart index 0342be9a41f..012109d3aed 100644 --- a/packages/smooth_app/lib/pages/product/summary_card.dart +++ b/packages/smooth_app/lib/pages/product/summary_card.dart @@ -3,13 +3,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:openfoodfacts/model/Attribute.dart'; import 'package:openfoodfacts/model/AttributeGroup.dart'; import 'package:openfoodfacts/openfoodfacts.dart'; -import 'package:openfoodfacts/personalized_search/matched_product.dart'; import 'package:openfoodfacts/personalized_search/preference_importance.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/cards/data_cards/score_card.dart'; import 'package:smooth_app/cards/product_cards/product_title_card.dart'; import 'package:smooth_app/cards/product_cards/question_card.dart'; import 'package:smooth_app/data_models/product_preferences.dart'; +import 'package:smooth_app/data_models/user_preferences.dart'; import 'package:smooth_app/database/category_product_query.dart'; import 'package:smooth_app/database/local_database.dart'; import 'package:smooth_app/database/product_query.dart'; @@ -19,6 +19,7 @@ import 'package:smooth_app/helpers/attributes_card_helper.dart'; import 'package:smooth_app/helpers/product_cards_helper.dart'; import 'package:smooth_app/helpers/product_compatibility_helper.dart'; import 'package:smooth_app/helpers/score_card_helper.dart'; +import 'package:smooth_app/helpers/smooth_matched_product.dart'; import 'package:smooth_app/helpers/ui_helpers.dart'; import 'package:smooth_app/pages/product/common/product_query_page_helper.dart'; @@ -261,8 +262,11 @@ class _SummaryCardState extends State { } Widget _buildProductCompatibilityHeader(BuildContext context) { - final MatchedProduct matchedProduct = - MatchedProduct(widget._product, widget._productPreferences); + final MatchedProduct matchedProduct = MatchedProduct.getMatchedProduct( + widget._product, + widget._productPreferences, + context.watch(), + ); final ProductCompatibilityHelper helper = ProductCompatibilityHelper(matchedProduct); return Container( diff --git a/packages/smooth_app/lib/pages/user_preferences_dev_mode.dart b/packages/smooth_app/lib/pages/user_preferences_dev_mode.dart index a6a170cf68e..a0b3cd46628 100644 --- a/packages/smooth_app/lib/pages/user_preferences_dev_mode.dart +++ b/packages/smooth_app/lib/pages/user_preferences_dev_mode.dart @@ -36,6 +36,7 @@ class UserPreferencesDevMode extends AbstractUserPreferences { static const String userPreferencesFlagProd = '__devWorkingOnProd'; static const String userPreferencesFlagUseMLKit = '__useMLKit'; + static const String userPreferencesFlagLenientMatching = '__lenientMatching'; @override bool isCollapsedByDefault() => true; @@ -147,5 +148,19 @@ class UserPreferencesDevMode extends AbstractUserPreferences { ); }, ), + ListTile( + title: const Text('Switch between strong and lenient matching'), + subtitle: Text( + 'Current matching level is ' + '${(userPreferences.getFlag(userPreferencesFlagLenientMatching) ?? false) ? 'strong' : 'lenient'}', + ), + onTap: () async { + await userPreferences.setFlag( + userPreferencesFlagLenientMatching, + !(userPreferences.getFlag(userPreferencesFlagLenientMatching) ?? + false)); + setState(() {}); + }, + ), ]; } From 1bd232c3dc3ab14da24e5544937c501642cb76a4 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 10 Feb 2022 18:52:22 +0100 Subject: [PATCH 2/4] feat: #1060 - new dev mode lenient/strong matching switch "lenient" means @jasmeet0817's version, "strong" means @monsieurtanuki's. Why? Because for @jasmeet0817 it's impossible to be an "unknown" match. For @monsieurtanuki with at least one unknown attribute, you're at best "unknown". New file: * `smooth_matched_product.dart`: replacing off-dart's "MatchedProduct" Impacted files: * `personalized_ranking_page.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `product_compatibility_helper.dart`: not using off's `matched_product.dart` anymore * `smooth_it_model.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `smooth_product_card_found.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `summary_card.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `user_preferences_dev_mode.dart`: added a "lenient/strong" matching switch (aka "Swiss switch") --- packages/smooth_app/lib/helpers/smooth_matched_product.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smooth_app/lib/helpers/smooth_matched_product.dart b/packages/smooth_app/lib/helpers/smooth_matched_product.dart index 6d8a7e346e7..d1795dda669 100644 --- a/packages/smooth_app/lib/helpers/smooth_matched_product.dart +++ b/packages/smooth_app/lib/helpers/smooth_matched_product.dart @@ -19,8 +19,6 @@ enum MatchedProductStatus { } abstract class MatchedProduct { - MatchedProduct(this.product); - static MatchedProduct getMatchedProduct( final Product product, final ProductPreferencesManager productPreferencesManager, @@ -32,6 +30,8 @@ abstract class MatchedProduct { ? _StrongMatchedProduct(product, productPreferencesManager) : _LenientMatchedProduct(product, productPreferencesManager); + MatchedProduct(this.product); + final Product product; double get score; MatchedProductStatus? get status; From 1be05759a14bcc3283bca78bc930e2fa5a64290e Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sat, 12 Feb 2022 15:23:58 +0100 Subject: [PATCH 3/4] feat: #1060 - new dev mode lenient/strong matching switch "lenient" means @jasmeet0817's version, "strong" means @monsieurtanuki's. Why? Because for @jasmeet0817 it's impossible to be an "unknown" match. For @monsieurtanuki with at least one unknown attribute, you're at best "unknown". New file: * `smooth_matched_product.dart`: replacing off-dart's "MatchedProduct" Impacted files: * `personalized_ranking_page.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `product_compatibility_helper.dart`: not using off's `matched_product.dart` anymore * `smooth_it_model.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `smooth_product_card_found.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `summary_card.dart`: added the `UserPreferences` parameter for dev mode lenient/strong matching switch; not using off's `matched_product.dart` anymore * `user_preferences_dev_mode.dart`: added a "lenient/strong" matching switch (aka "Swiss switch") --- packages/smooth_app/lib/helpers/smooth_matched_product.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/smooth_app/lib/helpers/smooth_matched_product.dart b/packages/smooth_app/lib/helpers/smooth_matched_product.dart index d1795dda669..6d8a7e346e7 100644 --- a/packages/smooth_app/lib/helpers/smooth_matched_product.dart +++ b/packages/smooth_app/lib/helpers/smooth_matched_product.dart @@ -19,6 +19,8 @@ enum MatchedProductStatus { } abstract class MatchedProduct { + MatchedProduct(this.product); + static MatchedProduct getMatchedProduct( final Product product, final ProductPreferencesManager productPreferencesManager, @@ -30,8 +32,6 @@ abstract class MatchedProduct { ? _StrongMatchedProduct(product, productPreferencesManager) : _LenientMatchedProduct(product, productPreferencesManager); - MatchedProduct(this.product); - final Product product; double get score; MatchedProductStatus? get status; From 3900f0647b18cd16c03cb7f054c39f8248219fec Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 13 Feb 2022 14:44:44 +0100 Subject: [PATCH 4/4] feat: #1060 - comment change --- packages/smooth_app/lib/helpers/smooth_matched_product.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/smooth_app/lib/helpers/smooth_matched_product.dart b/packages/smooth_app/lib/helpers/smooth_matched_product.dart index 6d8a7e346e7..dfd494cadeb 100644 --- a/packages/smooth_app/lib/helpers/smooth_matched_product.dart +++ b/packages/smooth_app/lib/helpers/smooth_matched_product.dart @@ -122,7 +122,7 @@ const Map _attributeImportanceWeight = { PreferenceImportance.ID_NOT_IMPORTANT: 0, }; -/// Lenient version (monsieurtanuki) (found back in #1046) +/// Lenient version (jasmeet0817) (found back in #1046) class _LenientMatchedProduct extends MatchedProduct { _LenientMatchedProduct( final Product product,