Restore demo-ready chatbot flow and sync resilience#3
Conversation
There was a problem hiding this comment.
Pull request overview
This PR restores an AI assistant chat flow for patient records while improving sync/extraction resilience and aligning Gemini model usage across edge functions and the mobile app.
Changes:
- Adds persistent AI chat storage in Supabase (conversations/messages) with RLS and a new
ai-chat-patientEdge Function. - Improves offline-to-cloud sync by triggering server-side extraction and syncing structured fields/status back to local storage.
- Introduces a shared context-selection utility with Jest coverage, plus UI entry points to the assistant.
Reviewed changes
Copilot reviewed 19 out of 21 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| supabase/migrations/004_ai_chat.sql | Adds AI chat tables, indexes, and RLS policies. |
| supabase/functions/ai-chat-patient/index.ts | New Edge Function for patient-specific AI chat with optional persistence. |
| supabase/functions/extract-handwriting/index.ts | Switches to image_url and adds URL→storage-path normalization. |
| supabase/functions/generate-narrative/index.ts | Updates Gemini model selection for narrative generation. |
| services/cloudSync.ts | Upserts richer record payloads and triggers server extraction during sync. |
| services/geminiService.ts | Adds retry/backoff logic for Gemini handwriting extraction. |
| services/patientDataService.ts | Aligns Gemini text model and reduces noisy error logging. |
| services/aiChat.ts | Adds client-side API wrapper with Edge → direct Gemini fallback. |
| lib/types.ts | Adds AI chat type definitions. |
| lib/contextSelector.ts | Adds shared context selection utilities. |
| tests/contextSelector.test.ts | Adds unit tests for the context selection utilities. |
| jest.config.js / jest.setup.ts | Configures Jest to run repo tests from __tests__. |
| hooks/useNetworkState.ts | Treats isInternetReachable: null as reachable when connected. |
| components/ActiveSession.tsx | Improves surfaced error details when extraction fails. |
| app/patient/summary.tsx | Adds an “AI” action to open the assistant for the current patient. |
| app/patient/[id]/index.tsx | Updates patient detail screen to link to the assistant. |
| app/patient/[id]/assistant.tsx | Adds the patient assistant chat UI. |
| app/patient/[id]/_layout.tsx | Adds a nested layout for patient routes. |
| app.json | Adds expo-font plugin and reformats platforms list. |
| package-lock.json | Updates lockfile entries related to dev dependency flags and peer packages. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| const { data: doctor, error: doctorErr } = await supabaseService | ||
| .from('doctors') | ||
| .select('id, clinic_id') | ||
| .eq('id', user.id) | ||
| .single(); | ||
|
|
||
| const doctorProfile = doctorErr || !doctor | ||
| ? { id: user.id, clinic_id: patient.clinic_id ?? null } | ||
| : doctor; | ||
|
|
||
| let persistenceEnabled = true; | ||
| const { error: chatTableProbeError } = await supabaseService | ||
| .from('ai_conversations') | ||
| .select('id') | ||
| .limit(1); | ||
|
|
||
| if (chatTableProbeError) { | ||
| persistenceEnabled = false; | ||
| console.warn('[ai-chat-patient] ai_conversations unavailable, using stateless mode:', chatTableProbeError.message); | ||
| } | ||
|
|
||
| if (!doctor || doctorErr) { | ||
| persistenceEnabled = false; | ||
| console.warn('[ai-chat-patient] Doctor profile missing, using stateless mode for chat.'); | ||
| } | ||
|
|
||
| if (patient.clinic_id && doctorProfile.clinic_id && patient.clinic_id !== doctorProfile.clinic_id) { | ||
| return jsonResponse({ error: 'Access denied: patient belongs to a different clinic' }, 403); | ||
| } |
| const { data: records } = await supabaseService | ||
| .from('records') | ||
| .select('id, extracted_data, doctor_corrections, status, created_at, session_id') | ||
| .in('session_id', | ||
| (await supabaseService | ||
| .from('sessions') | ||
| .select('id') | ||
| .eq('patient_id', patient_id) | ||
| ).data?.map((s: { id: string }) => s.id) ?? [] | ||
| ) | ||
| .in('status', ['approved', 'needs_review']) | ||
| .order('created_at', { ascending: false }) | ||
| .limit(MAX_RECORDS); | ||
|
|
| await updateRecordExtraction( | ||
| record.id, | ||
| cloudRecord.extracted_data, |
| for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { | ||
| const response = await fetch( | ||
| `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_PRIMARY_MODEL}:generateContent?key=${GEMINI_API_KEY}`, | ||
| { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(payload) | ||
| } | ||
| ); |
| function toStoragePath(imageUrl: string): string { | ||
| // Preferred format is already a storage path like "doctor/session/record.jpg". | ||
| if (!imageUrl.startsWith('http://') && !imageUrl.startsWith('https://')) { | ||
| return imageUrl; | ||
| } | ||
|
|
||
| // Fallback if a public URL was stored in older rows. | ||
| // Expected fragment: /storage/v1/object/public/scan-images/<path> | ||
| const marker = '/storage/v1/object/public/scan-images/'; | ||
| const idx = imageUrl.indexOf(marker); | ||
| if (idx === -1) return imageUrl; | ||
| return decodeURIComponent(imageUrl.substring(idx + marker.length)); | ||
| } |
| TO authenticated | ||
| WITH CHECK ( | ||
| doctor_id = auth.uid() | ||
| AND clinic_id IN (SELECT clinic_id FROM doctors WHERE id = auth.uid()) |
| @@ -0,0 +1,336 @@ | |||
| // @ts-nocheck | |||
| function scoreRelevance(text: string, queryTokens: string[]): number { | ||
| const lower = text.toLowerCase(); | ||
| let score = 0; | ||
| for (const token of queryTokens) { | ||
| if (lower.includes(token)) score++; | ||
| } | ||
| return score; | ||
| } |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@yassinekolsi I've opened a new pull request, #4, to work on those changes. Once the pull request is ready, I'll request review from you. |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@yassinekolsi I've opened a new pull request, #5, to work on those changes. Once the pull request is ready, I'll request review from you. |
… network retry, toStoragePath, RLS policy, ts-nocheck, shared contextSelector Co-authored-by: yassinekolsi <196232305+yassinekolsi@users.noreply.github.com>
Co-authored-by: yassinekolsi <196232305+yassinekolsi@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Fix security, resilience, and type-safety issues in AI chat and sync pipeline
No description provided.