From 277888c4075f862f652af8b698c85a6b2e932bad Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Fri, 8 May 2026 16:22:36 +0700 Subject: [PATCH] Use ontology traits in map UI --- lib/api/mapflow_api.dart | 5 +- lib/models/place_models.dart | 124 +++++++------------------------- lib/screens/mapflow_shell.dart | 18 ++--- lib/state/place_controller.dart | 23 +++--- 4 files changed, 47 insertions(+), 123 deletions(-) diff --git a/lib/api/mapflow_api.dart b/lib/api/mapflow_api.dart index b11d1c3..6fa6f21 100644 --- a/lib/api/mapflow_api.dart +++ b/lib/api/mapflow_api.dart @@ -120,7 +120,7 @@ class MapflowApi { } for (final tag in tags) { - final trait = _traitByName(tag.toString()); + final trait = _traitByTag(tag.toString()); if (trait != null) { traits.add(trait); } @@ -130,7 +130,8 @@ class MapflowApi { return traits; } - PlaceTrait? _traitByName(String name) { + PlaceTrait? _traitByTag(String tag) { + final name = tag.contains(':') ? tag.split(':').last : tag; for (final trait in PlaceTrait.values) { if (trait.name == name) { return trait; diff --git a/lib/models/place_models.dart b/lib/models/place_models.dart index acc52d7..b7c9d69 100644 --- a/lib/models/place_models.dart +++ b/lib/models/place_models.dart @@ -3,118 +3,48 @@ import 'package:latlong2/latlong.dart'; enum PlaceTrait { calm, - alive, - cozy, - cold, - status, - simple, - free, - formal, - private, + dynamic, + intimate, open, - social, solo, - beautiful, - neutral, - unusual, - clear, - focused, + group, + reset, + impress, transit, + clean, + expressive, } extension PlaceTraitText on PlaceTrait { String get label { return switch (this) { PlaceTrait.calm => 'спокойное', - PlaceTrait.alive => 'живое', - PlaceTrait.cozy => 'уютное', - PlaceTrait.cold => 'холодное', - PlaceTrait.status => 'статусное', - PlaceTrait.simple => 'простое', - PlaceTrait.free => 'свободное', - PlaceTrait.formal => 'формальное', - PlaceTrait.private => 'приватное', + PlaceTrait.dynamic => 'живое', + PlaceTrait.intimate => 'камерное', PlaceTrait.open => 'открытое', - PlaceTrait.social => 'для общения', PlaceTrait.solo => 'для себя', - PlaceTrait.beautiful => 'красивое', - PlaceTrait.neutral => 'нейтральное', - PlaceTrait.unusual => 'необычное', - PlaceTrait.clear => 'понятное', - PlaceTrait.focused => 'помогает собраться', + PlaceTrait.group => 'для компании', + PlaceTrait.reset => 'выдохнуть', + PlaceTrait.impress => 'впечатлить', PlaceTrait.transit => 'транзитное', - }; - } -} - -enum UserIntent { exhale, date, meet, focus, move, surprise, alone, impress } - -extension UserIntentText on UserIntent { - String get title { - return switch (this) { - UserIntent.exhale => 'выдохнуть', - UserIntent.date => 'свидание', - UserIntent.meet => 'встретиться', - UserIntent.focus => 'поработать', - UserIntent.move => 'движ', - UserIntent.surprise => 'удивиться', - UserIntent.alone => 'побыть одному', - UserIntent.impress => 'привести кого-то', - }; - } - - String get subtitle { - return switch (this) { - UserIntent.exhale => 'тихо, мягко, без давления', - UserIntent.date => 'красиво и лично', - UserIntent.meet => 'общение без лишней формальности', - UserIntent.focus => 'собраться и не выпадать', - UserIntent.move => 'живо, шумно, с энергией', - UserIntent.surprise => 'не как обычно', - UserIntent.alone => 'быть в своем ритме', - UserIntent.impress => 'место должно держать момент', + PlaceTrait.clean => 'чистое', + PlaceTrait.expressive => 'выразительное', }; } IconData get icon { return switch (this) { - UserIntent.exhale => Icons.air_outlined, - UserIntent.date => Icons.favorite_border, - UserIntent.meet => Icons.forum_outlined, - UserIntent.focus => Icons.center_focus_strong_outlined, - UserIntent.move => Icons.bolt_outlined, - UserIntent.surprise => Icons.auto_awesome_outlined, - UserIntent.alone => Icons.person_outline, - UserIntent.impress => Icons.diamond_outlined, - }; - } - - Set get traits { - return switch (this) { - UserIntent.exhale => {PlaceTrait.calm, PlaceTrait.cozy, PlaceTrait.solo}, - UserIntent.date => { - PlaceTrait.private, - PlaceTrait.beautiful, - PlaceTrait.social, - }, - UserIntent.meet => {PlaceTrait.social, PlaceTrait.free, PlaceTrait.alive}, - UserIntent.focus => { - PlaceTrait.focused, - PlaceTrait.calm, - PlaceTrait.neutral, - }, - UserIntent.move => {PlaceTrait.alive, PlaceTrait.open, PlaceTrait.social}, - UserIntent.surprise => { - PlaceTrait.unusual, - PlaceTrait.alive, - PlaceTrait.open, - }, - UserIntent.alone => {PlaceTrait.solo, PlaceTrait.free, PlaceTrait.calm}, - UserIntent.impress => { - PlaceTrait.status, - PlaceTrait.beautiful, - PlaceTrait.private, - }, + PlaceTrait.calm => Icons.air_outlined, + PlaceTrait.dynamic => Icons.bolt_outlined, + PlaceTrait.intimate => Icons.lock_outline, + PlaceTrait.open => Icons.public_outlined, + PlaceTrait.solo => Icons.person_outline, + PlaceTrait.group => Icons.forum_outlined, + PlaceTrait.reset => Icons.spa_outlined, + PlaceTrait.impress => Icons.diamond_outlined, + PlaceTrait.transit => Icons.near_me_outlined, + PlaceTrait.clean => Icons.wb_sunny_outlined, + PlaceTrait.expressive => Icons.auto_awesome_outlined, }; } } @@ -144,14 +74,12 @@ class VoiceReviewDraft { required this.placeName, required this.duration, required this.extractedTraits, - required this.suggestedIntents, required this.evidence, }); final String placeName; final Duration duration; final Set extractedTraits; - final Set suggestedIntents; final List evidence; bool get isLongEnough => duration.inSeconds >= 30; @@ -160,14 +88,12 @@ class VoiceReviewDraft { String? placeName, Duration? duration, Set? extractedTraits, - Set? suggestedIntents, List? evidence, }) { return VoiceReviewDraft( placeName: placeName ?? this.placeName, duration: duration ?? this.duration, extractedTraits: extractedTraits ?? this.extractedTraits, - suggestedIntents: suggestedIntents ?? this.suggestedIntents, evidence: evidence ?? this.evidence, ); } diff --git a/lib/screens/mapflow_shell.dart b/lib/screens/mapflow_shell.dart index 169abc9..757b1ca 100644 --- a/lib/screens/mapflow_shell.dart +++ b/lib/screens/mapflow_shell.dart @@ -75,7 +75,7 @@ class _MapContent extends ConsumerWidget { SafeArea( child: Align( alignment: Alignment.topCenter, - child: _IntentBar(intent: state.intent), + child: _TraitBar(selectedTrait: state.selectedTrait), ), ), Align( @@ -187,10 +187,10 @@ class _MapError extends StatelessWidget { } } -class _IntentBar extends ConsumerWidget { - const _IntentBar({required this.intent}); +class _TraitBar extends ConsumerWidget { + const _TraitBar({required this.selectedTrait}); - final UserIntent intent; + final PlaceTrait selectedTrait; @override Widget build(BuildContext context, WidgetRef ref) { @@ -201,15 +201,15 @@ class _IntentBar extends ConsumerWidget { margin: const EdgeInsets.fromLTRB(10, 8, 10, 0), child: ListView.separated( scrollDirection: Axis.horizontal, - itemCount: UserIntent.values.length, + itemCount: PlaceTrait.values.length, separatorBuilder: (_, _) => const SizedBox(width: 8), itemBuilder: (context, index) { - final item = UserIntent.values[index]; + final item = PlaceTrait.values[index]; return ChoiceChip( avatar: Icon(item.icon, size: 17), - label: Text(item.title), - selected: item == intent, - onSelected: (_) => controller.selectIntent(item), + label: Text(item.label), + selected: item == selectedTrait, + onSelected: (_) => controller.selectTrait(item), backgroundColor: const Color(0xFFFFFBF5), selectedColor: Theme.of(context).colorScheme.primaryContainer, padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), diff --git a/lib/state/place_controller.dart b/lib/state/place_controller.dart index 3ce5c93..33941a1 100644 --- a/lib/state/place_controller.dart +++ b/lib/state/place_controller.dart @@ -9,23 +9,22 @@ final placeControllerProvider = class PlaceState { const PlaceState({ - required this.intent, + required this.selectedTrait, required this.places, required this.selectedPlaceId, required this.reviewDraft, }); - final UserIntent intent; + final PlaceTrait selectedTrait; final List places; final String? selectedPlaceId; final VoiceReviewDraft reviewDraft; List get recommendations { - final wanted = intent.traits; final ranked = [...places] ..sort((a, b) { - final aScore = a.traits.intersection(wanted).length; - final bScore = b.traits.intersection(wanted).length; + final aScore = a.traits.contains(selectedTrait) ? 1 : 0; + final bScore = b.traits.contains(selectedTrait) ? 1 : 0; return bScore.compareTo(aScore); }); return ranked.take(4).toList(); @@ -41,13 +40,13 @@ class PlaceState { } PlaceState copyWith({ - UserIntent? intent, + PlaceTrait? selectedTrait, List? places, String? selectedPlaceId, VoiceReviewDraft? reviewDraft, }) { return PlaceState( - intent: intent ?? this.intent, + selectedTrait: selectedTrait ?? this.selectedTrait, places: places ?? this.places, selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, reviewDraft: reviewDraft ?? this.reviewDraft, @@ -62,30 +61,29 @@ class PlaceController extends AsyncNotifier { Future build() async { final places = await _api.fetchPlaces(); return PlaceState( - intent: UserIntent.exhale, + selectedTrait: PlaceTrait.calm, places: places, selectedPlaceId: places.isEmpty ? null : places.first.id, reviewDraft: const VoiceReviewDraft( placeName: '', duration: Duration.zero, extractedTraits: {}, - suggestedIntents: {}, evidence: [], ), ); } - void selectIntent(UserIntent intent) { + void selectTrait(PlaceTrait trait) { final value = state.requireValue; PlaceRecommendation? next; for (final place in value.places) { - if (place.traits.intersection(intent.traits).isNotEmpty) { + if (place.traits.contains(trait)) { next = place; break; } } state = AsyncData( - value.copyWith(intent: intent, selectedPlaceId: next?.id), + value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id), ); } @@ -138,7 +136,6 @@ class PlaceController extends AsyncNotifier { placeName: '', duration: Duration.zero, extractedTraits: {}, - suggestedIntents: {}, evidence: [], ), ),