@@ -28,6 +28,13 @@ import {
2828 runHealthConnectDebugReport ,
2929 formatHealthConnectDebugReport ,
3030} from '@/services/healthConnect' ;
31+ import {
32+ trainAndValidate ,
33+ loadEnhancedModel ,
34+ clearEnhancedModel ,
35+ type EnhancedModel ,
36+ type ValidationResult ,
37+ } from '@/services/enhancedSleepClassifier' ;
3138
3239export default function SettingsScreen ( ) {
3340 const [ showSleepDebug , setShowSleepDebug ] = useState ( false ) ;
@@ -40,6 +47,9 @@ export default function SettingsScreen() {
4047 const [ showHCDebug , setShowHCDebug ] = useState ( false ) ;
4148 const [ hcDebugReport , setHcDebugReport ] = useState < string | null > ( null ) ;
4249 const [ isLoadingHCDebug , setIsLoadingHCDebug ] = useState ( false ) ;
50+ const [ showEnhancedTraining , setShowEnhancedTraining ] = useState ( false ) ;
51+ const [ enhancedModelStatus , setEnhancedModelStatus ] = useState < string | null > ( null ) ;
52+ const [ isTrainingEnhanced , setIsTrainingEnhanced ] = useState ( false ) ;
4353
4454 const health = useHealth ( ) ;
4555
@@ -85,6 +95,82 @@ export default function SettingsScreen() {
8595 }
8696 } ;
8797
98+ const formatEnhancedModelStatus = (
99+ model : EnhancedModel | null ,
100+ validation ?: ValidationResult
101+ ) => {
102+ if ( ! model ) return 'No enhanced model trained yet.' ;
103+
104+ let status = `ENHANCED MODEL STATUS\n` ;
105+ status += `━━━━━━━━━━━━━━━━━━━━━\n` ;
106+ status += `Nights analyzed: ${ model . nightsAnalyzed } \n` ;
107+ status += `Last updated: ${ new Date ( model . lastUpdated ) . toLocaleString ( ) } \n` ;
108+ status += `Temporal smoothing: ${ model . temporalSmoothingStrength . toFixed ( 2 ) } \n\n` ;
109+
110+ status += `FEATURE WEIGHTS\n` ;
111+ status += `HR: ${ model . featureWeights . hr . toFixed ( 2 ) } , HRV: ${ model . featureWeights . hrv . toFixed ( 2 ) } \n` ;
112+ status += `HRV Est: ${ model . featureWeights . hrvEst . toFixed ( 2 ) } , RR: ${ model . featureWeights . rr . toFixed ( 2 ) } \n\n` ;
113+
114+ if ( model . validationAccuracy !== null ) {
115+ status += `VALIDATION ACCURACY\n` ;
116+ status += `━━━━━━━━━━━━━━━━━━━━━\n` ;
117+ status += `Overall: ${ ( model . validationAccuracy * 100 ) . toFixed ( 1 ) } %\n\n` ;
118+ status += `Per-Stage:\n` ;
119+ status += ` Awake: ${ ( model . perStageAccuracy . awake * 100 ) . toFixed ( 1 ) } %\n` ;
120+ status += ` Light: ${ ( model . perStageAccuracy . light * 100 ) . toFixed ( 1 ) } %\n` ;
121+ status += ` Deep: ${ ( model . perStageAccuracy . deep * 100 ) . toFixed ( 1 ) } %\n` ;
122+ status += ` REM: ${ ( model . perStageAccuracy . rem * 100 ) . toFixed ( 1 ) } %\n` ;
123+ }
124+
125+ if ( validation ) {
126+ status += `\nCONFUSION MATRIX (rows=actual, cols=predicted)\n` ;
127+ status += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` ;
128+ const stages : Array < 'awake' | 'light' | 'deep' | 'rem' > = [ 'awake' , 'light' , 'deep' , 'rem' ] ;
129+ status += ` awake light deep rem\n` ;
130+ for ( const actual of stages ) {
131+ status += `${ actual . padEnd ( 6 ) } ` ;
132+ for ( const predicted of stages ) {
133+ status += `${ validation . confusionMatrix [ actual ] [ predicted ] . toString ( ) . padStart ( 6 ) } ` ;
134+ }
135+ status += '\n' ;
136+ }
137+ status += `\nTotal samples: ${ validation . totalSamples } \n` ;
138+ }
139+
140+ return status ;
141+ } ;
142+
143+ const handleLoadEnhancedModel = async ( ) => {
144+ const model = await loadEnhancedModel ( ) ;
145+ setEnhancedModelStatus ( formatEnhancedModelStatus ( model ) ) ;
146+ } ;
147+
148+ const handleTrainEnhanced = async ( ) => {
149+ setIsTrainingEnhanced ( true ) ;
150+ setEnhancedModelStatus ( 'Starting training...' ) ;
151+ try {
152+ const { model, validation, bestParams } = await trainAndValidate ( ( message ) => {
153+ setEnhancedModelStatus ( message ) ;
154+ } ) ;
155+ let status = formatEnhancedModelStatus ( model , validation ) ;
156+ status += `\nBEST PARAMETERS FOUND\n` ;
157+ status += `━━━━━━━━━━━━━━━━━━━━━\n` ;
158+ status += `Temporal smoothing: ${ bestParams . temporalSmoothing } \n` ;
159+ status += `HR weight: ${ bestParams . hrWeight } \n` ;
160+ status += `HRV weight: ${ bestParams . hrvWeight } \n` ;
161+ setEnhancedModelStatus ( status ) ;
162+ } catch ( error ) {
163+ setEnhancedModelStatus ( `Training failed: ${ error } ` ) ;
164+ } finally {
165+ setIsTrainingEnhanced ( false ) ;
166+ }
167+ } ;
168+
169+ const handleClearEnhanced = async ( ) => {
170+ await clearEnhancedModel ( ) ;
171+ setEnhancedModelStatus ( 'Enhanced model cleared.' ) ;
172+ } ;
173+
88174 useEffect ( ( ) => {
89175 if ( showWearable && health . status ?. permissionsGranted ) {
90176 health . startVitalsPolling ( ) ;
@@ -282,16 +368,20 @@ export default function SettingsScreen() {
282368 ) }
283369
284370 < View style = { styles . debugSection } >
285- < MenuRow
286- icon = "bug-outline"
287- label = "Sleep Detection Debug"
288- onPress = { ( ) => setShowSleepDebug ( ! showSleepDebug ) }
289- />
290- { showSleepDebug && < SleepDebugPanel /> }
371+ { Platform . OS === 'web' && (
372+ < >
373+ < MenuRow
374+ icon = "pulse-outline"
375+ label = "Test Sleep Detection"
376+ onPress = { ( ) => setShowSleepDebug ( ! showSleepDebug ) }
377+ />
378+ { showSleepDebug && < SleepDebugPanel /> }
379+ </ >
380+ ) }
291381
292382 < MenuRow
293383 icon = "analytics-outline"
294- label = "Sleep Model Debug (Fitbit/Wearable) "
384+ label = "Wearable Sleep Insights "
295385 onPress = { ( ) => setShowModelDebug ( ! showModelDebug ) }
296386 />
297387 { showModelDebug && (
@@ -305,10 +395,10 @@ export default function SettingsScreen() {
305395 { isLoadingModelDebug ? (
306396 < ActivityIndicator size = "small" color = { colors . primary [ 500 ] } />
307397 ) : (
308- < Ionicons name = "play " size = { 16 } color = { colors . primary [ 500 ] } />
398+ < Ionicons name = "eye-outline " size = { 16 } color = { colors . primary [ 500 ] } />
309399 ) }
310400 < Text variant = "caption" color = "primary" >
311- Run Debug Report
401+ View Analysis
312402 </ Text >
313403 </ Pressable >
314404 < Pressable
@@ -318,13 +408,13 @@ export default function SettingsScreen() {
318408 >
319409 < Ionicons name = "refresh" size = { 16 } color = { colors . accent . cyan } />
320410 < Text variant = "caption" color = "primary" >
321- Retrain Model
411+ Refresh
322412 </ Text >
323413 </ Pressable >
324414 < Pressable style = { styles . debugButton } onPress = { handleClearModel } >
325415 < Ionicons name = "trash-outline" size = { 16 } color = { colors . error } />
326416 < Text variant = "caption" color = "primary" >
327- Clear
417+ Reset
328418 </ Text >
329419 </ Pressable >
330420 </ View >
@@ -350,12 +440,86 @@ export default function SettingsScreen() {
350440 { ( Platform . OS === 'android' || Platform . OS === 'ios' ) && (
351441 < >
352442 < MenuRow
353- icon = "medkit-outline"
354- label = "Health Connect Data Debug"
443+ icon = "fitness-outline"
444+ label = "Sleep Stage Classifier Training"
445+ onPress = { ( ) => {
446+ setShowEnhancedTraining ( ! showEnhancedTraining ) ;
447+ if ( ! showEnhancedTraining && ! enhancedModelStatus ) {
448+ handleLoadEnhancedModel ( ) ;
449+ }
450+ } }
451+ />
452+ { showEnhancedTraining && (
453+ < View style = { styles . expandedSection } >
454+ < Text variant = "caption" color = "muted" style = { { marginBottom : spacing . sm } } >
455+ Learn your personal sleep patterns from your wearable data to improve REM
456+ detection accuracy.
457+ </ Text >
458+ < View style = { styles . debugButtonRow } >
459+ < Pressable
460+ style = { styles . debugButton }
461+ onPress = { handleLoadEnhancedModel }
462+ disabled = { isTrainingEnhanced }
463+ >
464+ < Ionicons name = "information-circle" size = { 16 } color = { colors . primary [ 500 ] } />
465+ < Text variant = "caption" color = "primary" >
466+ Check Status
467+ </ Text >
468+ </ Pressable >
469+ < Pressable
470+ style = { [ styles . debugButton , styles . trainButton ] }
471+ onPress = { handleTrainEnhanced }
472+ disabled = { isTrainingEnhanced }
473+ >
474+ { isTrainingEnhanced ? (
475+ < ActivityIndicator size = "small" color = { colors . gray [ 950 ] } />
476+ ) : (
477+ < Ionicons name = "flash" size = { 16 } color = { colors . gray [ 950 ] } />
478+ ) }
479+ < Text variant = "caption" style = { { color : colors . gray [ 950 ] } } >
480+ Train Model
481+ </ Text >
482+ </ Pressable >
483+ < Pressable
484+ style = { styles . debugButton }
485+ onPress = { handleClearEnhanced }
486+ disabled = { isTrainingEnhanced }
487+ >
488+ < Ionicons name = "trash-outline" size = { 16 } color = { colors . error } />
489+ < Text variant = "caption" color = "primary" >
490+ Clear
491+ </ Text >
492+ </ Pressable >
493+ </ View >
494+ { enhancedModelStatus && (
495+ < ScrollView
496+ style = { styles . debugOutput }
497+ horizontal = { false }
498+ nestedScrollEnabled = { true }
499+ >
500+ < Text
501+ variant = "caption"
502+ color = "muted"
503+ style = { styles . debugOutputText }
504+ selectable = { true }
505+ >
506+ { enhancedModelStatus }
507+ </ Text >
508+ </ ScrollView >
509+ ) }
510+ </ View >
511+ ) }
512+
513+ < MenuRow
514+ icon = "heart-outline"
515+ label = "View Health Data"
355516 onPress = { ( ) => setShowHCDebug ( ! showHCDebug ) }
356517 />
357518 { showHCDebug && (
358519 < View style = { styles . expandedSection } >
520+ < Text variant = "caption" color = "muted" style = { { marginBottom : spacing . sm } } >
521+ Recent health data from your wearable (last 24 hours)
522+ </ Text >
359523 < View style = { styles . debugButtonRow } >
360524 < Pressable
361525 style = { styles . debugButton }
@@ -365,16 +529,13 @@ export default function SettingsScreen() {
365529 { isLoadingHCDebug ? (
366530 < ActivityIndicator size = "small" color = { colors . primary [ 500 ] } />
367531 ) : (
368- < Ionicons name = "play " size = { 16 } color = { colors . primary [ 500 ] } />
532+ < Ionicons name = "refresh-outline " size = { 16 } color = { colors . primary [ 500 ] } />
369533 ) }
370534 < Text variant = "caption" color = "primary" >
371- Run Debug Report
535+ Load Data
372536 </ Text >
373537 </ Pressable >
374538 </ View >
375- < Text variant = "caption" color = "muted" style = { { marginBottom : spacing . sm } } >
376- Shows all Health Connect data available from your wearable (last 24h)
377- </ Text >
378539 { hcDebugReport && (
379540 < ScrollView
380541 style = { styles . debugOutput }
@@ -634,6 +795,9 @@ const styles = StyleSheet.create({
634795 paddingVertical : spacing . xs ,
635796 borderRadius : 6 ,
636797 } ,
798+ trainButton : {
799+ backgroundColor : colors . primary [ 500 ] ,
800+ } ,
637801 debugOutput : {
638802 maxHeight : 400 ,
639803 backgroundColor : colors . gray [ 900 ] ,
0 commit comments