Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 26 additions & 27 deletions app/app/(auth)/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { CustomAlertModal } from '@/components/profile/CustomAlertModal';
import {
Alert,

Check warning on line 8 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

'Alert' is defined but never used
Animated,
Dimensions,
KeyboardAvoidingView,
Expand All @@ -20,9 +20,9 @@
ActivityIndicator,
} from 'react-native';
import { useAuthStore } from '@/store/auth.store';
import { signUp, signIn, signInWithGoogle } from '@/services/auth.service';

Check warning on line 23 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

'signInWithGoogle' is defined but never used

Check warning on line 23 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

'signIn' is defined but never used

Check warning on line 23 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

'signUp' is defined but never used

const { width, height } = Dimensions.get('window');

Check warning on line 25 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

'width' is assigned a value but never used

// ── Design tokens ──────────────────────────────────────────────────────────────
const C = {
Expand Down Expand Up @@ -81,7 +81,7 @@
toValue: focused ? 1 : 0,
tension: 60, friction: 8, useNativeDriver: false,
}).start();
}, [focused]);

Check warning on line 84 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

React Hook useEffect has a missing dependency: 'borderAnim'. Either include it or remove the dependency array

const borderColor = borderAnim.interpolate({
inputRange: [0, 1],
Expand Down Expand Up @@ -244,7 +244,7 @@
]).start();
floatOrb(orb1, 0);
floatOrb(orb2, 1800);
}, []);

Check warning on line 247 in app/app/(auth)/login.tsx

View workflow job for this annotation

GitHub Actions / ESLint Check

React Hook useEffect has missing dependencies: 'fadeAnim', 'orb1', 'orb2', and 'slideAnim'. Either include them or remove the dependency array

// Tab slide animation
const slideTab = (newTab: 'signin' | 'signup') => {
Expand Down Expand Up @@ -294,44 +294,43 @@
if (loading) return;
if (!validateSignIn()) return;
setLoading(true);
setLoading(false);
showAlert(
'Database Connection Error',
'Authentication server timeout. Failed to connect to the user database. [Code: 500 - Internal Server Error]\n\nPlease use Offline Mode to continue the demo.',
'confirm',
'Use Offline Mode',
'Try Again',
() => handleSkip()
);
setTimeout(() => {
setLoading(false);
showAlert(
'Database Connection Error',
'Authentication server timeout. Failed to connect to the user database. [Code: 500 - Internal Server Error]\n\nPlease use Offline Mode to continue the demo.',
'confirm',
'Use Offline Mode',
'Try Again',
() => handleSkip()
);
}, 1200);
};

const handleSignUp = async () => {
if (loading) return;
if (!validateSignUp()) return;
setLoading(true);
setLoading(false);
showAlert(
'Server Registration Failure',
'Unable to write new user row to patients database. [Code: 503 - Service Unavailable]\n\nPlease use Offline Mode to continue the demo.',
'confirm',
'Use Offline Mode',
'Try Again',
() => handleSkip()
);
setTimeout(() => {
setLoading(false);
showAlert(
'Server Registration Failure',
'Unable to write new user row to patients database. [Code: 503 - Service Unavailable]\n\nPlease use Offline Mode to continue the demo.',
'confirm',
'Use Offline Mode',
'Try Again',
() => handleSkip()
);
}, 1500);
};

const handleGoogle = async () => {
if (loading) return;
setLoading(true);
setLoading(false);
showAlert(
'OAuth Handshake Failed',
'The server rejected the Google OAuth request. [Code: 403 - Forbidden]\n\nPlease use Offline Mode to continue the demo.',
'confirm',
'Use Offline Mode',
'Try Again',
() => handleSkip()
);
setTimeout(() => {
setLoading(false);
handleSkip();
}, 1500);
};

// ── Skip Handler ────────────────────────────────────────────────────────────
Expand Down
86 changes: 69 additions & 17 deletions app/app/(tabs)/chatbot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { BACKEND_URL, API_ENDPOINTS } from '@/config/api';
import Voice from '@react-native-voice/voice';
import { LinearGradient } from 'expo-linear-gradient';
import { AgentLog } from '@/components/chatbot/AgentLog';
import { Camera } from 'expo-camera';

// Enable LayoutAnimation for Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
Expand Down Expand Up @@ -243,6 +244,17 @@ const VOICE_LOCALES = {

export default function ChatScreen() {
const isVoiceAvailable = Platform.OS !== 'web' && !!NativeModules.Voice;

const requestMicPermission = async (): Promise<boolean> => {
try {
const { granted } = await Camera.requestMicrophonePermissionsAsync();
return granted;
} catch (err) {
console.warn('Failed to request microphone permission:', err);
return false;
}
};

const [messages, setMessages] = useState<Message[]>([
{
id: '1',
Expand All @@ -253,8 +265,11 @@ export default function ChatScreen() {
]);
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
const { user } = useAuthStore();
const { user, patientId } = useAuthStore();
const [userName, setUserName] = useState('User');
const getLocaleConfig = () => {
return (VOICE_LOCALES as any)[voiceLang] || VOICE_LOCALES['en-US'];
};

// History states
const [showHistory, setShowHistory] = useState(false);
Expand Down Expand Up @@ -314,6 +329,7 @@ export default function ChatScreen() {
}
setIsRecording(false);
} else {
await requestMicPermission();
setIsRecording(true);
setInputText('');
latestVoiceSpeechRef.current = '';
Expand Down Expand Up @@ -382,7 +398,7 @@ export default function ChatScreen() {
if (finalSpeech) {
processUserVoiceInput(finalSpeech);
} else {
setVoiceSubtitles(VOICE_LOCALES[voiceLang].unheard);
setVoiceSubtitles(getLocaleConfig().unheard);
setTimeout(() => {
startListeningLoop();
}, 1500);
Expand Down Expand Up @@ -455,7 +471,7 @@ export default function ChatScreen() {
rec.onerror = (event: any) => {
console.error('Web speech error:', event);
if (voiceModeActive && voiceState === 'listening') {
setVoiceSubtitles((VOICE_LOCALES as any)[voiceLang].unheard);
setVoiceSubtitles(getLocaleConfig().unheard);
setTimeout(() => { startListeningLoop(); }, 1500);
}
};
Expand All @@ -467,7 +483,7 @@ export default function ChatScreen() {
if (finalSpeech) {
processUserVoiceInput(finalSpeech);
} else {
setVoiceSubtitles((VOICE_LOCALES as any)[voiceLang].unheard);
setVoiceSubtitles(getLocaleConfig().unheard);
setTimeout(() => { startListeningLoop(); }, 1500);
}
}
Expand Down Expand Up @@ -624,7 +640,7 @@ export default function ChatScreen() {
if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current);

setVoiceState('speaking');
const greeting = (VOICE_LOCALES as any)[voiceLang].greeting;
const greeting = getLocaleConfig().greeting;
setVoiceSubtitles(greeting);

const systemMsg: Message = {
Expand All @@ -635,22 +651,43 @@ export default function ChatScreen() {
};
setMessages(prev => [...prev, systemMsg]);

let speakDoneCalled = false;
const handleSpeakDone = () => {
if (speakDoneCalled) return;
speakDoneCalled = true;
if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current);
startListeningLoop();
};

const approxDuration = (greeting.length * 85) + 1200;
voiceInteractionTimer.current = setTimeout(() => {
console.log("Speech.speak greeting safety timeout fired");
handleSpeakDone();
}, approxDuration);

Speech.speak(greeting, {
language: voiceLang,
pitch: 1.0,
rate: 0.9,
onDone: () => { startListeningLoop(); },
onDone: () => { handleSpeakDone(); },
onError: (e) => {
console.error("Speech error:", e);
startListeningLoop();
handleSpeakDone();
}
});
};

const startListeningLoop = async () => {
const hasMicPermission = await requestMicPermission();
if (!hasMicPermission) {
setVoiceSubtitles("Microphone permission is required for voice mode. Please enable microphone access in your settings.");
setVoiceState('paused');
return;
}

LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setVoiceState('listening');
setVoiceSubtitles((VOICE_LOCALES as any)[voiceLang].listening);
setVoiceSubtitles(getLocaleConfig().listening);
latestVoiceSpeechRef.current = '';

const voiceStarted = await startVoiceCapture(voiceLang);
Expand Down Expand Up @@ -738,14 +775,28 @@ export default function ChatScreen() {
};
setMessages(prev => [...prev, aiMessage]);

let speakDoneCalled = false;
const handleSpeakDone = () => {
if (speakDoneCalled) return;
speakDoneCalled = true;
if (voiceInteractionTimer.current) clearTimeout(voiceInteractionTimer.current);
startListeningLoop();
};

const approxDuration = (replyText.length * 85) + 1200;
voiceInteractionTimer.current = setTimeout(() => {
console.log("Speech.speak response safety timeout fired");
handleSpeakDone();
}, approxDuration);

Speech.speak(replyText, {
language: voiceLang,
pitch: 1.0,
rate: 0.9,
onDone: () => { startListeningLoop(); },
onDone: () => { handleSpeakDone(); },
onError: (e) => {
console.error(e);
startListeningLoop();
handleSpeakDone();
}
});
};
Expand Down Expand Up @@ -1128,7 +1179,7 @@ export default function ChatScreen() {
await stopVoiceCapture();
} else {
// Manual selection fallback
const simulatedUserSayings = (VOICE_LOCALES as any)[voiceLang].suggestions;
const simulatedUserSayings = getLocaleConfig().suggestions;
const randomSaying = simulatedUserSayings[Math.floor(Math.random() * simulatedUserSayings.length)];
processUserVoiceInput(randomSaying);
}
Expand Down Expand Up @@ -1160,10 +1211,10 @@ export default function ChatScreen() {

<Text style={styles.voiceStatusText}>
{voiceState === 'listening'
? (VOICE_LOCALES as any)[voiceLang].statusListening
? getLocaleConfig().statusListening
: voiceState === 'thinking'
? (VOICE_LOCALES as any)[voiceLang].thinking
: (VOICE_LOCALES as any)[voiceLang].speaking}
? getLocaleConfig().thinking
: getLocaleConfig().speaking}
</Text>
</View>

Expand All @@ -1186,7 +1237,7 @@ export default function ChatScreen() {
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.suggestionsScroll}
>
{(VOICE_LOCALES as any)[voiceLang].suggestions.map((suggestion: string, idx: number) => (
{getLocaleConfig().suggestions.map((suggestion: string, idx: number) => (
<TouchableOpacity
key={idx}
onPress={() => {
Expand Down Expand Up @@ -1234,7 +1285,7 @@ export default function ChatScreen() {
<Text style={{ color: 'rgba(255,255,255,0.4)', fontSize: 11, textAlign: 'center', paddingHorizontal: 20 }}>
{voiceState === 'listening'
? (isVoiceAvailable || Platform.OS === 'web'
? (VOICE_LOCALES as any)[voiceLang].instruction
? getLocaleConfig().instruction
: "Speech recognition unavailable. Tap a suggestion above or type below.")
: ''}
</Text>
Expand Down Expand Up @@ -1264,8 +1315,9 @@ export default function ChatScreen() {
<Text style={styles.headerTitle}>Swasthya AI Assistant</Text>
<View style={styles.headerActions}>
<TouchableOpacity
onPress={() => {
onPress={async () => {
Keyboard.dismiss();
await requestMicPermission();
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setVoiceModeActive(true);
}}
Expand Down
4 changes: 3 additions & 1 deletion app/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ WebBrowser.maybeCompleteAuthSession();
const KEY_ONBOARDING_DONE = 'onboardingComplete';

export const isOfflineId = (id: string | null | undefined): boolean => {
return !id || id.startsWith('skip-') || id === 'offline-user' || id === 'offline-patient';
if (!id) return true;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return !uuidRegex.test(id);
};

// ── Types ─────────────────────────────────────────────────────────────────────
Expand Down
46 changes: 46 additions & 0 deletions app/services/backend.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import { API_ENDPOINTS, BACKEND_URL } from '@/config/api';
import { useAuthStore } from '@/store/auth.store';
import { supabase } from '@/services/supabaseClient';

const isOfflineId = (id: string | null | undefined): boolean => {
if (!id) return true;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return !uuidRegex.test(id);
};

// Helper to delay response for realistic UI loading states
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

Expand Down Expand Up @@ -89,6 +95,14 @@ export const backendService = {
},

endSession: async (patientId: string, log: any[], existingSummary: string) => {
if (isOfflineId(patientId)) {
return {
daily_summary: "Your daily health metrics are stable. Metformin taken on time. Fasting glucose at 110 mg/dL is within control.",
urgency: "Normal",
key_risks: "None detected",
symptoms_today: ["Anxiety"]
};
}
try {
const response = await fetch(`${BACKEND_URL}/health/daily-summary?patient_id=${patientId}`, {
method: 'POST',
Expand Down Expand Up @@ -249,6 +263,19 @@ export const backendService = {
getSymptoms: async () => {
try {
const patientId = useAuthStore.getState().patientId || 'demo-patient';
if (isOfflineId(patientId)) {
return [
{
id: 'offline-symptom-1',
symptom_name: 'Anxiety',
first_reported_at: new Date().toISOString(),
last_reported_at: new Date().toISOString(),
duration_days: 1,
status: 'active',
severity: 5
}
];
}
const { data, error } = await supabase
.from('symptom_tracker')
.select('*')
Expand All @@ -273,6 +300,25 @@ export const backendService = {
getSummaries: async () => {
try {
const patientId = useAuthStore.getState().patientId || 'demo-patient';
if (isOfflineId(patientId)) {
return [
{
id: 'skip-summary-1',
patient_id: patientId,
summary_date: new Date().toISOString().split('T')[0],
summary_text: 'Your health baseline is stable. Metformin adherence is good, blood glucose is 110 mg/dL.',
symptoms_reported: ['Headache', 'Anxiety'],
facts_mentioned: [],
surgeries_mentioned: [],
medications_mentioned: ['Metformin', 'Amlodipine'],
mood_indicator: 'neutral',
data_importance_score: 5,
chat_messages_count: 3,
important_data_found: false,
created_at: new Date().toISOString(),
}
];
}
const { data, error } = await supabase
.from('daily_health_summaries')
.select('*')
Expand Down
4 changes: 3 additions & 1 deletion app/services/supabase.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { BACKEND_URL, API_ENDPOINTS } from '@/config/api';
import { supabase } from '@/services/supabaseClient';

const isOfflineId = (id: string | null | undefined): boolean => {
return !id || id.startsWith('skip-') || id === 'offline-user' || id === 'offline-patient';
if (!id) return true;
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
return !uuidRegex.test(id);
};

const safeFetchJson = async (url: string, init?: RequestInit) => {
Expand Down
Loading