From ea14ca689d929dd3befe7816a509f5758c92f72e Mon Sep 17 00:00:00 2001 From: indresh404 Date: Wed, 17 Jun 2026 00:01:20 +0530 Subject: [PATCH 01/17] UI changes --- app/app/(onboarding)/chat.tsx | 286 +++-- app/app/(tabs)/aibot/index.tsx | 19 +- app/components/chat/ChatBubble.tsx | 110 +- app/services/api.ts | 32 + app/services/backend.service.ts | 122 +- backend/main.py | 3 +- backend/ml/anomaly_detector.py | 108 -- backend/ml/predict.py | 69 -- backend/ml/train.py | 14 - backend/n8n/All_Workflow.json | 1039 ----------------- backend/n8n/async_qna_loop.json | 121 -- backend/n8n/doctor_agent.json | 112 -- backend/n8n/medicine_safety.json | 134 --- backend/n8n/patient_agent.json | 760 ------------ backend/n8n/safety_agent.json | 91 -- backend/prompts/importance_detector.py | 68 ++ backend/rag/embedder.py | 110 -- backend/rag/guidelines/icmr_diabetes_2022.pdf | Bin 1691 -> 0 bytes backend/rag/guidelines/who_cardiovascular.pdf | Bin 1715 -> 0 bytes backend/rag/guidelines/who_hypertension.pdf | Bin 1676 -> 0 bytes backend/rag/retriever.py | 47 - backend/requirements.txt | 4 +- backend/routes/health_chat.py | 282 +++++ backend/routes/risk.py | 12 +- backend/services/neo4j_health_service.py | 270 +++++ backend/test_hf_load.py | 30 - database/schema.sql | 139 ++- 27 files changed, 1193 insertions(+), 2789 deletions(-) create mode 100644 app/services/api.ts delete mode 100644 backend/ml/anomaly_detector.py delete mode 100644 backend/ml/predict.py delete mode 100644 backend/ml/train.py delete mode 100644 backend/n8n/All_Workflow.json delete mode 100644 backend/n8n/async_qna_loop.json delete mode 100644 backend/n8n/doctor_agent.json delete mode 100644 backend/n8n/medicine_safety.json delete mode 100644 backend/n8n/patient_agent.json delete mode 100644 backend/n8n/safety_agent.json create mode 100644 backend/prompts/importance_detector.py delete mode 100644 backend/rag/embedder.py delete mode 100644 backend/rag/guidelines/icmr_diabetes_2022.pdf delete mode 100644 backend/rag/guidelines/who_cardiovascular.pdf delete mode 100644 backend/rag/guidelines/who_hypertension.pdf delete mode 100644 backend/rag/retriever.py create mode 100644 backend/routes/health_chat.py create mode 100644 backend/services/neo4j_health_service.py delete mode 100644 backend/test_hf_load.py diff --git a/app/app/(onboarding)/chat.tsx b/app/app/(onboarding)/chat.tsx index fe77e57..1e8edec 100644 --- a/app/app/(onboarding)/chat.tsx +++ b/app/app/(onboarding)/chat.tsx @@ -40,6 +40,7 @@ interface Message { text: string; isUser: boolean; timestamp: Date; + saveStatus?: any; } interface AgentThought { @@ -137,6 +138,7 @@ export default function ChatScreen() { // History states const [showHistory, setShowHistory] = useState(false); + const [historyList, setHistoryList] = useState([]); const [expandedDays, setExpandedDays] = useState>({}); const slideAnim = useRef(new Animated.Value(0)).current; const { width: screenWidth } = Dimensions.get('window'); @@ -506,7 +508,7 @@ export default function ChatScreen() { }; try { - const response = await fetch(`${BACKEND_URL}${API_ENDPOINTS.CHAT.MESSAGE}`, { + const response = await fetch(`${BACKEND_URL}/health/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -518,7 +520,7 @@ export default function ChatScreen() { }); const data = await response.json(); const reply = data.bot_reply || (voiceLang === 'hi-IN' ? getFallbackReplyHindi(spokenText) : getFallbackReply(spokenText)); - speakAIVoiceResponse(reply); + speakAIVoiceResponse(reply, data.save_status || undefined); } catch (e) { console.error(e); const fallback = voiceLang === 'hi-IN' ? getFallbackReplyHindi(spokenText) : getFallbackReply(spokenText); @@ -526,7 +528,7 @@ export default function ChatScreen() { } }; - const speakAIVoiceResponse = (replyText: string) => { + const speakAIVoiceResponse = (replyText: string, saveStatus?: any) => { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setVoiceState('speaking'); setVoiceSubtitles(replyText); @@ -535,7 +537,8 @@ export default function ChatScreen() { id: 'voice-ai-' + Date.now(), text: replyText, isUser: false, - timestamp: new Date() + timestamp: new Date(), + saveStatus: saveStatus }; setMessages(prev => [...prev, aiMessage]); @@ -571,9 +574,57 @@ export default function ChatScreen() { useEffect(() => { if (user) { fetchUserName(); + fetchHistorySummaries(); } }, [user]); + const fetchHistorySummaries = async () => { + try { + const activePatientId = user?.id || 'demo-patient'; + const { data, error } = await supabase + .from('daily_health_summaries') + .select('*') + .eq('patient_id', activePatientId) + .order('summary_date', { ascending: false }); + + if (error) throw error; + if (data && data.length > 0) { + const formatted: HistoryItem[] = data.map((item) => { + const dateObj = new Date(item.summary_date); + const symptoms = item.symptoms_reported || []; + const facts = item.facts_mentioned || []; + const meds = item.medications_mentioned || []; + const surgeries = item.surgeries_mentioned || []; + + return { + id: item.id, + date: dateObj.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), + time: 'Daily Sync', + shortDate: dateObj.toLocaleDateString('en-US', { day: 'numeric', month: 'short' }), + overallSummary: item.summary_text || 'No summary text recorded.', + agents: [ + { + name: 'Clinical Extractor', + role: 'Data Miner', + thought: `Extracted details from today's chat. Identified ${symptoms.length} symptoms, ${meds.length} medications, ${facts.length} habits.` + }, + { + name: 'Neo4j Sync Agent', + role: 'Graph Database Writer', + thought: `Successfully committed ${symptoms.length + meds.length + facts.length + surgeries.length} items to AuraDB knowledge graph.` + } + ] + }; + }); + setHistoryList(formatted); + } else { + setHistoryList([]); + } + } catch (err) { + console.error('fetchHistorySummaries error:', err); + } + }; + const fetchUserName = async () => { const { data } = await supabase .from('patients') @@ -648,7 +699,7 @@ export default function ChatScreen() { pending_doctor_questions: [] }; - fetch(`${BACKEND_URL}${API_ENDPOINTS.CHAT.MESSAGE}`, { + fetch(`${BACKEND_URL}/health/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -666,6 +717,7 @@ export default function ChatScreen() { text: data.bot_reply || getFallbackReply(currentInput), isUser: false, timestamp: new Date(), + saveStatus: data.save_status || undefined }; setMessages(prev => [...prev, aiResponse]); }) @@ -713,13 +765,57 @@ export default function ChatScreen() { )} - - - {item.text} - - - {formatTime(item.timestamp)} - + + + + {item.text} + + + {formatTime(item.timestamp)} + + + + {/* Save Status Graph Card */} + {item.saveStatus && ( + + ✅ {item.saveStatus.action || "Saved to health graph"} + {item.saveStatus.message} + + {item.saveStatus.saved_data && Object.keys(item.saveStatus.saved_data).length > 0 && ( + + {Object.entries(item.saveStatus.saved_data).map(([key, list]) => { + if (!Array.isArray(list) || list.length === 0) return null; + return ( + + + {key.charAt(0).toUpperCase() + key.slice(1)}: + + {list.map((subItem, idx) => ( + • {subItem} + ))} + + ); + })} + + )} + + {item.saveStatus.importance_score !== undefined && ( + + + Graph Importance Score: {item.saveStatus.importance_score}/10 + + + + + + )} + + )} {item.isUser && ( @@ -765,65 +861,68 @@ export default function ChatScreen() { contentContainerStyle={styles.historyContent} showsVerticalScrollIndicator={false} > - {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} - - + {(() => { + const displayedHistory = historyList.length > 0 ? historyList : MOCK_HISTORY; + return displayedHistory.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}) + {item.overallSummary} + + {isExpanded && ( + + + Agent Diagnostics + + {item.agents.map((agent, aIdx) => ( + + + + {agent.name} + ({agent.role}) + + {agent.thought} - {agent.thought} - - ))} + ))} + + )} + + + {/* Right Column: Timeline line and node */} + + + + - )} - - - {/* Right Column: Timeline line and node */} - - - - + {item.shortDate} - {item.shortDate} - - ); - })} + ); + }); + })()} {/* Action Button at the Bottom */} @@ -1021,6 +1120,7 @@ export default function ChatScreen() { onPress={() => { Keyboard.dismiss(); LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + fetchHistorySummaries(); setShowHistory(true); }} style={styles.historyButton} @@ -1617,4 +1717,60 @@ const styles = StyleSheet.create({ fontSize: 15, fontWeight: '700', }, + savedDataCard: { + marginTop: 8, + backgroundColor: '#E8F5E9', + borderLeftWidth: 4, + borderLeftColor: '#27AE60', + padding: 8, + borderRadius: 8, + width: '100%', + }, + savedTitle: { + fontSize: 12, + fontWeight: '600', + color: '#27AE60', + marginBottom: 4, + }, + savedMessage: { + fontSize: 11, + color: '#475569', + marginBottom: 6, + }, + detailsContainer: { + marginTop: 4, + }, + detailItem: { + marginBottom: 4, + }, + detailLabel: { + fontSize: 11, + fontWeight: '600', + color: '#1E293B', + marginBottom: 2, + }, + detailValue: { + fontSize: 11, + color: '#475569', + marginLeft: 6, + }, + scoreBar: { + marginTop: 6, + }, + scoreText: { + fontSize: 10, + color: '#64748B', + marginBottom: 2, + }, + track: { + height: 4, + backgroundColor: '#CBD5E1', + borderRadius: 2, + width: '100%', + }, + scoreIndicator: { + height: 4, + backgroundColor: '#27AE60', + borderRadius: 2, + }, }); \ No newline at end of file diff --git a/app/app/(tabs)/aibot/index.tsx b/app/app/(tabs)/aibot/index.tsx index 24f8922..426e02d 100644 --- a/app/app/(tabs)/aibot/index.tsx +++ b/app/app/(tabs)/aibot/index.tsx @@ -16,11 +16,13 @@ interface ChatMessage { id: string; role: 'user' | 'assistant'; content: string; + timestamp?: Date; + saveStatus?: any; } export default function AIBotScreen() { const [text, setText] = useState(''); - const [messages, setMessages] = useState([{ id: '1', role: 'assistant', content: 'I am online and ready to help with your health questions.' }]); + const [messages, setMessages] = useState([{ id: '1', role: 'assistant', content: 'I am online and ready to help with your health questions.', timestamp: new Date() }]); const [isLoading, setIsLoading] = useState(false); const [userTurnCount, setUserTurnCount] = useState(0); const { user } = useAuthStore(); @@ -86,7 +88,7 @@ export default function AIBotScreen() { const handleSend = async () => { if (!text.trim() || isLoading) return; - const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: text }; + const userMsg: ChatMessage = { id: Date.now().toString(), role: 'user', content: text, timestamp: new Date() }; const updatedMessages = [...messages, userMsg]; setMessages(updatedMessages); setText(''); @@ -109,7 +111,9 @@ export default function AIBotScreen() { const botMsg: ChatMessage = { id: Date.now().toString(), role: 'assistant' as const, - content: res.bot_reply + content: res.bot_reply, + timestamp: new Date(), + saveStatus: res.save_status || undefined }; const finalMessages = [...updatedMessages, botMsg]; setMessages(finalMessages); @@ -149,7 +153,14 @@ export default function AIBotScreen() { } + renderItem={({ item }) => ( + + )} keyExtractor={(item) => item.id} contentContainerStyle={styles.messageList} style={styles.flatList} diff --git a/app/components/chat/ChatBubble.tsx b/app/components/chat/ChatBubble.tsx index d72a3e5..31ba376 100644 --- a/app/components/chat/ChatBubble.tsx +++ b/app/components/chat/ChatBubble.tsx @@ -9,9 +9,18 @@ interface ChatBubbleProps { role: 'user' | 'assistant'; content: string; timestamp?: Date; + saveStatus?: { + action: string; + message: string; + symptoms_created?: string[]; + symptoms_updated?: string[]; + symptoms_resolved?: string[]; + saved_data?: Record; + importance_score?: number; + }; } -export const ChatBubble: React.FC = ({ role, content, timestamp }) => { +export const ChatBubble: React.FC = ({ role, content, timestamp, saveStatus }) => { const isUser = role === 'user'; return ( @@ -37,6 +46,49 @@ export const ChatBubble: React.FC = ({ role, content, timestamp {content} )} + + {/* Save Status Graph Card */} + {saveStatus && ( + + ✅ {saveStatus.action || "Saved to health graph"} + {saveStatus.message} + + {saveStatus.saved_data && Object.keys(saveStatus.saved_data).length > 0 && ( + + {Object.entries(saveStatus.saved_data).map(([key, list]) => { + if (!Array.isArray(list) || list.length === 0) return null; + return ( + + + {key.charAt(0).toUpperCase() + key.slice(1)}: + + {list.map((item, idx) => ( + • {item} + ))} + + ); + })} + + )} + + {saveStatus.importance_score !== undefined && ( + + + Graph Importance Score: {saveStatus.importance_score}/10 + + + + + + )} + + )} + {timestamp && ( {timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} @@ -105,4 +157,60 @@ const styles = StyleSheet.create({ color: COLORS.gray[400], textAlign: 'left', }, + savedDataCard: { + marginTop: SPACING.sm, + backgroundColor: '#E8F5E9', + borderLeftWidth: 4, + borderLeftColor: '#27AE60', + padding: SPACING.sm, + borderRadius: 8, + width: '100%', + }, + savedTitle: { + fontSize: 12, + fontWeight: '600', + color: '#27AE60', + marginBottom: 4, + }, + savedMessage: { + fontSize: 11, + color: '#475569', + marginBottom: 6, + }, + detailsContainer: { + marginTop: 4, + }, + detailItem: { + marginBottom: 4, + }, + detailLabel: { + fontSize: 11, + fontWeight: '600', + color: '#1E293B', + marginBottom: 2, + }, + detailValue: { + fontSize: 11, + color: '#475569', + marginLeft: 6, + }, + scoreBar: { + marginTop: 6, + }, + scoreText: { + fontSize: 10, + color: '#64748B', + marginBottom: 2, + }, + track: { + height: 4, + backgroundColor: '#CBD5E1', + borderRadius: 2, + width: '100%', + }, + scoreIndicator: { + height: 4, + backgroundColor: '#27AE60', + borderRadius: 2, + }, }); \ No newline at end of file diff --git a/app/services/api.ts b/app/services/api.ts new file mode 100644 index 0000000..eabc2a4 --- /dev/null +++ b/app/services/api.ts @@ -0,0 +1,32 @@ +import { BACKEND_URL } from '../config/api'; + +export const api = { + get: async (endpoint: string) => { + const response = await fetch(`${BACKEND_URL}${endpoint}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`GET Request to ${endpoint} failed: ${response.statusText}`); + } + const data = await response.json(); + return { data }; + }, + + post: async (endpoint: string, body?: any) => { + const response = await fetch(`${BACKEND_URL}${endpoint}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!response.ok) { + throw new Error(`POST Request to ${endpoint} failed: ${response.statusText}`); + } + const data = await response.json(); + return { data }; + }, +}; diff --git a/app/services/backend.service.ts b/app/services/backend.service.ts index a25a82e..45d0b7e 100644 --- a/app/services/backend.service.ts +++ b/app/services/backend.service.ts @@ -1,5 +1,6 @@ -// app/services/backend.service.ts -import { API_ENDPOINTS } from '@/config/api'; +import { API_ENDPOINTS, BACKEND_URL } from '@/config/api'; +import { useAuthStore } from '@/store/auth.store'; +import { supabase } from '@/services/supabaseClient'; // Helper to delay response for realistic UI loading states const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); @@ -39,15 +40,29 @@ export const backendService = { }; }, - // Chat End Session endSession: async (patientId: string, log: any[], existingSummary: string) => { - await delay(1500); - return { - daily_summary: "Overall stable condition. Active monitor shows mild heart rate elevation after climbing stairs, which normalized quickly. Recommended to maintain hydration and consistent medication intake.", - urgency: "Normal", - key_risks: "None detected", - symptoms_today: [] as string[] - }; + try { + const response = await fetch(`${BACKEND_URL}/health/daily-summary?patient_id=${patientId}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + if (!response.ok) throw new Error("Daily summary request failed"); + const result = await response.json(); + return { + daily_summary: result.summary || "Summary generated successfully.", + urgency: "Normal", + key_risks: "None detected", + symptoms_today: (result.symptoms_reported || []) as string[] + }; + } catch (e) { + console.error("endSession API error:", e); + return { + daily_summary: "Summary could not be synchronized with server.", + urgency: "Normal", + key_risks: "None detected", + symptoms_today: [] + }; + } }, // Risk Scoring @@ -140,29 +155,36 @@ export const backendService = { // Main Chat sendMessage: async (patientId: string, message: string, context: any) => { - await delay(1000); - let bot_reply = "I'm your AI health assistant. Everything is running in offline demo mode. Let me know how I can assist you with your medications, health tracking, or symptoms!"; - - const lowerMsg = message.toLowerCase(); - if (lowerMsg.includes('heart') || lowerMsg.includes('bp') || lowerMsg.includes('pulse')) { - bot_reply = "Your heart rate and blood pressure trends appear stable based on latest entries. If you notice any sudden chest pain, shortness of breath, or dizziness, please consult a physician immediately."; - } else if (lowerMsg.includes('med') || lowerMsg.includes('pill') || lowerMsg.includes('drug') || lowerMsg.includes('alternative')) { - bot_reply = "Remember to take your medications on schedule. You can check for generic alternatives at local Jan Aushadhi Kendras to save up to 80% on brand prescriptions."; - } else if (lowerMsg.includes('risk') || lowerMsg.includes('score')) { - bot_reply = "Your health risk profile is classified as Moderate risk. To improve your score, focus on regular cardiovascular workouts, diet tracking, and consistent sleep cycles."; + try { + const response = await fetch(`${BACKEND_URL}/health/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ patient_id: patientId, message }) + }); + if (!response.ok) throw new Error("Chat message request failed"); + const res = await response.json(); + + return { + bot_reply: res.ai_reply, + medical_event: res.saving, + save_status: res.saving ? { + action: "Saved to health graph", + symptoms_created: res.saved_data?.symptoms || [], + symptoms_updated: [], + symptoms_resolved: [], + message: `Saved to Neo4j graph (Score: ${res.importance_score}/10)` + } : null + }; + } catch (e) { + console.error("sendMessage API error:", e); + return { + bot_reply: "I am having trouble communicating with the backend. Please check connection.", + medical_event: false, + save_status: null + }; } - - return { - bot_reply, - extracted_symptom: null, - clarification_needed: false, - save_ready: false, - confirmation_required: false, - session_updated: false - }; }, - // Medical Report Extraction extractReport: async (fileUri: string, fileName: string, fileType: string) => { await delay(2000); return { @@ -174,5 +196,45 @@ export const backendService = { recommendations: "Continue a balanced diet. Repeat lipid profile in 6 months." } }; + }, + + getSymptoms: async () => { + try { + const patientId = useAuthStore.getState().patientId || 'demo-patient'; + const { data, error } = await supabase + .from('symptom_tracker') + .select('*') + .eq('user_id', patientId) + .order('last_reported_at', { ascending: false }); + if (error) throw error; + return (data || []).map(item => ({ + id: item.id, + symptom_name: item.symptom_name, + first_reported_at: item.first_reported_at, + last_reported_at: item.last_reported_at, + duration_days: item.reported_duration_days || 0, + status: item.status, + severity: item.current_severity || 5 + })); + } catch (e) { + console.error("getSymptoms error:", e); + return []; + } + }, + + getSummaries: async () => { + try { + const patientId = useAuthStore.getState().patientId || 'demo-patient'; + const { data, error } = await supabase + .from('daily_health_summaries') + .select('*') + .eq('patient_id', patientId) + .order('summary_date', { ascending: false }); + if (error) throw error; + return data || []; + } catch (e) { + console.error("getSummaries error:", e); + return []; + } } }; diff --git a/backend/main.py b/backend/main.py index a08be53..04ef830 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from routes import auth, profiles, chat +from routes import auth, profiles, chat, health_chat import os app = FastAPI( @@ -34,6 +34,7 @@ async def health_check(): app.include_router(auth.router) app.include_router(profiles.router) app.include_router(chat.router) +app.include_router(health_chat.router) @app.get("/") async def root(): diff --git a/backend/ml/anomaly_detector.py b/backend/ml/anomaly_detector.py deleted file mode 100644 index dab90e1..0000000 --- a/backend/ml/anomaly_detector.py +++ /dev/null @@ -1,108 +0,0 @@ -import numpy as np -from sklearn.ensemble import IsolationForest -import joblib -import os - -MODEL_DIR = "ml/saved_models" -os.makedirs(MODEL_DIR, exist_ok=True) - -def train_model(patient_id: str, metric: str, baseline_values: list[float]) -> str: - """ - Train an Isolation Forest on 14-day baseline readings. - Unsupervised — no labels needed. - Learns what 'normal' looks like for this specific patient and metric. - contamination=0.05 means it expects ~5% of readings to be anomalies. - Minimum 7 data points required to train. Returns model path. - """ - if len(baseline_values) < 7: - return None - - X = np.array(baseline_values).reshape(-1, 1) - model = IsolationForest( - n_estimators=100, - contamination=0.05, - random_state=42 - ) - model.fit(X) - path = f"{MODEL_DIR}/{patient_id}_{metric}.joblib" - joblib.dump(model, path) - return path - -def predict_anomaly(patient_id: str, metric: str, - current_values: list[float], - baseline_14day: list[float]) -> dict: - """ - Hybrid detection: Isolation Forest + statistical fallback. - Both are run. Result is the union — anomaly if either fires. - Returns detection_source so caller knows which method triggered. - - Isolation Forest: -1 = anomaly, 1 = normal. - score_samples(): more negative = more anomalous (range ~-0.7 to -0.1). - confidence normalized to 0–1 from raw score magnitude. - """ - path = f"{MODEL_DIR}/{patient_id}_{metric}.joblib" - - # --- Statistical check (always runs) --- - mean_b = float(np.mean(baseline_14day)) - std_b = float(np.std(baseline_14day)) - stat_anomaly = all(v > mean_b + 1.5 * std_b for v in current_values) if std_b > 0 else False - stat_deviation = ( - (current_values[-1] - mean_b) / std_b if std_b > 0 else 0.0 - ) - - # --- ML check (runs only if model exists) --- - if not os.path.exists(path): - return { - "anomaly_detected": stat_anomaly, - "ml_anomaly": False, - "stat_anomaly": stat_anomaly, - "anomaly_score": round(stat_deviation, 4), - "ml_confidence": 0.0, - "days_elevated": sum( - 1 for v in current_values if v > mean_b + 1.5 * std_b - ) if std_b > 0 else 0, - "detection_source": "statistical_fallback" if stat_anomaly else "none", - "mean_baseline": round(mean_b, 2), - "std_baseline": round(std_b, 2), - } - - model = joblib.load(path) - X = np.array([[current_values[-1]]]) - prediction = model.predict(X)[0] - raw_score = model.score_samples(X)[0] - ml_confidence = min(1.0, max(0.0, abs(raw_score) * 2)) - ml_anomaly = prediction == -1 and ml_confidence > 0.6 - - # Final decision: stat OR high-confidence ML - anomaly_detected = stat_anomaly or ml_anomaly - - source = "isolation_forest" if ml_anomaly else ( - "statistical_fallback" if stat_anomaly else "none" - ) - - return { - "anomaly_detected": anomaly_detected, - "ml_anomaly": ml_anomaly, - "stat_anomaly": stat_anomaly, - "anomaly_score": round(float(raw_score), 4), - "ml_confidence": round(ml_confidence, 2), - "days_elevated": sum( - 1 for v in current_values if v > mean_b + 1.5 * std_b - ) if std_b > 0 else 0, - "detection_source": source, - "mean_baseline": round(mean_b, 2), - "std_baseline": round(std_b, 2), - } - -def retrain_if_stale(patient_id: str, metric: str, - baseline_values: list[float], - days_since_train: int) -> bool: - """ - Retrain if model is 7+ days old or does not exist. - Returns True if retrained, False if skipped. - """ - path = f"{MODEL_DIR}/{patient_id}_{metric}.joblib" - if days_since_train >= 7 or not os.path.exists(path): - result = train_model(patient_id, metric, baseline_values) - return result is not None - return False diff --git a/backend/ml/predict.py b/backend/ml/predict.py deleted file mode 100644 index f7353e3..0000000 --- a/backend/ml/predict.py +++ /dev/null @@ -1,69 +0,0 @@ -import numpy as np -from sklearn.linear_model import LinearRegression - -def calculate_trajectory( - risk_scores_history: list[dict], # [{date, final_score}] oldest first - symptom_history: list[dict], # [{date, severity}] -) -> dict: - """ - Pure Python. No LLM. No ML model needed. - Calculates slope, volatility, trajectory class, and 7-day projection. - - score_slope > 0 = worsening (score going up) - score_slope < 0 = improving (score going down) - """ - if len(risk_scores_history) < 3: - return { - "score_slope": 0.0, - "volatility": 0.0, - "trajectory": "Stable", - "projected_scores": [risk_scores_history[-1]["final_score"]] * 7 - if risk_scores_history else [0] * 7, - "symptom_frequency_ratio": 1.0, - "data_days_used": len(risk_scores_history), - } - - scores = [r["final_score"] for r in risk_scores_history] - days = np.arange(len(scores)).reshape(-1, 1) - - # Linear regression slope - reg = LinearRegression().fit(days, scores) - slope = float(reg.coef_[0]) - - # Volatility - volatility = float(np.std(scores)) - - # Symptom frequency: last 7 days vs prior 7 days - recent_symptom_days = len(set( - s["date"] for s in symptom_history[-7:] - )) - prior_symptom_days = len(set( - s["date"] for s in symptom_history[-14:-7] - )) - ratio = (recent_symptom_days / max(prior_symptom_days, 1)) - - # Trajectory classification - if slope > 3 and ratio > 1.2: - trajectory = "Worsening" - elif slope < -3 and ratio < 0.8: - trajectory = "Improving" - elif volatility >= 10: - trajectory = "Unstable" - else: - trajectory = "Stable" - - # 7-day projection - current = scores[-1] - projected = [ - int(min(100, max(0, current + slope * d))) - for d in range(1, 8) - ] - - return { - "score_slope": round(slope, 2), - "volatility": round(volatility, 2), - "trajectory": trajectory, - "projected_scores": projected, - "symptom_frequency_ratio": round(ratio, 2), - "data_days_used": len(risk_scores_history), - } diff --git a/backend/ml/train.py b/backend/ml/train.py deleted file mode 100644 index 4b8d2be..0000000 --- a/backend/ml/train.py +++ /dev/null @@ -1,14 +0,0 @@ -from ml.anomaly_detector import retrain_if_stale - -def run_training(patient_id: str, metric: str, baseline_values: list[float], days_since_last_train: int) -> dict: - """ - Wrapper to call retrain_if_stale and return status. - Called by /ml/train-patient-model route. - """ - trained = retrain_if_stale(patient_id, metric, baseline_values, days_since_last_train) - - return { - "trained": trained, - "model_path": f"ml/saved_models/{patient_id}_{metric}.joblib" if trained else None, - "skipped_reason": "Insufficient data or model not stale" if not trained else None - } diff --git a/backend/n8n/All_Workflow.json b/backend/n8n/All_Workflow.json deleted file mode 100644 index 103263a..0000000 --- a/backend/n8n/All_Workflow.json +++ /dev/null @@ -1,1039 +0,0 @@ -{ - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "supabase-webhook-qna", - "responseMode": "responseNode", - "options": {} - }, - "id": "e4476327-bece-491d-86eb-23a62259d24f", - "name": "Supabase Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [576, 528], - "webhookId": "bf645229-3e27-46c5-bd74-d26c5643d442" - }, - { - "parameters": { - "conditions": { - "string": [ - { - "value1": "={{$json.body.new.status}}", - "value2": "answered" - } - ] - } - }, - "id": "4ace1e26-e27b-4337-a2b1-db867d933dcc", - "name": "IF — Is Answered?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [784, 528] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/pending_checkin_questions?id=eq.{{$json.body.new.id}}&select=*,patients(name,doctor_email,doctor_fcm_token,doctor_id)&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "ba2c5558-39c7-4a85-8454-f0173ef220fb", - "name": "Supabase — GET Doctor Info", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [992, 448] - }, - { - "parameters": { - "method": "POST", - "url": "https://fcm.googleapis.com/fcm/send", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Authorization", - "value": "key={{$env.FCM_SERVER_KEY}}" - }, - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "{\n \"to\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patients.doctor_fcm_token}}\",\n \"notification\": {\n \"title\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patients.name}} answered your query\",\n \"body\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patient_answer}}\"\n },\n \"data\": {\n \"patient_id\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patient_id}}\",\n \"question_id\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].id}}\",\n \"action\": \"view_answer\"\n }\n}", - "options": {} - }, - "id": "efd58d6f-5c7e-403a-9c2d-f718ade82c57", - "name": "FCM — Notify Doctor", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1184, 448] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/qna_response_log", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "question_id", - "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].id}}" - }, - { - "name": "patient_id", - "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].patient_id}}" - }, - { - "name": "doctor_id", - "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].patients.doctor_id}}" - }, - { - "name": "original_question", - "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].original_doctor_question}}" - }, - { - "name": "patient_answer", - "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].patient_answer}}" - }, - { - "name": "notified_at", - "value": "={{$now}}" - } - ] - }, - "options": {} - }, - "id": "log-qna", - "name": "Supabase — Log QnA", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1184, 560] - }, - { - "parameters": { - "httpMethod": "POST", - "path": "doctor", - "responseMode": "responseNode", - "options": {} - }, - "id": "332c25ed-163a-4fb9-b006-3686044d6ac0", - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [1216, 736], - "webhookId": "eaff1270-ac25-443f-958e-d193de0733fa" - }, - { - "parameters": { - "dataPropertyName": "action", - "rules": { - "rules": [ - { - "value": "view" - }, - { - "value": "ask" - }, - { - "value": "family" - }, - { - "value": "briefing" - } - ] - } - }, - "id": "3b251550-0270-4753-845a-186b2473e369", - "name": "Switch", - "type": "n8n-nodes-base.switch", - "typeVersion": 1, - "position": [1424, 736] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/agent/doctor-answer", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "{\n \"question\": \"{{$json.question}}\",\n \"patient_id\": \"{{$json.patient_id}}\",\n \"full_context\": {\n \"all_daily_summaries\": {{$node[\"Supabase — GET Full Context\"].json.daily_summaries}},\n \"profile_summary\": \"{{$node[\"Supabase — GET Full Context\"].json.profile_summary}}\",\n \"symptom_records\": {{$node[\"Supabase — GET Full Context\"].json.symptoms}},\n \"adherence_log\": {{$node[\"Supabase — GET Full Context\"].json.adherence}},\n \"wearable_flags\": {{$node[\"Supabase — GET Full Context\"].json.wearable_flags}},\n \"prediction_data\": {{$node[\"Supabase — GET Full Context\"].json.prediction}}\n }\n}", - "options": {} - }, - "id": "79ff0afc-b79e-4de6-a99e-b42b638d492f", - "name": "Python — /agent/doctor-answer", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1680, 736] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{$json}}", - "options": {} - }, - "id": "82b7453f-af5d-44e1-b290-4fd79b8fff67", - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [1968, 736] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/safety/drug-interaction", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "{\n \"new_medicine\": \"{{$json.new_medicine}}\",\n \"active_medicines\": {{$node[\"Supabase — GET Active Meds\"].json}},\n \"patient_conditions\": {{$node[\"Supabase — GET Patient Conditions\"].json[0].conditions}}\n}", - "options": {} - }, - "id": "76932a83-6839-4535-ad5f-b9b8c1ef0a90", - "name": "Python — /safety/drug-interaction", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1600, 1104] - }, - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.conflict_found}}", - "value2": true - } - ] - } - }, - "id": "709c7785-1f6d-449b-91df-b730b76fa1d7", - "name": "IF — Conflict?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [1808, 1104] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={\n \"blocked\": true,\n \"warning\": \"{{$json.warning_text}}\",\n \"severity_label\": \"{{$json.severity_label}}\",\n \"medicine_risk_score\": {{$json.medicine_risk_score}},\n \"recommendation\": \"{{$json.recommendation}}\",\n \"source\": \"{{$json.source}}\"\n}", - "options": {} - }, - "id": "7b67fb84-77bd-4f2f-a55a-459628cd428c", - "name": "Respond (Blocking)", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [2016, 1040] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/medicines", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "patient_id", - "value": "={{$node[\"Webhook1\"].json.patient_id}}" - }, - { - "name": "medicine_name", - "value": "={{$node[\"Webhook1\"].json.new_medicine}}" - }, - { - "name": "prescribed_at", - "value": "={{$now}}" - }, - { - "name": "is_active", - "value": true - }, - { - "name": "conflict_overridden", - "value": false - } - ] - }, - "options": {} - }, - "id": "61fdbfdd-3b7a-4d3c-92ea-9d1bec34e4a8", - "name": "Supabase — POST Medicine", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [2016, 1200] - }, - { - "parameters": { - "httpMethod": "POST", - "path": "medicine/add", - "responseMode": "responseNode", - "options": {} - }, - "id": "458655b7-484f-4989-a1a7-aa10b60ab9da", - "name": "Webhook1", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [1216, 1104], - "webhookId": "41c74126-0585-4fa1-8b11-ac67b4f04f84" - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/medicines?patient_id=eq.{{$json.patient_id}}&is_active=eq.true", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "meds-get", - "name": "Supabase — GET Active Meds", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1424, 1104] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patients?patient_id=eq.{{$json.patient_id}}&select=conditions&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "conditions-get", - "name": "Supabase — GET Patient Conditions", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1424, 1168] - }, - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.endChat}}", - "value2": true - } - ] - } - }, - "id": "2d61d32d-b250-4939-816a-9ca22d498cae", - "name": "IF — endChat?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [-320, 960] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patients?patient_id=eq.{{$json.patient_id}}&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "f1468a53-d29c-4f78-9429-4e15a19a63b3", - "name": "Supabase — GET Profile", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-112, 816] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/pending_checkin_questions?patient_id=eq.{{$json.patient_id}}&status=eq.pending", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "pending-get", - "name": "Supabase — GET Pending Questions", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-112, 880] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/chat/message", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "{\n \"message\": \"{{$node[\"Webhook2\"].json.message}}\",\n \"patient_id\": \"{{$node[\"Webhook2\"].json.patient_id}}\",\n \"session_id\": \"{{$node[\"Webhook2\"].json.session_id}}\",\n \"patient_context\": {\n \"rolling_summary\": \"{{$node[\"Supabase — GET Profile\"].json[0].rolling_summary}}\",\n \"profile_summary\": \"{{$node[\"Supabase — GET Profile\"].json[0].profile_summary}}\",\n \"last_7_summaries\": {{$node[\"Supabase — GET Profile\"].json[0].last_7_summaries}},\n \"active_medications\": {{$node[\"Supabase — GET Profile\"].json[0].medications}},\n \"pending_doctor_questions\": {{$node[\"Supabase — GET Pending Questions\"].json}}\n }\n}", - "options": {} - }, - "id": "9ee5fb3f-f935-435b-a8bf-f16f420acccd", - "name": "Python — /chat/message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [80, 816] - }, - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.save_ready}}", - "value2": true - } - ] - } - }, - "id": "a09dfdad-5e4a-4757-8c94-5f8475b01e36", - "name": "IF — save_ready?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [288, 816] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/symptoms", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "patient_id", - "value": "={{$node[\"Webhook2\"].json.patient_id}}" - }, - { - "name": "symptom_name", - "value": "={{$json.extracted_symptom.symptom}}" - }, - { - "name": "body_zone", - "value": "={{$json.extracted_symptom.body_zone}}" - }, - { - "name": "severity", - "value": "={{$json.extracted_symptom.severity}}" - }, - { - "name": "onset", - "value": "={{$json.extracted_symptom.onset}}" - }, - { - "name": "session_id", - "value": "={{$node[\"Webhook2\"].json.session_id}}" - }, - { - "name": "created_at", - "value": "={{$now}}" - } - ] - }, - "options": {} - }, - "id": "4c34094e-941e-4567-8efb-07428a1af614", - "name": "Supabase — POST Symptom", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [480, 752] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/agent/escalate", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "Content-Type", - "value": "application/json" - } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "{\n \"symptoms\": [{{$json.extracted_symptom}}],\n \"patient_conditions\": {{$node[\"Supabase — GET Profile\"].json[0].conditions}},\n \"age\": {{$node[\"Supabase — GET Profile\"].json[0].age}},\n \"wearable_flags\": {{$node[\"Supabase — GET Profile\"].json[0].wearable_flags}},\n \"message_context\": \"{{$node[\"Webhook2\"].json.message}}\"\n}", - "options": {} - }, - "id": "escalate", - "name": "Python — /agent/escalate", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [480, 880] - }, - { - "parameters": { - "httpMethod": "POST", - "path": "chat", - "responseMode": "responseNode", - "options": {} - }, - "id": "4c4bd5c5-0c38-4449-a6a7-8121b767dab2", - "name": "Webhook2", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [-512, 960], - "webhookId": "9d2412c2-3dd5-416f-8b97-b94b93fc42dd" - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={\n \"reply\": \"{{$node[\"Python — /chat/message\"].json.bot_reply}}\",\n \"session_id\": \"{{$node[\"Webhook2\"].json.session_id}}\",\n \"clarification_needed\": {{$node[\"Python — /chat/message\"].json.clarification_needed}},\n \"confirmation_required\": {{$node[\"Python — /chat/message\"].json.confirmation_required}}\n}", - "options": {} - }, - "id": "9c6c3276-fdaa-4d6d-ab4a-e4a1a7267ce0", - "name": "Respond to Webhook1", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [640, 928] - }, - { - "parameters": { - "rule": { - "interval": [ - { - "field": "cronExpression", - "expression": "0 8 * * *" - } - ] - } - }, - "id": "8311fabe-e601-4bf8-95b9-ce7a2882be9f", - "name": "Schedule — 8AM Check-in", - "type": "n8n-nodes-base.scheduleTrigger", - "typeVersion": 1.1, - "position": [96, 1264] - }, - { - "parameters": { - "rule": { - "interval": [ - { - "field": "cronExpression", - "expression": "0 */6 * * *" - } - ] - } - }, - "id": "eab7bc97-d24e-41b4-8f28-7464c2aea81d", - "name": "Schedule — 6-hourly Wearable", - "type": "n8n-nodes-base.scheduleTrigger", - "typeVersion": 1.1, - "position": [256, 1440] - }, - { - "parameters": { - "httpMethod": "POST", - "path": "safety-alert", - "responseMode": "responseNode", - "options": {} - }, - "id": "737e3e10-fcef-4618-b7cb-4d0fc21272e5", - "name": "Webhook Trigger", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [384, 1200], - "webhookId": "c5a19546-6226-4159-be0f-e683674cded0" - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/doctor_alerts", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Prefer", - "value": "return=representation" - } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { - "name": "patient_id", - "value": "={{$json.patient_id}}" - }, - { - "name": "risk_score", - "value": "={{$json.risk_score}}" - }, - { - "name": "risk_level", - "value": "={{$json.risk_level}}" - }, - { - "name": "risk_reason", - "value": "={{$json.risk_reason}}" - }, - { - "name": "alert_type", - "value": "auto" - }, - { - "name": "trigger", - "value": "={{$json.trigger}}" - }, - { - "name": "created_at", - "value": "={{$now}}" - } - ] - }, - "options": {} - }, - "id": "63005e60-3348-48ff-bc97-1d2097e4c44f", - "name": "Supabase — POST doctor_alerts", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [640, 1200] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/daily_summaries?patient_id=eq.{{$json.patient_id}}&order=date.desc&limit=30", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-daily", - "name": "Supabase — GET Daily Summaries", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 672] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/symptoms?patient_id=eq.{{$json.patient_id}}&order=created_at.desc&limit=30", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-symptoms", - "name": "Supabase — GET Symptoms", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 736] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patients?patient_id=eq.{{$json.patient_id}}&select=profile_summary&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-profile", - "name": "Supabase — GET Profile Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 800] - }, - { - "parameters": { - "method": "GET", - "url": "={{$env.SUPABASE_URL}}/rest/v1/adherence_log?patient_id=eq.{{$json.patient_id}}&order=taken_at.desc&limit=30", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-adherence", - "name": "Supabase — GET Adherence", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 864] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/wearable_anomaly_flags?patient_id=eq.{{$json.patient_id}}&resolved=eq.false", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-wearable", - "name": "Supabase — GET Wearable Flags", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 928] - }, - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/health_predictions?patient_id=eq.{{$json.patient_id}}&order=predicted_at.desc&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { - "name": "apikey", - "value": "={{$env.SUPABASE_ANON_KEY}}" - }, - { - "name": "Authorization", - "value": "Bearer {{$env.SUPABASE_ANON_KEY}}" - } - ] - }, - "options": {} - }, - "id": "context-prediction", - "name": "Supabase — GET Prediction", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1552, 992] - }, - { - "parameters": { - "jsCode": "// Assemble full context from all parallel GET requests\nconst dailySummaries = $input.all()[0].json;\nconst symptoms = $input.all()[1].json;\nconst profileSummary = $input.all()[2].json[0]?.profile_summary || '';\nconst adherence = $input.all()[3].json;\nconst wearableFlags = $input.all()[4].json;\nconst prediction = $input.all()[5].json[0] || {};\n\nreturn {\n daily_summaries: dailySummaries.map(s => s.daily_summary),\n symptoms: symptoms,\n profile_summary: profileSummary,\n adherence: adherence,\n wearable_flags: wearableFlags,\n prediction: prediction\n};" - }, - "id": "context-assemble", - "name": "Assemble Full Context", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [1760, 672] - } - ], - "connections": { - "Supabase Webhook": { - "main": [[{"node": "IF — Is Answered?", "type": "main", "index": 0}]] - }, - "IF — Is Answered?": { - "main": [[{"node": "Supabase — GET Doctor Info", "type": "main", "index": 0}]] - }, - "Supabase — GET Doctor Info": { - "main": [ - [{"node": "FCM — Notify Doctor", "type": "main", "index": 0}], - [{"node": "Supabase — Log QnA", "type": "main", "index": 0}] - ] - }, - "Webhook": { - "main": [[{"node": "Switch", "type": "main", "index": 0}]] - }, - "Switch": { - "main": [ - [], - [ - {"node": "Supabase — GET Daily Summaries", "type": "main", "index": 0}, - {"node": "Supabase — GET Symptoms", "type": "main", "index": 0}, - {"node": "Supabase — GET Profile Summary", "type": "main", "index": 0}, - {"node": "Supabase — GET Adherence", "type": "main", "index": 0}, - {"node": "Supabase — GET Wearable Flags", "type": "main", "index": 0}, - {"node": "Supabase — GET Prediction", "type": "main", "index": 0} - ], - [], - [] - ] - }, - "Supabase — GET Daily Summaries": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Supabase — GET Symptoms": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Supabase — GET Profile Summary": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Supabase — GET Adherence": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Supabase — GET Wearable Flags": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Supabase — GET Prediction": { - "main": [[{"node": "Assemble Full Context", "type": "main", "index": 0}]] - }, - "Assemble Full Context": { - "main": [[{"node": "Python — /agent/doctor-answer", "type": "main", "index": 0}]] - }, - "Python — /agent/doctor-answer": { - "main": [[{"node": "Respond to Webhook", "type": "main", "index": 0}]] - }, - "Python — /safety/drug-interaction": { - "main": [[{"node": "IF — Conflict?", "type": "main", "index": 0}]] - }, - "IF — Conflict?": { - "main": [ - [{"node": "Respond (Blocking)", "type": "main", "index": 0}], - [{"node": "Supabase — POST Medicine", "type": "main", "index": 0}] - ] - }, - "Webhook1": { - "main": [ - [ - {"node": "Supabase — GET Active Meds", "type": "main", "index": 0}, - {"node": "Supabase — GET Patient Conditions", "type": "main", "index": 0} - ] - ] - }, - "Supabase — GET Active Meds": { - "main": [[{"node": "Python — /safety/drug-interaction", "type": "main", "index": 0}]] - }, - "Supabase — GET Patient Conditions": { - "main": [[{"node": "Python — /safety/drug-interaction", "type": "main", "index": 0}]] - }, - "Supabase — POST Medicine": { - "main": [[{"node": "Respond (Blocking)", "type": "main", "index": 0}]] - }, - "IF — endChat?": { - "main": [ - [], - [ - {"node": "Supabase — GET Profile", "type": "main", "index": 0}, - {"node": "Supabase — GET Pending Questions", "type": "main", "index": 0} - ] - ] - }, - "Supabase — GET Profile": { - "main": [[{"node": "Python — /chat/message", "type": "main", "index": 0}]] - }, - "Supabase — GET Pending Questions": { - "main": [[{"node": "Python — /chat/message", "type": "main", "index": 0}]] - }, - "Python — /chat/message": { - "main": [[{"node": "IF — save_ready?", "type": "main", "index": 0}]] - }, - "IF — save_ready?": { - "main": [ - [ - {"node": "Supabase — POST Symptom", "type": "main", "index": 0}, - {"node": "Python — /agent/escalate", "type": "main", "index": 0} - ], - [{"node": "Respond to Webhook1", "type": "main", "index": 0}] - ] - }, - "Supabase — POST Symptom": { - "main": [[{"node": "Python — /agent/escalate", "type": "main", "index": 0}]] - }, - "Python — /agent/escalate": { - "main": [[{"node": "Respond to Webhook1", "type": "main", "index": 0}]] - }, - "Webhook2": { - "main": [[{"node": "IF — endChat?", "type": "main", "index": 0}]] - }, - "Webhook Trigger": { - "main": [[{"node": "Supabase — POST doctor_alerts", "type": "main", "index": 0}]] - } - }, - "pinData": {}, - "meta": { - "instanceId": "local-docker-n8n", - "templateCredsSetupCompleted": true - } -} \ No newline at end of file diff --git a/backend/n8n/async_qna_loop.json b/backend/n8n/async_qna_loop.json deleted file mode 100644 index f6e4d48..0000000 --- a/backend/n8n/async_qna_loop.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "name": "Async QnA Loop", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "supabase-webhook-qna", - "options": {} - }, - "id": "supabase-trigger", - "name": "Supabase Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [250, 300] - }, - { - "parameters": { - "conditions": { - "string": [ - { - "value1": "={{$json.new.status}}", - "value2": "answered" - } - ] - } - }, - "id": "if-answered", - "name": "IF — Is Answered?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [450, 300] - }, - { - "parameters": { - "method": "GET", - "url": "={{$env.SUPABASE_URL}}/rest/v1/pending_checkin_questions?id=eq.{{$json.new.id}}&select=*,patients(name,doctor_email,doctor_fcm_token)&limit=1", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "get-doctor-info", - "name": "Supabase — GET Doctor Info", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [650, 300] - }, - { - "parameters": { - "method": "POST", - "url": "https://fcm.googleapis.com/fcm/send", - "authentication": "genericCredentialType", - "genericAuthType": "httpHeaderAuth", - "headerParameters": { - "parameters": [ - { "name": "Authorization", "value": "=key={{$env.FCM_SERVER_KEY}}" } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "to", "value": "={{$node[\"Supabase — GET Doctor Info\"].json[0].patients.doctor_fcm_token}}" }, - { "name": "notification", "value": "={ \"title\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patients.name}} answered your query\", \"body\": \"{{$node[\"Supabase — GET Doctor Info\"].json[0].patient_answer}}\" }" } - ] - }, - "options": {} - }, - "id": "fcm-notify", - "name": "FCM — Notify Doctor", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [900, 300] - } - ], - "connections": { - "Supabase Webhook": { - "main": [ - [ - { - "node": "IF — Is Answered?", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF — Is Answered?": { - "main": [ - [ - { - "node": "Supabase — GET Doctor Info", - "type": "main", - "index": 0 - } - ] - ] - }, - "Supabase — GET Doctor Info": { - "main": [ - [ - { - "node": "FCM — Notify Doctor", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "settings": {}, - "staticData": null, - "meta": { - "templateId": null - }, - "pinData": {} -} diff --git a/backend/n8n/doctor_agent.json b/backend/n8n/doctor_agent.json deleted file mode 100644 index 81688d7..0000000 --- a/backend/n8n/doctor_agent.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "name": "Doctor Agent", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "doctor", - "options": {} - }, - "id": "webhook-trigger", - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [250, 300] - }, - { - "parameters": { - "dataType": "string", - "value": "={{$json.action}}", - "rules": { - "rules": [ - { "value": "view", "output": 0 }, - { "value": "ask", "output": 1 }, - { "value": "family", "output": 2 }, - { "value": "briefing", "output": 3 } - ] - } - }, - "id": "switch-action", - "name": "Switch", - "type": "n8n-nodes-base.switch", - "typeVersion": 1, - "position": [450, 300] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/agent/doctor-answer", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "question", "value": "={{$json.question}}" }, - { "name": "patient_id", "value": "={{$json.patient_id}}" }, - { "name": "full_context", "value": "={}" } - ] - }, - "options": {} - }, - "id": "python-ask", - "name": "Python — /agent/doctor-answer", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [700, 300] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{$json}}", - "options": {} - }, - "id": "respond-doctor", - "name": "Respond to Webhook", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [1000, 300] - } - ], - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Switch", - "type": "main", - "index": 0 - } - ] - ] - }, - "Switch": { - "main": [ - [], - [ - { - "node": "Python — /agent/doctor-answer", - "type": "main", - "index": 0 - } - ], - [], - [] - ] - }, - "Python — /agent/doctor-answer": { - "main": [ - [ - { - "node": "Respond to Webhook", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "settings": {}, - "staticData": null, - "meta": { - "templateId": null - }, - "pinData": {} -} diff --git a/backend/n8n/medicine_safety.json b/backend/n8n/medicine_safety.json deleted file mode 100644 index 39d2282..0000000 --- a/backend/n8n/medicine_safety.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "name": "Medicine Safety", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "medicine/add", - "options": {} - }, - "id": "webhook-trigger", - "name": "Webhook", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [250, 300] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/safety/drug-interaction", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "new_medicine", "value": "={{$json.new_medicine}}" }, - { "name": "active_medicines", "value": "=[]" }, - { "name": "patient_conditions", "value": "=[]" } - ] - }, - "options": {} - }, - "id": "python-safety", - "name": "Python — /safety/drug-interaction", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [500, 300] - }, - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.conflict_found}}", - "value2": true - } - ] - } - }, - "id": "if-conflict", - "name": "IF — Conflict?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [700, 300] - }, - { - "parameters": { - "respondWith": "json", - "responseBody": "={{$node[\"Python — /safety/drug-interaction\"].json}}", - "options": {} - }, - "id": "respond-blocking", - "name": "Respond (Blocking)", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [950, 250] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/medicines", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$node[\"Webhook\"].json.patient_id}}" }, - { "name": "medicine_name", "value": "={{$node[\"Webhook\"].json.new_medicine}}" }, - { "name": "is_active", "value": true } - ] - }, - "options": {} - }, - "id": "supabase-save", - "name": "Supabase — POST Medicine", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [950, 400] - } - ], - "connections": { - "Webhook": { - "main": [ - [ - { - "node": "Python — /safety/drug-interaction", - "type": "main", - "index": 0 - } - ] - ] - }, - "Python — /safety/drug-interaction": { - "main": [ - [ - { - "node": "IF — Conflict?", - "type": "main", - "index": 0 - } - ] - ] - }, - "IF — Conflict?": { - "main": [ - [ - { - "node": "Respond (Blocking)", - "type": "main", - "index": 0 - } - ], - [ - { - "node": "Supabase — POST Medicine", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "settings": {}, - "staticData": null, - "meta": { - "templateId": null - }, - "pinData": {} -} diff --git a/backend/n8n/patient_agent.json b/backend/n8n/patient_agent.json deleted file mode 100644 index c794e32..0000000 --- a/backend/n8n/patient_agent.json +++ /dev/null @@ -1,760 +0,0 @@ -{ - "name": "Swasthya — Patient Agent", - "nodes": [ - { - "parameters": { - "httpMethod": "POST", - "path": "chat", - "responseMode": "responseNode", - "options": {} - }, - "id": "3eebc991-a43b-494f-9f16-0bcafafa308b", - "name": "Webhook — Patient Chat", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [-1600, 300], - "webhookId": "swasthya-patient-chat" - }, - - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.endChat}}", - "value2": true - } - ] - } - }, - "id": "6355894d-aa91-4280-b8f1-2bc0c3003bc3", - "name": "IF — End Chat?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [-1380, 300] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patients?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&limit=1", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "23cdfd54-f654-4ea7-8c13-ae0144362aff", - "name": "Supabase — GET Profile", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-1120, 160] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/pending_checkin_questions?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&status=eq.pending", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "5f8372b5-762a-410a-957e-037d2226749e", - "name": "Supabase — GET Pending Questions", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-1120, 320] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/chat/message", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"message\": \"{{$node[\"Webhook — Patient Chat\"].json.message}}\",\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"session_id\": \"{{$node[\"Webhook — Patient Chat\"].json.session_id}}\",\n \"patient_context\": {\n \"rolling_summary\": \"{{$node[\"Supabase — GET Profile\"].json[0].rolling_summary}}\",\n \"profile_summary\": \"{{$node[\"Supabase — GET Profile\"].json[0].profile_summary}}\",\n \"last_7_summaries\": {{$node[\"Supabase — GET Profile\"].json[0].last_7_summaries || []}},\n \"active_medications\": {{$node[\"Supabase — GET Profile\"].json[0].medications || []}},\n \"pending_doctor_questions\": {{$node[\"Supabase — GET Pending Questions\"].json || []}}\n }\n}", - "options": {} - }, - "id": "4166d781-8156-41ae-959e-cc440b3f1196", - "name": "Python — /chat/message", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-880, 240] - }, - - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$json.save_ready}}", - "value2": true - } - ] - } - }, - "id": "267b7cd8-1074-4db1-85a5-44478a6f1de3", - "name": "IF — Save Ready?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [-660, 240] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/symptoms", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Prefer", "value": "return=representation" } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$node[\"Webhook — Patient Chat\"].json.patient_id}}" }, - { "name": "symptom_name", "value": "={{$node[\"Python — /chat/message\"].json.extracted_symptom.symptom}}" }, - { "name": "body_zone", "value": "={{$node[\"Python — /chat/message\"].json.extracted_symptom.body_zone}}" }, - { "name": "severity", "value": "={{$node[\"Python — /chat/message\"].json.extracted_symptom.severity}}" }, - { "name": "onset", "value": "={{$node[\"Python — /chat/message\"].json.extracted_symptom.onset}}" }, - { "name": "session_id", "value": "={{$node[\"Webhook — Patient Chat\"].json.session_id}}" }, - { "name": "created_at", "value": "={{$now}}" } - ] - }, - "options": {} - }, - "id": "1b5a9a5a-21db-46c9-8c20-96794c9f4ecc", - "name": "Supabase — POST Symptom", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-440, 120] - }, - - { - "parameters": { - "conditions": { - "string": [ - { - "value1": "={{$node[\"Python — /chat/message\"].json.extracted_symptom?.pending_question_id}}", - "operation": "isNotEmpty" - } - ] - } - }, - "id": "pending-question-if", - "name": "IF — Pending Question?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [-220, 120] - }, - - { - "parameters": { - "method": "PATCH", - "url": "={{$env.SUPABASE_URL}}/rest/v1/pending_checkin_questions?id=eq.{{$node[\"Python — /chat/message\"].json.extracted_symptom.pending_question_id}}", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_answer\": \"{{$node[\"Python — /chat/message\"].json.extracted_symptom.symptom}}\",\n \"status\": \"answered\",\n \"answered_at\": \"{{$now}}\"\n}", - "options": {} - }, - "id": "patch-pending-question", - "name": "Supabase — PATCH Answered Question", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-0, 60] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/agent/escalate", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"symptoms\": [{{$node[\"Python — /chat/message\"].json.extracted_symptom || {}}}],\n \"patient_conditions\": {{$node[\"Supabase — GET Profile\"].json[0].conditions || []}},\n \"age\": {{$node[\"Supabase — GET Profile\"].json[0].age || 0}},\n \"wearable_flags\": {{$node[\"Supabase — GET Profile\"].json[0].wearable_flags || []}},\n \"message_context\": \"{{$node[\"Webhook — Patient Chat\"].json.message}}\"\n}", - "options": {} - }, - "id": "5608cfd4-1a48-4745-8c8c-88432f749799", - "name": "Python — /agent/escalate", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [220, 240] - }, - - { - "parameters": { - "conditions": { - "string": [ - { - "value1": "={{$json.escalation_level}}", - "value2": "HOME_MONITORING", - "operation": "notEqual" - } - ] - } - }, - "id": "escalation-fired-if", - "name": "IF — Escalation Fired?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [440, 240] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.N8N_INTERNAL_URL}}/webhook/safety-agent", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"trigger\": \"escalation\",\n \"escalation_level\": \"{{$node[\"Python — /agent/escalate\"].json.escalation_level}}\",\n \"triggered_by\": \"{{$node[\"Python — /agent/escalate\"].json.triggered_by}}\",\n \"reasoning\": \"{{$node[\"Python — /agent/escalate\"].json.reasoning}}\"\n}", - "options": {} - }, - "id": "trigger-safety-escalation", - "name": "Webhook — Trigger Safety (Escalation)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [660, 160] - }, - - { - "parameters": { - "respondWith": "json", - "responseBody": "={\n \"reply\": \"{{$node[\"Python — /chat/message\"].json.bot_reply}}\",\n \"session_id\": \"{{$node[\"Webhook — Patient Chat\"].json.session_id}}\",\n \"clarification_needed\": {{$node[\"Python — /chat/message\"].json.clarification_needed}},\n \"confirmation_required\": {{$node[\"Python — /chat/message\"].json.confirmation_required}}\n}", - "options": {} - }, - "id": "8aaba1ab-184d-4297-aeed-f5e6a903b939", - "name": "Respond to Webhook — Branch A", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [880, 300] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/conversation_messages?session_id=eq.{{$node[\"Webhook — Patient Chat\"].json.session_id}}&order=created_at.asc", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "a3c7b9b4-bc61-427b-b4bc-942575cc3fed", - "name": "Supabase — GET Conversation Log", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-1120, 520] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patients?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&limit=1", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "profile-endchat", - "name": "Supabase — GET Profile (EndChat)", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-1120, 660] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/chat/end-session", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"full_conversation_log\": {{$node[\"Supabase — GET Conversation Log\"].json}},\n \"existing_rolling_summary\": \"{{$node[\"Supabase — GET Profile (EndChat)\"].json[0].rolling_summary}}\"\n}", - "options": {} - }, - "id": "65b9a722-1213-4598-9af1-62b1a20ec0c9", - "name": "Python — /chat/end-session", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-860, 580] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/daily_summaries", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Prefer", "value": "return=representation" } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$node[\"Webhook — Patient Chat\"].json.patient_id}}" }, - { "name": "date", "value": "={{$now.format(\"YYYY-MM-DD\")}}" }, - { "name": "daily_summary", "value": "={{$node[\"Python — /chat/end-session\"].json.daily_summary}}" }, - { "name": "rolling_summary", "value": "={{$node[\"Python — /chat/end-session\"].json.rolling_summary}}" }, - { "name": "symptoms_today", "value": "={{JSON.stringify($node[\"Python — /chat/end-session\"].json.symptoms_today)}}" }, - { "name": "key_risks", "value": "={{$node[\"Python — /chat/end-session\"].json.key_risks}}" }, - { "name": "urgency", "value": "={{$node[\"Python — /chat/end-session\"].json.urgency}}" } - ] - }, - "options": {} - }, - "id": "efdc3fb0-c9f6-450e-8ec9-402913cb419f", - "name": "Supabase — POST Daily Summary", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-640, 580] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/daily_summaries?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&order=date.asc&limit=14", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "4ddca270-e6bb-4731-b600-e40d7a693ed3", - "name": "Supabase — GET Historical Summaries", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-420, 480] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/patient_risk_scores?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&order=updated_at.asc&limit=14", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "06311efb-ef04-417d-8eaf-08843a134af8", - "name": "Supabase — GET Historical Risks", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-420, 600] - }, - - { - "parameters": { - "url": "={{$env.SUPABASE_URL}}/rest/v1/symptoms?patient_id=eq.{{$node[\"Webhook — Patient Chat\"].json.patient_id}}&order=created_at.asc&limit=100", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" } - ] - }, - "options": {} - }, - "id": "symptoms-history-node", - "name": "Supabase — GET Symptoms History", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [-420, 720] - }, - - { - "parameters": { - "jsCode": "const summaries = $node[\"Supabase — GET Historical Summaries\"].json;\nconst risks = $node[\"Supabase — GET Historical Risks\"].json;\nconst symptoms = $node[\"Supabase — GET Symptoms History\"].json;\nconst profile = $node[\"Supabase — GET Profile (EndChat)\"].json[0];\nconst session = $node[\"Python — /chat/end-session\"].json;\n\nconst summaryTexts = Array.isArray(summaries)\n ? summaries.map(s => s.daily_summary || '').filter(Boolean)\n : [];\n\nreturn [{\n json: {\n summaryTexts,\n risks: Array.isArray(risks) ? risks : [],\n symptoms: Array.isArray(symptoms) ? symptoms : [],\n profile,\n session\n }\n}];" - }, - "id": "build-predict-payload", - "name": "Function — Build Predict Payload", - "type": "n8n-nodes-base.code", - "typeVersion": 2, - "position": [-160, 600] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/risk/generate", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"summary\": \"{{$node[\"Python — /chat/end-session\"].json.daily_summary}}\",\n \"symptoms\": {{$node[\"Python — /chat/end-session\"].json.symptoms_today || []}},\n \"conditions\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].conditions || []}},\n \"family_history\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].family_history || []}},\n \"missed_meds_days\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].missed_meds_streak || 0}},\n \"wearable_flags\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].wearable_flags || []}},\n \"age\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].age || 0}}\n}", - "options": {} - }, - "id": "efbd63f8-9d2f-4b42-b87e-ce273c340cb7", - "name": "Python — /risk/generate", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [80, 520] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/risk/predict", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"daily_summaries\": {{$node[\"Function — Build Predict Payload\"].json.summaryTexts}},\n \"risk_scores_history\": {{$node[\"Function — Build Predict Payload\"].json.risks}},\n \"symptoms_history\": {{$node[\"Function — Build Predict Payload\"].json.symptoms}},\n \"wearable_trend\": [],\n \"current_medications\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].medications || []}},\n \"age\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].age || 0}},\n \"conditions\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].conditions || []}}\n}", - "options": {} - }, - "id": "678816cb-a5c8-45d7-8704-c4b87e18f15f", - "name": "Python — /risk/predict", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [80, 680] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/patient_risk_scores", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Prefer", "value": "resolution=merge-duplicates" } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$node[\"Webhook — Patient Chat\"].json.patient_id}}" }, - { "name": "final_score", "value": "={{$node[\"Python — /risk/generate\"].json.final_score}}" }, - { "name": "base_score", "value": "={{$node[\"Python — /risk/generate\"].json.base_score}}" }, - { "name": "rag_adjustment", "value": "={{$node[\"Python — /risk/generate\"].json.rag_adjustment}}" }, - { "name": "risk_level", "value": "={{$node[\"Python — /risk/generate\"].json.risk_level}}" }, - { "name": "risk_reason", "value": "={{$node[\"Python — /risk/generate\"].json.risk_reason}}" }, - { "name": "guideline_reference", "value": "={{$node[\"Python — /risk/generate\"].json.guideline_reference}}" }, - { "name": "confidence", "value": "={{$node[\"Python — /risk/generate\"].json.confidence}}" }, - { "name": "updated_at", "value": "={{$now}}" } - ] - }, - "options": {} - }, - "id": "4642da86-1247-4b8f-a95b-212e6efc99d7", - "name": "Supabase — Upsert Risk Score", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [320, 520] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/health_predictions", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "apikey", "value": "={{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Authorization", "value": "=Bearer {{$env.SUPABASE_ANON_KEY}}" }, - { "name": "Prefer", "value": "resolution=merge-duplicates" } - ] - }, - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$node[\"Webhook — Patient Chat\"].json.patient_id}}" }, - { "name": "trajectory", "value": "={{$node[\"Python — /risk/predict\"].json.trajectory}}" }, - { "name": "score_slope", "value": "={{$node[\"Python — /risk/predict\"].json.score_slope}}" }, - { "name": "projected_scores", "value": "={{JSON.stringify($node[\"Python — /risk/predict\"].json.projected_scores)}}" }, - { "name": "predicted_risk_at_day_7", "value": "={{$node[\"Python — /risk/predict\"].json.predicted_risk_at_day_7}}" }, - { "name": "predicted_risk_level_day_7", "value": "={{$node[\"Python — /risk/predict\"].json.predicted_risk_level_day_7}}" }, - { "name": "early_warning", "value": "={{$node[\"Python — /risk/predict\"].json.early_warning}}" }, - { "name": "early_warning_symptom", "value": "={{$node[\"Python — /risk/predict\"].json.early_warning_symptom}}" }, - { "name": "prediction_summary", "value": "={{$node[\"Python — /risk/predict\"].json.prediction_summary}}" }, - { "name": "watch_for", "value": "={{JSON.stringify($node[\"Python — /risk/predict\"].json.watch_for)}}" }, - { "name": "confidence", "value": "={{$node[\"Python — /risk/predict\"].json.confidence}}" }, - { "name": "predicted_at", "value": "={{$now}}" } - ] - }, - "options": {} - }, - "id": "upsert-prediction", - "name": "Supabase — Upsert Prediction", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [320, 680] - }, - - { - "parameters": { - "conditions": { - "boolean": [ - { - "value1": "={{$node[\"Python — /risk/generate\"].json.final_score > 70 || $node[\"Python — /risk/predict\"].json.early_warning === true}}", - "value2": true - } - ] - } - }, - "id": "e21a537a-c0cb-4b75-b1c1-7b297cee22d2", - "name": "IF — High Risk or Early Warning?", - "type": "n8n-nodes-base.if", - "typeVersion": 1, - "position": [560, 600] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.PYTHON_API_URL}}/schemes/match", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"age\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].age || 0}},\n \"income_category\": \"{{$node[\"Supabase — GET Profile (EndChat)\"].json[0].income_category}}\",\n \"state\": \"{{$node[\"Supabase — GET Profile (EndChat)\"].json[0].state}}\",\n \"confirmed_conditions\": {{$node[\"Supabase — GET Profile (EndChat)\"].json[0].conditions || []}},\n \"current_risk_level\": \"{{$node[\"Python — /risk/generate\"].json.risk_level}}\"\n}", - "options": {} - }, - "id": "e7c7ee00-4d2e-481c-8c35-7bedca0216c3", - "name": "Python — /schemes/match", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [780, 520] - }, - - { - "parameters": { - "method": "POST", - "url": "={{$env.N8N_INTERNAL_URL}}/webhook/safety-agent", - "sendHeaders": true, - "headerParameters": { - "parameters": [ - { "name": "Content-Type", "value": "application/json" } - ] - }, - "sendBody": true, - "specifyBody": "json", - "jsonBody": "={\n \"patient_id\": \"{{$node[\"Webhook — Patient Chat\"].json.patient_id}}\",\n \"trigger\": \"high_risk\",\n \"risk_score\": {{$node[\"Python — /risk/generate\"].json.final_score}},\n \"risk_level\": \"{{$node[\"Python — /risk/generate\"].json.risk_level}}\",\n \"risk_reason\": \"{{$node[\"Python — /risk/generate\"].json.risk_reason}}\",\n \"trajectory\": \"{{$node[\"Python — /risk/predict\"].json.trajectory}}\",\n \"early_warning\": {{$node[\"Python — /risk/predict\"].json.early_warning}},\n \"early_warning_symptom\": \"{{$node[\"Python — /risk/predict\"].json.early_warning_symptom}}\"\n}", - "options": {} - }, - "id": "13e272c7-2ecf-4c0c-a6b1-548461a1b060", - "name": "Webhook — Trigger Safety Agent", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [1000, 520] - }, - - { - "parameters": { - "respondWith": "json", - "responseBody": "={\n \"status\": \"session_saved\",\n \"risk_level\": \"{{$node[\"Python — /risk/generate\"].json.risk_level}}\",\n \"trajectory\": \"{{$node[\"Python — /risk/predict\"].json.trajectory}}\",\n \"early_warning\": {{$node[\"Python — /risk/predict\"].json.early_warning}}\n}", - "options": {} - }, - "id": "respond-endchat", - "name": "Respond to Webhook — Branch B", - "type": "n8n-nodes-base.respondToWebhook", - "typeVersion": 1, - "position": [1200, 600] - } - ], - - "connections": { - "Webhook — Patient Chat": { - "main": [[{ "node": "IF — End Chat?", "type": "main", "index": 0 }]] - }, - - "IF — End Chat?": { - "main": [ - [ - { "node": "Supabase — GET Conversation Log", "type": "main", "index": 0 }, - { "node": "Supabase — GET Profile (EndChat)", "type": "main", "index": 0 } - ], - [ - { "node": "Supabase — GET Profile", "type": "main", "index": 0 }, - { "node": "Supabase — GET Pending Questions", "type": "main", "index": 0 } - ] - ] - }, - - "Supabase — GET Profile": { - "main": [[{ "node": "Python — /chat/message", "type": "main", "index": 0 }]] - }, - "Supabase — GET Pending Questions": { - "main": [[{ "node": "Python — /chat/message", "type": "main", "index": 0 }]] - }, - - "Python — /chat/message": { - "main": [[{ "node": "IF — Save Ready?", "type": "main", "index": 0 }]] - }, - - "IF — Save Ready?": { - "main": [ - [{ "node": "Supabase — POST Symptom", "type": "main", "index": 0 }], - [{ "node": "Python — /agent/escalate", "type": "main", "index": 0 }] - ] - }, - - "Supabase — POST Symptom": { - "main": [[{ "node": "IF — Pending Question?", "type": "main", "index": 0 }]] - }, - - "IF — Pending Question?": { - "main": [ - [{ "node": "Supabase — PATCH Answered Question", "type": "main", "index": 0 }], - [{ "node": "Python — /agent/escalate", "type": "main", "index": 0 }] - ] - }, - - "Supabase — PATCH Answered Question": { - "main": [[{ "node": "Python — /agent/escalate", "type": "main", "index": 0 }]] - }, - - "Python — /agent/escalate": { - "main": [[{ "node": "IF — Escalation Fired?", "type": "main", "index": 0 }]] - }, - - "IF — Escalation Fired?": { - "main": [ - [{ "node": "Webhook — Trigger Safety (Escalation)", "type": "main", "index": 0 }], - [{ "node": "Respond to Webhook — Branch A", "type": "main", "index": 0 }] - ] - }, - - "Webhook — Trigger Safety (Escalation)": { - "main": [[{ "node": "Respond to Webhook — Branch A", "type": "main", "index": 0 }]] - }, - - "Supabase — GET Conversation Log": { - "main": [[{ "node": "Python — /chat/end-session", "type": "main", "index": 0 }]] - }, - "Supabase — GET Profile (EndChat)": { - "main": [[{ "node": "Python — /chat/end-session", "type": "main", "index": 0 }]] - }, - - "Python — /chat/end-session": { - "main": [[{ "node": "Supabase — POST Daily Summary", "type": "main", "index": 0 }]] - }, - - "Supabase — POST Daily Summary": { - "main": [ - [ - { "node": "Supabase — GET Historical Summaries", "type": "main", "index": 0 }, - { "node": "Supabase — GET Historical Risks", "type": "main", "index": 0 }, - { "node": "Supabase — GET Symptoms History", "type": "main", "index": 0 } - ] - ] - }, - - "Supabase — GET Historical Summaries": { - "main": [[{ "node": "Function — Build Predict Payload", "type": "main", "index": 0 }]] - }, - "Supabase — GET Historical Risks": { - "main": [[{ "node": "Function — Build Predict Payload", "type": "main", "index": 0 }]] - }, - "Supabase — GET Symptoms History": { - "main": [[{ "node": "Function — Build Predict Payload", "type": "main", "index": 0 }]] - }, - - "Function — Build Predict Payload": { - "main": [ - [ - { "node": "Python — /risk/generate", "type": "main", "index": 0 }, - { "node": "Python — /risk/predict", "type": "main", "index": 0 } - ] - ] - }, - - "Python — /risk/generate": { - "main": [[{ "node": "Supabase — Upsert Risk Score", "type": "main", "index": 0 }]] - }, - "Python — /risk/predict": { - "main": [[{ "node": "Supabase — Upsert Prediction", "type": "main", "index": 0 }]] - }, - - "Supabase — Upsert Risk Score": { - "main": [[{ "node": "IF — High Risk or Early Warning?", "type": "main", "index": 0 }]] - }, - "Supabase — Upsert Prediction": { - "main": [[{ "node": "IF — High Risk or Early Warning?", "type": "main", "index": 0 }]] - }, - - "IF — High Risk or Early Warning?": { - "main": [ - [{ "node": "Python — /schemes/match", "type": "main", "index": 0 }], - [{ "node": "Respond to Webhook — Branch B", "type": "main", "index": 0 }] - ] - }, - - "Python — /schemes/match": { - "main": [[{ "node": "Webhook — Trigger Safety Agent", "type": "main", "index": 0 }]] - }, - - "Webhook — Trigger Safety Agent": { - "main": [[{ "node": "Respond to Webhook — Branch B", "type": "main", "index": 0 }]] - } - }, - - "pinData": {}, - "meta": { - "instanceId": "5eadcf7e8b907856f283509781e4e82b606160fbf32835473d52a300a01a8683" - } -} \ No newline at end of file diff --git a/backend/n8n/safety_agent.json b/backend/n8n/safety_agent.json deleted file mode 100644 index 00b47f4..0000000 --- a/backend/n8n/safety_agent.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "name": "Safety Agent", - "nodes": [ - { - "parameters": { - "rule": { - "interval": [ - { - "field": "cronExpression", - "expression": "0 8 * * *" - } - ] - } - }, - "id": "cron-8am", - "name": "Schedule — 8AM Check-in", - "type": "n8n-nodes-base.scheduleTrigger", - "typeVersion": 1.1, - "position": [250, 100] - }, - { - "parameters": { - "rule": { - "interval": [ - { - "field": "cronExpression", - "expression": "0 */6 * * *" - } - ] - } - }, - "id": "cron-6hourly", - "name": "Schedule — 6-hourly Wearable", - "type": "n8n-nodes-base.scheduleTrigger", - "typeVersion": 1.1, - "position": [250, 400] - }, - { - "parameters": { - "httpMethod": "POST", - "path": "safety-alert", - "options": {} - }, - "id": "webhook-trigger", - "name": "Webhook Trigger", - "type": "n8n-nodes-base.webhook", - "typeVersion": 1, - "position": [250, 700] - }, - { - "parameters": { - "method": "POST", - "url": "={{$env.SUPABASE_URL}}/rest/v1/doctor_alerts", - "sendBody": true, - "bodyParameters": { - "parameters": [ - { "name": "patient_id", "value": "={{$json.patient_id}}" }, - { "name": "risk_score", "value": "={{$json.risk_score}}" }, - { "name": "risk_level", "value": "={{$json.risk_level}}" }, - { "name": "risk_reason", "value": "={{$json.risk_reason}}" } - ] - }, - "options": {} - }, - "id": "supabase-alert", - "name": "Supabase — /doctor_alerts", - "type": "n8n-nodes-base.httpRequest", - "typeVersion": 4.1, - "position": [500, 700] - } - ], - "connections": { - "Webhook Trigger": { - "main": [ - [ - { - "node": "Supabase — /doctor_alerts", - "type": "main", - "index": 0 - } - ] - ] - } - }, - "settings": {}, - "staticData": null, - "meta": { - "templateId": null - }, - "pinData": {} -} diff --git a/backend/prompts/importance_detector.py b/backend/prompts/importance_detector.py new file mode 100644 index 0000000..b719874 --- /dev/null +++ b/backend/prompts/importance_detector.py @@ -0,0 +1,68 @@ +# This prompt decides if extracted data is important enough to save + +IMPORTANCE_DETECTION_PROMPT = """ +You are a medical data importance detector. Given extracted health data, decide if it's important to save in the long-term health knowledge graph. + +IMPORTANT DATA (save to database): +- Health conditions (Diabetes, Hypertension, Thyroid, etc.) +- Symptoms (Fever, Headaches, Cough, Fatigue, Body Pain, etc.) +- Medication usage details (Crocin, Metformin, Vitamins, etc.) +- Major lifestyle patterns/habits (Gym, Smoking, Night shifts, Yoga, Diet types, Alcohol) +- Sleep records (hours, quality) +- Past surgeries or medical procedures +- Vaccine receipts +- Doctor visit details or hospitalizations +- Lab test results (HbA1c, Vitamin D, CBC, etc.) +- Family relationships and emergency contacts + +NOT IMPORTANT (ignore, don't save): +- Casual mentions ("I might have a headache" - not confirmed) +- One-time minor issues that resolve immediately +- Vague complaints without detail +- General chat not health-related +- Duplicate data already saved recently + +Given this data, respond with ONLY JSON: +{{ + "should_save": true/false, + "importance_score": 1-10, + "reason": "why you decided this", + "data_types": ["condition", "symptom", "medication", "allergy", "surgery", "vaccination", "habit", "sleep", "diet", "visit", "hospitalization", "lab", "blood_group", "emergency_contact"] +}} + +Data: +{extracted_data} + +Respond ONLY with JSON. +""" + +def check_importance(extracted_data: dict, groq_client) -> dict: + """ + Returns {should_save, importance_score, reason, data_types} + """ + formatted_prompt = IMPORTANCE_DETECTION_PROMPT.format(extracted_data=extracted_data) + + response = groq_client.chat.completions.create( + model="llama-3.3-70b-versatile", + messages=[ + { + "role": "system", + "content": formatted_prompt + }, + {"role": "user", "content": "Decide if this data is important."} + ], + temperature=0.3 + ) + + import json + try: + content = response.choices[0].message.content.strip() + if content.startswith("```json"): + content = content.split("```json")[1].split("```")[0].strip() + elif content.startswith("```"): + content = content.split("```")[1].split("```")[0].strip() + result = json.loads(content) + return result + except Exception as e: + print(f"[check_importance] Parsing error: {e}. Raw response: {response.choices[0].message.content}") + return {"should_save": False, "importance_score": 0, "reason": f"Parse error: {e}", "data_types": []} diff --git a/backend/rag/embedder.py b/backend/rag/embedder.py deleted file mode 100644 index 2bb56e5..0000000 --- a/backend/rag/embedder.py +++ /dev/null @@ -1,110 +0,0 @@ -import os -import fitz # PyMuPDF -import faiss -import numpy as np -from sentence_transformers import SentenceTransformer -import pickle - -GUIDELINES_DIR = "rag/guidelines" -INDEX_DIR = "rag/index" -INDEX_PATH = f"{INDEX_DIR}/faiss_index.index" -METADATA_PATH = f"{INDEX_DIR}/metadata.pkl" - -os.makedirs(INDEX_DIR, exist_ok=True) - -MODEL_NAME = 'all-MiniLM-L6-v2' -CACHE_DIR = "rag/model_cache" -os.makedirs(CACHE_DIR, exist_ok=True) - -_model = None - -def get_model(): - """Lazy initialization of the SentenceTransformer model with local caching and error handling.""" - global _model - if _model is None: - try: - print(f"🔄 Initializing embedding model: {MODEL_NAME}...") - # Use local cache to ensure persistence and control - _model = SentenceTransformer(MODEL_NAME, cache_folder=CACHE_DIR) - print("✅ Embedding model loaded successfully.") - except Exception as e: - print(f"❌ Failed to load embedding model: {e}") - print("💡 TIP: Check your internet connection or try setting HF_ENDPOINT=https://hf-mirror.com in your .env") - # Return a dummy model or raise if critical - raise e - return _model - -def get_metadata(filename): - """Assign metadata based on filename for sample implementation""" - metadata = { - "condition_tags": [], - "age_group": "adult", - "severity_context": "chronic", - "source_document": filename - } - - if "cardio" in filename.lower(): - metadata["condition_tags"] = ["cardiovascular", "heart"] - elif "diabetes" in filename.lower(): - metadata["condition_tags"] = ["diabetes", "sugar"] - elif "hypertension" in filename.lower(): - metadata["condition_tags"] = ["hypertension", "bp"] - - return metadata - -def build_index(): - if os.path.exists(INDEX_PATH) and os.path.exists(METADATA_PATH): - print("Index already exists, skipping build.") - return - - all_chunks = [] - all_metadata = [] - - if not os.path.exists(GUIDELINES_DIR): - os.makedirs(GUIDELINES_DIR) - print("No guidelines directory found. Creating empty directory - RAG will fallback to LLM-only mode.") - # Create empty index so app doesn't crash - dimension = 384 # all-MiniLM-L6-v2 dimension - index = faiss.IndexFlatL2(dimension) - faiss.write_index(index, INDEX_PATH) - with open(METADATA_PATH, "wb") as f: - pickle.dump([], f) - print("Empty FAISS index created. RAG retriever will return no results until guidelines are added.") - return - - for filename in os.listdir(GUIDELINES_DIR): - if filename.endswith(".pdf"): - path = os.path.join(GUIDELINES_DIR, filename) - doc = fitz.open(path) - file_meta = get_metadata(filename) - - text = "" - for page in doc: - text += page.get_text() - - # Simple chunking: 512 tokens (roughly 2000 chars) with overlap - chunk_size = 1500 - overlap = 200 - - for i in range(0, len(text), chunk_size - overlap): - chunk = text[i:i + chunk_size] - all_chunks.append(chunk) - all_metadata.append({**file_meta, "text": chunk}) - - if not all_chunks: - print("No documents found to index.") - return - - embeddings = get_model().encode(all_chunks) - dimension = embeddings.shape[1] - index = faiss.IndexFlatL2(dimension) - index.add(np.array(embeddings).astype('float32')) - - faiss.write_index(index, INDEX_PATH) - with open(METADATA_PATH, "wb") as f: - pickle.dump(all_metadata, f) - - print(f"Index built with {len(all_chunks)} chunks.") - -if __name__ == "__main__": - build_index() diff --git a/backend/rag/guidelines/icmr_diabetes_2022.pdf b/backend/rag/guidelines/icmr_diabetes_2022.pdf deleted file mode 100644 index 7362b7fc83116bf8c77ea4acb4fea96946e1bcbc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1691 zcmah~TX&*J5Ps)Zv{5veQILy5#xW|yTf8F1G3M4tt7Nbn0e6#q*gvrQw(t8}w=r?z zN%oxK0lKQIs=xZGsAh*oBUi{v4ExXDfBg$gXX$YQraI0C1{ zM>r(AV1YQM00cnnC#GCx_(lPUR5Y1Ng+bVf)vyE24FYjce!R1z7oQ3ieN#F9&>b4<=qTyvD4lLms39oDcmUr+jtlGL z8O*6H07X;sVo50iRW9UJRa3zo;D<1Xvn#%OnCLab5bQIHVaKV`Si^uU#wenp_?Q;n zLeDg%_6Dv{{q3v2ecRxU0tCECB1$85=dA$ZeaKkPfI})n>U@-nOid50#alh%(0nH$ z>Jv;Cu_`^?eQp_kuD;Dq3mT2OSq3PL$^b_+xyymwbjUe~n)7`%WSxIOBs zTaMdvL(Bez7PR4ZIP}rNUAoAj=UHy#QOG+S#f!ZrfWA-Lly5*UJSH+O=7C6^P!C!} z{>r7m=OFGEGcNoqgL_hs_=`K|;M#D#h=QjTuZ{I~TU881yEbk?kFxhB&`4c`SJ4{C zvJeLfBTDgxX~M0vuxlf3o!V-?(=(%QiDdRBmC-u={pwm6Kr=OyZ<4vTF}X;L zYgTu&Eq5lv$6NL>nhQq92nYIuNUFOmhi4l8+AmB`g0Qmbwe1;EQdbi(U(J6R`blNW z<*!<%xhATb$;yVC;5S^ORDDe2rysI1(zl!8_5E6^+O2%!T*9N=kMe98eV+=~=EUH4 zRrXs;8JwcgJQ{w*%U1vLtm))e_*^#!m6DY|`QG+@#pq&EvIhN3KV2**SYhGaKCD(1 zvt~ODE!(jkq=D0PHnU6>vg=hN)3Y-5`!i+ijhbmgdt#@GECs@CUsz7^W|-}XO)sbV z$*G#Ie&LeSbdOxPC!VZY^^91kcKKnH3ar#p&Mwm=m?a)%XL^3q;>IxSRoIL^Qhl~l zuau`O7fkCb>1OBsS)mi`t%7f}>^VS7MgS@!{Cq%BQ6vQ{!B1V0PKo#d_+6*bDfmHG z(6moFS@>)xm*~9u;9HVDpHb3|?4kg=9t!p|xorSuUWT?RqD6`R-L||eX;6SpK`4oG z0g6aOwp#hM#T~;QT@9=MzZ!Bm;J9=+eYhkTzKxdz#3RXM=rkk|h({b=mt_if+zw{8bz z{>~3;VdB3l`Y6C43(DVY=IKjBg%SOwa<f{;RQyBuqP)PNZt8SjKXcmOwWK_Dp%BbFBO@Z?pVu?dc>jm z-X^$DLb}OR$qwi%7Y&r674i0BD0?w#n{+{9z@dRS?RVv#~LS@ZMb+d0l@Vqxc1!fazXW zP4oFJH?-_aY}1C@gh_;L-}SLW?;H8)ibCFmX|~yFViZNRP5CBTCHq8X#oQ6uAk>2v zk-zjQ@OzN;%P=eaD}!57_xQ^?XXo1VS1AQgD_*;4wA;F3VcNA>3$7@8Zvu_f#i5rj zfUL?{pd_Uf2TTjaxnuQkq7{!z_0fQ1)j4-7o;Qv-T|OVHh?JfRXL;wyd7M6shTeCx zHy0=D`k|X}3P0(>=9!g72NoGr+v{}$9%{DvJ&0J+U**TE4*SH;uFPUDxV~+PeRF2j zB52~t!SZg-p7n-z>6dtNW7f*UKHC|QrQ0mg*((uY4TY0lWxsWYS->^T5K$@?K96kf(6@;YVmoA6X6JV zgg$#z@A{r{{8%7mE65eF9<=Vn^JsAp*DeH`y{N9Q+8%V{d3D7#tiG6QHxZP^*0Et1 zD#-d)EsxU2hmw767u7-7ZJt)s-oSlW5y_fHx56p>vqC4^TV>y7+;faw1_GUdKR=)< zilhJ+{M4y*dSo96-*pC^jvsV_VSLib@Uxw)(K+_PSCc;9p&5I2X^i|8j<>VAZ2{)K zj5JABToH+y3pLSk4M}xG7rD@ub!V>6E5EjwG3?%x;r;)UadHA2pAM}Lj|aoILzjST lR1FzAB}odh(MR_;{vVh!y8RcSINee#NV3eZ?8TtT{09RZaV^ks@XwXukgnr!~XO4U;hFVha^fakqtUHz!6HY3#bKfiL-gaK?3q5 z#6e6pk%J2i4>&;VN2Xq9pw0t zumteN3r>UBdsi6f34=5!9Bu66C8WZb{!%$z=n0Lrca^wzRf`@`s4=LfcmTY@u7{fB z8BD3n0a=reg{rIoRpO6TRa1cl-~dHwcFL=ViC;5}!8WrLcA6@UHHe5a!U+upQ(E8{ zKhu=DPiTpoZ)@vq?E;Ge1kfZ2rIEVBafE_x$W+gOT`B|WyqAhZO?RxNM_0t9`A%YN zkbrJdRlEcG%0&w$$S2Fa81i0>(@pX~B9KP`30|JaCN2QjiZDt%60}of`Surq6BJJ3 zIgaNvPA+kBj^lDWyN}Kn_~myJxiQF73ySVnK`3YL#=#WAdv~SeU3zweq8CsBv%RjG z6$)E!XxSgJLmO@r#36RPg@;{w-`Goh3V9PG>1L~mP#DrSg*x)%eInCh?ucv=>OqSL zFFXo-3etWt)558RE1s&X6Cu1#Cer|i86G}0vwmdOK< z1UU^9CzRqRri0c_WG4>fvo-Cqdf&jr?SH-Z<{9HmD4c8aK~X)qoerCJnA1D6P|KdH zw^2ON>pYUw$Wv>pwR`f=SzYNTb_aEg>lDV})|Dm%(!uW|!CE#3)9S>&b}w z4vle-=StV2#VJYTtNKwvwsU)X_N0W|B_X zD;w)zBHbR&k({~Ab7Ey^n5W~fj|D3ztxh&0HOyQuGN&C@mf*ObpL=G1cxyd4cknu! z`?Enu)?3NmhSzh17YqkfhWq&eMV3VwEWl5lLdQb-f#6-I(XsbI$7|Xroy2{%ld5z; zeekV{pYNz@dv-~LJRe8fA>3^PW?FZ#WJ|74Rb5#W7Pf>0gt1~{S6oy%#B+^bTUZRc z_ZBSw|1GFgfa}p|^x;)tU^iG0kj@~Jq2rGvAf0J+Kl1*-l+o?q14YS}VqWBAhGozC GI`bdhrra0+ diff --git a/backend/rag/retriever.py b/backend/rag/retriever.py deleted file mode 100644 index 1abea85..0000000 --- a/backend/rag/retriever.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import faiss -import numpy as np -import pickle -from sentence_transformers import SentenceTransformer - -INDEX_DIR = "rag/index" -INDEX_PATH = f"{INDEX_DIR}/faiss_index.index" -METADATA_PATH = f"{INDEX_DIR}/metadata.pkl" - -model = SentenceTransformer('all-MiniLM-L6-v2') - -def retrieve(query: str, top_k: int = 5, condition_tags: list[str] = None): - if not os.path.exists(INDEX_PATH) or not os.path.exists(METADATA_PATH): - return [] - - index = faiss.read_index(INDEX_PATH) - with open(METADATA_PATH, "rb") as f: - all_metadata = pickle.load(f) - - query_embedding = model.encode([query]) - - # Simple filtering: find matching tags first if provided - # For now, we perform local search then filter or vice-versa. - # A better way is to use a subset of the index, but for small data, this works: - - distances, indices = index.search(np.array(query_embedding).astype('float32'), top_k * 2) - - results = [] - for idx in indices[0]: - if idx == -1: continue - meta = all_metadata[idx] - - # Optional tag filtering - if condition_tags: - match = any(tag in meta["condition_tags"] for tag in condition_tags) - if not match: continue - - results.append({ - "text": meta["text"], - "source": meta["source_document"] - }) - - if len(results) >= top_k: - break - - return results diff --git a/backend/requirements.txt b/backend/requirements.txt index f1594f1..ec4398c 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -7,4 +7,6 @@ httpx python-dotenv python-multipart qrcode -psycopg2-binary \ No newline at end of file +psycopg2-binary +neo4j +groq \ No newline at end of file diff --git a/backend/routes/health_chat.py b/backend/routes/health_chat.py new file mode 100644 index 0000000..121a0e2 --- /dev/null +++ b/backend/routes/health_chat.py @@ -0,0 +1,282 @@ +from fastapi import APIRouter, HTTPException, Query +from groq import Groq +from datetime import datetime +import json +import os +from prompts.importance_detector import check_importance +from services.neo4j_health_service import neo4j_service +from services.supabase_service import supabase +from pydantic import BaseModel +from typing import List, Dict, Optional + +router = APIRouter(prefix="/health", tags=["health"]) + +class ChatRequest(BaseModel): + patient_id: str + message: str + +class ChatResponse(BaseModel): + ai_reply: str + bot_reply: Optional[str] = None + saving: bool # Show "saving..." loading indicator + saved_data: Optional[dict] = None + importance_score: Optional[int] = None + +EXTRACTION_PROMPT = """ +You are a precise clinical information extractor. Extract any relevant health, lifestyle, and history details from the patient message. +Respond ONLY with a valid JSON object matching the following structure: +{ + "conditions": [{"name": "Diabetes", "status": "active/managed/resolved", "date": "YYYY-MM-DD"}], + "symptoms": [{"name": "Fever", "severity": 8, "date": "YYYY-MM-DD"}], + "medications": [{"name": "Paracetamol", "dosage": "500mg", "frequency": "twice daily", "start_date": "YYYY-MM-DD"}], + "allergies": [{"allergen": "Penicillin", "severity": "high/medium/low", "date": "YYYY-MM-DD"}], + "surgeries": [{"name": "Appendectomy", "date": "YYYY-MM-DD", "notes": "notes"}], + "vaccinations": [{"name": "COVID Booster", "date": "YYYY-MM-DD"}], + "habits": [{"name": "Smoking", "frequency": "daily/weekly/occasional", "duration": "5 years"}], + "sleep": [{"hours": 7.5, "quality": "poor/good/excellent"}], + "diets": [{"type": "Vegetarian/Keto/etc."}], + "visits": [{"doctor": "Dr. Sharma", "reason": "regular checkup", "date": "YYYY-MM-DD"}], + "hospitalizations": [{"reason": "Dengue", "date": "YYYY-MM-DD"}], + "labs": [{"test_name": "HbA1c", "value": "6.5", "unit": "%", "date": "YYYY-MM-DD"}], + "blood_group": "O+/A-/etc.", + "emergency_contacts": [{"name": "Jane Doe", "phone": "1234567890", "relation": "Spouse"}], + "family_member_link": "relative_patient_id" +} + +Fill in missing details or dates with today's date ("{today}") if applicable. Only include sections where actual information was mentioned. + +Message: +"{message}" +""" + +@router.post("/chat", response_model=ChatResponse) +async def health_chat(request: ChatRequest): + """ + Chat with patient → extract comprehensive health graph nodes → check importance → + save to Neo4j if important → return AI reply with saved metadata + """ + client = Groq(api_key=os.getenv("GROQ_API_KEY")) + today = datetime.now().strftime("%Y-%m-%d") + + # 1. Get compassionate health assistant reply + reply = client.chat.completions.create( + model="llama-3.3-70b-versatile", + messages=[ + {"role": "system", "content": "You are a compassionate health assistant. Reply empathetically."}, + {"role": "user", "content": request.message} + ] + ) + ai_reply = reply.choices[0].message.content + + # 2. Extract structured fields using LLM + extract = client.chat.completions.create( + model="llama-3.3-70b-versatile", + messages=[ + {"role": "system", "content": EXTRACTION_PROMPT.format(today=today, message=request.message)} + ], + temperature=0.1 + ) + + try: + content = extract.choices[0].message.content.strip() + if content.startswith("```json"): + content = content.split("```json")[1].split("```")[0].strip() + elif content.startswith("```"): + content = content.split("```")[1].split("```")[0].strip() + extracted = json.loads(content) + except Exception as e: + print(f"[health_chat] Extraction error: {e}. Content: {extract.choices[0].message.content}") + extracted = {} + + # 3. Assess data importance + importance = check_importance(extracted, client) + should_save = importance.get("should_save", False) + importance_score = importance.get("importance_score", 0) + + saved_data = {} + + # 4. Save to Neo4j health graph if score threshold met (>= 4 for detailed schemas) + if should_save and importance_score >= 4: + # Conditions + for cond in extracted.get("conditions", []): + if cond.get("name"): + neo4j_service.save_condition(request.patient_id, cond["name"], cond.get("status", "active"), cond.get("date", today)) + saved_data.setdefault("conditions", []).append(cond["name"]) + + # Symptoms + for symptom in extracted.get("symptoms", []): + if symptom.get("name"): + neo4j_service.save_symptom(request.patient_id, symptom["name"], symptom.get("severity", 5), symptom.get("date", today)) + saved_data.setdefault("symptoms", []).append(symptom["name"]) + + # Medications + for med in extracted.get("medications", []): + if med.get("name"): + neo4j_service.save_medication(request.patient_id, med["name"], med.get("dosage", ""), med.get("frequency", ""), med.get("start_date", today)) + saved_data.setdefault("medications", []).append(med["name"]) + + # Allergies + for alg in extracted.get("allergies", []): + if alg.get("allergen"): + neo4j_service.save_allergy(request.patient_id, alg["allergen"], alg.get("severity", "medium"), alg.get("date", today)) + saved_data.setdefault("allergies", []).append(alg["allergen"]) + + # Surgeries + for surg in extracted.get("surgeries", []): + if surg.get("name"): + neo4j_service.save_surgery(request.patient_id, surg["name"], surg.get("date", today), surg.get("notes", "")) + saved_data.setdefault("surgeries", []).append(surg["name"]) + + # Vaccinations + for vac in extracted.get("vaccinations", []): + if vac.get("name"): + neo4j_service.save_vaccination(request.patient_id, vac["name"], vac.get("date", today)) + saved_data.setdefault("vaccinations", []).append(vac["name"]) + + # Habits + for habit in extracted.get("habits", []): + if habit.get("name"): + neo4j_service.save_habit(request.patient_id, habit["name"], habit.get("frequency", "daily"), habit.get("duration", ""), today) + saved_data.setdefault("habits", []).append(habit["name"]) + + # Sleep + for sl in extracted.get("sleep", []): + if sl.get("hours") is not None: + neo4j_service.save_sleep(request.patient_id, float(sl["hours"]), sl.get("quality", "good"), today) + saved_data.setdefault("sleep", []).append(f"{sl['hours']}h ({sl.get('quality', 'good')})") + + # Diets + for diet in extracted.get("diets", []): + if diet.get("type"): + neo4j_service.save_diet(request.patient_id, diet["type"], today) + saved_data.setdefault("diets", []).append(diet["type"]) + + # Doctor Visits + for visit in extracted.get("visits", []): + if visit.get("doctor"): + neo4j_service.save_doctor_visit(request.patient_id, visit["doctor"], visit.get("reason", ""), visit.get("date", today)) + saved_data.setdefault("visits", []).append(visit["doctor"]) + + # Hospitalizations + for hosp in extracted.get("hospitalizations", []): + if hosp.get("reason"): + neo4j_service.save_hospitalization(request.patient_id, hosp["reason"], hosp.get("date", today)) + saved_data.setdefault("hospitalizations", []).append(hosp["reason"]) + + # Labs + for lab in extracted.get("labs", []): + if lab.get("test_name"): + neo4j_service.save_lab_result(request.patient_id, lab["test_name"], str(lab.get("value", "")), lab.get("unit", ""), lab.get("date", today)) + saved_data.setdefault("labs", []).append(lab["test_name"]) + + # Blood Group + bg = extracted.get("blood_group") + if bg: + neo4j_service.save_blood_group(request.patient_id, bg) + saved_data["blood_group"] = [bg] + + # Emergency Contacts + for contact in extracted.get("emergency_contacts", []): + if contact.get("name") and contact.get("phone"): + neo4j_service.save_emergency_contact(request.patient_id, contact["name"], contact["phone"], contact.get("relation", "Contact")) + saved_data.setdefault("emergency_contacts", []).append(contact["name"]) + + # Family link + fl = extracted.get("family_member_link") + if fl: + neo4j_service.link_family_member(request.patient_id, fl) + saved_data["family_member_link"] = [fl] + + return ChatResponse( + ai_reply=ai_reply, + bot_reply=ai_reply, + saving=bool(should_save and importance_score >= 4), + saved_data=saved_data if (should_save and importance_score >= 4) else None, + importance_score=importance_score if (should_save and importance_score >= 4) else None + ) + +@router.post("/daily-summary") +async def generate_daily_summary(patient_id: str): + """ + Generate daily summary from Neo4j data collected today + Store in Supabase table + """ + today = datetime.now().strftime("%Y-%m-%d") + + with neo4j_service.driver.session() as session: + result = session.run(""" + MATCH (u:User {id: $patient_id})-[r]->(n) + WHERE r.last_reported = $date OR r.since = $date OR r.started_date = $date + OR r.date = $date OR r.last_updated = $date OR r.diagnosed_date = $date + RETURN type(r) as rel_type, labels(n)[0] as node_type, + coalesce(n.name, n.text, n.type) as label + """, patient_id=patient_id, date=today) + data = [dict(record) for record in result] + + symptoms = [d["label"] for d in data if d["node_type"] == "Symptom"] + facts = [d["label"] for d in data if d["node_type"] == "HealthFact"] + surgeries = [d["label"] for d in data if d["node_type"] == "Surgery"] + medications = [d["label"] for d in data if d["node_type"] == "Medication"] + + # Fallback to general count if empty + client = Groq(api_key=os.getenv("GROQ_API_KEY")) + summary_prompt = f""" + Generate a brief daily health summary based on today's logs: + Symptoms: {symptoms} + Health facts/habits: {facts} + Surgeries: {surgeries} + Medications: {medications} + + Write 1-2 sentences summarizing the patient's health status today. + """ + + summary_response = client.chat.completions.create( + model="llama-3.3-70b-versatile", + messages=[ + {"role": "system", "content": "You are a medical summarizer. Be concise."}, + {"role": "user", "content": summary_prompt} + ] + ) + summary_text = summary_response.choices[0].message.content + + supabase.table("daily_health_summaries").upsert({ + "patient_id": patient_id, + "summary_date": today, + "summary_text": summary_text, + "symptoms_reported": symptoms, + "facts_mentioned": facts, + "surgeries_mentioned": surgeries, + "medications_mentioned": medications, + "data_importance_score": len(data), + "important_data_found": len(data) > 0, + "chat_messages_count": len(data) + }).execute() + + return { + "status": "summary_saved", + "date": today, + "summary": summary_text, + "data_count": len(data) + } + +# --- Hackathon Insights APIs --- + +@router.get("/insights/family-disease-risk") +async def get_family_disease_risk(patient_id: str = Query(...)): + return neo4j_service.get_family_disease_risk(patient_id) + +@router.get("/insights/shared-medications") +async def get_shared_medications(patient_id: str = Query(...)): + return neo4j_service.get_shared_medications(patient_id) + +@router.get("/insights/symptom-trends") +async def get_symptom_trends(patient_id: str = Query(...)): + return neo4j_service.get_symptom_trends(patient_id) + +@router.get("/insights/habit-correlations") +async def get_habit_correlations(patient_id: str = Query(...)): + return neo4j_service.get_habit_correlations(patient_id) + +@router.get("/insights/timeline") +async def get_health_timeline(patient_id: str = Query(...)): + return neo4j_service.get_health_timeline(patient_id) diff --git a/backend/routes/risk.py b/backend/routes/risk.py index 1cfe632..086c90e 100644 --- a/backend/routes/risk.py +++ b/backend/routes/risk.py @@ -3,10 +3,18 @@ from schemas.models import RiskGenerateInput, RiskScore, RiskPredictInput, HealthPrediction, SymptomObj from services.groq_client import call_groq from prompts.risk import RISK_ADJUSTMENT_PROMPT, RISK_PREDICTION_PROMPT -from rag.retriever import retrieve -from ml.predict import calculate_trajectory import logging +def retrieve(query: str, top_k: int = 3, condition_tags: list = None): + return [] + +def calculate_trajectory(risk_scores_history: list, symptoms_history: list): + return { + "trajectory": "stable", + "projected_scores": [60, 60, 60] + } + + router = APIRouter(prefix="/risk", tags=["risk"]) def calculate_base_score(data: RiskGenerateInput) -> int: diff --git a/backend/services/neo4j_health_service.py b/backend/services/neo4j_health_service.py new file mode 100644 index 0000000..965c549 --- /dev/null +++ b/backend/services/neo4j_health_service.py @@ -0,0 +1,270 @@ +from neo4j import GraphDatabase +from typing import Dict, List +import os + +class Neo4jHealthService: + def __init__(self): + # Support both NEO4J_USERNAME and NEO4J_USER + uri = os.getenv("NEO4J_URI") + user = os.getenv("NEO4J_USERNAME") or os.getenv("NEO4J_USER") or "neo4j" + password = os.getenv("NEO4J_PASSWORD") + + if not uri or not password: + print("[Neo4jHealthService] Warning: NEO4J_URI or NEO4J_PASSWORD not set in environment.") + + self.driver = GraphDatabase.driver( + uri or "bolt://localhost:7687", + auth=(user, password or "") + ) + + def create_user(self, patient_id: str, name: str, age: int): + """Create user node""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + SET u.name = $name, + u.age = $age, + u.created_at = datetime() + """, patient_id=patient_id, name=name, age=age) + + def link_family_member(self, patient_id: str, relative_id: str): + """Create bidirectional family connection""" + with self.driver.session() as session: + session.run(""" + MATCH (u1:User {id: $patient_id}) + MATCH (u2:User {id: $relative_id}) + MERGE (u1)-[:FAMILY_MEMBER]->(u2) + MERGE (u2)-[:FAMILY_MEMBER]->(u1) + """, patient_id=patient_id, relative_id=relative_id) + + def save_symptom(self, patient_id: str, symptom_name: str, severity: int, date: str): + """Save symptom""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (s:Symptom {name: $symptom_name}) + MERGE (u)-[r:HAS_SYMPTOM]->(s) + SET r.severity = $severity, + r.since = $date, + r.last_reported = $date, + r.status = 'active' + """, patient_id=patient_id, symptom_name=symptom_name, + severity=severity, date=date) + + def save_condition(self, patient_id: str, condition_name: str, status: str, date: str): + """Save condition""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (c:Condition {name: $condition_name}) + MERGE (u)-[r:HAS_CONDITION]->(c) + SET r.status = $status, + r.diagnosed_date = $date + """, patient_id=patient_id, condition_name=condition_name, status=status, date=date) + + def save_fact(self, patient_id: str, fact_text: str, category: str, date: str): + """Save general health fact""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + CREATE (f:HealthFact { + text: $fact_text, + category: $category, + date: $date, + id: randomUUID() + }) + CREATE (u)-[r:HAS_HEALTH_FACT]->(f) + """, patient_id=patient_id, fact_text=fact_text, + category=category, date=date) + + def save_allergy(self, patient_id: str, allergen: str, severity: str, date: str): + """Save allergy""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (a:Allergy {name: $allergen}) + MERGE (u)-[r:HAS_ALLERGY]->(a) + SET r.severity = $severity, + r.last_reported = $date + """, patient_id=patient_id, allergen=allergen, severity=severity, date=date) + + def save_surgery(self, patient_id: str, surgery_name: str, date: str, notes: str = ""): + """Save surgery""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + CREATE (s:Surgery { + name: $surgery_name, + date: $date, + notes: $notes, + id: randomUUID() + }) + CREATE (u)-[r:HAD_SURGERY]->(s) + """, patient_id=patient_id, surgery_name=surgery_name, + date=date, notes=notes) + + def save_medication(self, patient_id: str, med_name: str, dosage: str, frequency: str, date: str): + """Save medication""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (m:Medication {name: $med_name}) + MERGE (u)-[r:TAKES_MEDICATION]->(m) + SET r.dosage = $dosage, + r.frequency = $frequency, + r.started_date = $date + """, patient_id=patient_id, med_name=med_name, + dosage=dosage, frequency=frequency, date=date) + + def save_vaccination(self, patient_id: str, vaccine_name: str, date: str): + """Save vaccine""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (v:Vaccination {name: $vaccine_name}) + MERGE (u)-[r:RECEIVED_VACCINE]->(v) + SET r.date = $date + """, patient_id=patient_id, vaccine_name=vaccine_name, date=date) + + def save_habit(self, patient_id: str, habit_name: str, frequency: str, duration: str, date: str): + """Save lifestyle habit""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (h:Habit {name: $habit_name}) + MERGE (u)-[r:HAS_HEALTH_HABIT]->(h) + SET r.frequency = $frequency, + r.duration = $duration, + r.last_updated = $date + """, patient_id=patient_id, habit_name=habit_name, frequency=frequency, duration=duration, date=date) + + def save_sleep(self, patient_id: str, hours: float, quality: str, date: str): + """Save sleep record""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + CREATE (s:Sleep { + hours: $hours, + quality: $quality, + date: $date, + id: randomUUID() + }) + CREATE (u)-[r:HAS_SLEEP_PATTERN]->(s) + """, patient_id=patient_id, hours=hours, quality=quality, date=date) + + def save_diet(self, patient_id: str, diet_type: str, date: str): + """Save diet pattern""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (d:Diet {type: $diet_type}) + MERGE (u)-[r:FOLLOWS_DIET]->(d) + SET r.last_updated = $date + """, patient_id=patient_id, diet_type=diet_type, date=date) + + def save_doctor_visit(self, patient_id: str, doctor_name: str, reason: str, date: str): + """Save doctor visit""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + CREATE (d:Doctor {name: $doctor_name}) + CREATE (u)-[r:VISITED {reason: $reason, date: $date}]->(d) + """, patient_id=patient_id, doctor_name=doctor_name, reason=reason, date=date) + + def save_hospitalization(self, patient_id: str, reason: str, date: str): + """Save hospitalization""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + CREATE (h:Hospitalization {reason: $reason, date: $date, id: randomUUID()}) + CREATE (u)-[r:HOSPITALIZED_FOR]->(h) + """, patient_id=patient_id, reason=reason, date=date) + + def save_lab_result(self, patient_id: str, test_name: str, value: str, unit: str, date: str): + """Save lab result""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (l:LabTest {name: $test_name}) + CREATE (u)-[r:HAS_LAB_RESULT {value: $value, unit: $unit, date: $date}]->(l) + """, patient_id=patient_id, test_name=test_name, value=value, unit=unit, date=date) + + def save_blood_group(self, patient_id: str, group_name: str): + """Save blood group""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (b:BloodGroup {name: $group_name}) + MERGE (u)-[:HAS_BLOOD_GROUP]->(b) + """, patient_id=patient_id, group_name=group_name) + + def save_emergency_contact(self, patient_id: str, contact_name: str, phone: str, relation: str): + """Save emergency contact""" + with self.driver.session() as session: + session.run(""" + MERGE (u:User {id: $patient_id}) + MERGE (c:EmergencyContact {name: $contact_name, phone: $phone}) + MERGE (u)-[r:EMERGENCY_CONTACT]->(c) + SET r.relation = $relation + """, patient_id=patient_id, contact_name=contact_name, phone=phone, relation=relation) + + + # --- Hackathon-Winning Query Analytics --- + + def get_family_disease_risk(self, patient_id: str) -> List[Dict]: + """Which conditions occur most in the family network?""" + with self.driver.session() as session: + res = session.run(""" + MATCH (u:User {id: $patient_id})-[:FAMILY_MEMBER*1..2]-(member:User)-[:HAS_CONDITION]->(c:Condition) + RETURN c.name as condition, count(distinct member) as occurrence_count, collect(member.name) as affected_family + ORDER BY occurrence_count DESC + """, patient_id=patient_id) + return [dict(r) for r in res] + + def get_shared_medications(self, patient_id: str) -> List[Dict]: + """Which medicines are commonly used in the family?""" + with self.driver.session() as session: + res = session.run(""" + MATCH (u:User {id: $patient_id})-[:FAMILY_MEMBER*1..2]-(member:User)-[:TAKES_MEDICATION]->(m:Medication) + RETURN m.name as medication, count(distinct member) as user_count, collect(member.name) as users + ORDER BY user_count DESC + """, patient_id=patient_id) + return [dict(r) for r in res] + + def get_symptom_trends(self, patient_id: str) -> List[Dict]: + """Which symptoms are active/increasing in the family network?""" + with self.driver.session() as session: + res = session.run(""" + MATCH (u:User {id: $patient_id})-[:FAMILY_MEMBER*1..2]-(member:User)-[r:HAS_SYMPTOM]->(s:Symptom) + RETURN s.name as symptom, count(r) as reports, avg(r.severity) as avg_severity + ORDER BY reports DESC + """, patient_id=patient_id) + return [dict(r) for r in res] + + def get_habit_correlations(self, patient_id: str) -> List[Dict]: + """Correlate sleep quality with symptom occurrence across the user base/family""" + with self.driver.session() as session: + res = session.run(""" + MATCH (u:User)-[:HAS_SLEEP_PATTERN]->(s:Sleep) + WHERE s.quality IN ['poor', 'restless'] + MATCH (u)-[r:HAS_SYMPTOM]->(sym:Symptom) + RETURN sym.name as symptom, count(distinct u) as affected_users, avg(r.severity) as avg_severity + ORDER BY affected_users DESC + """, patient_id=patient_id) + return [dict(r) for r in res] + + def get_health_timeline(self, patient_id: str) -> List[Dict]: + """Show full user health journey chronological log""" + with self.driver.session() as session: + res = session.run(""" + MATCH (u:User {id: $patient_id})-[r]->(n) + WHERE r.date IS NOT NULL OR r.since IS NOT NULL OR r.started_date IS NOT NULL OR r.diagnosed_date IS NOT NULL + RETURN coalesce(r.date, r.since, r.started_date, r.diagnosed_date) as event_date, + type(r) as event_type, + labels(n)[0] as node_type, + properties(n) as details + ORDER BY event_date DESC + """, patient_id=patient_id) + return [dict(r) for r in res] + +neo4j_service = Neo4jHealthService() diff --git a/backend/test_hf_load.py b/backend/test_hf_load.py deleted file mode 100644 index 2653d34..0000000 --- a/backend/test_hf_load.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from dotenv import load_dotenv - -# Load .env BEFORE importing sentence_transformers -load_dotenv() - -# Explicitly set them again just to be absolutely sure -os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com' -os.environ['HF_HUB_DISABLE_SYMLINKS_WARNING'] = '1' - -from sentence_transformers import SentenceTransformer - -print(f"HF_ENDPOINT (environ): {os.environ.get('HF_ENDPOINT')}") -print(f"CACHE_DIR: rag/model_cache") - -model_name = 'all-MiniLM-L6-v2' -cache_dir = "rag/model_cache" - -try: - print(f"--- Attempting to load '{model_name}' via mirror ---") - model = SentenceTransformer(model_name, cache_folder=cache_dir) - print("DONE: Model loaded.") - - # Test encoding - print("--- Testing encoding ---") - embedding = model.encode(["Hello World"]) - print(f"DONE: Encoding successful. Vector shape: {embedding.shape}") - -except Exception as e: - print(f"ERROR: Error during model load: {e}") diff --git a/database/schema.sql b/database/schema.sql index a280eba..8abb42d 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -1,50 +1,89 @@ --- WARNING: This schema is for context only and is not meant to be run. --- Table order and constraints may not be valid for execution. - -CREATE TABLE public.patients ( - id text NOT NULL DEFAULT (gen_random_uuid())::text, - full_name text NOT NULL, - age integer, - phone_number text NOT NULL UNIQUE, - gender text, - location text, - created_at timestamp with time zone DEFAULT now(), - CONSTRAINT patients_pkey PRIMARY KEY (id) -); -CREATE TABLE public.family_groups ( - id text NOT NULL DEFAULT (gen_random_uuid())::text, - family_name text NOT NULL, - family_code text NOT NULL UNIQUE CHECK (family_code ~ '^[0-9]{6}$'::text), - created_by text NOT NULL, - created_at timestamp with time zone DEFAULT now(), - CONSTRAINT family_groups_pkey PRIMARY KEY (id), - CONSTRAINT family_groups_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.patients(id) -); -CREATE TABLE public.family_members ( - id text NOT NULL DEFAULT (gen_random_uuid())::text, - family_id text NOT NULL, - patient_id text NOT NULL, - role text DEFAULT 'member'::text, - joined_at timestamp with time zone DEFAULT now(), - CONSTRAINT family_members_pkey PRIMARY KEY (id), - CONSTRAINT family_members_family_id_fkey FOREIGN KEY (family_id) REFERENCES public.family_groups(id), - CONSTRAINT family_members_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id) -); -CREATE TABLE public.medical_information ( - id text NOT NULL DEFAULT (gen_random_uuid())::text, - patient_id text NOT NULL UNIQUE, - weight text, - height text, - blood_type text, - allergies text, - blood_pressure text, - heart_rate text, - oxygen_level text, - surgeries text, - chronic_conditions text, - vaccinations text, - family_genetics text, - updated_at timestamp with time zone DEFAULT now(), - CONSTRAINT medical_information_pkey PRIMARY KEY (id), - CONSTRAINT medical_information_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id) -); \ No newline at end of file +-- WARNING: This schema is for context only and is not meant to be run. +-- Table order and constraints may not be valid for execution. + +CREATE TABLE public.patients ( + id text NOT NULL DEFAULT (gen_random_uuid())::text, + full_name text NOT NULL, + age integer, + phone_number text NOT NULL UNIQUE, + gender text, + location text, + created_at timestamp with time zone DEFAULT now(), + risk_level text DEFAULT 'Low'::text, + CONSTRAINT patients_pkey PRIMARY KEY (id) +); +CREATE TABLE public.family_groups ( + id text NOT NULL DEFAULT (gen_random_uuid())::text, + family_name text NOT NULL, + family_code text NOT NULL UNIQUE CHECK (family_code ~ '^[0-9]{6}$'::text), + created_by text NOT NULL, + created_at timestamp with time zone DEFAULT now(), + health_summary text, + CONSTRAINT family_groups_pkey PRIMARY KEY (id), + CONSTRAINT family_groups_created_by_fkey FOREIGN KEY (created_by) REFERENCES public.patients(id) +); +CREATE TABLE public.family_members ( + id text NOT NULL DEFAULT (gen_random_uuid())::text, + family_id text NOT NULL, + patient_id text NOT NULL, + role text DEFAULT 'member'::text, + joined_at timestamp with time zone DEFAULT now(), + health_summary text, + CONSTRAINT family_members_pkey PRIMARY KEY (id), + CONSTRAINT family_members_family_id_fkey FOREIGN KEY (family_id) REFERENCES public.family_groups(id), + CONSTRAINT family_members_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id) +); +CREATE TABLE public.medical_information ( + id text NOT NULL DEFAULT (gen_random_uuid())::text, + patient_id text NOT NULL UNIQUE, + weight text, + height text, + blood_type text, + allergies text, + blood_pressure text, + heart_rate text, + oxygen_level text, + surgeries text, + chronic_conditions text, + vaccinations text, + family_genetics text, + updated_at timestamp with time zone DEFAULT now(), + CONSTRAINT medical_information_pkey PRIMARY KEY (id), + CONSTRAINT medical_information_patient_id_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id) +); +CREATE TABLE public.Medicines ( + sub_category text, + product_name text, + salt_composition text, + product_price text, + product_manufactured text, + medicine_desc text, + side_effects text, + drug_interactions jsonb, + Id uuid NOT NULL DEFAULT gen_random_uuid(), + CONSTRAINT Medicines_pkey PRIMARY KEY (Id) +); + +CREATE TABLE public.daily_health_summaries ( + id uuid NOT NULL DEFAULT gen_random_uuid(), + patient_id text NOT NULL, + summary_date date NOT NULL DEFAULT CURRENT_DATE, + summary_text text NOT NULL, + -- Breakdown + symptoms_reported text[], + facts_mentioned text[], + surgeries_mentioned text[], + medications_mentioned text[], + mood_indicator text, -- 'positive', 'neutral', 'negative', 'anxious' + data_importance_score integer, -- 1-10 (how much important data was collected) + -- Meta + chat_messages_count integer DEFAULT 0, + important_data_found boolean DEFAULT false, + created_at timestamptz DEFAULT now(), + CONSTRAINT daily_summaries_pkey PRIMARY KEY (id), + CONSTRAINT daily_summaries_unique UNIQUE (patient_id, summary_date), + CONSTRAINT daily_summaries_patient_fkey FOREIGN KEY (patient_id) REFERENCES public.patients(id) +); + +CREATE INDEX idx_daily_summaries_patient ON public.daily_health_summaries(patient_id); +CREATE INDEX idx_daily_summaries_date ON public.daily_health_summaries(summary_date); \ No newline at end of file From 168d6c7e8749e6e5e3307711cfbb75df24fdf93c Mon Sep 17 00:00:00 2001 From: indresh404 Date: Wed, 17 Jun 2026 01:19:48 +0530 Subject: [PATCH 02/17] fixed folder structrue --- app/app/{auth => (auth)}/callback.tsx | 0 app/app/(auth)/login.tsx | 3 + app/app/(auth)/otp.tsx | 547 ----- app/app/(onboarding)/_layout.tsx | 5 +- app/app/(onboarding)/agent-log.tsx | 696 ------ app/app/(onboarding)/chat.tsx | 1992 ++++------------- app/app/(onboarding)/confirm.tsx | 109 - app/app/(onboarding)/family-setup.tsx | 59 +- app/app/(onboarding)/medical-profile.tsx | 344 --- app/app/(onboarding)/summary.tsx | 258 ++- app/app/(onboarding)/user-details.tsx | 1036 --------- app/app/(onboarding)/welcome.tsx | 172 -- app/app/(tabs)/_layout.tsx | 14 +- app/app/(tabs)/{aibot => chatbot}/index.tsx | 4 +- app/app/(tabs)/history/index.tsx | 524 ----- app/app/_layout.tsx | 21 +- app/app/chat/index.tsx | 288 --- app/app/index.tsx | 4 +- app/components/{chat => chatbot}/AgentLog.tsx | 2 +- .../{chat => chatbot}/ChatBubble.tsx | 0 .../{chat => chatbot}/ChatHeader.tsx | 0 .../{chat => chatbot}/ChatInput.tsx | 0 .../{chat => chatbot}/SavingBanner.tsx | 0 .../{chat => chatbot}/SymptomBadge.tsx | 0 .../{chat => chatbot}/TypingIndicator.tsx | 0 app/scratch/check_db.js | 18 + app/services/auth.service.ts | 6 +- backend/routes/health_graph.py | 636 ++++++ backend/services/neo4j_service.py | 38 + docs/App structure/file structure.md | 66 + docs/app_structure.md | 70 - 31 files changed, 1406 insertions(+), 5506 deletions(-) rename app/app/{auth => (auth)}/callback.tsx (100%) delete mode 100644 app/app/(auth)/otp.tsx delete mode 100644 app/app/(onboarding)/agent-log.tsx delete mode 100644 app/app/(onboarding)/confirm.tsx delete mode 100644 app/app/(onboarding)/medical-profile.tsx delete mode 100644 app/app/(onboarding)/user-details.tsx delete mode 100644 app/app/(onboarding)/welcome.tsx rename app/app/(tabs)/{aibot => chatbot}/index.tsx (98%) delete mode 100644 app/app/(tabs)/history/index.tsx delete mode 100644 app/app/chat/index.tsx rename app/components/{chat => chatbot}/AgentLog.tsx (99%) rename app/components/{chat => chatbot}/ChatBubble.tsx (100%) rename app/components/{chat => chatbot}/ChatHeader.tsx (100%) rename app/components/{chat => chatbot}/ChatInput.tsx (100%) rename app/components/{chat => chatbot}/SavingBanner.tsx (100%) rename app/components/{chat => chatbot}/SymptomBadge.tsx (100%) rename app/components/{chat => chatbot}/TypingIndicator.tsx (100%) create mode 100644 app/scratch/check_db.js create mode 100644 backend/routes/health_graph.py create mode 100644 backend/services/neo4j_service.py create mode 100644 docs/App structure/file structure.md delete mode 100644 docs/app_structure.md 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..66284d1 100644 --- a/app/app/(auth)/login.tsx +++ b/app/app/(auth)/login.tsx @@ -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 { @@ -326,6 +328,7 @@ export default function LoginScreen() { }; const handleGoogle = async () => { + if (loading) return; setLoading(true); try { const result = await signInWithGoogle(); diff --git a/app/app/(auth)/otp.tsx b/app/app/(auth)/otp.tsx deleted file mode 100644 index b69a400..0000000 --- a/app/app/(auth)/otp.tsx +++ /dev/null @@ -1,547 +0,0 @@ -// app/(auth)/otp.tsx -import { useAuthStore } from '@/store/auth.store'; -import { Ionicons } from '@expo/vector-icons'; -import { LinearGradient } from 'expo-linear-gradient'; -import { useLocalSearchParams, useRouter } from 'expo-router'; -import React, { useEffect, useRef, useState } from 'react'; -import { Alert, Keyboard, KeyboardAvoidingView, Platform, SafeAreaView, StyleSheet, Text, TextInput, TouchableOpacity, View, ActivityIndicator } from 'react-native'; -import { getPatientByPhone, normalizePhone, getStoredOTP, clearStoredOTP } from '@/services/auth.service'; - -// Define colors directly (no external imports) -const COLORS = { - primary: '#3B82F6', - surface: '#F8FAFC', - white: '#FFFFFF', - blue: { - 500: '#3B82F6', - 900: '#1E3A8A', - }, - gray: { - 300: '#D1D5DB', - 400: '#9CA3AF', - 500: '#6B7280', - }, - text: { - primary: '#1F2937', - secondary: '#4B5563', - muted: '#6B7280', - }, -}; - -const TYPOGRAPHY = { - fonts: { - regular: 'System', - medium: 'System', - semibold: 'System', - bold: 'System', - }, -}; - -export default function OTPVerifyScreen() { - const router = useRouter(); - const setSessionState = useAuthStore((state) => 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 1e8edec..cb966bb 100644 --- a/app/app/(onboarding)/chat.tsx +++ b/app/app/(onboarding)/chat.tsx @@ -11,1766 +11,536 @@ 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; text: string; isUser: boolean; timestamp: Date; - saveStatus?: any; } -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 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...", - } -}; +const { width } = Dimensions.get('window'); -export default function ChatScreen() { - const isVoiceAvailable = Platform.OS !== 'web' && !!NativeModules.Voice; - const [messages, setMessages] = useState([ +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: 'Rahul Kumar', + age: 24, + gender: 'Male', + blood_group: 'O+', + height: '175cm', + weight: '70kg', + 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[] = [ { - id: '1', - text: 'Hello! I am your Swasthya AI Assistant. How can I help you today?', - isUser: false, - timestamp: new Date(), + 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 }; + }, }, - ]); - 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 [historyList, setHistoryList] = useState([]); - 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); + { + 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] : '70kg', + }; + }, + }, + { + 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; + } } - } - 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); + let allergies = 'None'; + if (input.toLowerCase().includes('allergy') || input.toLowerCase().includes('allergies')) { + allergies = input; } - } - - 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); - } - } - }; - - 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); + 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' }; } - } - }; - 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}/health/chat`, { - 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, data.save_status || undefined); - } catch (e) { - console.error(e); - const fallback = voiceLang === 'hi-IN' ? getFallbackReplyHindi(spokenText) : getFallbackReply(spokenText); - speakAIVoiceResponse(fallback); - } - }; - - const speakAIVoiceResponse = (replyText: string, saveStatus?: any) => { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceState('speaking'); - setVoiceSubtitles(replyText); + 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 }; + }, + }, + ]; - const aiMessage: Message = { - id: 'voice-ai-' + Date.now(), - text: replyText, + const [messages, setMessages] = useState([ + { + id: 'welcome-bot', + text: steps[0].question, isUser: false, timestamp: new Date(), - saveStatus: saveStatus - }; - 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(); - fetchHistorySummaries(); - } - }, [user]); - - const fetchHistorySummaries = async () => { - try { - const activePatientId = user?.id || 'demo-patient'; - const { data, error } = await supabase - .from('daily_health_summaries') - .select('*') - .eq('patient_id', activePatientId) - .order('summary_date', { ascending: false }); - - if (error) throw error; - if (data && data.length > 0) { - const formatted: HistoryItem[] = data.map((item) => { - const dateObj = new Date(item.summary_date); - const symptoms = item.symptoms_reported || []; - const facts = item.facts_mentioned || []; - const meds = item.medications_mentioned || []; - const surgeries = item.surgeries_mentioned || []; - - return { - id: item.id, - date: dateObj.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }), - time: 'Daily Sync', - shortDate: dateObj.toLocaleDateString('en-US', { day: 'numeric', month: 'short' }), - overallSummary: item.summary_text || 'No summary text recorded.', - agents: [ - { - name: 'Clinical Extractor', - role: 'Data Miner', - thought: `Extracted details from today's chat. Identified ${symptoms.length} symptoms, ${meds.length} medications, ${facts.length} habits.` - }, - { - name: 'Neo4j Sync Agent', - role: 'Graph Database Writer', - thought: `Successfully committed ${symptoms.length + meds.length + facts.length + surgeries.length} items to AuraDB knowledge graph.` - } - ] - }; - }); - setHistoryList(formatted); - } else { - setHistoryList([]); - } - } catch (err) { - console.error('fetchHistorySummaries error:', err); + // Load patient name from auth store if possible + if (patientId) { + getPatientById(patientId).then((record) => { + if (record && record.name) { + setProfileData(prev => ({ ...prev, full_name: record.name })); + } + }).catch(err => console.log('Error loading patient name in chat', err)); } - }; + }, [patientId]); - 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, + // Add user message + 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); - - 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: [] - }; + Keyboard.dismiss(); + + // Process current step data + const activeStep = steps[currentStep]; + const parsedFields = activeStep.fieldParser(trimmed); + const updatedData = { ...profileData, ...parsedFields }; + setProfileData(updatedData); + + // Transition to next step + const nextStepIndex = currentStep + 1; + if (nextStepIndex < steps.length) { + setCurrentStep(nextStepIndex); + addBotReply(steps[nextStepIndex].question); + } else { + // Completed onboarding chat + 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); + } + }; - fetch(`${BACKEND_URL}/health/chat`, { - 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(), - saveStatus: data.save_status || undefined - }; - 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 handleSkipCurrent = () => { + handleSend('Skip this'); }; - 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 handleSkipAll = () => { + // Navigate straight to summary with current (partially filled or default) profile data + router.push({ + pathname: '/(onboarding)/summary', + params: { profileData: JSON.stringify(profileData) }, + }); }; - const formatTime = (date: Date) => { - return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + const handleChipPress = (chip: string) => { + if (chip === 'Skip this') { + handleSkipCurrent(); + } else { + handleSend(chip); + } }; - const renderMessage = ({ item }: { item: Message }) => ( - - {!item.isUser && ( - - - - )} - - - - {item.text} - - - {formatTime(item.timestamp)} - + return ( + + + + {/* Header */} + + + + + + + Swasthya Assistant + Guided Setup + + + Skip All + + + - {/* Save Status Graph Card */} - {item.saveStatus && ( - - ✅ {item.saveStatus.action || "Saved to health graph"} - {item.saveStatus.message} - - {item.saveStatus.saved_data && Object.keys(item.saveStatus.saved_data).length > 0 && ( - - {Object.entries(item.saveStatus.saved_data).map(([key, list]) => { - if (!Array.isArray(list) || list.length === 0) return null; - return ( - - - {key.charAt(0).toUpperCase() + key.slice(1)}: - - {list.map((subItem, idx) => ( - • {subItem} - ))} - - ); - })} - - )} - - {item.saveStatus.importance_score !== undefined && ( - - - Graph Importance Score: {item.saveStatus.importance_score}/10 - - - - + {/* Messages list */} + item.id} + contentContainerStyle={styles.listContent} + renderItem={({ item }) => ( + + {!item.isUser && ( + + )} + + {item.text} + )} - - {item.isUser && ( - - {getUserInitials()} - - )} - - ); - - const renderHistoryDrawer = () => { - const translateX = slideAnim.interpolate({ - inputRange: [0, 1], - outputRange: [screenWidth, 0], - }); - - return ( - - - {/* History Header */} - - { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setShowHistory(false); - }} - style={styles.historyCloseButton} - > - - - Daily Health History - + onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} + /> + + {/* Typing indicator */} + {isTyping && ( + + + - - {/* History Scrollable Timeline */} - - {(() => { - const displayedHistory = historyList.length > 0 ? historyList : MOCK_HISTORY; - return displayedHistory.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 - - + + + Typing health profile... - - - ); - }; - - const renderVoiceOverlay = () => { - if (!voiceModeActive) return null; + + )} - return ( - - - {/* Header */} - + {/* Suggestions Chips */} + + + {steps[currentStep].chips.map((chip, idx) => ( { - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceModeActive(false); - }} - style={styles.voiceCloseButton} + key={idx} + style={[styles.chip, chip === 'Skip this' && styles.skipChip]} + onPress={() => handleChipPress(chip)} > - + {chip} - 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 : ''} - - - - - ); - }; - - return ( - - + ))} + + + {/* Bottom input bar */} - {/* Header */} - - router.back()} style={styles.backButton}> - - - - - - Swasthya AI Assistant - - { - Keyboard.dismiss(); - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setVoiceModeActive(true); - }} - style={styles.headerActionButton} - > - - - - { - Keyboard.dismiss(); - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - fetchHistorySummaries(); - setShowHistory(true); - }} - style={styles.historyButton} - > - - History - - - - - {/* Messages */} - item.id} - contentContainerStyle={styles.messagesList} - showsVerticalScrollIndicator={false} - /> - - {/* Loading Indicator */} - {isLoading && ( - - - AI is thinking... - - )} - - {/* Input Area */} - - - - - - handleSend()} + returnKeyType="send" /> - + handleSend()}> - - {/* History Drawer Overlay */} - {renderHistoryDrawer()} - - {/* Voice Overlay */} - {renderVoiceOverlay()} ); } const styles = StyleSheet.create({ - safeArea: { + container: { flex: 1, - backgroundColor: '#171717', + backgroundColor: '#07111f', }, 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, - }, - 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, + skipChipText: { + color: '#EF4444', }, - 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', - }, - savedDataCard: { - marginTop: 8, - backgroundColor: '#E8F5E9', - borderLeftWidth: 4, - borderLeftColor: '#27AE60', - padding: 8, - borderRadius: 8, - width: '100%', - }, - savedTitle: { - fontSize: 12, - fontWeight: '600', - color: '#27AE60', - marginBottom: 4, - }, - savedMessage: { - fontSize: 11, - color: '#475569', - marginBottom: 6, - }, - detailsContainer: { - marginTop: 4, - }, - detailItem: { - marginBottom: 4, - }, - detailLabel: { - fontSize: 11, - fontWeight: '600', - color: '#1E293B', - marginBottom: 2, - }, - detailValue: { - fontSize: 11, - color: '#475569', - marginLeft: 6, - }, - scoreBar: { - marginTop: 6, - }, - scoreText: { - fontSize: 10, - color: '#64748B', - marginBottom: 2, - }, - track: { - height: 4, - backgroundColor: '#CBD5E1', - borderRadius: 2, - width: '100%', - }, - scoreIndicator: { - height: 4, - backgroundColor: '#27AE60', - borderRadius: 2, + 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} - - ))} - -