Skip to content

Commit

Permalink
1 onboarding profile builder (#18)
Browse files Browse the repository at this point in the history
* restructure main to get initial user if exists

* fix

* woo hoo
  • Loading branch information
seesharpguy authored Nov 8, 2024
1 parent e4b012f commit 93e3f4c
Show file tree
Hide file tree
Showing 16 changed files with 1,062 additions and 240 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: test, seed, assets
.PHONY: test seed assets

clean:
flutter clean && flutter pub get
Expand Down
2 changes: 1 addition & 1 deletion android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<!-- <uses-permission android:name="com.google.android.gms.permission.AD_ID"/> -->

<application
android:usesCleartextTraffic="true"
Expand Down
6 changes: 4 additions & 2 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
<intent>
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent>
<!-- Add more queries as needed -->
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>

Expand All @@ -84,7 +86,7 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<!-- <uses-permission android:name="com.google.android.gms.permission.AD_ID"/> -->

<uses-permission android:name="android.permission.BODY_SENSORS"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
Expand Down
96 changes: 86 additions & 10 deletions lib/app/models/user.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,97 @@
//User Model
class UserModel {
import 'package:cloud_firestore/cloud_firestore.dart';

class Zone2User {
final String uid;
final String email;
String name;
Map<String, dynamic> fcmTokenMap;

UserModel(
{required this.uid, required this.email, required this.name, required this.fcmTokenMap});
final bool onboardingComplete;
ZoneSettings? zoneSettings;
Zone2User(
{required this.uid,
required this.email,
required this.name,
required this.onboardingComplete,
this.zoneSettings});

factory UserModel.fromJson(Map data) {
return UserModel(
factory Zone2User.fromJson(Map data) {
return Zone2User(
uid: data['uid'],
email: data['email'] ?? '',
name: data['name'] ?? '',
fcmTokenMap: data['fcmTokenMap'] ?? {});
onboardingComplete: data['onboardingComplete'] ?? false,
zoneSettings: ZoneSettings.fromJson(data['zoneSettings'] ?? {}));
}

Map<String, dynamic> toJson() => {
"uid": uid,
"email": email,
"name": name,
"onboardingComplete": onboardingComplete,
"zoneSettings": zoneSettings?.toJson() ?? {}
};
}

class ZoneSettings {
final Timestamp journeyStartDate;
final int dailyWaterGoalInOz;
final int dailyZonePointsGoal;
final int dailyCalorieIntakeGoal;
final int dailyCaloriesBurnedGoal;
final int dailyStepsGoal;
final String reasonForStartingJourney;
final double initialWeightInLbs;
final double targetWeightInLbs;
final double heightInInches;
final int heightInFeet;
final String birthDate;
final String gender;

ZoneSettings(
{required this.journeyStartDate,
required this.dailyWaterGoalInOz,
required this.dailyZonePointsGoal,
required this.dailyCalorieIntakeGoal,
required this.dailyCaloriesBurnedGoal,
required this.dailyStepsGoal,
required this.reasonForStartingJourney,
required this.initialWeightInLbs,
required this.targetWeightInLbs,
required this.heightInInches,
required this.heightInFeet,
required this.birthDate,
required this.gender});

factory ZoneSettings.fromJson(Map data) {
return ZoneSettings(
journeyStartDate: data['journeyStartDate'] as Timestamp? ?? Timestamp.now(),
dailyWaterGoalInOz: (data['dailyWaterGoalInOz'] as num?)?.toInt() ?? 100,
dailyZonePointsGoal: (data['dailyZonePointsGoal'] as num?)?.toInt() ?? 100,
dailyCalorieIntakeGoal: (data['dailyCalorieIntakeGoal'] as num?)?.toInt() ?? 0,
dailyCaloriesBurnedGoal: (data['dailyCaloriesBurnedGoal'] as num?)?.toInt() ?? 0,
dailyStepsGoal: (data['dailyStepsGoal'] as num?)?.toInt() ?? 10000,
reasonForStartingJourney: data['reasonForStartingJourney'] as String? ?? '',
initialWeightInLbs: (data['initialWeightInLbs'] as num?)?.toDouble() ?? 0.0,
targetWeightInLbs: (data['targetWeightInLbs'] as num?)?.toDouble() ?? 0.0,
heightInInches: (data['heightInInches'] as num?)?.toDouble() ?? 0.0,
heightInFeet: (data['heightInFeet'] as num?)?.toInt() ?? 0,
birthDate: data['birthDate'] as String? ?? '',
gender: data['gender'] as String? ?? '');
}

Map<String, dynamic> toJson() =>
{"uid": uid, "email": email, "name": name, "fcmTokenMap": fcmTokenMap};
Map<String, dynamic> toJson() => {
"journeyStartDate": journeyStartDate,
"dailyWaterGoalInOz": dailyWaterGoalInOz,
"dailyZonePointsGoal": dailyZonePointsGoal,
"dailyCalorieIntakeGoal": dailyCalorieIntakeGoal,
"dailyCaloriesBurnedGoal": dailyCaloriesBurnedGoal,
"dailyStepsGoal": dailyStepsGoal,
"reasonForStartingJourney": reasonForStartingJourney,
"initialWeightInLbs": initialWeightInLbs,
"targetWeightInLbs": targetWeightInLbs,
"heightInInches": heightInInches,
"heightInFeet": heightInFeet,
"birthDate": birthDate,
"gender": gender
};
}
170 changes: 162 additions & 8 deletions lib/app/modules/diary/controllers/diary_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:speech_to_text/speech_recognition_error.dart';
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart';
import 'package:syncfusion_flutter_charts/charts.dart';
import 'package:zone2/app/modules/diary/controllers/activity_manager.dart';
import 'package:zone2/app/models/food.dart';
import 'package:zone2/app/services/food_service.dart';
Expand All @@ -17,6 +18,64 @@ import 'package:intl/intl.dart'; // Added for date formatting
import 'package:zone2/app/services/notification_service.dart';
import 'package:zone2/app/services/openai_service.dart';

class FoodVoiceResult {
final String label;
final String searchTerm;
final double quantity;
final String unit;
final MealType mealType;

FoodVoiceResult({
required this.label,
required this.searchTerm,
required this.quantity,
required this.unit,
required this.mealType,
});

// Factory method to create a FoodVoiceResult from JSON
factory FoodVoiceResult.fromJson(Map<String, dynamic> json) {
return FoodVoiceResult(
label: json['label'],
searchTerm: json['searchTerm'],
quantity: json['quantity'].toDouble(),
unit: json['unit'],
mealType: _parseMealType(json['mealType']),
);
}

// Factory method to create a list of FoodVoiceResult from OpenAI completion
static List<FoodVoiceResult> fromOpenAiCompletion(List<dynamic> items) {
return items.map((item) {
// Handle nulls for food items
final food = item['food'];
return FoodVoiceResult(
label: food?['label'] as String? ?? 'Unknown Food', // Default value for label
searchTerm: food?['searchTerm'] as String? ?? '', // Default to empty string
quantity: (food?['quantity'] as double?) ?? 0.0, // Default to 0.0
unit: food?['unit'] as String? ?? 'units', // Default unit
mealType: _parseMealType(food?['mealType'] as String? ?? 'UNKNOWN'), // Default to UNKNOWN
);
}).toList();
}

// Helper method to parse meal type from string
static MealType _parseMealType(String type) {
switch (type.toUpperCase()) {
case 'BREAKFAST':
return MealType.BREAKFAST;
case 'LUNCH':
return MealType.LUNCH;
case 'DINNER':
return MealType.DINNER;
case 'SNACK':
return MealType.SNACK;
default:
return MealType.UNKNOWN;
}
}
}

class DiaryController extends GetxController {
final logger = Get.find<Logger>();
final healthService = Get.find<HealthService>();
Expand Down Expand Up @@ -68,13 +127,21 @@ class DiaryController extends GetxController {
final lastError = Rxn<SpeechRecognitionError>();
final recognizedWords = ''.obs;
final systemLocale = Rxn<LocaleName>();
final isTestMode = false.obs; // Toggle this for testing

final voiceResults = RxList<FoodVoiceResult>();

String selectedVoiceFood = '';
RxList<FoodVoiceResult> searchResults = RxList<FoodVoiceResult>();

// Barcode scanning
final Rxn<Barcode> barcode = Rxn<Barcode>();
final Rxn<BarcodeCapture> capture = Rxn<BarcodeCapture>();
late MobileScannerController scannerController;
StreamSubscription<Object?>? scannerSubscription;

ChartSeriesController? chartController;

@override
void onInit() async {
super.onInit();
Expand Down Expand Up @@ -113,11 +180,12 @@ class DiaryController extends GetxController {
if (!isAvailable.value || isListening.value) return;

try {
isListening.value = await speech.listen(
await speech.listen(
onResult: _onSpeechResult,
listenOptions: SpeechListenOptions(partialResults: true),
localeId: currentLocaleId.value,
);
isListening.value = true;
} catch (e) {
logger.e('Error starting speech recognition: $e');
isListening.value = false;
Expand Down Expand Up @@ -200,6 +268,7 @@ class DiaryController extends GetxController {
}
}

// TODO: Each Health Data Type should be its own method, this should aggregate them all
Future<void> getHealthDataForSelectedDay() async {
// Retrieve weight data
final sameDay = diaryDate.value.year == DateTime.now().year &&
Expand Down Expand Up @@ -411,12 +480,97 @@ class DiaryController extends GetxController {
}

Future<void> extractFoodItemsOpenAI(String text) async {
final result = await OpenAIService.to.extractFoodsFromText(text);
matchedFoods.value = result.choices.first.message.content
?.map((item) => item.text)
.whereType<String>()
.toList() ??
[];
logger.i('Extracted food items: $result');
try {
isProcessing.value = true;

if (isTestMode.value) {
await Future.delayed(const Duration(seconds: 1));
voiceResults.value = [
FoodVoiceResult(
label: "2 scrambled eggs with spinach",
searchTerm: "eggs",
quantity: 2,
unit: "large",
mealType: MealType.BREAKFAST,
),
FoodVoiceResult(
label: "1 slice whole grain toast with avocado",
searchTerm: "whole grain bread",
quantity: 1,
unit: "slice",
mealType: MealType.BREAKFAST,
),
// ... other test items
];
matchedFoods.value = voiceResults.map((r) => r.label).toList();
} else {
final openAIChatCompletion = await OpenAIService.to.extractFoodsFromText(text);
final newItems = FoodVoiceResult.fromOpenAiCompletion(openAIChatCompletion['foods']['items'] as List<dynamic>);
voiceResults.value = newItems;

matchedFoods.value = voiceResults.map((r) => r.label).toList();
}
} catch (e) {
logger.e('Error extracting foods: $e');
NotificationService.to
.showError('Error', 'Failed to process speech input. Please try again.');
voiceResults.clear();
matchedFoods.clear();
} finally {
isProcessing.value = false;
}
}

MealType _parseMealType(String type) {
switch (type.toUpperCase()) {
case 'BREAKFAST':
return MealType.BREAKFAST;
case 'LUNCH':
return MealType.LUNCH;
case 'DINNER':
return MealType.DINNER;
case 'SNACK':
return MealType.SNACK;
default:
return MealType.UNKNOWN;
}
}

void selectFoodFromVoice(String foodDescription) async {
try {
// Store the selected food description
selectedVoiceFood = foodDescription;

// Reset search results before new search
foodSearchResults.value = null;

await EasyLoading.show(
status: 'Searching for food...',
maskType: EasyLoadingMaskType.black,
);

// Perform the search
final results = await foodService.searchFood(foodDescription);
foodSearchResults.value = results;

if (results.foods.isNotEmpty) {
// Navigate to search results view
Get.snackbar('Got results', 'Found ${results.foods.length} results');
} else {
Get.snackbar(
'No Results',
'No foods found matching "$foodDescription"',
snackPosition: SnackPosition.TOP,
);
}
} catch (error) {
Get.snackbar(
'Error',
'Failed to search for food: ${error.toString()}',
snackPosition: SnackPosition.TOP,
);
} finally {
await EasyLoading.dismiss();
}
}
}
Loading

0 comments on commit 93e3f4c

Please sign in to comment.