diff --git a/apps/mobile/src/components/CoachingCards.tsx b/apps/mobile/src/components/CoachingCards.tsx
new file mode 100644
index 0000000..11b7dc6
--- /dev/null
+++ b/apps/mobile/src/components/CoachingCards.tsx
@@ -0,0 +1,439 @@
+import React, { useState } from 'react';
+import {
+ View,
+ Text,
+ StyleSheet,
+ TouchableOpacity,
+ Linking,
+ Alert,
+} from 'react-native';
+import { Button } from './ui/Button';
+import { Card } from './ui/Card';
+import { colors } from '../constants/Colors';
+import { typography } from '../constants/Typography';
+import { spacing, borderRadius } from '../constants/Layout';
+import { CheckinResponse } from '../services/api';
+
+// Types for coaching data
+type CoachingData = CheckinResponse['coaching'];
+type ResourceCategory = 'counseling' | 'meditation' | 'emergency';
+
+interface CoachingCardsProps {
+ coaching: CoachingData;
+ sentimentScore?: number;
+}
+
+interface BreathingExerciseCardProps {
+ exercise: CoachingData['breathingExercise'];
+ sentimentScore?: number;
+}
+
+interface StretchExerciseCardProps {
+ exercise: CoachingData['stretchExercise'];
+}
+
+interface ResourcesCardProps {
+ resources: CoachingData['resources'];
+}
+
+// Resource category styling
+const getCategoryStyles = (category: ResourceCategory) => {
+ switch (category) {
+ case 'emergency':
+ return {
+ backgroundColor: colors.error + '15',
+ borderColor: colors.error,
+ iconColor: colors.error,
+ icon: '🚨',
+ };
+ case 'counseling':
+ return {
+ backgroundColor: colors.primary + '15',
+ borderColor: colors.primary,
+ iconColor: colors.primary,
+ icon: '💬',
+ };
+ case 'meditation':
+ return {
+ backgroundColor: colors.success + '15',
+ borderColor: colors.success,
+ iconColor: colors.success,
+ icon: '🧘',
+ };
+ default:
+ return {
+ backgroundColor: colors.surfaceLight,
+ borderColor: colors.border,
+ iconColor: colors.text,
+ icon: '📋',
+ };
+ }
+};
+
+// Breathing Exercise Card with Enhanced Timer
+export function BreathingExerciseCard({
+ exercise,
+ sentimentScore,
+}: BreathingExerciseCardProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+ const [isActive, setIsActive] = useState(false);
+
+ // Get sentiment-specific messaging
+ const getSentimentMessage = (score?: number) => {
+ if (!score) return 'Take a moment to focus on your breathing';
+ if (score < 0.4) return "Let's calm your mind with guided breathing";
+ if (score < 0.7) return 'Center yourself with mindful breathing';
+ return 'Energize your positive mood with rhythmic breathing';
+ };
+
+ const startExercise = () => {
+ setIsActive(true);
+ setIsExpanded(true);
+ };
+
+ return (
+
+ setIsExpanded(!isExpanded)}
+ style={styles.cardHeader}
+ >
+
+ 💨
+
+ {exercise.title}
+
+ {getSentimentMessage(sentimentScore)}
+
+
+
+ {isExpanded ? 'â–¼' : 'â–¶'}
+
+
+ {isExpanded && (
+
+
+ Duration: {Math.floor(exercise.duration / 60)}:
+ {(exercise.duration % 60).toString().padStart(2, '0')} minutes
+
+
+
+ {exercise.instructions.map((instruction, index) => (
+
+ {index + 1}
+ {instruction}
+
+ ))}
+
+
+
+
+ )}
+
+ );
+}
+
+// Stretch Exercise Card
+export function StretchExerciseCard({ exercise }: StretchExerciseCardProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ return (
+
+ setIsExpanded(!isExpanded)}
+ style={styles.cardHeader}
+ >
+
+ 🤸
+
+ {exercise.title}
+
+ Gentle stretches to release tension
+
+
+
+ {isExpanded ? 'â–¼' : 'â–¶'}
+
+
+ {isExpanded && (
+
+
+ {exercise.instructions.map((instruction, index) => (
+
+ {index + 1}
+ {instruction}
+
+ ))}
+
+
+ {exercise.imageUrl && (
+
+ 💡 Visual guide available in the full app
+
+ )}
+
+
+ )}
+
+ );
+}
+
+// Resources Card with Categorized Links
+export function ResourcesCard({ resources }: ResourcesCardProps) {
+ const [isExpanded, setIsExpanded] = useState(false);
+
+ const handleResourcePress = async (url: string, title: string) => {
+ try {
+ const canOpen = await Linking.canOpenURL(url);
+ if (canOpen) {
+ await Linking.openURL(url);
+ } else {
+ Alert.alert(
+ 'Cannot Open Link',
+ `Unable to open ${title}. Please visit: ${url}`,
+ [{ text: 'OK' }]
+ );
+ }
+ } catch (error) {
+ Alert.alert('Error', `Failed to open ${title}. Please visit: ${url}`, [
+ { text: 'OK' },
+ ]);
+ }
+ };
+
+ // Group resources by category
+ const groupedResources = resources.reduce(
+ (acc, resource) => {
+ const category = resource.category || 'other';
+ if (!acc[category]) acc[category] = [];
+ acc[category].push(resource);
+ return acc;
+ },
+ {} as Record
+ );
+
+ // Sort categories by priority
+ const categoryOrder: ResourceCategory[] = [
+ 'emergency',
+ 'counseling',
+ 'meditation',
+ ];
+ const sortedCategories = categoryOrder.filter(cat => groupedResources[cat]);
+
+ return (
+
+ setIsExpanded(!isExpanded)}
+ style={styles.cardHeader}
+ >
+
+ 📞
+
+ Support Resources
+
+ {resources.length} resource{resources.length !== 1 ? 's' : ''}{' '}
+ available
+
+
+
+ {isExpanded ? 'â–¼' : 'â–¶'}
+
+
+ {isExpanded && (
+
+ {sortedCategories.map(category => {
+ const categoryStyles = getCategoryStyles(category);
+ return (
+
+
+ {categoryStyles.icon}{' '}
+ {category.charAt(0).toUpperCase() + category.slice(1)}
+
+ {groupedResources[category].map((resource, index) => (
+
+ handleResourcePress(resource.url, resource.title)
+ }
+ >
+
+ {resource.title}
+
+ {resource.description}
+
+
+ →
+
+ ))}
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+// Main Coaching Cards Component
+export function CoachingCards({
+ coaching,
+ sentimentScore: _sentimentScore,
+}: CoachingCardsProps) {
+ return (
+
+ {/* Resources */}
+ {coaching.resources && coaching.resources.length > 0 && (
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ cardHeader: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ },
+ cardIcon: {
+ fontSize: 24,
+ marginRight: spacing.md,
+ },
+ cardSubtitle: {
+ ...typography.bodySmall,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ cardTitle: {
+ ...typography.h3,
+ color: colors.text,
+ },
+ categorySection: {
+ marginBottom: spacing.lg,
+ },
+ categoryTitle: {
+ ...typography.h4,
+ fontWeight: '600',
+ marginBottom: spacing.md,
+ },
+ container: {
+ gap: spacing.lg,
+ },
+ durationText: {
+ ...typography.bodySmall,
+ color: colors.primary,
+ fontWeight: '600',
+ marginBottom: spacing.md,
+ textAlign: 'center',
+ },
+ exerciseButton: {
+ marginTop: spacing.md,
+ },
+ exerciseCard: {
+ backgroundColor: colors.surfaceLight,
+ },
+ expandIcon: {
+ ...typography.body,
+ color: colors.textSecondary,
+ },
+ expandedContent: {
+ marginTop: spacing.lg,
+ },
+ headerContent: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ },
+ headerText: {
+ flex: 1,
+ },
+ imageNote: {
+ ...typography.bodySmall,
+ color: colors.textSecondary,
+ fontStyle: 'italic',
+ marginTop: spacing.md,
+ textAlign: 'center',
+ },
+ instructionItem: {
+ alignItems: 'flex-start',
+ flexDirection: 'row',
+ marginBottom: spacing.md,
+ },
+ instructionNumber: {
+ ...typography.bodySmall,
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.full,
+ color: colors.white,
+ fontWeight: '600',
+ marginRight: spacing.md,
+ minWidth: 24,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ textAlign: 'center',
+ },
+ instructionText: {
+ ...typography.body,
+ color: colors.text,
+ flex: 1,
+ lineHeight: 22,
+ },
+ instructionsContainer: {
+ marginBottom: spacing.md,
+ },
+ resourceArrow: {
+ ...typography.h3,
+ color: colors.textSecondary,
+ },
+ resourceContent: {
+ flex: 1,
+ },
+ resourceDescription: {
+ ...typography.bodySmall,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ resourceItem: {
+ alignItems: 'center',
+ borderRadius: borderRadius.md,
+ borderWidth: 1,
+ flexDirection: 'row',
+ marginBottom: spacing.sm,
+ padding: spacing.md,
+ },
+ resourceTitle: {
+ ...typography.body,
+ color: colors.text,
+ fontWeight: '600',
+ },
+ resourcesCard: {
+ borderColor: colors.info,
+ borderWidth: 2,
+ },
+});
diff --git a/apps/mobile/src/components/SentimentMeter.tsx b/apps/mobile/src/components/SentimentMeter.tsx
index 4b374fc..7575917 100644
--- a/apps/mobile/src/components/SentimentMeter.tsx
+++ b/apps/mobile/src/components/SentimentMeter.tsx
@@ -201,6 +201,95 @@ export function SentimentMeter({
}
// Compact version for smaller spaces
+// Simple progress bar version for cleaner UI
+export function SimpleSentimentBar({
+ score,
+ confidence,
+ label: _label,
+ customColors,
+ style,
+}: Pick<
+ SentimentMeterProps,
+ 'score' | 'confidence' | 'label' | 'customColors' | 'style'
+>) {
+ const progressAnimation = useRef(new Animated.Value(0)).current;
+
+ useEffect(() => {
+ Animated.timing(progressAnimation, {
+ toValue: score,
+ duration: 1500,
+ useNativeDriver: false,
+ }).start();
+ }, [score, progressAnimation]);
+
+ const getSentimentColor = (sentimentScore: number) => {
+ if (customColors) {
+ if (sentimentScore >= 0.7) return customColors.primary;
+ if (sentimentScore >= 0.4) return customColors.secondary;
+ return colors.info;
+ }
+
+ if (sentimentScore >= 0.7) return colors.success;
+ if (sentimentScore >= 0.4) return colors.warning;
+ return colors.info;
+ };
+
+ const getSentimentLevel = (sentimentScore: number) => {
+ if (sentimentScore >= 0.8) return 'Very Positive';
+ if (sentimentScore >= 0.6) return 'Positive';
+ if (sentimentScore >= 0.4) return 'Neutral';
+ if (sentimentScore >= 0.2) return 'Negative';
+ return 'Very Negative';
+ };
+
+ const sentimentColor = getSentimentColor(score);
+ const sentimentLevel = getSentimentLevel(score);
+
+ const animatedWidth = progressAnimation.interpolate({
+ inputRange: [0, 1],
+ outputRange: ['0%', '100%'],
+ });
+
+ return (
+
+
+
+ {sentimentLevel}
+
+
+ {Math.round(score * 100)}%
+
+
+
+
+
+
+
+
+ Analysis confidence: {Math.round(confidence * 100)}%
+
+
+ );
+}
+
export function CompactSentimentMeter({
score,
confidence,
@@ -265,10 +354,42 @@ const styles = StyleSheet.create({
meterContainer: {
position: 'relative',
},
+ progressBarBackground: {
+ borderRadius: 8,
+ height: 8,
+ overflow: 'hidden',
+ width: '100%',
+ },
+ progressBarFill: {
+ borderRadius: 8,
+ height: '100%',
+ },
scoreText: {
...typography.h2,
fontWeight: 'bold',
},
+ simpleConfidenceText: {
+ ...typography.bodySmall,
+ marginTop: spacing.sm,
+ textAlign: 'center',
+ },
+ simpleContainer: {
+ width: '100%',
+ },
+ simpleHeader: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: spacing.sm,
+ },
+ simpleScoreText: {
+ ...typography.h4,
+ fontWeight: 'bold',
+ },
+ simpleSentimentText: {
+ ...typography.h4,
+ fontWeight: '600',
+ },
svg: {
transform: [{ rotate: '180deg' }], // Start from top
},
diff --git a/apps/mobile/src/components/TranscriptDisplay.tsx b/apps/mobile/src/components/TranscriptDisplay.tsx
index f3d9abe..956a400 100644
--- a/apps/mobile/src/components/TranscriptDisplay.tsx
+++ b/apps/mobile/src/components/TranscriptDisplay.tsx
@@ -1,5 +1,12 @@
import React, { useState, useEffect, useRef } from 'react';
-import { View, Text, StyleSheet, ScrollView } from 'react-native';
+import {
+ View,
+ Text,
+ StyleSheet,
+ ScrollView,
+ TouchableOpacity,
+ Alert,
+} from 'react-native';
import { colors } from '../constants/Colors';
import { typography } from '../constants/Typography';
import { spacing, borderRadius } from '../constants/Layout';
@@ -17,6 +24,7 @@ export interface TranscriptDisplayProps {
typewriterSpeed?: number; // milliseconds per character
showDetailedConfidence?: boolean;
onTypewriterComplete?: () => void;
+ audioUrl?: string; // Optional coaching audio URL
}
export function TranscriptDisplay({
@@ -27,6 +35,7 @@ export function TranscriptDisplay({
typewriterSpeed = 50,
showDetailedConfidence = false,
onTypewriterComplete,
+ audioUrl,
}: TranscriptDisplayProps) {
const [displayedText, setDisplayedText] = useState('');
const [isTyping, setIsTyping] = useState(false);
@@ -136,11 +145,52 @@ export function TranscriptDisplay({
style={styles.detailedConfidence}
/>
)}
+
+ {/* Minimalistic Audio Player */}
+ {audioUrl && (
+
+
+ 🎵
+ Coaching Audio
+
+
+ Alert.alert('Audio Player', 'Audio playback feature coming soon!')
+ }
+ >
+ â–¶
+
+
+ )}
);
}
const styles = StyleSheet.create({
+ audioPlayerContainer: {
+ alignItems: 'center',
+ backgroundColor: colors.surfaceLight,
+ borderRadius: borderRadius.sm,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: spacing.md,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ },
+ audioPlayerIcon: {
+ fontSize: 16,
+ marginRight: spacing.sm,
+ },
+ audioPlayerInfo: {
+ alignItems: 'center',
+ flex: 1,
+ flexDirection: 'row',
+ },
+ audioPlayerText: {
+ ...typography.bodySmall,
+ color: colors.textSecondary,
+ },
container: {
marginBottom: spacing.lg,
maxHeight: 300, // Limit height to prevent taking up entire screen
@@ -165,6 +215,19 @@ const styles = StyleSheet.create({
fontStyle: 'italic',
textAlign: 'center',
},
+ playButton: {
+ alignItems: 'center',
+ backgroundColor: colors.primary,
+ borderRadius: borderRadius.full,
+ height: 32,
+ justifyContent: 'center',
+ width: 32,
+ },
+ playButtonText: {
+ color: colors.surface,
+ fontSize: 14,
+ fontWeight: 'bold',
+ },
streamingBar: {
alignItems: 'center',
backgroundColor: colors.surfaceLight,
diff --git a/apps/mobile/src/context/AppContext.tsx b/apps/mobile/src/context/AppContext.tsx
index 5671f8a..d323231 100644
--- a/apps/mobile/src/context/AppContext.tsx
+++ b/apps/mobile/src/context/AppContext.tsx
@@ -7,6 +7,7 @@ import React, {
ReactNode,
} from 'react';
import { apiService, CheckinResponse } from '../services/api';
+import { enhanceCoachingData } from '../utils/dummyCoachingData';
export interface AppState {
currentScreen: 'home' | 'recording' | 'results';
@@ -215,7 +216,10 @@ function appReducer(state: AppState, action: AppAction): AppState {
sessionId: action.payload.data.sessionId,
transcript: action.payload.data.transcript,
sentiment: action.payload.data.sentiment,
- coaching: action.payload.data.coaching,
+ coaching: enhanceCoachingData(
+ action.payload.data.coaching,
+ action.payload.data.sentiment?.score
+ ),
audioUrl: action.payload.data.audioUrl,
},
processingTime: action.payload.processingTime || null,
@@ -240,7 +244,10 @@ function appReducer(state: AppState, action: AppAction): AppState {
sessionId: action.payload.sessionId,
transcript: action.payload.transcript,
sentiment: action.payload.sentiment,
- coaching: action.payload.coaching,
+ coaching: enhanceCoachingData(
+ action.payload.coaching,
+ action.payload.sentiment?.score
+ ),
audioUrl: action.payload.audioUrl,
},
loading: {
diff --git a/apps/mobile/src/screens/ResultsScreen.tsx b/apps/mobile/src/screens/ResultsScreen.tsx
index 7ff4a8b..10f517d 100644
--- a/apps/mobile/src/screens/ResultsScreen.tsx
+++ b/apps/mobile/src/screens/ResultsScreen.tsx
@@ -1,14 +1,6 @@
import React from 'react';
-import {
- View,
- Text,
- StyleSheet,
- SafeAreaView,
- ScrollView,
- Alert,
-} from 'react-native';
+import { View, Text, StyleSheet, SafeAreaView, ScrollView } from 'react-native';
import { StatusBar } from 'expo-status-bar';
-import * as Linking from 'expo-linking';
import { colors } from '../constants/Colors';
import { typography } from '../constants/Typography';
import { spacing, borderRadius } from '../constants/Layout';
@@ -24,8 +16,8 @@ import {
CoachingSkeleton,
} from '../components/SkeletonLoader';
import { TranscriptDisplay } from '../components/TranscriptDisplay';
-import { SentimentMeter } from '../components/SentimentMeter';
-import { BreathingGuide } from '../components/BreathingGuide';
+import { SimpleSentimentBar } from '../components/SentimentMeter';
+import { CoachingCards } from '../components/CoachingCards';
import { useAppContext } from '../context/AppContext';
export function ResultsScreen() {
@@ -194,7 +186,7 @@ export function ResultsScreen() {
}
// Get data from context
- const { checkinData, recordingData } = state;
+ const { checkinData } = state;
const hasData =
checkinData.sessionId &&
(checkinData.transcript || checkinData.sentiment || checkinData.coaching);
@@ -265,26 +257,6 @@ export function ResultsScreen() {
showsVerticalScrollIndicator={false}
>
- {/* Recording Info */}
-
- Session Summary
-
-
- Duration: {Math.floor(recordingData.duration / 60)}:
- {(recordingData.duration % 60).toString().padStart(2, '0')}
-
- {recordingData.timestamp && (
-
- Date: {new Date(recordingData.timestamp).toLocaleDateString()}
-
- )}
-
-
-
{/* Transcript */}
{checkinData.transcript && (
)}
@@ -304,12 +277,10 @@ export function ResultsScreen() {
>
Mood Analysis
-
- {/* Motivational Message */}
- {checkinData.coaching.motivationalMessage && (
-
-
- Personal Message
-
-
- {checkinData.coaching.motivationalMessage}
-
-
- )}
-
- {/* Emergency Contact */}
-
- Need Support?
-
-
- Campus Help
-
-
- 24/7 mental health support for students
-
-
-
- >
- )}
-
- {/* Breathing Exercise Guide */}
- {checkinData.sentiment && (
-
-
- Breathing Exercise
-
-
- {checkinData.sentiment.score < 0.4
- ? 'Take a moment to calm your mind with guided breathing'
- : checkinData.sentiment.score < 0.7
- ? 'Center yourself with mindful breathing'
- : 'Energize your positive mood with rhythmic breathing'}
-
-
-
- )}
-
- {/* Audio Playback */}
- {checkinData.audioUrl && (
-
- 🎵 Your Coaching Audio
-
- Listen to your personalized coaching message
-
-
+
)}
{/* Actions */}
@@ -476,55 +325,6 @@ const styles = StyleSheet.create({
gap: spacing.md,
marginTop: spacing.lg,
},
- audioButton: {
- alignSelf: 'center',
- },
- audioCard: {
- backgroundColor: colors.surfaceLight,
- marginBottom: spacing.lg,
- },
- audioDescription: {
- ...typography.body,
- color: colors.textSecondary,
- marginBottom: spacing.md,
- textAlign: 'center',
- },
- breathingCard: {
- backgroundColor: colors.surfaceLight,
- marginBottom: spacing.lg,
- },
- breathingDescription: {
- ...typography.body,
- marginBottom: spacing.md,
- textAlign: 'center',
- },
-
- cardTitle: {
- ...typography.h3,
- color: colors.text,
- marginBottom: spacing.md,
- },
- contactButton: {
- alignSelf: 'center',
- marginTop: spacing.md,
- },
- contactCard: {
- marginBottom: spacing.lg,
- },
- contactDescription: {
- ...typography.body,
- marginBottom: spacing.sm,
- textAlign: 'center',
- },
- contactItem: {
- alignItems: 'center',
- },
- contactTitle: {
- ...typography.h4,
- fontWeight: '600',
- marginBottom: spacing.xs,
- textAlign: 'center',
- },
container: {
backgroundColor: colors.background,
flex: 1,
@@ -561,16 +361,6 @@ const styles = StyleSheet.create({
headerSpacer: {
width: spacing.xxl,
},
- infoText: {
- ...typography.body,
- color: colors.textSecondary,
- },
- motivationText: {
- ...typography.body,
- color: colors.text,
- lineHeight: 22,
- textAlign: 'center',
- },
noDataButton: {
minWidth: 200,
},
@@ -589,12 +379,6 @@ const styles = StyleSheet.create({
color: colors.text,
marginBottom: spacing.md,
},
- recordingInfo: {
- gap: spacing.xs,
- },
- recordingInfoCard: {
- marginBottom: spacing.lg,
- },
scrollView: {
flex: 1,
},