From cbfecf26d5421ffb8cb8cf200d340ee0facbced2 Mon Sep 17 00:00:00 2001 From: Gabino Ocotl Date: Wed, 9 Jul 2025 20:08:41 -0500 Subject: [PATCH] Integrated phase 5, added breathing circle and ui for analysis screen --- .github/workflows/ci.yml | 2 - apps/mobile/App.tsx | 5 +- apps/mobile/src/components/BreathingGuide.tsx | 420 +++++++++++++++ apps/mobile/src/components/SentimentMeter.tsx | 275 ++++++++++ apps/mobile/src/constants/SentimentTheme.ts | 192 +++++++ apps/mobile/src/context/AppContext.tsx | 73 ++- .../src/context/SentimentThemeProvider.tsx | 116 +++++ apps/mobile/src/screens/HomeScreen.tsx | 1 - apps/mobile/src/screens/ResultsScreen.tsx | 486 +++++++++--------- 9 files changed, 1317 insertions(+), 253 deletions(-) create mode 100644 apps/mobile/src/components/BreathingGuide.tsx create mode 100644 apps/mobile/src/components/SentimentMeter.tsx create mode 100644 apps/mobile/src/constants/SentimentTheme.ts create mode 100644 apps/mobile/src/context/SentimentThemeProvider.tsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61cbbed..ac59bcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - name: Enable pnpm run: corepack enable @@ -98,7 +97,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '20' - cache: 'npm' - name: Enable pnpm run: corepack enable diff --git a/apps/mobile/App.tsx b/apps/mobile/App.tsx index f3ec980..1d7a615 100644 --- a/apps/mobile/App.tsx +++ b/apps/mobile/App.tsx @@ -1,11 +1,14 @@ import React from 'react'; import { AppProvider } from './src/context/AppContext'; +import { SentimentThemeProvider } from './src/context/SentimentThemeProvider'; import { AppNavigator } from './src/components/AppNavigator'; export default function App() { return ( - + + + ); } diff --git a/apps/mobile/src/components/BreathingGuide.tsx b/apps/mobile/src/components/BreathingGuide.tsx new file mode 100644 index 0000000..7f405d7 --- /dev/null +++ b/apps/mobile/src/components/BreathingGuide.tsx @@ -0,0 +1,420 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { View, Text, StyleSheet, Animated } from 'react-native'; +import { typography } from '../constants/Typography'; +import { spacing, borderRadius } from '../constants/Layout'; +import { Button } from './ui/Button'; +import { useSentimentTheme } from '../context/SentimentThemeProvider'; + +export interface BreathingGuideProps { + sentimentScore?: number; + onComplete?: () => void; + style?: object; +} + +type BreathingPhase = 'inhale' | 'hold' | 'exhale' | 'pause'; + +interface BreathingPattern { + inhale: number; + hold: number; + exhale: number; + pause: number; + cycles: number; + name: string; + description: string; +} + +export function BreathingGuide({ + sentimentScore = 0.5, + onComplete, + style, +}: BreathingGuideProps) { + const { theme } = useSentimentTheme(); + const [isActive, setIsActive] = useState(false); + const [currentPhase, setCurrentPhase] = useState('pause'); + const [currentCycle, setCurrentCycle] = useState(0); + const [timeRemaining, setTimeRemaining] = useState(0); + + // Animations + const circleScale = useRef(new Animated.Value(0.8)).current; + const circleOpacity = useRef(new Animated.Value(0.8)).current; + const backgroundPulse = useRef(new Animated.Value(1)).current; + + // Get breathing pattern based on sentiment + const getBreathingPattern = (sentiment: number): BreathingPattern => { + if (sentiment < 0.4) { + // Stressed/anxious - calming 4-7-8 pattern + return { + inhale: 4, + hold: 5, + exhale: 8, + pause: 2, + cycles: 4, + name: 'Calming Breath', + description: 'Deep breathing to calm your nervous system', + }; + } else if (sentiment < 0.7) { + // Neutral - balanced 4-4-6 pattern + return { + inhale: 4, + hold: 4, + exhale: 6, + pause: 2, + cycles: 5, + name: 'Balanced Breath', + description: 'Steady breathing for mental balance', + }; + } else { + // Positive - energizing 4-4-4 pattern + return { + inhale: 4, + hold: 2, + exhale: 4, + pause: 1, + cycles: 6, + name: 'Energizing Breath', + description: 'Rhythmic breathing to maintain positive energy', + }; + } + }; + + const pattern = getBreathingPattern(sentimentScore); + + const startBreathing = () => { + setIsActive(true); + setCurrentCycle(0); + setPhaseIndex(0); + setCurrentPhase('inhale'); + setTimeRemaining(pattern.inhale); + }; + + const stopBreathing = () => { + setIsActive(false); + setCurrentPhase('pause'); + setCurrentCycle(0); + + // Reset animations + Animated.parallel([ + Animated.timing(circleScale, { + toValue: 0.8, + duration: 500, + useNativeDriver: true, + }), + Animated.timing(circleOpacity, { + toValue: 0.8, + duration: 500, + useNativeDriver: true, + }), + Animated.timing(backgroundPulse, { + toValue: 1, + duration: 500, + useNativeDriver: true, + }), + ]).start(); + }; + + // Phase management + const [phaseIndex, setPhaseIndex] = useState(0); + const phases = React.useMemo( + () => ['inhale', 'hold', 'exhale', 'pause'] as BreathingPhase[], + [] + ); + + // Timer effect for countdown + useEffect(() => { + if (!isActive || timeRemaining <= 0) return; + + const timer = setTimeout(() => { + setTimeRemaining(prev => prev - 1); + }, 1000); + + return () => clearTimeout(timer); + }, [isActive, timeRemaining]); + + // Phase transition effect + useEffect(() => { + if (!isActive) return; + if (timeRemaining > 0) return; + + // Move to next phase + const nextPhaseIndex = (phaseIndex + 1) % phases.length; + + // Check if we completed a full cycle + if (nextPhaseIndex === 0) { + const nextCycle = currentCycle + 1; + setCurrentCycle(nextCycle); + + if (nextCycle >= pattern.cycles) { + setIsActive(false); + setCurrentPhase('pause'); + setPhaseIndex(0); + onComplete?.(); + return; + } + } + + // Update to next phase + setPhaseIndex(nextPhaseIndex); + const nextPhase = phases[nextPhaseIndex]; + setCurrentPhase(nextPhase); + setTimeRemaining(pattern[nextPhase]); + }, [ + isActive, + timeRemaining, + phaseIndex, + currentCycle, + pattern.cycles, + pattern, + phases, + onComplete, + ]); + + // Animation effect + useEffect(() => { + if (!isActive) return; + + const animationDuration = pattern[currentPhase] * 1000; + + switch (currentPhase) { + case 'inhale': + Animated.parallel([ + Animated.timing(circleScale, { + toValue: 1.4, + duration: animationDuration, + useNativeDriver: true, + }), + Animated.timing(circleOpacity, { + toValue: 0.9, + duration: animationDuration, + useNativeDriver: true, + }), + Animated.timing(backgroundPulse, { + toValue: 1.05, + duration: animationDuration, + useNativeDriver: true, + }), + ]).start(); + break; + + case 'hold': + // Maintain current size during hold + break; + + case 'exhale': + Animated.parallel([ + Animated.timing(circleScale, { + toValue: 0.8, + duration: animationDuration, + useNativeDriver: true, + }), + Animated.timing(circleOpacity, { + toValue: 0.8, + duration: animationDuration, + useNativeDriver: true, + }), + Animated.timing(backgroundPulse, { + toValue: 1, + duration: animationDuration, + useNativeDriver: true, + }), + ]).start(); + break; + + case 'pause': + // Brief pause between cycles + break; + } + }, [ + currentPhase, + isActive, + pattern, + circleScale, + circleOpacity, + backgroundPulse, + ]); + + const getPhaseInstruction = (phase: BreathingPhase): string => { + if (!isActive) return 'Ready'; + + switch (phase) { + case 'inhale': + return 'Breathe In'; + case 'hold': + return 'Hold'; + case 'exhale': + return 'Breathe Out'; + case 'pause': + return 'Rest'; + default: + return 'Ready'; + } + }; + + const getPhaseColor = (phase: BreathingPhase): string => { + switch (phase) { + case 'inhale': + return theme.primary || '#4CAF50'; + case 'hold': + return theme.secondary || '#FF9800'; + case 'exhale': + return theme.accent || '#2196F3'; + default: + return (theme.primary || '#4CAF50') + '40'; // Semi-transparent primary for pause + } + }; + + return ( + + + + {pattern.name} + + + {pattern.description} + + + + + {/* Breathing Circle */} + + + {/* Center Content */} + + + {getPhaseInstruction(currentPhase)} + + {isActive && ( + + {timeRemaining}s + + )} + + + + + {/* Progress Info */} + {isActive && ( + + + Cycle {currentCycle + 1} of {pattern.cycles} + + + )} + + + {/* Controls */} + + {!isActive ? ( +