diff --git a/lib/models/behavioral_inputs.dart b/lib/models/behavioral_inputs.dart index 875c6b8..171dd07 100644 --- a/lib/models/behavioral_inputs.dart +++ b/lib/models/behavioral_inputs.dart @@ -1,12 +1,20 @@ import 'enums.dart'; class BehavioralInputs { + // Screen 1: Basic behaviors final SmokingStatus smoking; - final int cigarettesPerDay; // only relevant if smoking == current + final int cigarettesPerDay; final AlcoholLevel alcohol; final double sleepHours; final bool sleepConsistent; final ActivityLevel activity; + + // Screen 2: Lifestyle factors + final DietQuality diet; + final ProcessedFoodLevel processedFood; + final DrugUse drugUse; + final SocialConnection social; + final StressLevel stress; final DrivingExposure driving; final WorkHoursLevel workHours; @@ -17,6 +25,11 @@ class BehavioralInputs { required this.sleepHours, required this.sleepConsistent, required this.activity, + required this.diet, + required this.processedFood, + required this.drugUse, + required this.social, + required this.stress, required this.driving, required this.workHours, }); @@ -28,10 +41,41 @@ class BehavioralInputs { sleepHours: 7.5, sleepConsistent: true, activity: ActivityLevel.high, + diet: DietQuality.excellent, + processedFood: ProcessedFoodLevel.rarely, + drugUse: DrugUse.none, + social: SocialConnection.strong, + stress: StressLevel.low, driving: DrivingExposure.low, workHours: WorkHoursLevel.normal, ); + /// Create partial inputs from screen 1 (with defaults for screen 2) + factory BehavioralInputs.fromScreen1({ + required SmokingStatus smoking, + int cigarettesPerDay = 0, + required AlcoholLevel alcohol, + required double sleepHours, + required bool sleepConsistent, + required ActivityLevel activity, + }) { + return BehavioralInputs( + smoking: smoking, + cigarettesPerDay: cigarettesPerDay, + alcohol: alcohol, + sleepHours: sleepHours, + sleepConsistent: sleepConsistent, + activity: activity, + diet: DietQuality.fair, + processedFood: ProcessedFoodLevel.frequent, + drugUse: DrugUse.none, + social: SocialConnection.moderate, + stress: StressLevel.moderate, + driving: DrivingExposure.low, + workHours: WorkHoursLevel.normal, + ); + } + Map toJson() => { 'smoking': smoking.name, 'cigarettesPerDay': cigarettesPerDay, @@ -39,6 +83,11 @@ class BehavioralInputs { 'sleepHours': sleepHours, 'sleepConsistent': sleepConsistent, 'activity': activity.name, + 'diet': diet.name, + 'processedFood': processedFood.name, + 'drugUse': drugUse.name, + 'social': social.name, + 'stress': stress.name, 'driving': driving.name, 'workHours': workHours.name, }; @@ -51,6 +100,14 @@ class BehavioralInputs { sleepHours: (json['sleepHours'] as num).toDouble(), sleepConsistent: json['sleepConsistent'] as bool, activity: ActivityLevel.values.byName(json['activity'] as String), + diet: DietQuality.values.byName(json['diet'] as String? ?? 'fair'), + processedFood: ProcessedFoodLevel.values + .byName(json['processedFood'] as String? ?? 'frequent'), + drugUse: DrugUse.values.byName(json['drugUse'] as String? ?? 'none'), + social: SocialConnection.values + .byName(json['social'] as String? ?? 'moderate'), + stress: + StressLevel.values.byName(json['stress'] as String? ?? 'moderate'), driving: DrivingExposure.values.byName(json['driving'] as String), workHours: WorkHoursLevel.values.byName(json['workHours'] as String), ); @@ -62,6 +119,11 @@ class BehavioralInputs { double? sleepHours, bool? sleepConsistent, ActivityLevel? activity, + DietQuality? diet, + ProcessedFoodLevel? processedFood, + DrugUse? drugUse, + SocialConnection? social, + StressLevel? stress, DrivingExposure? driving, WorkHoursLevel? workHours, }) => @@ -72,6 +134,11 @@ class BehavioralInputs { sleepHours: sleepHours ?? this.sleepHours, sleepConsistent: sleepConsistent ?? this.sleepConsistent, activity: activity ?? this.activity, + diet: diet ?? this.diet, + processedFood: processedFood ?? this.processedFood, + drugUse: drugUse ?? this.drugUse, + social: social ?? this.social, + stress: stress ?? this.stress, driving: driving ?? this.driving, workHours: workHours ?? this.workHours, ); diff --git a/lib/models/enums.dart b/lib/models/enums.dart index 3c8d723..c1505ba 100644 --- a/lib/models/enums.dart +++ b/lib/models/enums.dart @@ -13,3 +13,14 @@ enum WorkHoursLevel { normal, elevated, high, extreme } enum Diagnosis { cardiovascular, diabetes, cancer, copd, hypertension } enum Confidence { high, moderate, emerging } + +// Lifestyle factors +enum DietQuality { poor, fair, good, excellent } + +enum ProcessedFoodLevel { daily, frequent, occasional, rarely } + +enum DrugUse { none, occasional, regular, daily } + +enum SocialConnection { isolated, limited, moderate, strong } + +enum StressLevel { low, moderate, high, chronic } diff --git a/lib/risk_engine/calculator.dart b/lib/risk_engine/calculator.dart index fed79ea..29c00c5 100644 --- a/lib/risk_engine/calculator.dart +++ b/lib/risk_engine/calculator.dart @@ -3,7 +3,7 @@ import '../models/models.dart'; import 'hazard_ratios.dart'; import 'mortality_tables.dart'; -const String modelVersion = '1.0'; +const String modelVersion = '1.1'; /// Maximum combined hazard ratio (prevents unrealistic compounding). const double _maxCombinedHR = 4.0; @@ -19,11 +19,19 @@ const double _highMultiplier = 1.2; double computeCombinedHazard(BehavioralInputs inputs, double bmi) { double hr = 1.0; + // Screen 1 factors hr *= getSmokingHR(inputs.smoking, inputs.cigarettesPerDay); hr *= getAlcoholHR(inputs.alcohol); hr *= getSleepHR(inputs.sleepHours, inputs.sleepConsistent); hr *= getActivityHR(inputs.activity); hr *= getBmiHR(bmi); + + // Screen 2 factors + hr *= getDietHR(inputs.diet); + hr *= getProcessedFoodHR(inputs.processedFood); + hr *= getDrugUseHR(inputs.drugUse); + hr *= getSocialHR(inputs.social); + hr *= getStressHR(inputs.stress); hr *= getDrivingHR(inputs.driving); hr *= getWorkHoursHR(inputs.workHours); @@ -38,7 +46,6 @@ LifespanDelta _computeDelta( String behaviorKey, ) { if (currentHR <= modifiedHR) { - // No improvement possible or already optimal return LifespanDelta( lowMonths: 0, highMonths: 0, @@ -46,11 +53,9 @@ LifespanDelta _computeDelta( ); } - // Delta years ≈ baselineYears × (1 - modifiedHR/currentHR) × dampingFactor final rawDeltaYears = baselineYears * (1 - modifiedHR / currentHR) * _dampingFactor; - // Convert to months with uncertainty range final midpointMonths = rawDeltaYears * 12; final lowMonths = (midpointMonths * _lowMultiplier).round(); final highMonths = (midpointMonths * _highMultiplier).round(); @@ -76,6 +81,16 @@ BehavioralInputs _setToOptimal(BehavioralInputs inputs, String behaviorKey) { return inputs.copyWith(sleepHours: 7.5, sleepConsistent: true); case 'activity': return inputs.copyWith(activity: ActivityLevel.high); + case 'diet': + return inputs.copyWith(diet: DietQuality.excellent); + case 'processedFood': + return inputs.copyWith(processedFood: ProcessedFoodLevel.rarely); + case 'drugUse': + return inputs.copyWith(drugUse: DrugUse.none); + case 'social': + return inputs.copyWith(social: SocialConnection.strong); + case 'stress': + return inputs.copyWith(stress: StressLevel.low); case 'driving': return inputs.copyWith(driving: DrivingExposure.low); case 'workHours': @@ -99,6 +114,16 @@ bool _isOptimal(BehavioralInputs inputs, String behaviorKey) { inputs.sleepConsistent; case 'activity': return inputs.activity == ActivityLevel.high; + case 'diet': + return inputs.diet == DietQuality.excellent; + case 'processedFood': + return inputs.processedFood == ProcessedFoodLevel.rarely; + case 'drugUse': + return inputs.drugUse == DrugUse.none; + case 'social': + return inputs.social == SocialConnection.strong; + case 'stress': + return inputs.stress == StressLevel.low; case 'driving': return inputs.driving == DrivingExposure.low; case 'workHours': @@ -114,6 +139,11 @@ const _modifiableBehaviors = [ 'alcohol', 'sleep', 'activity', + 'diet', + 'processedFood', + 'drugUse', + 'social', + 'stress', 'driving', 'workHours', ]; @@ -123,28 +153,22 @@ CalculationResult calculateRankedFactors( UserProfile profile, BehavioralInputs inputs, ) { - // Get baseline remaining life expectancy final baselineYears = getRemainingLifeExpectancy( profile.age, profile.sex, profile.country, ); - // Apply existing condition modifiers (reduces baseline) final conditionHR = getDiagnosisHR(profile.diagnoses); final adjustedBaselineYears = baselineYears / conditionHR; - // Calculate current combined HR from behaviors final currentHR = computeCombinedHazard(inputs, profile.bmi); - // Calculate delta for each modifiable behavior final factors = []; for (final behaviorKey in _modifiableBehaviors) { - // Skip if already optimal if (_isOptimal(inputs, behaviorKey)) continue; - // Compute HR with this behavior set to optimal final modifiedInputs = _setToOptimal(inputs, behaviorKey); final modifiedHR = computeCombinedHazard(modifiedInputs, profile.bmi); @@ -155,7 +179,6 @@ CalculationResult calculateRankedFactors( behaviorKey, ); - // Only include if there's meaningful gain (> 1 month) if (delta.highMonths >= 1) { factors.add(RankedFactor( behaviorKey: behaviorKey, @@ -165,8 +188,8 @@ CalculationResult calculateRankedFactors( } } - // Sort by midpoint delta descending - factors.sort((a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths)); + factors.sort( + (a, b) => b.delta.midpointMonths.compareTo(a.delta.midpointMonths)); return CalculationResult( rankedFactors: factors, diff --git a/lib/risk_engine/hazard_ratios.dart b/lib/risk_engine/hazard_ratios.dart index 8d56325..633bd9d 100644 --- a/lib/risk_engine/hazard_ratios.dart +++ b/lib/risk_engine/hazard_ratios.dart @@ -43,7 +43,6 @@ double getSleepHR(double hours, bool consistent) { hr = 1.10; } - // Inconsistent sleep schedule adds additional risk if (!consistent) { hr *= 1.05; } @@ -70,8 +69,7 @@ double getBmiHR(double bmi) { if (bmi >= 30 && bmi < 35) return 1.2; if (bmi >= 35 && bmi < 40) return 1.4; if (bmi >= 40) return 1.8; - // Underweight - return 1.15; + return 1.15; // Underweight } double getDrivingHR(DrivingExposure level) { @@ -100,6 +98,78 @@ double getWorkHoursHR(WorkHoursLevel level) { } } +// --- New lifestyle factors --- + +/// Diet quality - based on Mediterranean diet studies +double getDietHR(DietQuality level) { + switch (level) { + case DietQuality.excellent: + return 1.0; + case DietQuality.good: + return 1.05; + case DietQuality.fair: + return 1.15; + case DietQuality.poor: + return 1.3; + } +} + +/// Processed food consumption - ultra-processed food studies +double getProcessedFoodHR(ProcessedFoodLevel level) { + switch (level) { + case ProcessedFoodLevel.rarely: + return 1.0; + case ProcessedFoodLevel.occasional: + return 1.05; + case ProcessedFoodLevel.frequent: + return 1.12; + case ProcessedFoodLevel.daily: + return 1.2; + } +} + +/// Drug use - excluding alcohol/tobacco (cannabis, recreational drugs) +double getDrugUseHR(DrugUse level) { + switch (level) { + case DrugUse.none: + return 1.0; + case DrugUse.occasional: + return 1.05; + case DrugUse.regular: + return 1.15; + case DrugUse.daily: + return 1.35; + } +} + +/// Social connection - loneliness/isolation meta-analyses +double getSocialHR(SocialConnection level) { + switch (level) { + case SocialConnection.strong: + return 1.0; + case SocialConnection.moderate: + return 1.05; + case SocialConnection.limited: + return 1.2; + case SocialConnection.isolated: + return 1.45; + } +} + +/// Chronic stress - based on allostatic load research +double getStressHR(StressLevel level) { + switch (level) { + case StressLevel.low: + return 1.0; + case StressLevel.moderate: + return 1.05; + case StressLevel.high: + return 1.15; + case StressLevel.chronic: + return 1.35; + } +} + /// Existing conditions modify baseline mortality but are NOT modifiable. double getDiagnosisHR(Set diagnoses) { double hr = 1.0; @@ -131,11 +201,16 @@ Confidence getConfidenceForBehavior(String behaviorKey) { case 'smoking': case 'alcohol': case 'activity': + case 'social': return Confidence.high; case 'sleep': case 'workHours': + case 'diet': + case 'stress': return Confidence.moderate; case 'driving': + case 'processedFood': + case 'drugUse': return Confidence.emerging; default: return Confidence.moderate; @@ -157,6 +232,16 @@ String getDisplayName(String behaviorKey) { return 'Driving Exposure'; case 'workHours': return 'Work Hours'; + case 'diet': + return 'Diet Quality'; + case 'processedFood': + return 'Processed Food'; + case 'drugUse': + return 'Drug Use'; + case 'social': + return 'Social Connection'; + case 'stress': + return 'Chronic Stress'; default: return behaviorKey; } diff --git a/lib/screens/behavioral_screen.dart b/lib/screens/behavioral_screen.dart index 874dde6..bb97b43 100644 --- a/lib/screens/behavioral_screen.dart +++ b/lib/screens/behavioral_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import '../models/models.dart'; import '../storage/local_storage.dart'; import '../theme.dart'; -import 'results_screen.dart'; +import 'lifestyle_screen.dart'; class BehavioralScreen extends StatefulWidget { final UserProfile profile; @@ -21,8 +21,6 @@ class _BehavioralScreenState extends State { double _sleepHours = 7.5; bool _sleepConsistent = true; ActivityLevel _activity = ActivityLevel.moderate; - DrivingExposure _driving = DrivingExposure.low; - WorkHoursLevel _workHours = WorkHoursLevel.normal; @override void initState() { @@ -40,8 +38,6 @@ class _BehavioralScreenState extends State { _sleepHours = behaviors.sleepHours; _sleepConsistent = behaviors.sleepConsistent; _activity = behaviors.activity; - _driving = behaviors.driving; - _workHours = behaviors.workHours; }); } } @@ -50,7 +46,7 @@ class _BehavioralScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Behaviors'), + title: const Text('Habits'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.pop(context), @@ -62,12 +58,12 @@ class _BehavioralScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Modifiable Factors', + 'Daily Habits', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 8), Text( - 'These behaviors can be changed. We\'ll identify which has the largest impact.', + 'Substances, sleep, and activity levels.', style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 32), @@ -100,26 +96,14 @@ class _BehavioralScreenState extends State { _buildSectionLabel('Physical Activity'), const SizedBox(height: 12), _buildActivitySelector(), - const SizedBox(height: 28), - - // Driving Exposure - _buildSectionLabel('Driving Exposure'), - const SizedBox(height: 12), - _buildDrivingSelector(), - const SizedBox(height: 28), - - // Work Hours - _buildSectionLabel('Work Hours'), - const SizedBox(height: 12), - _buildWorkHoursSelector(), const SizedBox(height: 40), - // Calculate button + // Continue button SizedBox( width: double.infinity, child: ElevatedButton( - onPressed: _calculate, - child: const Text('Calculate'), + onPressed: _continue, + child: const Text('Continue'), ), ), const SizedBox(height: 16), @@ -166,7 +150,6 @@ class _BehavioralScreenState extends State { max: 40, divisions: 39, onChanged: (value) { - // Haptic feedback at 20 (one pack) if (value.round() == 20 && _cigarettesPerDay != 20) { HapticFeedback.mediumImpact(); } @@ -264,32 +247,6 @@ class _BehavioralScreenState extends State { ); } - Widget _buildDrivingSelector() { - return _buildSegmentedControl( - value: _driving, - options: [ - (DrivingExposure.low, '<50 mi/wk'), - (DrivingExposure.moderate, '50-150'), - (DrivingExposure.high, '150-300'), - (DrivingExposure.veryHigh, '300+'), - ], - onChanged: (value) => setState(() => _driving = value), - ); - } - - Widget _buildWorkHoursSelector() { - return _buildSegmentedControl( - value: _workHours, - options: [ - (WorkHoursLevel.normal, '<40'), - (WorkHoursLevel.elevated, '40-55'), - (WorkHoursLevel.high, '55-70'), - (WorkHoursLevel.extreme, '70+'), - ], - onChanged: (value) => setState(() => _workHours = value), - ); - } - Widget _buildSegmentedControl({ required T value, required List<(T, String)> options, @@ -329,29 +286,19 @@ class _BehavioralScreenState extends State { ); } - void _calculate() async { - final behaviors = BehavioralInputs( - smoking: _smoking, - cigarettesPerDay: _cigarettesPerDay, - alcohol: _alcohol, - sleepHours: _sleepHours, - sleepConsistent: _sleepConsistent, - activity: _activity, - driving: _driving, - workHours: _workHours, - ); - - await LocalStorage.saveBehaviors(behaviors); - - if (mounted) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ResultsScreen( - profile: widget.profile, - behaviors: behaviors, - ), + void _continue() { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => LifestyleScreen( + profile: widget.profile, + smoking: _smoking, + cigarettesPerDay: _cigarettesPerDay, + alcohol: _alcohol, + sleepHours: _sleepHours, + sleepConsistent: _sleepConsistent, + activity: _activity, ), - ); - } + ), + ); } } diff --git a/lib/screens/lifestyle_screen.dart b/lib/screens/lifestyle_screen.dart new file mode 100644 index 0000000..7af5288 --- /dev/null +++ b/lib/screens/lifestyle_screen.dart @@ -0,0 +1,323 @@ +import 'package:flutter/material.dart'; +import '../models/models.dart'; +import '../storage/local_storage.dart'; +import '../theme.dart'; +import 'results_screen.dart'; + +class LifestyleScreen extends StatefulWidget { + final UserProfile profile; + final SmokingStatus smoking; + final int cigarettesPerDay; + final AlcoholLevel alcohol; + final double sleepHours; + final bool sleepConsistent; + final ActivityLevel activity; + + const LifestyleScreen({ + super.key, + required this.profile, + required this.smoking, + required this.cigarettesPerDay, + required this.alcohol, + required this.sleepHours, + required this.sleepConsistent, + required this.activity, + }); + + @override + State createState() => _LifestyleScreenState(); +} + +class _LifestyleScreenState extends State { + DietQuality _diet = DietQuality.fair; + ProcessedFoodLevel _processedFood = ProcessedFoodLevel.frequent; + DrugUse _drugUse = DrugUse.none; + SocialConnection _social = SocialConnection.moderate; + StressLevel _stress = StressLevel.moderate; + DrivingExposure _driving = DrivingExposure.low; + WorkHoursLevel _workHours = WorkHoursLevel.normal; + + @override + void initState() { + super.initState(); + _loadExistingBehaviors(); + } + + Future _loadExistingBehaviors() async { + final behaviors = await LocalStorage.getBehaviors(); + if (behaviors != null) { + setState(() { + _diet = behaviors.diet; + _processedFood = behaviors.processedFood; + _drugUse = behaviors.drugUse; + _social = behaviors.social; + _stress = behaviors.stress; + _driving = behaviors.driving; + _workHours = behaviors.workHours; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Lifestyle'), + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lifestyle Factors', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Diet, social life, stress, and daily exposures.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 32), + + // Diet Quality + _buildSectionLabel('Diet Quality'), + _buildSectionHint('Vegetables, whole foods, variety'), + const SizedBox(height: 12), + _buildDietSelector(), + const SizedBox(height: 28), + + // Processed Food + _buildSectionLabel('Processed Food'), + _buildSectionHint('Fast food, packaged snacks, sugary drinks'), + const SizedBox(height: 12), + _buildProcessedFoodSelector(), + const SizedBox(height: 28), + + // Drug Use + _buildSectionLabel('Recreational Drugs'), + _buildSectionHint('Cannabis, stimulants, other substances'), + const SizedBox(height: 12), + _buildDrugUseSelector(), + const SizedBox(height: 28), + + // Social Connection + _buildSectionLabel('Social Connection'), + _buildSectionHint('Time with friends, family, community'), + const SizedBox(height: 12), + _buildSocialSelector(), + const SizedBox(height: 28), + + // Stress Level + _buildSectionLabel('Stress Level'), + _buildSectionHint('Work pressure, anxiety, life demands'), + const SizedBox(height: 12), + _buildStressSelector(), + const SizedBox(height: 28), + + // Driving Exposure + _buildSectionLabel('Driving Exposure'), + const SizedBox(height: 12), + _buildDrivingSelector(), + const SizedBox(height: 28), + + // Work Hours + _buildSectionLabel('Work Hours'), + const SizedBox(height: 12), + _buildWorkHoursSelector(), + const SizedBox(height: 40), + + // Calculate button + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: _calculate, + child: const Text('Calculate'), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + Widget _buildSectionLabel(String label) { + return Text( + label, + style: Theme.of(context).textTheme.labelLarge, + ); + } + + Widget _buildSectionHint(String hint) { + return Text( + hint, + style: Theme.of(context).textTheme.bodySmall, + ); + } + + Widget _buildDietSelector() { + return _buildSegmentedControl( + value: _diet, + options: [ + (DietQuality.poor, 'Poor'), + (DietQuality.fair, 'Fair'), + (DietQuality.good, 'Good'), + (DietQuality.excellent, 'Excellent'), + ], + onChanged: (value) => setState(() => _diet = value), + ); + } + + Widget _buildProcessedFoodSelector() { + return _buildSegmentedControl( + value: _processedFood, + options: [ + (ProcessedFoodLevel.daily, 'Daily'), + (ProcessedFoodLevel.frequent, 'Often'), + (ProcessedFoodLevel.occasional, 'Sometimes'), + (ProcessedFoodLevel.rarely, 'Rarely'), + ], + onChanged: (value) => setState(() => _processedFood = value), + ); + } + + Widget _buildDrugUseSelector() { + return _buildSegmentedControl( + value: _drugUse, + options: [ + (DrugUse.none, 'None'), + (DrugUse.occasional, 'Occasional'), + (DrugUse.regular, 'Regular'), + (DrugUse.daily, 'Daily'), + ], + onChanged: (value) => setState(() => _drugUse = value), + ); + } + + Widget _buildSocialSelector() { + return _buildSegmentedControl( + value: _social, + options: [ + (SocialConnection.isolated, 'Isolated'), + (SocialConnection.limited, 'Limited'), + (SocialConnection.moderate, 'Moderate'), + (SocialConnection.strong, 'Strong'), + ], + onChanged: (value) => setState(() => _social = value), + ); + } + + Widget _buildStressSelector() { + return _buildSegmentedControl( + value: _stress, + options: [ + (StressLevel.low, 'Low'), + (StressLevel.moderate, 'Moderate'), + (StressLevel.high, 'High'), + (StressLevel.chronic, 'Chronic'), + ], + onChanged: (value) => setState(() => _stress = value), + ); + } + + Widget _buildDrivingSelector() { + return _buildSegmentedControl( + value: _driving, + options: [ + (DrivingExposure.low, '<50 mi/wk'), + (DrivingExposure.moderate, '50-150'), + (DrivingExposure.high, '150-300'), + (DrivingExposure.veryHigh, '300+'), + ], + onChanged: (value) => setState(() => _driving = value), + ); + } + + Widget _buildWorkHoursSelector() { + return _buildSegmentedControl( + value: _workHours, + options: [ + (WorkHoursLevel.normal, '<40'), + (WorkHoursLevel.elevated, '40-55'), + (WorkHoursLevel.high, '55-70'), + (WorkHoursLevel.extreme, '70+'), + ], + onChanged: (value) => setState(() => _workHours = value), + ); + } + + Widget _buildSegmentedControl({ + required T value, + required List<(T, String)> options, + required ValueChanged onChanged, + }) { + return Container( + decoration: BoxDecoration( + color: AppColors.surfaceVariant, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: options.map((option) { + final isSelected = value == option.$1; + return Expanded( + child: GestureDetector( + onTap: () => onChanged(option.$1), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected ? AppColors.primary : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + option.$2, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: isSelected ? Colors.white : AppColors.textSecondary, + ), + ), + ), + ), + ); + }).toList(), + ), + ); + } + + void _calculate() async { + final behaviors = BehavioralInputs( + smoking: widget.smoking, + cigarettesPerDay: widget.cigarettesPerDay, + alcohol: widget.alcohol, + sleepHours: widget.sleepHours, + sleepConsistent: widget.sleepConsistent, + activity: widget.activity, + diet: _diet, + processedFood: _processedFood, + drugUse: _drugUse, + social: _social, + stress: _stress, + driving: _driving, + workHours: _workHours, + ); + + await LocalStorage.saveBehaviors(behaviors); + + if (mounted) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => ResultsScreen( + profile: widget.profile, + behaviors: behaviors, + ), + ), + ); + } + } +} diff --git a/lib/screens/screens.dart b/lib/screens/screens.dart index 2200bc5..e01de27 100644 --- a/lib/screens/screens.dart +++ b/lib/screens/screens.dart @@ -2,4 +2,5 @@ export 'onboarding_screen.dart'; export 'welcome_screen.dart'; export 'baseline_screen.dart'; export 'behavioral_screen.dart'; +export 'lifestyle_screen.dart'; export 'results_screen.dart'; diff --git a/lib/screens/welcome_screen.dart b/lib/screens/welcome_screen.dart index 56e90b0..b799900 100644 --- a/lib/screens/welcome_screen.dart +++ b/lib/screens/welcome_screen.dart @@ -13,60 +13,44 @@ class WelcomeScreen extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( children: [ - const Spacer(flex: 2), - // Icon or logo area - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: AppColors.surfaceVariant, - borderRadius: BorderRadius.circular(20), - ), - child: const Icon( - Icons.timeline, - size: 40, - color: AppColors.primary, - ), - ), - const SizedBox(height: 32), - // Title + const Spacer(flex: 1), + // Main question - large and centered Text( - 'Add Months', - style: Theme.of(context).textTheme.headlineLarge, - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - // Description - Text( - 'Identify the single change most likely to extend your lifespan.', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: AppColors.textSecondary, + 'Simple questions.\nHonest answers.', + style: Theme.of(context).textTheme.headlineLarge?.copyWith( + fontSize: 32, + height: 1.3, ), textAlign: TextAlign.center, ), - const SizedBox(height: 48), - // Features list - _buildFeatureItem( - context, - Icons.shield_outlined, - 'Private', - 'All data stays on your device', + const SizedBox(height: 40), + Text( + "What's the single biggest change I can make to live a longer, healthier life?", + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w400, + color: AppColors.textSecondary, + height: 1.4, + ), + textAlign: TextAlign.center, ), - const SizedBox(height: 16), - _buildFeatureItem( - context, - Icons.science_outlined, - 'Evidence-based', - 'Hazard ratios from meta-analyses', + const Spacer(flex: 2), + // Privacy note + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.lock_outline, + size: 16, + color: AppColors.textSecondary, + ), + const SizedBox(width: 8), + Text( + 'All data stays on your device', + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), - const SizedBox(height: 16), - _buildFeatureItem( - context, - Icons.trending_up_outlined, - 'Actionable', - 'Focus on what matters most', - ), - const Spacer(flex: 3), + const SizedBox(height: 24), // Start button SizedBox( width: double.infinity, @@ -83,43 +67,6 @@ class WelcomeScreen extends StatelessWidget { ); } - Widget _buildFeatureItem( - BuildContext context, - IconData icon, - String title, - String description, - ) { - return Row( - children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: AppColors.surfaceVariant, - borderRadius: BorderRadius.circular(12), - ), - child: Icon(icon, color: AppColors.primary, size: 22), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: Theme.of(context).textTheme.labelLarge, - ), - Text( - description, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - ], - ); - } - void _navigateToBaseline(BuildContext context) { Navigator.of(context).push( MaterialPageRoute(builder: (_) => const BaselineScreen()), diff --git a/test/risk_engine/calculator_test.dart b/test/risk_engine/calculator_test.dart index b848016..03a9a05 100644 --- a/test/risk_engine/calculator_test.dart +++ b/test/risk_engine/calculator_test.dart @@ -94,6 +94,11 @@ void main() { sleepHours: 5.0, sleepConsistent: false, activity: ActivityLevel.sedentary, + diet: DietQuality.poor, + processedFood: ProcessedFoodLevel.daily, + drugUse: DrugUse.regular, + social: SocialConnection.isolated, + stress: StressLevel.chronic, driving: DrivingExposure.veryHigh, workHours: WorkHoursLevel.extreme, ); @@ -121,6 +126,11 @@ void main() { sleepHours: 7.5, sleepConsistent: true, activity: ActivityLevel.high, + diet: DietQuality.excellent, + processedFood: ProcessedFoodLevel.rarely, + drugUse: DrugUse.none, + social: SocialConnection.strong, + stress: StressLevel.low, driving: DrivingExposure.veryHigh, workHours: WorkHoursLevel.normal, ); @@ -138,7 +148,7 @@ void main() { test('result includes model version', () { final result = calculateRankedFactors(healthyProfile, unhealthyInputs); - expect(result.modelVersion, equals('1.0')); + expect(result.modelVersion, equals('1.1')); }); test('sedentary person sees activity as factor', () { @@ -149,6 +159,11 @@ void main() { sleepHours: 7.5, sleepConsistent: true, activity: ActivityLevel.sedentary, + diet: DietQuality.excellent, + processedFood: ProcessedFoodLevel.rarely, + drugUse: DrugUse.none, + social: SocialConnection.strong, + stress: StressLevel.low, driving: DrivingExposure.low, workHours: WorkHoursLevel.normal, ); @@ -205,6 +220,11 @@ void main() { sleepHours: 7.5, sleepConsistent: true, activity: ActivityLevel.sedentary, + diet: DietQuality.excellent, + processedFood: ProcessedFoodLevel.rarely, + drugUse: DrugUse.none, + social: SocialConnection.strong, + stress: StressLevel.low, driving: DrivingExposure.low, workHours: WorkHoursLevel.normal, );