Skip to content

Commit

Permalink
feat: #1060 - new dev mode lenient/strong matching switch (#1133)
Browse files Browse the repository at this point in the history
"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")
  • Loading branch information
monsieurtanuki authored Feb 13, 2022
1 parent 278e9e8 commit af2e5c9
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<ProductPreferences>(),
context.watch<UserPreferences>(),
);
final ProductCompatibilityHelper helper =
ProductCompatibilityHelper(matchedProduct);
Expand Down
11 changes: 8 additions & 3 deletions packages/smooth_app/lib/data_models/smooth_it_model.dart
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -17,9 +18,13 @@ class SmoothItModel {
void refresh(
final List<Product> products,
final ProductPreferences productPreferences,
final UserPreferences userPreferences,
) {
final List<MatchedProduct> allProducts =
MatchedProduct.sort(products, productPreferences);
final List<MatchedProduct> allProducts = MatchedProduct.sort(
products,
productPreferences,
userPreferences,
);
_categorizedProducts.clear();
_categorizedProducts[MatchTab.ALL] = allProducts;
for (final MatchedProduct matchedProduct in allProducts) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
178 changes: 178 additions & 0 deletions packages/smooth_app/lib/helpers/smooth_matched_product.dart
Original file line number Diff line number Diff line change
@@ -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<MatchedProduct> sort(
final List<Product> products,
final ProductPreferencesManager productPreferencesManager,
final UserPreferences userPreferences,
) {
final List<MatchedProduct> result = <MatchedProduct>[];
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<AttributeGroup>? 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<String, int> _attributeImportanceWeight = <String, int>{
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 (jasmeet0817) (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;
}
9 changes: 7 additions & 2 deletions packages/smooth_app/lib/pages/personalized_ranking_page.dart
Original file line number Diff line number Diff line change
@@ -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({
Expand Down Expand Up @@ -52,7 +53,11 @@ class _PersonalizedRankingPageState extends State<PersonalizedRankingPage> {
context.watch<ProductPreferences>();
final LocalDatabase localDatabase = context.watch<LocalDatabase>();
final DaoProductList daoProductList = DaoProductList(localDatabase);
_model.refresh(widget.products, productPreferences);
_model.refresh(
widget.products,
productPreferences,
context.watch<UserPreferences>(),
);
final AppLocalizations appLocalizations = AppLocalizations.of(context)!;
final List<Color> colors = <Color>[];
final List<String> titles = <String>[];
Expand Down
10 changes: 7 additions & 3 deletions packages/smooth_app/lib/pages/product/summary_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -271,8 +272,11 @@ class _SummaryCardState extends State<SummaryCard> {
}

Widget _buildProductCompatibilityHeader(BuildContext context) {
final MatchedProduct matchedProduct =
MatchedProduct(widget._product, widget._productPreferences);
final MatchedProduct matchedProduct = MatchedProduct.getMatchedProduct(
widget._product,
widget._productPreferences,
context.watch<UserPreferences>(),
);
final ProductCompatibilityHelper helper =
ProductCompatibilityHelper(matchedProduct);
return Container(
Expand Down
15 changes: 15 additions & 0 deletions packages/smooth_app/lib/pages/user_preferences_dev_mode.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class UserPreferencesDevMode extends AbstractUserPreferences {

static const String userPreferencesFlagProd = '__devWorkingOnProd';
static const String userPreferencesFlagUseMLKit = '__useMLKit';
static const String userPreferencesFlagLenientMatching = '__lenientMatching';
static const String userPreferencesFlagAdditionalButton =
'__additionalButtonOnProductPage';

Expand Down Expand Up @@ -160,5 +161,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(() {});
},
),
];
}

0 comments on commit af2e5c9

Please sign in to comment.