diff --git a/app/app/auth/callback.tsx b/app/app/(auth)/callback.tsx similarity index 100% rename from app/app/auth/callback.tsx rename to app/app/(auth)/callback.tsx diff --git a/app/app/(auth)/login.tsx b/app/app/(auth)/login.tsx index bb51359..3fc9f1a 100644 --- a/app/app/(auth)/login.tsx +++ b/app/app/(auth)/login.tsx @@ -24,7 +24,7 @@ import { signUp, signIn, signInWithGoogle } from '@/services/auth.service'; const { width, height } = Dimensions.get('window'); -// ── Design tokens — same palette as the rest of Swasthya AI ────────────────── +// ── Design tokens ────────────────────────────────────────────────────────────── const C = { primary: '#0474FC', primaryDark: '#0355C5', @@ -278,6 +278,7 @@ export default function LoginScreen() { // ── Handlers ───────────────────────────────────────────────────────────────── const handleSignIn = async () => { + if (loading) return; if (!validateSignIn()) return; setLoading(true); try { @@ -304,6 +305,7 @@ export default function LoginScreen() { }; const handleSignUp = async () => { + if (loading) return; if (!validateSignUp()) return; setLoading(true); try { @@ -315,7 +317,6 @@ export default function LoginScreen() { isLoggedIn: true, hasProfile: false, hasFamilyGroup: false, - onboardingComplete: false, }); router.replace('/'); } catch (e: any) { @@ -326,6 +327,7 @@ export default function LoginScreen() { }; const handleGoogle = async () => { + if (loading) return; setLoading(true); try { const result = await signInWithGoogle(); @@ -349,6 +351,21 @@ export default function LoginScreen() { } }; + // ── Skip Handler ──────────────────────────────────────────────────────────── + const handleSkip = () => { + setSessionState({ + userId: 'skip-user-123', + patientId: 'skip-patient-123', + phoneNumber: '+91 9324474812', + isLoggedIn: true, + hasProfile: false, + hasFamilyGroup: false, + isHydrated: true, + hasShownIntro: true, + }); + router.replace('/(onboarding)/family-setup'); + }; + // ── Render ─────────────────────────────────────────────────────────────────── return ( @@ -363,6 +380,18 @@ export default function LoginScreen() { + {/* Skip Button */} + + + Skip → + + + state.setSessionState); - const params = useLocalSearchParams(); - const [code, setCode] = useState(['', '', '', '', '', '']); - const [focusedIndex, setFocusedIndex] = useState(-1); - const [timer, setTimer] = useState(30); - const [isVerifying, setIsVerifying] = useState(false); - const [isResending, setIsResending] = useState(false); - const [displayOTP, setDisplayOTP] = useState(''); // For dev testing - const inputs = useRef<(TextInput | null)[]>([]); - - const phoneNumber = normalizePhone((params.phone as string) || ''); - const formattedPhone = phoneNumber ? `+91 ${phoneNumber}` : '+91 XXXXX XXXXX'; - - // Load and auto-fill OTP on mount - useEffect(() => { - const loadOTP = async () => { - try { - let storedOtp = await getStoredOTP(phoneNumber); - console.log('Loaded OTP for phone', phoneNumber, ':', storedOtp); - - if (!storedOtp) { - storedOtp = '123456'; - } - - setDisplayOTP(storedOtp); // Show for dev testing - const otpDigits = storedOtp.split(''); - setCode(otpDigits); - console.log('Auto-filled OTP:', otpDigits); - } catch (error) { - console.error('Error loading OTP:', error); - } - }; - - if (phoneNumber) { - loadOTP(); - } - }, [phoneNumber]); - - // Timer for OTP expiry - useEffect(() => { - let interval: ReturnType; - if (timer > 0) { - interval = setInterval(() => { - setTimer((prev) => prev - 1); - }, 1000); - } - return () => clearInterval(interval); - }, [timer]); - - // Focus first input on mount - useEffect(() => { - setTimeout(() => { - inputs.current[0]?.focus(); - }, 100); - }, []); - - const handleChange = (text: string, index: number) => { - const newCode = [...code]; - newCode[index] = text; - setCode(newCode); - - if (text !== '' && index < 5) { - inputs.current[index + 1]?.focus(); - } - - if (text !== '' && index === 5 && newCode.every(digit => digit !== '')) { - setTimeout(() => { - handleVerify(); - }, 100); - } - }; - - const handleKeyPress = (e: any, index: number) => { - if (e.nativeEvent.key === 'Backspace' && index > 0 && code[index] === '') { - inputs.current[index - 1]?.focus(); - } - }; - - const handleBack = () => { - router.back(); - }; - - const handleVerify = async () => { - Keyboard.dismiss(); - const otpCode = code.join(''); - - console.log('=== OTP Verification Started ==='); - console.log('Entered OTP:', otpCode); - console.log('Phone Number:', phoneNumber); - console.log('Entered OTP Length:', otpCode.length); - console.log('Raw Code Array:', JSON.stringify(code)); - console.log('EXPO_PUBLIC_SUPABASE_URL:', process.env.EXPO_PUBLIC_SUPABASE_URL); - console.log('EXPO_PUBLIC_SUPABASE_ANON_KEY length:', process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY?.length || 0); - - if (otpCode.length !== 6) { - console.log('Verification cancelled: OTP length is not 6'); - Alert.alert('Error', 'Please enter the complete 6-digit OTP'); - return; - } - - setIsVerifying(true); - - try { - // Get the stored OTP for verification - console.log('Fetching stored OTP for phone:', phoneNumber); - const storedOtp = await getStoredOTP(phoneNumber); - console.log('Stored OTP:', storedOtp); - console.log('Verification - Entered:', otpCode, 'Stored:', storedOtp, 'Match:', otpCode === storedOtp || otpCode === '123456'); - - // Verify the entered OTP matches the stored one or the static 123456 placeholder - const isValid = (otpCode === '123456') || (storedOtp && otpCode === storedOtp); - - if (!isValid) { - console.log('OTP Mismatch!'); - Alert.alert('Verification Failed', 'Invalid OTP. Please try again.'); - setCode(['', '', '', '', '', '']); - inputs.current[0]?.focus(); - setIsVerifying(false); - return; - } - - console.log('OTP Verified Successfully!'); - - // Clear the stored OTP after successful verification - await clearStoredOTP(); - - // Check if patient already exists - const existingPatient = await getPatientByPhone(phoneNumber); - console.log('Patient found:', existingPatient?.id); - - const { saveUserSession, createPhoneAuthUser } = await import('@/services/auth.service'); - const AsyncStorage = (await import('@react-native-async-storage/async-storage')).default; - - if (existingPatient) { - // Save user session for persistence - await saveUserSession(phoneNumber, existingPatient.id); - await AsyncStorage.setItem(`user_profile_${existingPatient.id}`, JSON.stringify(existingPatient)); - await AsyncStorage.setItem(`user_profile_${phoneNumber}`, JSON.stringify(existingPatient)); - - // Existing user - update auth state and route - setSessionState({ - userId: existingPatient.id, - patientId: existingPatient.id, - phoneNumber, - isLoggedIn: true, - hasProfile: Boolean(existingPatient.age && existingPatient.gender), - hasFamilyGroup: Boolean(existingPatient.family_id), - }); - - console.log('Existing user logged in:', existingPatient.id); - - if (existingPatient.family_id) { - console.log('Routing to home (has family)'); - router.replace('/(tabs)/home'); - } else if (existingPatient.age && existingPatient.gender) { - console.log('Routing to family setup (has profile)'); - router.replace('/(onboarding)/family-setup'); - } else { - console.log('Routing to user details (incomplete profile)'); - router.replace({ - pathname: '/(onboarding)/user-details', - params: { phone: phoneNumber }, - }); - } - } else { - // New user - create temporary auth user and save session - console.log('New user detected, creating phone auth user'); - const nameParam = (params.name as string) || ''; - const newUser = await createPhoneAuthUser(phoneNumber, nameParam); - const userId = newUser?.id || `user_${phoneNumber}`; - - await saveUserSession(phoneNumber, userId); - if (newUser) { - await AsyncStorage.setItem(`user_profile_${userId}`, JSON.stringify(newUser)); - await AsyncStorage.setItem(`user_profile_${phoneNumber}`, JSON.stringify(newUser)); - } - - setSessionState({ - userId: userId, - patientId: userId, - phoneNumber, - isLoggedIn: true, - hasProfile: false, - hasFamilyGroup: false, - }); - - router.replace({ - pathname: '/(onboarding)/user-details', - params: { phone: phoneNumber }, - }); - } - } catch (error: any) { - console.error('Error verifying OTP. Full error details:', error); - if (error && typeof error === 'object') { - console.error('Error message:', error?.message); - console.error('Error name:', error?.name); - console.error('Error stack:', error?.stack); - try { - console.error('Error JSON:', JSON.stringify(error)); - } catch {} - } - Alert.alert('Verification Failed', error?.message || String(error) || 'Failed to verify OTP. Please try again.'); - - // Clear OTP on failure - setCode(['', '', '', '', '', '']); - inputs.current[0]?.focus(); - } finally { - setIsVerifying(false); - } - }; - - const handleResendOTP = async () => { - setIsResending(true); - console.log('=== Resending OTP ==='); - - try { - // Generate new random OTP - const { generateRandomOTP, storeOTPLocally } = await import('@/services/auth.service'); - const newOtp = generateRandomOTP(); - console.log('Generated new OTP:', newOtp); - - await storeOTPLocally(phoneNumber, newOtp); - console.log('New OTP stored'); - - // Update display - setDisplayOTP(newOtp); - - // Reset timer and form - setTimer(30); - setCode(['', '', '', '', '', '']); - inputs.current[0]?.focus(); - - Alert.alert('OTP Resent', `A new verification code has been sent to ${formattedPhone}`); - setIsResending(false); - } catch (error: any) { - console.error('Error resending OTP:', error); - Alert.alert('Error', 'Failed to resend OTP. Please try again.'); - setIsResending(false); - } - }; - - return ( - - - - {/* Back Button */} - - - - - - - {/* Header Section */} - - Verify your number - - We sent a verification code to - - {formattedPhone} - - - {/* Dev OTP Display */} - {displayOTP && ( - - OTP Code (auto-filled below): - {displayOTP} - - )} - - {/* OTP Inputs */} - - {code.map((digit, index) => ( - - - - { inputs.current[index] = ref; }} - style={styles.otpBox} - keyboardType="number-pad" - maxLength={1} - value={digit} - onChangeText={(text) => handleChange(text, index)} - onKeyPress={(e) => handleKeyPress(e, index)} - onFocus={() => setFocusedIndex(index)} - onBlur={() => setFocusedIndex(-1)} - selectTextOnFocus - /> - - - - ))} - - - {/* Resend Section */} - - {timer > 0 ? ( - Resend code in {timer}s - ) : ( - - {isResending ? ( - - ) : ( - Resend OTP - )} - - )} - - - - - {/* Verify Button */} - - - {isVerifying ? ( - - ) : ( - <> - Verify - - - )} - - - - - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#F8FAFC', - }, - keyboardView: { - flex: 1, - }, - content: { - flex: 1, - paddingHorizontal: 24, - paddingTop: 50, - paddingBottom: 50, - }, - backButton: { - marginBottom: 24, - width: 40, - height: 40, - }, - backButtonBg: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#FFFFFF', - justifyContent: 'center', - alignItems: 'center', - ...Platform.select({ - ios: { - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - }, - android: { - elevation: 3, - }, - }), - }, - headerSection: { - marginBottom: 48, - }, - title: { - fontFamily: TYPOGRAPHY.fonts.bold, - fontSize: 28, - fontWeight: '700', - color: '#1E3A8A', - marginBottom: 8, - }, - subtitle: { - fontFamily: TYPOGRAPHY.fonts.regular, - fontSize: 14, - color: '#64748B', - marginBottom: 4, - }, - phoneNumber: { - fontFamily: TYPOGRAPHY.fonts.semibold, - fontSize: 18, - fontWeight: '600', - color: '#1E293B', - marginTop: 4, - }, - otpContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 32, - gap: 10, - }, - otpBoxWrapper: { - flex: 1, - aspectRatio: 1, - maxWidth: 55, - }, - otpGradientBorder: { - flex: 1, - padding: 2, - borderRadius: 12, - }, - otpInnerBox: { - flex: 1, - backgroundColor: '#FFFFFF', - borderRadius: 10, - justifyContent: 'center', - alignItems: 'center', - }, - otpBox: { - width: '100%', - height: '100%', - textAlign: 'center', - textAlignVertical: 'center', - fontSize: 22, - fontWeight: '700', - fontFamily: TYPOGRAPHY.fonts.bold, - color: '#1E293B', - padding: 0, - margin: 0, - }, - resendContainer: { - alignItems: 'center', - marginTop: 8, - }, - timerText: { - fontSize: 14, - fontFamily: TYPOGRAPHY.fonts.regular, - color: '#64748B', - }, - resendText: { - fontSize: 14, - fontFamily: TYPOGRAPHY.fonts.semibold, - fontWeight: '600', - color: '#2563EB', - }, - verifyButton: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - gap: 12, - height: 56, - borderRadius: 14, - ...Platform.select({ - ios: { - shadowColor: '#2563EB', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.25, - shadowRadius: 8, - }, - android: { - elevation: 5, - }, - }), - }, - verifyButtonText: { - color: '#FFFFFF', - fontSize: 16, - fontFamily: TYPOGRAPHY.fonts.semibold, - fontWeight: '600', - }, - otpDisplayBox: { - backgroundColor: '#FEF3C7', - borderWidth: 1, - borderColor: '#FBBF24', - borderRadius: 12, - padding: 12, - marginBottom: 16, - alignItems: 'center', - }, - otpDisplayLabel: { - fontSize: 12, - fontFamily: TYPOGRAPHY.fonts.regular, - color: '#92400E', - marginBottom: 6, - }, - otpDisplayValue: { - fontSize: 24, - fontFamily: TYPOGRAPHY.fonts.bold, - fontWeight: '700', - color: '#D97706', - letterSpacing: 4, - }, -}); diff --git a/app/app/(onboarding)/_layout.tsx b/app/app/(onboarding)/_layout.tsx index 7ef90d6..39aca27 100644 --- a/app/app/(onboarding)/_layout.tsx +++ b/app/app/(onboarding)/_layout.tsx @@ -9,10 +9,9 @@ export default function OnboardingLayout() { contentStyle: { backgroundColor: '#F9FAFB' }, }} > - - - + + ); } \ No newline at end of file diff --git a/app/app/(onboarding)/agent-log.tsx b/app/app/(onboarding)/agent-log.tsx deleted file mode 100644 index 96a668b..0000000 --- a/app/app/(onboarding)/agent-log.tsx +++ /dev/null @@ -1,696 +0,0 @@ -// app/(onboarding)/agent-log.tsx -import React, { useEffect, useRef, useState } from 'react'; -import { - Animated, - SafeAreaView, - ScrollView, - StatusBar, - StyleSheet, - Text, - TouchableOpacity, - View, - Platform, - UIManager, -} from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { router, useLocalSearchParams } from 'expo-router'; -import Svg, { Circle } from 'react-native-svg'; -import { backendService } from '@/services/backend.service'; -import { useAuthStore } from '@/store/auth.store'; - -// Enable LayoutAnimation for Android -if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); -} - -// ── Types ────────────────────────────────────────────────────────────────────── -type StepStatus = 'waiting' | 'running' | 'done' | 'error'; - -interface AgentStep { - id: string; - title: string; - subtitle: string; - icon: string; - color: string; - logLines: string[]; - durationMs: number; -} - -// ── Pipeline definition ──────────────────────────────────────────────────────── -const PIPELINE: AgentStep[] = [ - { - id: 'health_summary', - title: 'Health Summarization', - subtitle: 'Sarvam Chat Agent - Summarizing overall health', - icon: 'chatbubble-ellipses-outline', - color: '#0474FC', - durationMs: 2000, - logLines: [], - }, - { - id: 'medical_scan', - title: 'Medical Report Scanner', - subtitle: 'Medical Scan Agent - Scanning past medical issues', - icon: 'document-text-outline', - color: '#3B82F6', - durationMs: 2500, - logLines: [], - }, - { - id: 'smartwatch', - title: 'Wearable Data Tracker', - subtitle: 'Smartwatch Risk Agent - Tracking smartwatch data', - icon: 'watch-outline', - color: '#10B981', - durationMs: 2500, - logLines: [], - }, - { - id: 'family_history', - title: 'Family Genetics Assessor', - subtitle: 'Family Genetics Agent - Tracking family similarity issues', - icon: 'people-outline', - color: '#8B5CF6', - durationMs: 2200, - logLines: [], - }, - { - id: 'risk_score', - title: 'Clinical Risk Evaluator', - subtitle: 'Risk Scoring Agent - Generating a risk score', - icon: 'speedometer-outline', - color: '#F59E0B', - durationMs: 3000, - logLines: [], - }, - { - id: 'final_summary', - title: 'Clinical Report Finalizer', - subtitle: 'Doctor Q&A Agent - Finalizing summary', - icon: 'checkmark-done-circle-outline', - color: '#EF4444', - durationMs: 2000, - logLines: [], - }, -]; - -// ── Log Line Component ───────────────────────────────────────────────────────── -const LogLine = ({ line, delay }: { line: string; delay: number }) => { - const opacity = useRef(new Animated.Value(0)).current; - const translateY = useRef(new Animated.Value(6)).current; - - useEffect(() => { - const timer = setTimeout(() => { - Animated.parallel([ - Animated.timing(opacity, { toValue: 1, duration: 280, useNativeDriver: true }), - Animated.timing(translateY, { toValue: 0, duration: 280, useNativeDriver: true }), - ]).start(); - }, delay); - return () => clearTimeout(timer); - }, []); - - const isSuccess = line.startsWith('✓'); - const isArrow = line.startsWith('→'); - - return ( - - - {line} - - - ); -}; - -// ── Step Card Component ──────────────────────────────────────────────────────── -const StepCard = ({ - step, - status, - visibleLogLines, -}: { - step: AgentStep; - status: StepStatus; - visibleLogLines: number; -}) => { - const pulseAnim = useRef(new Animated.Value(1)).current; - const slideIn = useRef(new Animated.Value(40)).current; - const fadeIn = useRef(new Animated.Value(0)).current; - - useEffect(() => { - if (status !== 'waiting') { - Animated.parallel([ - Animated.timing(slideIn, { toValue: 0, duration: 350, useNativeDriver: true }), - Animated.timing(fadeIn, { toValue: 1, duration: 350, useNativeDriver: true }), - ]).start(); - } - }, [status]); - - useEffect(() => { - if (status === 'running') { - Animated.loop( - Animated.sequence([ - Animated.timing(pulseAnim, { toValue: 1.15, duration: 500, useNativeDriver: true }), - Animated.timing(pulseAnim, { toValue: 1, duration: 500, useNativeDriver: true }), - ]) - ).start(); - } else { - pulseAnim.stopAnimation(); - pulseAnim.setValue(1); - } - }, [status]); - - if (status === 'waiting') return null; - - const borderColor = status === 'done' ? step.color + '40' : step.color; - - return ( - - {/* Header */} - - - - - - {step.title} - {step.subtitle} - - - {status === 'running' ? ( - Running - ) : ( - ✓ Done - )} - - - - {/* Logs */} - - {step.logLines.slice(0, visibleLogLines).map((line, idx) => ( - - ))} - {status === 'running' && visibleLogLines < step.logLines.length && ( - - )} - - - ); -}; - -// ── Circular Progress Component ──────────────────────────────────────────────── -const AnimatedCircle = Animated.createAnimatedComponent(Circle); - -const CircularProgress = ({ score, size = 120, strokeWidth = 10 }: { score: number; size?: number; strokeWidth?: number }) => { - const animatedValue = useRef(new Animated.Value(0)).current; - const radius = (size - strokeWidth) / 2; - const circumference = radius * 2 * Math.PI; - - useEffect(() => { - Animated.timing(animatedValue, { - toValue: score, - duration: 1500, - useNativeDriver: false, - }).start(); - }, [score]); - - const strokeDashoffsetTarget = animatedValue.interpolate({ - inputRange: [0, 100], - outputRange: [circumference, circumference - circumference * (score / 100)], - }); - - const getRiskColor = (s: number) => { - if (s < 40) return '#10B981'; - if (s < 70) return '#F97316'; - return '#EF4444'; - }; - - const progressColor = getRiskColor(score); - - return ( - - - {/* Background Circle */} - - {/* Animated Circle */} - - - - {score} - - {score < 40 ? 'Low Risk' : score < 70 ? 'Moderate' : 'High Risk'} - - - - ); -}; - -// ── Main Screen ──────────────────────────────────────────────────────────────── -export default function AgentLogScreen() { - const [stepStatuses, setStepStatuses] = useState( - PIPELINE.map(() => 'waiting') - ); - const [visibleLogLines, setVisibleLogLines] = useState( - PIPELINE.map(() => 0) - ); - const [allDone, setAllDone] = useState(false); - const headerAnim = useRef(new Animated.Value(0)).current; - - const { user, patientId: storePatientId } = useAuthStore(); - const patientId = user?.id || storePatientId || 'demo-patient'; - - useEffect(() => { - Animated.timing(headerAnim, { toValue: 1, duration: 600, useNativeDriver: true }).start(); - runPipeline(); - }, []); - - const runPipeline = async () => { - // ── STEP 1: Health Summarization ────────────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[0] = 'running'; return n; }); - const logs0 = [ - '→ Fetching session messages from Supabase...', - '→ Translating patient speech/text input into clinical English...', - '→ Running entity extractor for symptoms (severity/duration)...' - ]; - for (let l = 1; l <= logs0.length; l++) { - PIPELINE[0].logLines = logs0.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[0] = l; return n; }); - await delay(500); - } - const finalLogs0 = [ - ...logs0, - '✓ Extracted clinical entities successfully.', - '✓ daily_summary: Compiled overall health summary notes.', - '✓ Symptom logged: Headache (severity 6/10, onset 3 days ago).' - ]; - PIPELINE[0].logLines = finalLogs0; - setVisibleLogLines(prev => { const n = [...prev]; n[0] = finalLogs0.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[0] = 'done'; return n; }); - await delay(400); - - // ── STEP 2: Medical Scan Agent ─────────────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[1] = 'running'; return n; }); - const logs1 = [ - '→ Accessing historical patient clinical documents...', - '→ Running OCR/structure engine on lipid profile report...', - '→ Inspecting past glucose HbA1c values...' - ]; - for (let l = 1; l <= logs1.length; l++) { - PIPELINE[1].logLines = logs1.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[1] = l; return n; }); - await delay(500); - } - const finalLogs1 = [ - ...logs1, - '✓ Extracted lab values (Fasting glucose: 110 mg/dL).', - '✓ Past history scanned: Hypertension, Pre-diabetes.' - ]; - PIPELINE[1].logLines = finalLogs1; - setVisibleLogLines(prev => { const n = [...prev]; n[1] = finalLogs1.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[1] = 'done'; return n; }); - await delay(400); - - // ── STEP 3: Smartwatch Data Tracker ────────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[2] = 'running'; return n; }); - const logs2 = [ - '→ Querying sensor stream data (PPG and accelerometer)...', - '→ Scanning heart rate samples around reported symptom times...', - '→ Inspecting daily sleep cycles and step levels...' - ]; - for (let l = 1; l <= logs2.length; l++) { - PIPELINE[2].logLines = logs2.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[2] = l; return n; }); - await delay(500); - } - const finalLogs2 = [ - ...logs2, - '✓ Correlated symptom: Peak heart rate of 105 bpm during headache.', - '✓ HRV is healthy (45ms). No critical arrhythmia or AFib.' - ]; - PIPELINE[2].logLines = finalLogs2; - setVisibleLogLines(prev => { const n = [...prev]; n[2] = finalLogs2.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[2] = 'done'; return n; }); - await delay(400); - - // ── STEP 4: Family Genetics Assessor ───────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[3] = 'running'; return n; }); - const logs3 = [ - '→ Pulling family history profile records...', - '→ Evaluating genetics risk factor mapping...', - '→ Cross-referencing maternal cardiovascular/diabetic history...' - ]; - for (let l = 1; l <= logs3.length; l++) { - PIPELINE[3].logLines = logs3.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[3] = l; return n; }); - await delay(500); - } - const finalLogs3 = [ - ...logs3, - '✓ Mapped inherited risk for diabetes (maternal side).', - '✓ Hereditary score adjustment calculated successfully.' - ]; - PIPELINE[3].logLines = finalLogs3; - setVisibleLogLines(prev => { const n = [...prev]; n[3] = finalLogs3.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[3] = 'done'; return n; }); - await delay(400); - - // ── STEP 5: Clinical Risk Evaluator ────────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[4] = 'running'; return n; }); - const logs4 = [ - '→ Fetching clinical guidelines (AHA/ACC Hypertension 2017)...', - '→ Adjusting calculations for age, vitals, and genetics...', - '→ Adjusting score for missed Metformin compliance flag (+3)...' - ]; - for (let l = 1; l <= logs4.length; l++) { - PIPELINE[4].logLines = logs4.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[4] = l; return n; }); - await delay(600); - } - const finalLogs4 = [ - ...logs4, - '✓ Base score calculated: 65.', - '✓ Final adjusted score: 68.', - '✓ Risk category: Moderate Risk (BP & missed dose correction).' - ]; - PIPELINE[4].logLines = finalLogs4; - setVisibleLogLines(prev => { const n = [...prev]; n[4] = finalLogs4.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[4] = 'done'; return n; }); - await delay(400); - - // ── STEP 6: Clinical Report Finalizer ──────────────────────────────────────────── - setStepStatuses(prev => { const n = [...prev]; n[5] = 'running'; return n; }); - const logs5 = [ - '→ Writing finalized daily summaries into Supabase tables...', - '→ Formatting analysis logs for physician report dashboard...', - '→ Creating tailored patient clinical insights...' - ]; - for (let l = 1; l <= logs5.length; l++) { - PIPELINE[5].logLines = logs5.slice(0, l); - setVisibleLogLines(prev => { const n = [...prev]; n[5] = l; return n; }); - await delay(500); - } - const finalLogs5 = [ - ...logs5, - '✓ Summarized daily chat, smartwatch, history, and scans.', - '✓ Pipeline completed: All agent systems processed.', - '✓ Generating risk scoreboard.' - ]; - PIPELINE[5].logLines = finalLogs5; - setVisibleLogLines(prev => { const n = [...prev]; n[5] = finalLogs5.length; return n; }); - setStepStatuses(prev => { const n = [...prev]; n[5] = 'done'; return n; }); - - // Mark completion - setAllDone(true); - }; - - const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); - - const completedCount = stepStatuses.filter(s => s === 'done').length; - const progressPct = (completedCount / PIPELINE.length) * 100; - - return ( - - - - {/* Top bar */} - - router.back()} style={styles.backBtn}> - - - - Agent Pipeline - - {allDone ? 'All agents complete' : `Running agent ${completedCount + 1} of ${PIPELINE.length}…`} - - - - {completedCount}/{PIPELINE.length} - - - - {/* Progress bar */} - - - - - - {/* Intro card */} - - - - - Multi-Agent AI Pipeline - - 6 specialised clinical agents processing your session - - - - - - {/* Step Cards */} - {PIPELINE.map((step, i) => ( - - ))} - - {/* Done state */} - {allDone && ( - - 🎉 - Pipeline Complete - - All diagnostic analyses have finished successfully. - - - {/* Circular Risk Score Progress */} - - - - - {/* AI Insights box */} - - - - AI Clinical Insights - - - • Symptom Severity: Reported headaches rating 6/10. Vitals show moderate heart rate increase (105 bpm) matching symptoms. - - - • Adherence: Warning issued for missed morning Metformin dose. Compliance reminder scheduled. - - - • Wearable Status: Correlated smartwatch logs confirm resting blood pressure remains elevated at 135/88. - - - • Advice: Ensure hydration, follow evening Amlodipine schedule, and consult doctor for missed Metformin advice. - - - - router.replace('/(tabs)/home')} - > - - - Back to Dashboard - - - - )} - - - - - ); -} - -// ── Styles ───────────────────────────────────────────────────────────────────── -const styles = StyleSheet.create({ - safeArea: { flex: 1, backgroundColor: '#121212' }, - topBar: { - flexDirection: 'row', - alignItems: 'center', - paddingHorizontal: 16, - paddingTop: Platform.OS === 'ios' ? 12 : 40, - paddingBottom: 14, - backgroundColor: '#171717', - borderBottomWidth: 1, - borderBottomColor: '#2D2D2D', - }, - backBtn: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: '#2A2A2A', - alignItems: 'center', - justifyContent: 'center', - }, - topTitle: { fontSize: 16, fontWeight: '700', color: '#FFFFFF' }, - topSub: { fontSize: 12, color: '#9CA3AF', marginTop: 1 }, - counterBadge: { - backgroundColor: '#2A2A2A', - paddingHorizontal: 10, - paddingVertical: 5, - borderRadius: 12, - }, - counterText: { fontSize: 13, fontWeight: '700', color: '#0474FC' }, - progressOuter: { height: 3, backgroundColor: '#2D2D2D' }, - progressInner: { height: 3, backgroundColor: '#0474FC' }, - scroll: { padding: 16, paddingTop: 20 }, - - introCard: { marginBottom: 20, borderRadius: 16, overflow: 'hidden' }, - introGradient: { - flexDirection: 'row', - alignItems: 'center', - padding: 18, - }, - introTitle: { fontSize: 16, fontWeight: '700', color: '#FFFFFF' }, - introSub: { fontSize: 12, color: 'rgba(255,255,255,0.8)', marginTop: 3 }, - - stepCard: { - backgroundColor: '#1E1E1E', - borderRadius: 14, - padding: 16, - marginBottom: 12, - borderLeftWidth: 3, - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 3.84, - elevation: 5, - }, - stepHeader: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, - stepIconBg: { - width: 40, - height: 40, - borderRadius: 10, - alignItems: 'center', - justifyContent: 'center', - }, - stepTitle: { fontSize: 14, fontWeight: '700', color: '#FFFFFF' }, - stepSubtitle: { fontSize: 11, color: '#9CA3AF', marginTop: 2 }, - statusBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 8, - }, - statusBadgeRunning: { - backgroundColor: 'rgba(245, 158, 11, 0.15)', - }, - statusBadgeDone: { - backgroundColor: 'rgba(52, 211, 153, 0.15)', - }, - statusText: { fontSize: 11, fontWeight: '600' }, - - logContainer: { - backgroundColor: '#0F172A', - borderRadius: 8, - padding: 10, - minHeight: 40, - }, - logLine: { fontSize: 11, color: '#94A3B8', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', lineHeight: 18 }, - logLineSuccess: { color: '#34D399' }, - logLineArrow: { color: '#60A5FA' }, - cursor: { color: '#60A5FA', fontSize: 14 }, - - doneCard: { - backgroundColor: '#1E1E1E', - borderRadius: 20, - padding: 24, - alignItems: 'center', - marginTop: 8, - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 4.65, - elevation: 8, - }, - doneEmoji: { fontSize: 40, marginBottom: 12 }, - doneTitle: { fontSize: 20, fontWeight: '700', color: '#FFFFFF', marginBottom: 8 }, - doneSub: { fontSize: 14, color: '#9CA3AF', textAlign: 'center', lineHeight: 20, marginBottom: 20 }, - progressCardSection: { - marginVertical: 20, - alignItems: 'center', - justifyContent: 'center', - }, - insightsContainer: { - width: '100%', - backgroundColor: '#171717', - borderRadius: 12, - padding: 16, - marginBottom: 24, - borderWidth: 1, - borderColor: '#2D2D2D', - }, - insightsHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: 8, - marginBottom: 12, - }, - insightsTitle: { - color: '#0474FC', - fontSize: 14, - fontWeight: '700', - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - insightsText: { - color: '#ECECF1', - fontSize: 13, - lineHeight: 18, - marginBottom: 8, - }, - doneBtn: { width: '100%', borderRadius: 14, overflow: 'hidden' }, - doneBtnGradient: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - paddingVertical: 14, - gap: 10, - }, - doneBtnText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' }, -}); diff --git a/app/app/(onboarding)/chat.tsx b/app/app/(onboarding)/chat.tsx index fe77e57..e8c79e6 100644 --- a/app/app/(onboarding)/chat.tsx +++ b/app/app/(onboarding)/chat.tsx @@ -11,29 +11,17 @@ import { FlatList, KeyboardAvoidingView, Platform, - NativeModules, ActivityIndicator, Animated, Dimensions, - UIManager, - LayoutAnimation, - ScrollView, Keyboard, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { router } from 'expo-router'; import { useAuthStore } from '@/store/auth.store'; -import * as Speech from 'expo-speech'; -import { supabase } from '@/services/supabaseClient'; -import { backendService } from '@/services/backend.service'; -import { BACKEND_URL, API_ENDPOINTS } from '@/config/api'; -import Voice from '@react-native-voice/voice'; import { LinearGradient } from 'expo-linear-gradient'; - -// Enable LayoutAnimation for Android -if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { - UIManager.setLayoutAnimationEnabledExperimental(true); -} +import { COLORS, SPACING, TYPOGRAPHY } from '@/theme'; +import { getPatientById } from '@/services/auth.service'; interface Message { id: string; @@ -42,1579 +30,534 @@ interface Message { timestamp: Date; } -interface AgentThought { - name: string; - role: string; - thought: string; +interface QuestionStep { + key: string; + question: string; + placeholder: string; + chips: string[]; + fieldParser: (input: string) => Partial; } -interface HistoryItem { - id: string; - date: string; - time: string; - shortDate: string; - overallSummary: string; - agents: AgentThought[]; +interface ProfileData { + full_name: string; + age: number; + gender: string; + blood_group: string; + height: string; + weight: string; + allergies: string; + current_medication: string; + chronic_diseases: string; + family_history: string; + smoking: string; + alcohol: string; + emergency_contact: string; + surgeries: string; + vaccinations: string; } -const MOCK_HISTORY: HistoryItem[] = [ - { - id: '1', - date: 'June 15, 2026', - time: '10:30 PM', - shortDate: '15 Jun', - overallSummary: 'Reported headache (severity 6/10) and missed morning Metformin dose. Heart rate spiked to 105 bpm during symptoms.', - agents: [ - { name: 'Sarvam Chat Agent', role: 'Conversational Front-end', thought: 'Handled user input in Hindi. Translated symptoms, validated severity, and escalated warnings on heart rate.' }, - { name: 'Check-in Agent', role: 'Daily Symptoms & Adherence', thought: 'Detected missing Metformin dose. Educated patient on diabetes adherence.' }, - { name: 'Smartwatch Risk Agent', role: 'Vitals & PPG Analysis', thought: 'Correlated headache onset with 105 bpm heart rate spike. No signs of critical arrhythmia.' }, - { name: 'Escalation Agent', role: 'Emergency & Triage', thought: 'Checked severity. Since pain was 6/10 and heart rate resolved, flagged for doctor review rather than emergency ER trigger.' }, - { name: 'Medicine Reminder Agent', role: 'Prescription Tracking', thought: 'Scheduled push notifications for evening Amlodipine. Verified patient confirmed ingestion at 8:00 PM.' } - ] - }, - { - id: '2', - date: 'June 14, 2026', - time: '09:15 PM', - shortDate: '14 Jun', - overallSummary: 'Blood pressure was elevated at 135/88 mmHg. Complained of mild chest tightness which resolved after resting.', - agents: [ - { name: 'Sarvam Chat Agent', role: 'Conversational Front-end', thought: 'Processed query about chest tightness. Prompted patient to rest and check BP.' }, - { name: 'Smartwatch Risk Agent', role: 'Vitals & PPG Analysis', thought: 'PPG sensor showed elevated peripheral resistance. Heart rate stable at 72 bpm.' }, - { name: 'Escalation Agent', role: 'Emergency & Triage', thought: 'Evaluated chest tightness. Advised emergency call if pain radiates or increases. Patient confirmed resolution after 5 min rest.' }, - { name: 'Doctor Q&A Agent', role: 'Clinical Liaison', thought: 'Synthesized symptoms into a query for Dr. Sharma regarding Amlodipine dosage adjustment.' } - ] - }, - { - id: '3', - date: 'June 13, 2026', - time: '08:45 PM', - shortDate: '13 Jun', - overallSummary: 'A normal health day. Vitals within target ranges. Fasting glucose at 110 mg/dL. Walked 8,500 steps.', - agents: [ - { name: 'Check-in Agent', role: 'Daily Symptoms & Adherence', thought: 'Logged fasting blood glucose of 110 mg/dL. Encouraged patient for keeping it under 125 mg/dL.' }, - { name: 'Medicine Reminder Agent', role: 'Prescription Tracking', thought: 'All daily medications (Metformin, Amlodipine, Vitamin D3) marked as taken on time.' }, - { name: 'Smartwatch Risk Agent', role: 'Vitals & PPG Analysis', thought: 'Steps: 8,500. Sleep: 7.5 hours. Heart rate variability (HRV) is healthy at 45ms.' } - ] - } -]; +const { width } = Dimensions.get('window'); -const VOICE_LOCALES = { - 'hi-IN': { - greeting: "नमस्ते! मैं आपका स्वास्थ्य वॉइस असिस्टेंट हूँ। आज आप कैसा महसूस कर रहे हैं?", - listening: "सुन रहा हूँ... बोलिए!", - thinking: "सोच रहा हूँ...", - speaking: "बोल रहा हूँ...", - unheard: "मैंने कुछ नहीं सुना। कृपया फिर से बोलें...", - instruction: "बोलना बंद करें या सबमिट करने के लिए केंद्र पर टैप करें", - statusListening: "सुन रहा हूँ...", - }, - 'en-US': { - greeting: "Hello! I am your health voice assistant. How are you feeling today?", - listening: "Listening... Speak now!", - thinking: "Thinking...", - speaking: "Speaking...", - unheard: "I didn't hear anything. Please try speaking again...", - instruction: "Tap the core to stop speaking and submit", - statusListening: "Listening...", - } -}; +export default function OnboardingChatScreen() { + const { patientId, setSessionState } = useAuthStore(); + const flatListRef = useRef(null); + const [inputText, setInputText] = useState(''); + const [currentStep, setCurrentStep] = useState(0); + const [isTyping, setIsTyping] = useState(false); + const [profileData, setProfileData] = useState>({ + full_name: 'Indresh', + age: 20, + gender: 'Male', + blood_group: 'O+', + height: '175cm', + weight: '72kg', + allergies: 'None', + current_medication: 'None', + chronic_diseases: 'None', + family_history: 'None', + smoking: 'Non-smoker', + alcohol: 'Never', + emergency_contact: 'None', + surgeries: 'None', + vaccinations: 'Up to date', + }); + + const steps: QuestionStep[] = [ + { + key: 'age_gender', + question: "Hello! Welcome to Swasthya AI. 👋 Let's configure your health profile. To start, what is your age and gender?", + placeholder: "e.g., 25 years, Female", + chips: ['24, Male', '28, Female', '30, Other', 'Skip this'], + fieldParser: (input) => { + const ageMatch = input.match(/\d+/); + const age = ageMatch ? parseInt(ageMatch[0], 10) : 24; + let gender = 'Male'; + if (input.toLowerCase().includes('female')) gender = 'Female'; + else if (input.toLowerCase().includes('other')) gender = 'Other'; + return { age, gender }; + }, + }, + { + key: 'height_weight', + question: "Great! Next, could you tell me your height (e.g., in cm) and weight (e.g., in kg)?", + placeholder: "e.g., 170 cm, 65 kg", + chips: ['170 cm, 60 kg', '175 cm, 70 kg', '180 cm, 80 kg', 'Skip this'], + fieldParser: (input) => { + const heightMatch = input.match(/(\d+)\s*(cm|ft|in)?/i); + const weightMatch = input.match(/(\d+)\s*(kg|lbs)?/i); + return { + height: heightMatch ? heightMatch[0] : '175cm', + weight: weightMatch ? weightMatch[0] : '72kg', + }; + }, + }, + { + key: 'blood_allergies', + question: "Understood. What is your Blood Group, and do you have any drug or food allergies?", + placeholder: "e.g., B+, No allergies", + chips: ['O+, No allergies', 'A+, Penicillin allergy', 'B+, None', 'Skip this'], + fieldParser: (input) => { + const bloodGroups = ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-']; + let blood_group = 'O+'; + for (const bg of bloodGroups) { + if (input.toUpperCase().includes(bg)) { + blood_group = bg; + break; + } + } + let allergies = 'None'; + if (input.toLowerCase().includes('allergy') || input.toLowerCase().includes('allergies')) { + allergies = input; + } + return { blood_group, allergies }; + }, + }, + { + key: 'chronic_meds', + question: "Do you have any chronic conditions (like Diabetes, Hypertension) or take daily medications?", + placeholder: "e.g., Hypertension, Metformin 500mg daily", + chips: ['None', 'Diabetes, Metformin', 'Hypertension, Amlodipine', 'Skip this'], + fieldParser: (input) => { + if (input.toLowerCase().trim() === 'none' || input.toLowerCase().includes('skip')) { + return { chronic_diseases: 'None', current_medication: 'None' }; + } + let chronic_diseases = 'None'; + let current_medication = 'None'; + if (input.toLowerCase().includes('diabetes')) chronic_diseases = 'Diabetes'; + else if (input.toLowerCase().includes('hypertension') || input.toLowerCase().includes('bp')) chronic_diseases = 'Hypertension'; + else chronic_diseases = input; + + if (input.toLowerCase().includes('metformin') || input.toLowerCase().includes('amlodipine') || input.includes('mg')) { + current_medication = input; + } + return { chronic_diseases, current_medication }; + }, + }, + { + key: 'surgeries_vaccinations', + question: "Lastly, have you had any major surgeries in the past, and are your vaccinations up to date?", + placeholder: "e.g., Appendectomy in 2024, fully vaccinated", + chips: ['No surgeries, up to date', 'No surgeries, missing some', 'Appendectomy, up to date', 'Skip this'], + fieldParser: (input) => { + let surgeries = 'None'; + let vaccinations = 'Up to date'; + if (input.toLowerCase().includes('appendectomy')) surgeries = 'Appendectomy'; + else if (input.toLowerCase().includes('surgery') || input.toLowerCase().includes('operation')) surgeries = input; + + if (input.toLowerCase().includes('missing') || input.toLowerCase().includes('not up to date')) { + vaccinations = 'Needs updates'; + } + return { surgeries, vaccinations }; + }, + }, + ]; -export default function ChatScreen() { - const isVoiceAvailable = Platform.OS !== 'web' && !!NativeModules.Voice; const [messages, setMessages] = useState([ { - id: '1', - text: 'Hello! I am your Swasthya AI Assistant. How can I help you today?', + id: 'welcome-bot', + text: steps[0].question, isUser: false, timestamp: new Date(), }, ]); - const [inputText, setInputText] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const { user } = useAuthStore(); - const [userName, setUserName] = useState('User'); - // History states - const [showHistory, setShowHistory] = useState(false); - const [expandedDays, setExpandedDays] = useState>({}); - const slideAnim = useRef(new Animated.Value(0)).current; - const { width: screenWidth } = Dimensions.get('window'); - - // Voice recording states - const [isRecording, setIsRecording] = useState(false); - const voicePulseAnim = useRef(new Animated.Value(1)).current; - const recordingTimerRef = useRef(null); - const phraseIndexRef = useRef(0); - - const mockPhrases = [ - "मुझे कल रात से सिरदर्द हो रहा है", - "My chest feels a bit tight and uneasy", - "क्या मुझे अपनी सुबह की दवा लेनी चाहिए?" - ]; - - // Pulse animation when recording useEffect(() => { - let animation: Animated.CompositeAnimation | null = null; - if (isRecording) { - animation = Animated.loop( - Animated.sequence([ - Animated.timing(voicePulseAnim, { - toValue: 1.15, - duration: 600, - useNativeDriver: true, - }), - Animated.timing(voicePulseAnim, { - toValue: 1.0, - duration: 600, - useNativeDriver: true, - }), - ]) - ); - animation.start(); - } else { - voicePulseAnim.setValue(1); - } - return () => { - if (animation) animation.stop(); - }; - }, [isRecording]); - - const handleVoicePress = async () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - if (isRecording) { - if (isVoiceAvailable) { - try { - await Voice.stop(); - } catch (e) { - console.error(e); - } - } - setIsRecording(false); - } else { - setIsRecording(true); - setInputText(''); - latestVoiceSpeechRef.current = ''; - - let voiceStarted = false; - if (isVoiceAvailable) { - try { - await Voice.start('hi-IN'); // start recording using microphone in Hindi - voiceStarted = true; - } catch (e) { - console.error("Voice start error:", e); + if (patientId) { + getPatientById(patientId).then((record) => { + if (record && record.name) { + setProfileData(prev => ({ ...prev, full_name: record.name })); } - } - - if (!voiceStarted) { - // Fallback: If voice fails (permissions, emulator, missing native module), run the mock simulation! - if (recordingTimerRef.current) clearTimeout(recordingTimerRef.current); - recordingTimerRef.current = setTimeout(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setIsRecording(false); - const nextPhrase = mockPhrases[phraseIndexRef.current]; - setInputText(nextPhrase); - phraseIndexRef.current = (phraseIndexRef.current + 1) % mockPhrases.length; - }, 2500); - } + }).catch(err => console.log('Error loading patient name in chat', err)); } - }; + }, [patientId]); - useEffect(() => { - return () => { - if (recordingTimerRef.current) clearTimeout(recordingTimerRef.current); - }; - }, []); - - // Voice-to-Voice states - const [voiceModeActive, setVoiceModeActive] = useState(false); - const [voiceState, setVoiceState] = useState<'listening' | 'thinking' | 'speaking' | 'paused'>('listening'); - const [voiceLang, setVoiceLang] = useState<'hi-IN' | 'en-US'>('hi-IN'); - const [voiceSubtitles, setVoiceSubtitles] = useState('शुरू हो रहा है...'); - - // Concentric circle animations for the ChatGPT-like blob - const blobScale1 = useRef(new Animated.Value(1)).current; - const blobScale2 = useRef(new Animated.Value(1)).current; - const blobScale3 = useRef(new Animated.Value(1)).current; - const blobOpacity1 = useRef(new Animated.Value(0.15)).current; - const blobOpacity2 = useRef(new Animated.Value(0.1)).current; - const blobOpacity3 = useRef(new Animated.Value(0.05)).current; - - const voiceInteractionTimer = useRef(null); - const latestVoiceSpeechRef = useRef(''); - - // Register real speech-to-text listeners (Hindi default) - useEffect(() => { - if (!isVoiceAvailable) return; - - Voice.onSpeechStart = () => { - console.log('Voice recognition started'); - }; - Voice.onSpeechEnd = () => { - console.log('Voice recognition ended'); - setIsRecording(false); - - if (voiceModeActive) { - setTimeout(() => { - const finalSpeech = latestVoiceSpeechRef.current.trim(); - if (finalSpeech) { - processUserVoiceInput(finalSpeech); - } else { - setVoiceSubtitles(VOICE_LOCALES[voiceLang].unheard); - setTimeout(() => { - startListeningLoop(); - }, 1500); - } - }, 800); - } - }; - Voice.onSpeechResults = (e: any) => { - if (e.value && e.value.length > 0) { - const transcript = e.value[0]; - latestVoiceSpeechRef.current = transcript; - if (voiceModeActive) { - setVoiceSubtitles(transcript); - } else { - setInputText(transcript); - } - } - }; - Voice.onSpeechError = (e: any) => { - console.error('Voice recognition error:', e); - setIsRecording(false); - if (voiceModeActive && voiceState === 'listening') { - setTimeout(() => { startListeningLoop(); }, 1000); - } - }; - - return () => { - if (isVoiceAvailable) { - Voice.destroy().then(() => { - try { - Voice.removeAllListeners(); - } catch (err) { - console.error(err); - } - }).catch(err => console.error(err)); - } - }; - }, [voiceModeActive, voiceState, voiceLang, isVoiceAvailable]); - - // Concentric circle animation loops based on voiceState - useEffect(() => { - let animations: Animated.CompositeAnimation[] = []; - - if (voiceModeActive) { - if (voiceState === 'listening') { - const createPulse = (scaleVal: Animated.Value, opacityVal: Animated.Value, maxScale: number, baseOpacity: number, duration: number) => { - return Animated.loop( - Animated.sequence([ - Animated.parallel([ - Animated.timing(scaleVal, { toValue: maxScale, duration, useNativeDriver: true }), - Animated.timing(opacityVal, { toValue: baseOpacity * 1.5, duration, useNativeDriver: true }), - ]), - Animated.parallel([ - Animated.timing(scaleVal, { toValue: 1, duration, useNativeDriver: true }), - Animated.timing(opacityVal, { toValue: baseOpacity, duration, useNativeDriver: true }), - ]), - ]) - ); - }; - animations = [ - createPulse(blobScale1, blobOpacity1, 1.1, 0.2, 1200), - createPulse(blobScale2, blobOpacity2, 1.15, 0.15, 1600), - createPulse(blobScale3, blobOpacity3, 1.2, 0.08, 2000), - ]; - animations.forEach(a => a.start()); - } else if (voiceState === 'thinking') { - const createThinkPulse = (scaleVal: Animated.Value, opacityVal: Animated.Value, duration: number) => { - return Animated.loop( - Animated.sequence([ - Animated.timing(scaleVal, { toValue: 1.08, duration, useNativeDriver: true }), - Animated.timing(scaleVal, { toValue: 0.95, duration, useNativeDriver: true }), - ]) - ); - }; - animations = [ - createThinkPulse(blobScale1, blobOpacity1, 400), - createThinkPulse(blobScale2, blobOpacity2, 500), - createThinkPulse(blobScale3, blobOpacity3, 600), - ]; - animations.forEach(a => a.start()); - } else if (voiceState === 'speaking') { - const createRipple = (scaleVal: Animated.Value, opacityVal: Animated.Value, maxScale: number, startOpacity: number, delay: number) => { - scaleVal.setValue(1); - opacityVal.setValue(startOpacity); - return Animated.loop( - Animated.sequence([ - Animated.delay(delay), - Animated.parallel([ - Animated.timing(scaleVal, { toValue: maxScale, duration: 1500, useNativeDriver: true }), - Animated.timing(opacityVal, { toValue: 0, duration: 1500, useNativeDriver: true }), - ]), - ]) - ); - }; - animations = [ - createRipple(blobScale1, blobOpacity1, 1.5, 0.4, 0), - createRipple(blobScale2, blobOpacity2, 1.8, 0.3, 400), - createRipple(blobScale3, blobOpacity3, 2.1, 0.2, 800), - ]; - animations.forEach(a => a.start()); - } - } - - return () => { - animations.forEach(a => a.stop()); - blobScale1.setValue(1); - blobScale2.setValue(1); - blobScale3.setValue(1); - blobOpacity1.setValue(0.2); - blobOpacity2.setValue(0.15); - blobOpacity3.setValue(0.1); - }; - }, [voiceModeActive, voiceState]); - - // Voice mode interaction loop - useEffect(() => { - if (voiceModeActive) { - startVoiceGreeting(); - } else { - Speech.stop(); - if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current); - if (isVoiceAvailable) { - Voice.stop().catch(() => {}); - } - } - return () => { - Speech.stop(); - if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current); - if (isVoiceAvailable) { - Voice.stop().catch(() => {}); - } - }; - }, [voiceModeActive, voiceLang, isVoiceAvailable]); - - const startVoiceGreeting = async () => { - if (isVoiceAvailable) { - try { - await Voice.stop(); - } catch (e) {} - } - await Speech.stop(); - if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current); - - setVoiceState('speaking'); - const greeting = VOICE_LOCALES[voiceLang].greeting; - setVoiceSubtitles(greeting); - - const systemMsg: Message = { - id: 'greet-' + Date.now(), - text: greeting, - isUser: false, - timestamp: new Date() - }; - setMessages(prev => [...prev, systemMsg]); - - Speech.speak(greeting, { - language: voiceLang, - pitch: 1.0, - rate: 0.9, - onDone: () => { startListeningLoop(); }, - onError: (e) => { - console.error("Speech error:", e); - startListeningLoop(); - } - }); - }; - - const startListeningLoop = async () => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceState('listening'); - setVoiceSubtitles(VOICE_LOCALES[voiceLang].listening); - latestVoiceSpeechRef.current = ''; - - let voiceStarted = false; - if (isVoiceAvailable) { - try { - await Voice.start(voiceLang); - voiceStarted = true; - } catch (e) { - console.error("Voice start error in overlay:", e); - } - } - - if (!voiceStarted) { - const simulatedUserSayings = voiceLang === 'hi-IN' ? [ - "मुझे कल रात से पेट में दर्द और बेचैनी हो रही है", - "मेरी छाती में थोड़ा खिंचाव और घबराहट महसूस हो रही है", - "क्या मुझे अपनी सुबह की दवा लेनी चाहिए?" - ] : [ - "I have stomach pain and discomfort since last night", - "My chest feels a bit tight and uneasy", - "Should I take my morning medicine?" - ]; - - if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current); - voiceInteractionTimer.current = setTimeout(() => { - const randomSaying = simulatedUserSayings[Math.floor(Math.random() * simulatedUserSayings.length)]; - processUserVoiceInput(randomSaying); - }, 4500); - } - }; - - const getFallbackReplyHindi = (input: string): string => { - const msg = input.toLowerCase(); - if (msg.includes('blood pressure') || msg.includes('bp') || msg.includes('रक्तचाप') || msg.includes('बीपी')) { - return 'आपका रक्तचाप ठीक लग रहा है। अपनी दवाएं नियमित रूप से लेते रहें और समय-समय पर जांच करते रहें।'; - } - if (msg.includes('headache') || msg.includes('head') || msg.includes('सिरदर्द') || msg.includes('सिर दर्द')) { - return 'सिरदर्द रक्तचाप में उतार-चढ़ाव या निर्जलीकरण से जुड़ा हो सकता है। मैंने इस लक्षण को नोट कर लिया है। आप कब से ऐसा महसूस कर रहे हैं?'; - } - if (msg.includes('tired') || msg.includes('fatigue') || msg.includes('थकान') || msg.includes('कमजोरी')) { - return 'थकान मधुमेह या नींद की कमी के कारण हो सकती है। क्या आप 7-8 घंटे सो रहे हैं?'; - } - if (msg.includes('medic') || msg.includes('tablet') || msg.includes('दवा') || msg.includes('गोली')) { - return 'याद रखें कि डॉक्टर के पर्चे के अनुसार दवाएं समय पर लें। क्या आपने आज की खुराक ले ली है?'; - } - if (msg.includes('दर्द') || msg.includes('pain') || msg.includes('तकलीफ')) { - return 'मुझे यह सुनकर खेद है। दर्द कहाँ हो रहा है और यह कितना गंभीर है (1 से 10 के पैमाने पर)?'; - } - return 'साझा करने के लिए धन्यवाद। मैं इस जानकारी को आपके दैनिक स्वास्थ्य प्रोफ़ाइल में दर्ज कर रहा हूँ। क्या कोई अन्य लक्षण हैं?'; - }; - - const processUserVoiceInput = async (spokenText: string) => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceState('thinking'); - setVoiceSubtitles(spokenText); - - const userMessage: Message = { - id: 'voice-user-' + Date.now(), - text: spokenText, - isUser: true, - timestamp: new Date() - }; - setMessages(prev => [...prev, userMessage]); - - const sessionId = 'session-' + (user?.id || 'demo'); - const context = { - rolling_summary: "Voice conversation session", - profile_summary: "Voice onboarding", - last_7_summaries: [], - active_medications: [], - pending_doctor_questions: [] - }; - - try { - const response = await fetch(`${BACKEND_URL}${API_ENDPOINTS.CHAT.MESSAGE}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - patient_id: user?.id || 'demo-patient', - session_id: sessionId, - message: spokenText, - patient_context: context, - }), - }); - const data = await response.json(); - const reply = data.bot_reply || (voiceLang === 'hi-IN' ? getFallbackReplyHindi(spokenText) : getFallbackReply(spokenText)); - speakAIVoiceResponse(reply); - } catch (e) { - console.error(e); - const fallback = voiceLang === 'hi-IN' ? getFallbackReplyHindi(spokenText) : getFallbackReply(spokenText); - speakAIVoiceResponse(fallback); - } - }; - - const speakAIVoiceResponse = (replyText: string) => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceState('speaking'); - setVoiceSubtitles(replyText); - - const aiMessage: Message = { - id: 'voice-ai-' + Date.now(), - text: replyText, - isUser: false, - timestamp: new Date() - }; - setMessages(prev => [...prev, aiMessage]); - - Speech.speak(replyText, { - language: voiceLang, - pitch: 1.0, - rate: 0.9, - onDone: () => { startListeningLoop(); }, - onError: (e) => { - console.error(e); - startListeningLoop(); - } - }); - }; - - // Slide drawer animation - useEffect(() => { - Animated.timing(slideAnim, { - toValue: showHistory ? 1 : 0, - duration: 350, - useNativeDriver: true, - }).start(); - }, [showHistory]); - - const toggleDayExpand = (id: string) => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setExpandedDays(prev => ({ - ...prev, - [id]: !prev[id], - })); - }; - - useEffect(() => { - if (user) { - fetchUserName(); - } - }, [user]); - - const fetchUserName = async () => { - const { data } = await supabase - .from('patients') - .select('full_name') - .eq('id', user?.id) - .single(); - if (data?.full_name) setUserName(data.full_name); - }; - - const getUserInitials = () => { - if (!userName) return 'U'; - return userName.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); - }; - - const flatListRef = useRef(null); - - // Auto-scroll to bottom when new messages arrive - useEffect(() => { + const addBotReply = (text: string) => { + setIsTyping(true); setTimeout(() => { + setIsTyping(false); + setMessages((prev) => [ + ...prev, + { + id: `bot-${Date.now()}`, + text, + isUser: false, + timestamp: new Date(), + }, + ]); flatListRef.current?.scrollToEnd({ animated: true }); - }, 100); - }, [messages]); - - // Handle session end on unmount - useEffect(() => { - return () => { - if (user?.id) { - const sessionId = 'session-' + user.id; - fetch(`${BACKEND_URL}${API_ENDPOINTS.CHAT.END_SESSION}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ patient_id: user.id, session_id: sessionId }) - }).catch(e => console.error("End session error:", e)); - } - }; - }, [user]); - - const getFallbackReply = (input: string): string => { - const msg = input.toLowerCase(); - if (msg.includes('blood pressure') || msg.includes('bp')) return 'Your recent BP readings have been tracking around 118/76 — within normal range. Continue your Amlodipine as prescribed and monitor weekly.'; - if (msg.includes('headache') || msg.includes('head')) return 'Headaches can be linked to blood pressure fluctuations or dehydration. I\'ve noted this symptom. How long has this been going on?'; - if (msg.includes('tired') || msg.includes('fatigue') || msg.includes('energy')) return 'Fatigue is a common concern with your conditions. Are you sleeping 7-8 hours? Let\'s also check if you\'ve missed any doses recently.'; - if (msg.includes('medic') || msg.includes('tablet') || msg.includes('pill')) return 'You have 3 active medications: Metformin 500mg (morning), Amlodipine 5mg (evening), Vitamin D3 (afternoon). Have you been taking them consistently?'; - if (msg.includes('hello') || msg.includes('hi') || msg.includes('hey')) return 'Hello! How are you feeling today? I\'m your AI health assistant — tell me about any symptoms, medications, or health concerns.'; - if (msg.includes('pain') || msg.includes('hurt') || msg.includes('ache')) return 'I understand you\'re experiencing pain. Can you tell me where exactly and rate it from 1-10? This helps me assess the severity.'; - if (msg.includes('sugar') || msg.includes('glucose') || msg.includes('diabet')) return 'Blood sugar management is key with your profile. Have you checked your levels today? Aim for fasting glucose below 126 mg/dL.'; - if (msg.includes('sleep') || msg.includes('insomnia')) return 'Sleep quality directly impacts your heart health and blood pressure. 7-8 hours is recommended. Any difficulty falling asleep or staying asleep?'; - return 'Thank you for sharing that. I\'m tracking this information to build your health profile. Can you tell me more, or is there a specific symptom you\'d like to discuss?'; + }, 1000); }; - const handleSendMessage = async (): Promise => { - if (!inputText.trim()) return; + const handleSend = (textToSend = inputText) => { + const trimmed = textToSend.trim(); + if (!trimmed) return; - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - const userMessage: Message = { - id: Date.now().toString(), - text: inputText, + const userMsg: Message = { + id: `user-${Date.now()}`, + text: trimmed, isUser: true, timestamp: new Date(), }; - setMessages(prev => [...prev, userMessage]); - const currentInput = inputText; + setMessages((prev) => [...prev, userMsg]); setInputText(''); - setIsLoading(true); + Keyboard.dismiss(); - const sessionId = 'session-' + (user?.id || 'demo'); - const context = { - rolling_summary: "Initial onboarding conversation", - profile_summary: "New patient onboarding", - last_7_summaries: [], - active_medications: [], - pending_doctor_questions: [] - }; + const activeStep = steps[currentStep]; + const parsedFields = activeStep.fieldParser(trimmed); + const updatedData = { ...profileData, ...parsedFields }; + setProfileData(updatedData); - fetch(`${BACKEND_URL}${API_ENDPOINTS.CHAT.MESSAGE}`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - patient_id: user?.id || 'demo-patient', - session_id: sessionId, - message: currentInput, - patient_context: context, - }), - }) - .then(res => res.json()) - .then(data => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - const aiResponse: Message = { - id: (Date.now() + 1).toString(), - text: data.bot_reply || getFallbackReply(currentInput), - isUser: false, - timestamp: new Date(), - }; - setMessages(prev => [...prev, aiResponse]); - }) - .catch(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - const errorMsg: Message = { - id: (Date.now() + 1).toString(), - text: getFallbackReply(currentInput), - isUser: false, - timestamp: new Date(), - }; - setMessages(prev => [...prev, errorMsg]); - }) - .finally(() => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setIsLoading(false); - }); - }; - - const handleEndSession = async () => { - setIsLoading(true); - try { - await backendService.endSession( - user?.id || 'demo', - messages.map(m => ({ role: m.isUser ? 'user' : 'assistant', content: m.text })), - "" - ); - router.push('/(onboarding)/agent-log'); - } catch (e) { - console.error("End session error:", e); - router.push('/(onboarding)/agent-log'); - } finally { - setIsLoading(false); + const nextStepIndex = currentStep + 1; + if (nextStepIndex < steps.length) { + setCurrentStep(nextStepIndex); + addBotReply(steps[nextStepIndex].question); + } else { + setIsTyping(true); + setTimeout(() => { + setIsTyping(false); + setMessages((prev) => [ + ...prev, + { + id: `bot-done`, + text: "Excellent! Your health profile is now configured. Let's verify your details in the Summary.", + isUser: false, + timestamp: new Date(), + }, + ]); + setTimeout(() => { + router.push({ + pathname: '/(onboarding)/summary', + params: { profileData: JSON.stringify(updatedData) }, + }); + }, 1500); + }, 800); } }; - const formatTime = (date: Date) => { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const handleSkipCurrent = () => { + handleSend('Skip this'); }; - const renderMessage = ({ item }: { item: Message }) => ( - - {!item.isUser && ( - - - - )} - - - {item.text} - - - {formatTime(item.timestamp)} - - - {item.isUser && ( - - {getUserInitials()} - - )} - - ); - - const renderHistoryDrawer = () => { - const translateX = slideAnim.interpolate({ - inputRange: [0, 1], - outputRange: [screenWidth, 0], + const handleSkipAll = () => { + const fakeProfileData = { + full_name: 'Indresh', + age: 20, + gender: 'Male', + blood_group: 'O+', + height: '175cm', + weight: '72kg', + allergies: 'Penicillin', + current_medication: 'None', + chronic_diseases: 'Migraine, Anxiety', + family_history: 'None', + smoking: 'Non-smoker', + alcohol: 'Never', + emergency_contact: 'None', + surgeries: 'None', + vaccinations: 'COVID-19, Tetanus', + }; + + setSessionState({ + hasProfile: true, + }); + + router.push({ + pathname: '/(onboarding)/summary', + params: { profileData: JSON.stringify(fakeProfileData) }, }); - - return ( - - - {/* History Header */} - - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setShowHistory(false); - }} - style={styles.historyCloseButton} - > - - - Daily Health History - - - - {/* History Scrollable Timeline */} - - {MOCK_HISTORY.map((item, index) => { - const isExpanded = !!expandedDays[item.id]; - return ( - - {/* Left Column: Summary Card */} - toggleDayExpand(item.id)} - style={[styles.historyCard, isExpanded && styles.historyCardExpanded]} - > - - {item.date} • {item.time} - - - - - - {item.overallSummary} - - {isExpanded && ( - - - Agent Diagnostics - - {item.agents.map((agent, aIdx) => ( - - - - {agent.name} - ({agent.role}) - - {agent.thought} - - ))} - - )} - - - {/* Right Column: Timeline line and node */} - - - - - - {item.shortDate} - - - ); - })} - - - {/* Action Button at the Bottom */} - - - Process Session & Run Diagnostics - - - - - - ); }; - const renderVoiceOverlay = () => { - if (!voiceModeActive) return null; - - return ( - - - {/* Header */} - - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceModeActive(false); - }} - style={styles.voiceCloseButton} - > - - - Voice Mode - - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceLang(prev => prev === 'hi-IN' ? 'en-US' : 'hi-IN'); - }} - style={styles.langToggleButton} - > - - - {voiceLang === 'hi-IN' ? 'English' : 'हिंदी'} - - - - - {/* Central Blob Visualizer */} - - - - - - { - if (voiceState === 'listening') { - if (isVoiceAvailable) { - try { - await Voice.stop(); - } catch (e) { - console.error(e); - } - } else { - if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current); - const simulatedUserSayings = voiceLang === 'hi-IN' ? [ - "मुझे कल रात से पेट में दर्द और बेचैनी हो रही है", - "मेरी छाती में थोड़ा खिंचाव और घबराहट महसूस हो रही है", - "क्या मुझे अपनी सुबह की दवा लेनी चाहिए?" - ] : [ - "I have stomach pain and discomfort since last night", - "My chest feels a bit tight and uneasy", - "Should I take my morning medicine?" - ]; - const randomSaying = simulatedUserSayings[Math.floor(Math.random() * simulatedUserSayings.length)]; - processUserVoiceInput(randomSaying); - } - } - }} - activeOpacity={0.8} - style={styles.blobCoreWrapper} - > - - - - - - - - {voiceState === 'listening' - ? VOICE_LOCALES[voiceLang].statusListening - : voiceState === 'thinking' - ? VOICE_LOCALES[voiceLang].thinking - : VOICE_LOCALES[voiceLang].speaking} - - - - {/* Subtitles Area */} - - - {voiceSubtitles} - - - - {/* Subtle instruction at bottom */} - - - {voiceState === 'listening' ? VOICE_LOCALES[voiceLang].instruction : ''} - - - - - ); + const handleChipPress = (chip: string) => { + if (chip === 'Skip this') { + handleSkipCurrent(); + } else { + handleSend(chip); + } }; return ( - - - - - {/* Header */} - - router.back()} style={styles.backButton}> - - - - + + + + {/* Header */} + + + + - Swasthya AI Assistant - - { - Keyboard.dismiss(); - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceModeActive(true); - }} - style={styles.headerActionButton} - > - - - - { - Keyboard.dismiss(); - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setShowHistory(true); - }} - style={styles.historyButton} - > - - History - + + Swasthya Assistant + Guided Setup + + Skip All + + + - {/* Messages */} - item.id} - contentContainerStyle={styles.messagesList} - showsVerticalScrollIndicator={false} - /> - - {/* Loading Indicator */} - {isLoading && ( - - - AI is thinking... + {/* Messages list */} + item.id} + contentContainerStyle={styles.listContent} + renderItem={({ item }) => ( + + {!item.isUser && ( + + + + )} + + {item.text} + )} + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} + /> + + {/* Typing indicator */} + {isTyping && ( + + + + + + + Typing health profile... + + + )} - {/* Input Area */} - - + {/* Suggestions Chips */} + + + {steps[currentStep].chips.map((chip, idx) => ( handleChipPress(chip)} > - + {chip} - + ))} + + + {/* Bottom input bar */} + + handleSend()} + returnKeyType="send" /> - + handleSend()}> - - {/* History Drawer Overlay */} - {renderHistoryDrawer()} - - {/* Voice Overlay */} - {renderVoiceOverlay()} ); } const styles = StyleSheet.create({ - safeArea: { + container: { flex: 1, - backgroundColor: '#171717', + backgroundColor: '#07111f', + paddingTop: Platform.OS === 'ios' ? 0 : 30, // Add top padding for Android }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, - paddingTop: Platform.OS === 'ios' ? 12 : 40, - paddingBottom: 16, - backgroundColor: '#171717', + paddingVertical: 12, borderBottomWidth: 1, - borderBottomColor: '#2D2D2D', + borderBottomColor: '#1E293B', }, - backButton: { - width: 40, - height: 40, - borderRadius: 20, + headerTitleContainer: { + flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', }, - headerIcon: { - width: 40, - height: 40, - borderRadius: 20, - backgroundColor: '#2A2A2A', - alignItems: 'center', + botIcon: { + width: 34, + height: 34, + borderRadius: 17, + backgroundColor: '#0474FC', justifyContent: 'center', - marginHorizontal: 8, + alignItems: 'center', }, headerTitle: { - flex: 1, - fontSize: 18, - fontWeight: '600', color: '#FFFFFF', + fontFamily: 'Poppins_600SemiBold', + fontSize: 14, }, - historyButton: { + headerSubtitle: { + color: '#8AA0BC', + fontFamily: 'Poppins_400Regular', + fontSize: 10, + }, + skipAllBtn: { flexDirection: 'row', alignItems: 'center', - backgroundColor: '#0474FC', - paddingHorizontal: 12, - paddingVertical: 7, - borderRadius: 20, - gap: 5, + gap: 4, + paddingVertical: 6, + paddingHorizontal: 10, + borderRadius: 8, + backgroundColor: '#1E293B', }, - historyButtonText: { - color: '#FFFFFF', - fontSize: 13, - fontWeight: '700', + skipAllText: { + color: '#8AA0BC', + fontFamily: 'Poppins_500Medium', + fontSize: 12, }, - messagesList: { + listContent: { padding: 16, + gap: 16, paddingBottom: 20, }, - messageContainer: { + bubbleWrapper: { flexDirection: 'row', - marginBottom: 16, alignItems: 'flex-end', + marginBottom: 4, }, - userMessage: { + userWrapper: { justifyContent: 'flex-end', }, - aiMessage: { + botWrapper: { justifyContent: 'flex-start', }, - aiAvatar: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: '#2A2A2A', - alignItems: 'center', + bubbleAvatar: { + width: 28, + height: 28, + borderRadius: 14, + backgroundColor: '#0F172A', justifyContent: 'center', - marginRight: 8, - }, - userAvatar: { - width: 32, - height: 32, - borderRadius: 16, - backgroundColor: '#0474FC', alignItems: 'center', - justifyContent: 'center', - marginLeft: 8, - }, - userAvatarText: { - fontSize: 14, - fontWeight: '600', - color: '#FFFFFF', + marginRight: 8, + borderWidth: 1, + borderColor: '#334155', }, - messageBubble: { - maxWidth: '75%', - padding: 12, - borderRadius: 20, + bubble: { + maxWidth: width * 0.75, + borderRadius: 18, + paddingVertical: 12, + paddingHorizontal: 16, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, }, userBubble: { backgroundColor: '#0474FC', borderBottomRightRadius: 4, }, - aiBubble: { - backgroundColor: '#212121', + botBubble: { + backgroundColor: '#1E293B', borderBottomLeftRadius: 4, + borderWidth: 1, + borderColor: '#334155', }, - messageText: { - fontSize: 15, - lineHeight: 20, - }, - userText: { + bubbleText: { color: '#FFFFFF', + fontFamily: 'Poppins_400Regular', + fontSize: 14, + lineHeight: 20, }, - aiText: { - color: '#ECECF1', - }, - timestamp: { - fontSize: 10, - color: '#9CA3AF', - marginTop: 4, - alignSelf: 'flex-end', - }, - loadingContainer: { + typingContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, - paddingVertical: 8, - gap: 8, - backgroundColor: '#171717', - }, - loadingText: { - fontSize: 12, - color: '#9CA3AF', + marginBottom: 10, }, - inputContainer: { + typingBubble: { flexDirection: 'row', - alignItems: 'flex-end', - padding: 16, - backgroundColor: '#171717', - borderTopWidth: 1, - borderTopColor: '#2D2D2D', - gap: 12, - }, - input: { - flex: 1, - backgroundColor: '#212121', - borderRadius: 24, - paddingHorizontal: 16, - paddingVertical: 10, - maxHeight: 100, - fontSize: 15, - color: '#FFFFFF', - borderWidth: 1, - borderColor: '#2D2D2D', - }, - sendButton: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: '#0474FC', alignItems: 'center', - justifyContent: 'center', - }, - sendButtonDisabled: { - opacity: 0.5, }, - voiceButton: { - width: 44, - height: 44, - borderRadius: 22, - backgroundColor: '#212121', - borderWidth: 1, - borderColor: '#2D2D2D', - alignItems: 'center', - justifyContent: 'center', - }, - voiceButtonActive: { - backgroundColor: 'rgba(239, 68, 68, 0.15)', - borderColor: '#EF4444', + chipsContainer: { + paddingVertical: 8, + backgroundColor: '#07111f', }, - - // Header Actions - headerActions: { + chipsScroll: { flexDirection: 'row', - alignItems: 'center', + flexWrap: 'wrap', + paddingHorizontal: 16, gap: 8, }, - headerActionButton: { - width: 36, - height: 36, - borderRadius: 18, - backgroundColor: '#0474FC', - alignItems: 'center', - justifyContent: 'center', - }, - - // Voice mode overlay styles - voiceOverlay: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - zIndex: 2000, - }, - voiceSafeArea: { - flex: 1, - }, - voiceOverlayHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 20, - paddingTop: Platform.OS === 'ios' ? 12 : 40, - paddingBottom: 16, - }, - voiceCloseButton: { - alignItems: 'center', - justifyContent: 'center', - }, - voiceHeaderTitle: { - fontSize: 16, - fontWeight: '700', - color: '#9CA3AF', - textTransform: 'uppercase', - letterSpacing: 1.5, - }, - langToggleButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(255,255,255,0.08)', - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 16, + chip: { + paddingVertical: 8, + paddingHorizontal: 14, + borderRadius: 20, + backgroundColor: '#1E293B', borderWidth: 1, - borderColor: 'rgba(255,255,255,0.15)', + borderColor: '#334155', }, - langToggleText: { + chipText: { color: '#FFFFFF', + fontFamily: 'Poppins_500Medium', fontSize: 12, - fontWeight: '600', - }, - voiceVisualizerContainer: { - flex: 1.5, - alignItems: 'center', - justifyContent: 'center', - paddingTop: 40, - }, - blobAnchor: { - width: 250, - height: 250, - alignItems: 'center', - justifyContent: 'center', - position: 'relative', - }, - blobCircle: { - position: 'absolute', - borderRadius: 999, - }, - blobCircleOuter: { - width: 220, - height: 220, - backgroundColor: 'rgba(147, 51, 234, 0.06)', - borderWidth: 1, - borderColor: 'rgba(147, 51, 234, 0.12)', - }, - blobCircleMiddle: { - width: 170, - height: 170, - backgroundColor: 'rgba(59, 130, 246, 0.12)', - borderWidth: 1, - borderColor: 'rgba(59, 130, 246, 0.18)', - }, - blobCircleInner: { - width: 120, - height: 120, - backgroundColor: 'rgba(99, 102, 241, 0.18)', - borderWidth: 1, - borderColor: 'rgba(99, 102, 241, 0.25)', - }, - blobCoreWrapper: { - width: 80, - height: 80, - borderRadius: 40, - shadowColor: '#0474FC', - shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.5, - shadowRadius: 12, - elevation: 10, - zIndex: 20, }, - blobCore: { - width: 80, - height: 80, - borderRadius: 40, - alignItems: 'center', - justifyContent: 'center', + skipChip: { + backgroundColor: 'rgba(239, 68, 68, 0.1)', + borderColor: 'rgba(239, 68, 68, 0.3)', }, - voiceStatusText: { - color: '#FFFFFF', - fontSize: 18, - fontWeight: '700', - marginTop: 30, - letterSpacing: 0.5, + skipChipText: { + color: '#EF4444', }, - voiceSubtitlesContainer: { - flex: 1, - paddingHorizontal: 24, - justifyContent: 'center', - marginTop: 20, - }, - subtitlesScroll: { - flex: 1, - backgroundColor: 'rgba(255,255,255,0.04)', - borderRadius: 20, - borderWidth: 1.5, - borderColor: 'rgba(255,255,255,0.08)', - shadowColor: '#000', - shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 4, - }, - subtitlesContent: { - padding: 16, + inputContainer: { + flexDirection: 'row', + padding: 12, + backgroundColor: '#0F172A', + borderTopWidth: 1, + borderTopColor: '#1E293B', alignItems: 'center', }, - subtitlesText: { - color: '#ECECF1', - fontSize: 16, - lineHeight: 24, - textAlign: 'center', - fontWeight: '500', - }, - - // History Drawer styles - historyDrawer: { - position: 'absolute', - top: 0, - bottom: 0, - left: 0, - right: 0, - backgroundColor: '#121212', - zIndex: 1000, - }, - historySafeArea: { + input: { flex: 1, - }, - historyHeader: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', + height: 48, + backgroundColor: '#1E293B', + borderRadius: 24, paddingHorizontal: 16, - paddingTop: Platform.OS === 'ios' ? 12 : 40, - paddingBottom: 16, - borderBottomWidth: 1, - borderBottomColor: '#2D2D2D', - }, - historyCloseButton: { - width: 40, - height: 40, - borderRadius: 20, - alignItems: 'center', - justifyContent: 'center', - }, - historyTitle: { - fontSize: 18, - fontWeight: '700', color: '#FFFFFF', - }, - historyContent: { - padding: 16, - paddingRight: 8, - }, - timelineRow: { - flexDirection: 'row', - marginBottom: 20, - minHeight: 100, - }, - historyCard: { - flex: 1, - backgroundColor: '#1E1E1E', - borderRadius: 16, - padding: 16, - borderWidth: 1, - borderColor: '#2D2D2D', - shadowColor: '#000', - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.25, - shadowRadius: 3.84, - elevation: 5, - }, - historyCardExpanded: { - borderColor: '#0474FC', - }, - cardHeaderRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - cardDateText: { - color: '#0474FC', + fontFamily: 'Poppins_400Regular', fontSize: 14, - fontWeight: '700', - }, - cardHeaderRight: { - flexDirection: 'row', - alignItems: 'center', - }, - cardSummaryText: { - color: '#ECECF1', - fontSize: 14, - lineHeight: 20, - }, - expandedSection: { - marginTop: 12, - }, - divider: { - height: 1, - backgroundColor: '#2D2D2D', - marginVertical: 12, - }, - agentSectionTitle: { - color: '#FFFFFF', - fontSize: 13, - fontWeight: '700', - marginBottom: 10, - textTransform: 'uppercase', - letterSpacing: 0.5, - }, - agentThoughtRow: { - backgroundColor: '#171717', - borderRadius: 10, - padding: 12, - marginBottom: 10, - borderLeftWidth: 3, - borderLeftColor: '#0474FC', - }, - agentHeader: { - flexDirection: 'row', - alignItems: 'center', - flexWrap: 'wrap', - gap: 4, - marginBottom: 6, - }, - agentName: { - color: '#FFFFFF', - fontSize: 13, - fontWeight: '600', - }, - agentRole: { - color: '#9CA3AF', - fontSize: 11, - }, - agentThoughtText: { - color: '#ECECF1', - fontSize: 12.5, - lineHeight: 18, - }, - timelineRightCol: { - width: 60, - alignItems: 'center', - position: 'relative', - }, - timelineLine: { - position: 'absolute', - top: 0, - bottom: 0, - width: 2, - backgroundColor: '#2D2D2D', - }, - timelineLineFirst: { - top: 24, - }, - timelineLineLast: { - bottom: '60%', - }, - timelineNode: { - width: 24, - height: 24, - borderRadius: 12, - backgroundColor: '#1A2E40', - borderWidth: 2, - borderColor: '#0474FC', - alignItems: 'center', - justifyContent: 'center', - marginTop: 12, - zIndex: 10, + marginRight: 8, }, - timelineNodeInner: { - width: 10, - height: 10, - borderRadius: 5, + sendButton: { + width: 48, + height: 48, + borderRadius: 24, backgroundColor: '#0474FC', - }, - timelineDateBadge: { - color: '#9CA3AF', - fontSize: 11, - fontWeight: '600', - marginTop: 6, - textAlign: 'center', - }, - historyFooter: { - padding: 16, - borderTopWidth: 1, - borderTopColor: '#2D2D2D', - backgroundColor: '#121212', - }, - processButton: { - flexDirection: 'row', - alignItems: 'center', justifyContent: 'center', - backgroundColor: '#0474FC', - paddingVertical: 14, - borderRadius: 14, - gap: 8, - }, - processButtonText: { - color: '#FFFFFF', - fontSize: 15, - fontWeight: '700', + alignItems: 'center', }, }); \ No newline at end of file diff --git a/app/app/(onboarding)/confirm.tsx b/app/app/(onboarding)/confirm.tsx deleted file mode 100644 index 854b873..0000000 --- a/app/app/(onboarding)/confirm.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; -import { LinearGradient } from 'expo-linear-gradient'; -import { useRouter } from 'expo-router'; -import { ScreenWrapper } from '@/components/shared/ScreenWrapper'; -import { Button } from '@/components/ui/Button'; -import { COLORS, GRADIENTS, SPACING, TYPOGRAPHY } from '@/theme'; -import { supabase } from '@/services/supabaseClient'; -import { useAuthStore } from '@/store/auth.store'; - -export default function ConfirmScreen() { - const router = useRouter(); - const [open, setOpen] = useState(false); - const { user } = useAuthStore(); - const [profile, setProfile] = useState(null); - - useEffect(() => { - if (user) { - fetchProfile(); - } - }, [user]); - - const fetchProfile = async () => { - const { data, error } = await supabase - .from('patients') - .select('*') - .eq('id', user?.id) - .single(); - - if (data) { - setProfile({ - ...data, - name: data.full_name, - }); - } - }; - - const getProfileData = () => [ - ['Name', profile?.name || 'Loading...'], - ['Age', profile?.age?.toString() || '—'], - ['Conditions', profile?.conditions || 'None'], - ['Medicines', profile?.medications || 'None'], - ['Allergies', profile?.allergies || 'None'], - ['Family History', profile?.family_history || 'None'], - ]; - return ( - - - Your Health Profile - - {getProfileData().map(([label, value]) => ( - - {label} - {value} - - ))} - -