Use ontology traits in map UI
All checks were successful
Build and deploy Flutter Web / build (push) Successful in 3m17s

This commit is contained in:
Ruslan Bakiev
2026-05-08 16:22:36 +07:00
parent 238521b11b
commit 277888c407
4 changed files with 47 additions and 123 deletions

View File

@@ -120,7 +120,7 @@ class MapflowApi {
} }
for (final tag in tags) { for (final tag in tags) {
final trait = _traitByName(tag.toString()); final trait = _traitByTag(tag.toString());
if (trait != null) { if (trait != null) {
traits.add(trait); traits.add(trait);
} }
@@ -130,7 +130,8 @@ class MapflowApi {
return traits; return traits;
} }
PlaceTrait? _traitByName(String name) { PlaceTrait? _traitByTag(String tag) {
final name = tag.contains(':') ? tag.split(':').last : tag;
for (final trait in PlaceTrait.values) { for (final trait in PlaceTrait.values) {
if (trait.name == name) { if (trait.name == name) {
return trait; return trait;

View File

@@ -3,118 +3,48 @@ import 'package:latlong2/latlong.dart';
enum PlaceTrait { enum PlaceTrait {
calm, calm,
alive, dynamic,
cozy, intimate,
cold,
status,
simple,
free,
formal,
private,
open, open,
social,
solo, solo,
beautiful, group,
neutral, reset,
unusual, impress,
clear,
focused,
transit, transit,
clean,
expressive,
} }
extension PlaceTraitText on PlaceTrait { extension PlaceTraitText on PlaceTrait {
String get label { String get label {
return switch (this) { return switch (this) {
PlaceTrait.calm => 'спокойное', PlaceTrait.calm => 'спокойное',
PlaceTrait.alive => 'живое', PlaceTrait.dynamic => 'живое',
PlaceTrait.cozy => 'уютное', PlaceTrait.intimate => 'камерное',
PlaceTrait.cold => 'холодное',
PlaceTrait.status => 'статусное',
PlaceTrait.simple => 'простое',
PlaceTrait.free => 'свободное',
PlaceTrait.formal => 'формальное',
PlaceTrait.private => 'приватное',
PlaceTrait.open => 'открытое', PlaceTrait.open => 'открытое',
PlaceTrait.social => 'для общения',
PlaceTrait.solo => 'для себя', PlaceTrait.solo => 'для себя',
PlaceTrait.beautiful => 'красивое', PlaceTrait.group => 'для компании',
PlaceTrait.neutral => 'нейтральное', PlaceTrait.reset => 'выдохнуть',
PlaceTrait.unusual => 'необычное', PlaceTrait.impress => 'впечатлить',
PlaceTrait.clear => 'понятное',
PlaceTrait.focused => 'помогает собраться',
PlaceTrait.transit => 'транзитное', PlaceTrait.transit => 'транзитное',
}; PlaceTrait.clean => 'чистое',
} PlaceTrait.expressive => 'выразительное',
}
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 => 'место должно держать момент',
}; };
} }
IconData get icon { IconData get icon {
return switch (this) { return switch (this) {
UserIntent.exhale => Icons.air_outlined, PlaceTrait.calm => Icons.air_outlined,
UserIntent.date => Icons.favorite_border, PlaceTrait.dynamic => Icons.bolt_outlined,
UserIntent.meet => Icons.forum_outlined, PlaceTrait.intimate => Icons.lock_outline,
UserIntent.focus => Icons.center_focus_strong_outlined, PlaceTrait.open => Icons.public_outlined,
UserIntent.move => Icons.bolt_outlined, PlaceTrait.solo => Icons.person_outline,
UserIntent.surprise => Icons.auto_awesome_outlined, PlaceTrait.group => Icons.forum_outlined,
UserIntent.alone => Icons.person_outline, PlaceTrait.reset => Icons.spa_outlined,
UserIntent.impress => Icons.diamond_outlined, PlaceTrait.impress => Icons.diamond_outlined,
}; PlaceTrait.transit => Icons.near_me_outlined,
} PlaceTrait.clean => Icons.wb_sunny_outlined,
PlaceTrait.expressive => Icons.auto_awesome_outlined,
Set<PlaceTrait> 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,
},
}; };
} }
} }
@@ -144,14 +74,12 @@ class VoiceReviewDraft {
required this.placeName, required this.placeName,
required this.duration, required this.duration,
required this.extractedTraits, required this.extractedTraits,
required this.suggestedIntents,
required this.evidence, required this.evidence,
}); });
final String placeName; final String placeName;
final Duration duration; final Duration duration;
final Set<PlaceTrait> extractedTraits; final Set<PlaceTrait> extractedTraits;
final Set<UserIntent> suggestedIntents;
final List<String> evidence; final List<String> evidence;
bool get isLongEnough => duration.inSeconds >= 30; bool get isLongEnough => duration.inSeconds >= 30;
@@ -160,14 +88,12 @@ class VoiceReviewDraft {
String? placeName, String? placeName,
Duration? duration, Duration? duration,
Set<PlaceTrait>? extractedTraits, Set<PlaceTrait>? extractedTraits,
Set<UserIntent>? suggestedIntents,
List<String>? evidence, List<String>? evidence,
}) { }) {
return VoiceReviewDraft( return VoiceReviewDraft(
placeName: placeName ?? this.placeName, placeName: placeName ?? this.placeName,
duration: duration ?? this.duration, duration: duration ?? this.duration,
extractedTraits: extractedTraits ?? this.extractedTraits, extractedTraits: extractedTraits ?? this.extractedTraits,
suggestedIntents: suggestedIntents ?? this.suggestedIntents,
evidence: evidence ?? this.evidence, evidence: evidence ?? this.evidence,
); );
} }

View File

@@ -75,7 +75,7 @@ class _MapContent extends ConsumerWidget {
SafeArea( SafeArea(
child: Align( child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: _IntentBar(intent: state.intent), child: _TraitBar(selectedTrait: state.selectedTrait),
), ),
), ),
Align( Align(
@@ -187,10 +187,10 @@ class _MapError extends StatelessWidget {
} }
} }
class _IntentBar extends ConsumerWidget { class _TraitBar extends ConsumerWidget {
const _IntentBar({required this.intent}); const _TraitBar({required this.selectedTrait});
final UserIntent intent; final PlaceTrait selectedTrait;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -201,15 +201,15 @@ class _IntentBar extends ConsumerWidget {
margin: const EdgeInsets.fromLTRB(10, 8, 10, 0), margin: const EdgeInsets.fromLTRB(10, 8, 10, 0),
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: UserIntent.values.length, itemCount: PlaceTrait.values.length,
separatorBuilder: (_, _) => const SizedBox(width: 8), separatorBuilder: (_, _) => const SizedBox(width: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = UserIntent.values[index]; final item = PlaceTrait.values[index];
return ChoiceChip( return ChoiceChip(
avatar: Icon(item.icon, size: 17), avatar: Icon(item.icon, size: 17),
label: Text(item.title), label: Text(item.label),
selected: item == intent, selected: item == selectedTrait,
onSelected: (_) => controller.selectIntent(item), onSelected: (_) => controller.selectTrait(item),
backgroundColor: const Color(0xFFFFFBF5), backgroundColor: const Color(0xFFFFFBF5),
selectedColor: Theme.of(context).colorScheme.primaryContainer, selectedColor: Theme.of(context).colorScheme.primaryContainer,
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),

View File

@@ -9,23 +9,22 @@ final placeControllerProvider =
class PlaceState { class PlaceState {
const PlaceState({ const PlaceState({
required this.intent, required this.selectedTrait,
required this.places, required this.places,
required this.selectedPlaceId, required this.selectedPlaceId,
required this.reviewDraft, required this.reviewDraft,
}); });
final UserIntent intent; final PlaceTrait selectedTrait;
final List<PlaceRecommendation> places; final List<PlaceRecommendation> places;
final String? selectedPlaceId; final String? selectedPlaceId;
final VoiceReviewDraft reviewDraft; final VoiceReviewDraft reviewDraft;
List<PlaceRecommendation> get recommendations { List<PlaceRecommendation> get recommendations {
final wanted = intent.traits;
final ranked = [...places] final ranked = [...places]
..sort((a, b) { ..sort((a, b) {
final aScore = a.traits.intersection(wanted).length; final aScore = a.traits.contains(selectedTrait) ? 1 : 0;
final bScore = b.traits.intersection(wanted).length; final bScore = b.traits.contains(selectedTrait) ? 1 : 0;
return bScore.compareTo(aScore); return bScore.compareTo(aScore);
}); });
return ranked.take(4).toList(); return ranked.take(4).toList();
@@ -41,13 +40,13 @@ class PlaceState {
} }
PlaceState copyWith({ PlaceState copyWith({
UserIntent? intent, PlaceTrait? selectedTrait,
List<PlaceRecommendation>? places, List<PlaceRecommendation>? places,
String? selectedPlaceId, String? selectedPlaceId,
VoiceReviewDraft? reviewDraft, VoiceReviewDraft? reviewDraft,
}) { }) {
return PlaceState( return PlaceState(
intent: intent ?? this.intent, selectedTrait: selectedTrait ?? this.selectedTrait,
places: places ?? this.places, places: places ?? this.places,
selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId, selectedPlaceId: selectedPlaceId ?? this.selectedPlaceId,
reviewDraft: reviewDraft ?? this.reviewDraft, reviewDraft: reviewDraft ?? this.reviewDraft,
@@ -62,30 +61,29 @@ class PlaceController extends AsyncNotifier<PlaceState> {
Future<PlaceState> build() async { Future<PlaceState> build() async {
final places = await _api.fetchPlaces(); final places = await _api.fetchPlaces();
return PlaceState( return PlaceState(
intent: UserIntent.exhale, selectedTrait: PlaceTrait.calm,
places: places, places: places,
selectedPlaceId: places.isEmpty ? null : places.first.id, selectedPlaceId: places.isEmpty ? null : places.first.id,
reviewDraft: const VoiceReviewDraft( reviewDraft: const VoiceReviewDraft(
placeName: '', placeName: '',
duration: Duration.zero, duration: Duration.zero,
extractedTraits: {}, extractedTraits: {},
suggestedIntents: {},
evidence: [], evidence: [],
), ),
); );
} }
void selectIntent(UserIntent intent) { void selectTrait(PlaceTrait trait) {
final value = state.requireValue; final value = state.requireValue;
PlaceRecommendation? next; PlaceRecommendation? next;
for (final place in value.places) { for (final place in value.places) {
if (place.traits.intersection(intent.traits).isNotEmpty) { if (place.traits.contains(trait)) {
next = place; next = place;
break; break;
} }
} }
state = AsyncData( state = AsyncData(
value.copyWith(intent: intent, selectedPlaceId: next?.id), value.copyWith(selectedTrait: trait, selectedPlaceId: next?.id),
); );
} }
@@ -138,7 +136,6 @@ class PlaceController extends AsyncNotifier<PlaceState> {
placeName: '', placeName: '',
duration: Duration.zero, duration: Duration.zero,
extractedTraits: {}, extractedTraits: {},
suggestedIntents: {},
evidence: [], evidence: [],
), ),
), ),