SpandaVidya is a production-grade AI healthcare application that combines Ayurvedic consultation (chat) with cataract detection via computer vision. Users interact through a React Native mobile app; all AI, ML, and data operations are handled exclusively by the NestJS backend — never client-side.
Architecture at a glance:
React Native (Expo) → NestJS Backend → Google Gemini (chat streaming)
→ HuggingFace ML Service (cataract detection)
→ AWS S3 (file storage)
→ PostgreSQL via Prisma (persistence)
Health https://spandavidyaai-app-production.up.railway.app/v1/health/live
Ready https://spandavidyaai-app-production.up.railway.app/v1/health/ready
Swagger https://spandavidyaai-app-production.up.railway.app/api
HUGGINGFACE_API_URL=https://sameer2210-cataractaiml.hf.space/predict
HUGGINGFACE_Test_URL=https://sameer2210-cataractaiml.hf.space/docs
Backend https://spandavidyaai-app-production.up.railway.app/v1
- Tech Stack
- Folder Structure
- Environment Variables
- Authentication Architecture
- API & System Design
- File Upload Architecture
- Cataract ML Service
- Image Prediction Flow
- Streaming (SSE)
- Backend Modules
- Database & Prisma
- Docker Setup
- CI/CD
- Security
- Testing Architecture
| Layer | Technology |
|---|---|
| Framework | NestJS (TypeScript) |
| ORM | Prisma |
| Database | PostgreSQL |
| Auth | JWT (access + refresh) + Google OAuth |
| AI Chat | Google Gemini 2.5 Flash (SSE streaming) |
| ML Inference | HuggingFace Spaces (EfficientNet-B3) |
| File Storage | AWS S3 (presigned URLs) |
| Monitoring | Prometheus + Sentry |
| Security | Helmet, CORS, Rate-Limit, HPP |
| Logging | Winston / nestjs-pino (JSON structured) |
| Testing | Jest + Supertest |
| Layer | Technology |
|---|---|
| Framework | Expo SDK 54 + React Native 0.81 |
| Language | TypeScript |
| Navigation | Expo Router |
| Styling | NativeWind (Tailwind CSS) |
| State | Zustand |
| Server State | TanStack React Query |
| HTTP | Axios |
| Forms | React Hook Form + Zod |
| Lists | FlashList |
| Animation | React Native Reanimated |
| UI Extras | Gorhom Bottom Sheet |
| Auth | @react-native-google-signin/google-signin |
Key frontend files:
| Area | Path |
|---|---|
| Auth API | src/features/auth/api/auth-api.ts |
| Session store | src/features/auth/store/session-store.ts |
| Token storage | src/shared/auth/token-storage.ts |
| HTTP client | src/shared/api/http-client.ts |
| Query client | src/shared/api/query-client.ts |
| Chat API | src/features/chat/api/chat-api.ts |
| Stream parser | src/features/chat/streaming/parse-stream-chunks.ts |
NODE_ENV=development
PORT=8080
# Database
DATABASE_URL="postgresql://postgres:@db.<ref>.supabase.co:5432/postgres"
DIRECT_URL="postgresql://postgres:@db.<ref>.supabase.co:5432/postgres"
# sendotp
RESEND_API_KEY=abc
# JWT
JWT_SECRET=<secret>
JWT_EXPIRES_IN=60m
JWT_REFRESH_SECRET=<refresh-secret>
JWT_REFRESH_EXPIRES_IN=7d
# PostgreSQL (Docker)
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=spandavidya
# ML Gateway
HUGGINGFACE_API_URL=https://sameer2210-cataractaiml.hf.space/predict
ML_GATEWAY_TIMEOUT_MS=60000
ML_GATEWAY_MAX_RETRIES=0
# Google Gemini
GOOGLE_API_KEY=<key>
GOOGLE_GEMINI_MODEL=gemini-2.5-flash
# AWS S3
AWS_ACCESS_KEY_ID=<key>
AWS_SECRET_ACCESS_KEY=<secret>
AWS_REGION=ap-south-1
AWS_S3_BUCKET_NAME=<bucket>
# Google OAuth
GOOGLE_WEB_CLIENT_ID=<id>
GOOGLE_ANDROID_CLIENT_ID=<id>
GOOGLE_IOS_CLIENT_ID=<id>EXPO_PUBLIC_API_URL=http://localhost:8080/v1
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=<id>
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=<id>
EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID=<id>Strategy: JWT access + refresh token rotation, backed by Google Native Sign-In.
Mobile (primary) and web flows are isolated.
graph TD
User([User]) --> AuthChoice{Select Auth Method}
AuthChoice -->|Google Native Sign-In| Google[Google Login Flow]
AuthChoice -->|Email OTP| OTP[OTP Verification Flow]
Google --> BackendVerify[Backend Verification]
OTP --> BackendVerify
BackendVerify --> JWTSession[JWT Session Creation]
JWTSession --> SessionStore[User Session Store]
SessionStore --> AppAccess[App Access Granted]
sequenceDiagram
actor User
participant App as React Native App
participant BE as NestJS Backend
participant Google as Google Auth API
participant DB as PostgreSQL DB
User->>App: Tap "Continue with Google"
App->>Google: Authenticate & Request ID Token
Google-->>App: Return ID Token
App->>BE: POST /v1/auth/google/verify { token: idToken }
BE->>Google: Verify ID Token signature & audience
Google-->>BE: Return User Profile
BE->>DB: Upsert User in DB
BE->>BE: Create JWT Access & Refresh Tokens
BE->>DB: Save/update Refresh Token record
BE-->>App: Return Tokens + User Object
App->>App: Store tokens in SecureStore & Hydrate Zustand
App->>User: Redirect to Home Screen
sequenceDiagram
actor User
participant App as React Native App
participant BE as NestJS Backend
participant DB as PostgreSQL DB
participant Mail as Email Service (Resend)
User->>App: Enter email & Request OTP
App->>BE: POST /v1/auth/email/request-otp { email }
BE->>DB: Check rate limits (max 20/day) & Cooldown (60s)
BE->>DB: Delete any existing active OTP for email
BE->>DB: Generate new OTP (10 min expiry) & Save to DB
BE->>Mail: Send OTP code email
BE-->>App: Return Success / Cooldown remaining
User->>App: Input OTP code
App->>BE: POST /v1/auth/email/verify-otp { email, code }
BE->>DB: Retrieve OTP record
alt OTP Valid & Not Expired
BE->>DB: Upsert User & Delete OTP record
BE->>BE: Generate JWT Access & Refresh Tokens
BE-->>App: Return Tokens + User Object
else OTP Invalid / Expired / Max Attempts Exceeded
BE->>DB: Increment failed attempts (max 5)
alt Failed Attempts >= 5
BE->>DB: Delete OTP record (invalidate)
end
BE-->>App: Return Error (Invalid / Expired)
end
Refresh token rotation: On every refresh, the old token is invalidated and a new pair is issued. Tokens are stored server-side (DB) to support revocation.
Email OTP Flow:
OTPs are requested via POST /v1/auth/email/request-otp and verified via POST /v1/auth/email/verify-otp.
- Rate limit: Max 20 OTP requests per email per day.
- Cooldown: Enforces 60-second wait between requests.
- Attempt limits: Max 5 failed verification attempts before the OTP is deleted.
- Expiry: OTPs expire and are invalidated after 10 minutes.
- Auditing: Logs events such as
OTP_REQUESTED,USER_REGISTERED, andUSER_LOGGED_INto the audit log.
Decorators: @Public() opts routes out of the JWT guard. @Roles() enforces RBAC.
- Versioning: URI-based (
/v1/...) viaapp.enableVersioning({ type: VersioningType.URI }). - Validation: Global
ValidationPipewithwhitelist: true,forbidNonWhitelisted: true. - Logging:
nestjs-pino— zero-allocation JSON, correlation IDs, Datadog/CloudWatch compatible. - Rate limiting:
@nestjs/throttlerbacked by Redis. Strict limits on/ai/generateto prevent billing attacks. - Error handling: Global exception filter + Prisma error mapper → normalized HTTP responses.
- Audit logging: Tracks user actions, metadata, and diffs on sensitive operations.
The application handles file uploads through two distinct mechanisms depending on the file type:
- General/Asset Uploads: The client requests a presigned S3 URL from the NestJS backend, and then uploads the file directly to S3.
- AI Eye Scan Prediction Images: The client performs a direct multipart
FormDataupload to the NestJS backend viaPOST /v1/ai/predict. The backend validates the image (size, type, dimensions), uploads the buffer directly to S3, and forwards it to the Hugging Face ML service.
- Select Image – User picks an eye image via the device camera or library.
- Validation – Client‑side MIME (JPG, JPEG, PNG, WEBP) and size (≤ 50 MB) checks; invalid files are rejected locally.
- Crop UI – Interactive crop overlay allows the user to scale and position the eye within a guided circle.
- Confirm Crop – Cropped image is saved in the upload‑workflow store (Zustand).
- Upload Another (optional) – Users can clear the workflow and restart.
- Upload & ML Processing – The client sends the image as a multipart
FormDatapayload directly to the NestJS backend usingPOST /v1/ai/predict(reusing the current chat contextchatIdif origin is Chat).
- Multer Interceptor – Receives the multipart file and validates MIME (JPG, JPEG, PNG, WEBP) and size (≤ 5 MB limit).
- Dimension Validator – Checks image dimensions to ensure they do not exceed 4096 × 4096 px.
- S3 Service – Backend uploads the validated image buffer directly to AWS S3.
- ML Inference Gateway – Proxies the image buffer to the Hugging Face Spaces ML service with timeout and retry logic.
- Database Transaction – Persists the
UploadandAiPredictionrecords, creates aSYSTEMmessage, and links them together.
- Returns
{ prediction, confidence, uploadedImageUrl, chatId }. - Frontend stores the prediction in the prediction store and navigates.
Note
S3 Access Audit: The S3 bucket is currently public for eye scan retrieval. Migrating to signed URLs is recommended as a security/privacy hardening measure post-launch. This is not a Google Play Store blocker since the app functions correctly and reviewers do not audit cloud storage policies.
This approach provides defense-in-depth validation, centralized security enforcement, AI processing control, auditability, and prediction persistence.
graph TD
Select[Select Eye Image] --> LocalVal{Local Validation}
LocalVal -->|Size > 50MB or Invalid MIME| RejectLocal[Show Error & Reject]
LocalVal -->|Valid JPG/PNG/WEBP| Workflow[Store in Upload Workflow Store]
Workflow --> Crop[Crop Image via EyeGuideOverlay]
Crop --> ConfirmCrop[Save cropped image in Store]
ConfirmCrop --> Analysis[Initiate Analysis Screen]
Analysis --> UploadBE[POST /v1/ai/predict - Multipart FormData]
UploadBE --> MulterVal{Multer Validator}
MulterVal -->|Size > 5MB or invalid type| RejectBE[Return HTTP 400/413 Error]
MulterVal -->|Valid| S3Upload[Upload to AWS S3]
S3Upload --> MLGateway[Proxy to HuggingFace Spaces]
MLGateway --> Predict[Run EfficientNet-B3 Inference]
Predict --> DBTrans[DB Transaction: Save Upload & Prediction]
DBTrans --> Result[Return Result Payload to Client]
Deployed: https://sameer2210-cataractaiml.hf.space
Docs: https://sameer2210-cataractaiml.hf.space/docs
A stateless FastAPI microservice. It receives an eye image, runs EfficientNet-B3 inference, and returns a prediction. It has no auth, no persistence, no business logic — all of that lives in the NestJS backend.
Model: EfficientNet-B3 (PyTorch, CPU inference)
Prediction classes:
| Label | Meaning |
|---|---|
Normal |
No cataract |
Immature |
Early cataract |
Mature |
Advanced cataract |
IOL_Inserted |
Post-surgery artificial lens |
Inference pipeline: Image → RGB conversion → Resize → torchvision transforms → tensor → EfficientNet-B3 → Softmax → top class + confidence score.
The cataract prediction workflow integrates client-side preprocessing with a robust NestJS backend proxying to the HuggingFace ML service.
User selects eye image
│
▼
Photo Picker
│
▼
Crop Workflow (EyeGuideOverlay)
│
▼
Image Processing & Local Validation
├── Crop Overlay Confirmation
├── Size Check (≤ 50 MB)
└── MIME Check (JPG, JPEG, PNG, WEBP)
│
└── POST /v1/ai/predict (multipart/form-data)
└── NestJS Controller (Rate Limit: 6 req/min)
├── Multer interceptor: size ≤ 5 MB limit check
├── MIME type validation: JPG, JPEG, PNG, WEBP
├── Secure AWS S3 Upload (Stores original image bytes)
│ │
│ ▼
└── AI ML Gateway (Service Layer)
└── Call HuggingFace Spaces (EfficientNet-B3)
├── Preprocessing (CLAHE, Hough Circle)
├── Model Inference (Top-Class + Confidence)
├── Circuit Breaker / Timeout (15s)
└── Retry Logic (on transient 503 errors)
│
▼
└── DB Persist (Transaction block)
├── Create SYSTEM message in target Chat
├── Link Upload record to Message
└── Create AiPrediction record
│
▼
└── Return Success Payload
{
prediction: "Immature",
confidence: 0.87,
uploadedImageUrl: "https://s3...",
chatId: "chat-uuid"
}
| Failure Case | Detection Layer | Action / Response |
|---|---|---|
| File > 50 MB | Frontend | Rejected locally: "Image size must be less than 50 MB" |
| Invalid Extension | Frontend | Rejected locally: "Only JPG, PNG, WEBP allowed" |
| File > 5 MB | Backend | Multer rejects with HTTP 413 Payload Too Large |
| Invalid MIME | Backend | Interceptor rejects with HTTP 400 Bad Request |
| S3 Upload Fail | Backend | Logger captures error, returns HTTP 500 Internal Server Error |
| ML Timeout (>15s) | Backend (Gateway) | Retries, then returns HTTP 503 AI service temporarily busy |
| ML API Unavailable | Backend (Gateway) | Retries, then returns HTTP 503 AI service temporarily unavailable |
| Invalid Chat ID | Backend | Validates ownership: returns HTTP 404 Target chat session not found |
Frontend validator → bad: show error, stop
→ valid: POST to backend
→ NestJS interceptor → bad: 400/413
→ valid: service layer recheck
→ S3 upload
→ HuggingFace call → bad: retryable 503
→ success: return result
Choice: Server-Sent Events over WebSockets.
For unidirectional AI token streaming (server → client), SSE is strictly better: operates over HTTP/2, no stateful connection overhead, automatic reconnect, and works natively with React Native.
WebSockets add unnecessary complexity (bidirectional handshake, persistent state) for a use case that is inherently one-directional.
Frontend stream parser: src/features/chat/streaming/parse-stream-chunks.ts
Handles token event parsing and optimistic message insertion into the FlashList.
| Module | Responsibility |
|---|---|
auth |
Google OAuth verification, JWT issue/refresh/revoke, guards |
users |
User CRUD, profile management & details |
chat |
Chat session creation, message persistence, history compile, streaming LLM responses |
ai |
Cataract ML model prediction pipeline, consultation setup |
uploads |
Presigned URL generation, direct multipart file uploads |
audit-log |
Record user actions, profile views, and metadata diff logs |
health |
Liveness/readiness endpoints |
config |
Environment config via NestJS ConfigModule |
# Push schema to DB and regenerate client
npx prisma db push --force-reset
npx prisma generate
# Visual browser
npx prisma studioKey packages:
npm install @prisma/client
npm install -D prismaCore schema entities: User, Chat, Message, Upload, AiPrediction, RefreshToken, AuditLog
Development stack (API + PostgreSQL + pgAdmin) is fully containerized via Docker Compose.
npm run prisma:restart # Reset DB inside DockerGitHub Actions pipeline runs on every push:
- Lint
- Type check
- Unit tests
- Build
| Control | Implementation |
|---|---|
| Helmet | HTTP security headers |
| CORS | Configured per environment |
| Rate limiting | @nestjs/throttler + Redis |
| HPP | HTTP Parameter Pollution protection |
| JWT | Short-lived access tokens + rotating refresh tokens |
| Validation | class-validator whitelist mode — strips unknown fields |
| ML isolation | HuggingFace never called from frontend |
| S3 isolation | Presigned URLs; backend never streams file bytes |
| Audit logging | All sensitive actions logged with user context and diffs |
The backend implements a complete testing pyramid. The environment automatically isolates integration/E2E databases using the TEST_DATABASE_URL variable to safeguard staging/production data.
- Unit Tests (
npm run test:unit): Fast, fully mocked specs located undersrc/to validate core validation pipes, filters, rate limit transactions, and error mappers without database or network overhead. - Integration Tests (
npm run test:integration): Validates database persistence and cross-service linkages using real Prisma services on the isolated test database. Spansauth,chat,upload, andaiintegration suites. - E2E Tests (
npm run test:e2e): Exercises controller endpoints using NestJS bootstrap andSupertest. Includes:auth.e2e-spec.ts&chat.e2e-spec.tsendpoint validations.stream.e2e-spec.ts: Validates SSE streams, token chunks, and message lifecycle syncing.pagination.e2e-spec.ts: Seeds 35, 60, and 100 messages to check for zero gaps/duplicates.full-user-journey.e2e-spec.ts: Full flow from registration -> login -> photo pick/crop -> prediction -> consultation trigger -> stream refetch -> logout.
To run the suite against the isolated test database, ensure TEST_DATABASE_URL is defined in your shell or configuration:
# Setup the test database schema
npx cross-env DATABASE_URL=YOUR_TEST_DB_URL npx prisma db push --skip-generate
# Run specific suite
npm run test:unit
npm run test:integration
npm run test:e2e
# Run all test suites
npm run test:all-
Frontend never calls AI providers directly. All Gemini and HuggingFace traffic routes through the NestJS backend.
-
All image uploads must pass frontend and backend validation before AI processing. Images are uploaded through the NestJS backend, stored in AWS S3, and then forwarded to the ML service. Oversized or invalid files must never reach the AI inference layer.
-
ML service is stateless. No auth, no persistence, no business logic in the HuggingFace service.
-
Refresh tokens are rotated on every use and stored server-side for revocation support.
SpandaVidya implements a structured, fully decoupled, and theme-compliant design system on the frontend. This system ensures consistent visuals across the application and supports dynamic transitions between Light and Dark modes.
Theme resolution is centralized and runs client-side:
┌────────────────────────┐
│ SecureStore │
└───────────┬────────────┘
│ hydrate on launch (once)
▼
┌──────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
│ OS Settings ├──►│ ThemeProvider ├──►│ ThemeContext │
└──────────────┘ └───────────┬────────────┘ └───────────┬────────────┘
│ │
│ provides Navigation Theme │ exposes hook
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ React Navigation Shell │ │ useTheme() │
└────────────────────────┘ └────────────────────────┘
- types.ts: Defines strict TypeScript types for color maps (
ColorTheme), spacings, radii, andThemeContextType. - colors.ts: Holds the color palettes for
lightColorsanddarkColors. - themes.ts: Configures spacings and border radii.
- navigation-theme.ts: Integrates App colors with
@react-navigation/nativeThemestructures. - storage.ts: Automatically persists user theme preferences (Light, Dark, System) using Expo
SecureStore(native) andlocalStorage(web).
- Zero Hardcoded Colors:
Do NOT use inline hex codes (e.g.
#FFFFFF), raw rgb/rgba strings (e.g.rgba(239, 68, 68, 0.15)), or direct Tailwind color values in components (such astext-[#F5FAFF]). All colors must be resolved from the theme state. - Accessing the Theme:
Import
useThemeinside custom screens/components:import { useTheme } from '@/theme'; const { theme, isDark, themeMode, setThemeMode } = useTheme(); // access tokens: theme.colors.accent.primary, theme.colors.background.base
- Extending the Theme:
If a custom color variation or background is needed:
- Add the key under the
ColorThemeinterface insrc/theme/types.ts. - Define the value for both light mode and dark mode palettes in
src/theme/colors.ts. - Never define colors directly in inline styles or files.
- Add the key under the
- Use Theme Components:
Utilize theme-aware components instead of standard elements:
- Use
ThemeTextinstead ofText. - Use
ThemeSurfaceinstead of plainViewfor containers. - Use
ThemeBadgeandThemeDividerto inherit standard semantic borders/spacings.
- Use
The application implements a decoupled, state-isolated routing and consultation model.
graph TD
Home[Home Screen] ──► Upload[Scan Upload]
Upload ──► Crop[Crop Screen]
Crop ──► Analysis[Analysis Screen]
Analysis ──►|POST /v1/ai/predict| Result[Result Screen]
Result ──►|Click "Discuss With AI"| Chat[Chat Screen]
Chat ──►|Auto Consultation Triggered| Consult[POST /v1/chats/:chatId/consultation]
Consult ──► Gemini[Gemini SSE Response]
Back Navigation Flow A:
- Result Screen / Error Result Screen: Header back and swipe gestures are disabled. Android physical back button replaces route with
/(tabs)(Home tab). - Crop Screen: Back arrow/cancel calls
router.back()to return toScan Upload. - Scan Upload: Back returns to
Home Tab.
graph TD
Chat[Chat Screen] ──►|Attach Image| Crop[Crop Screen]
Crop ──► Analysis[Analysis Screen]
Analysis ──►|POST /v1/ai/predict with chatId| Return[Return to Same Chat Screen]
Return ──►|Auto Consultation Triggered| Consult[POST /v1/chats/:chatId/consultation]
Consult ──► Gemini[Gemini SSE Response]
Back Navigation Flow B:
- Crop Screen: Cancel/back arrow calls
router.back()to return to the activeChat Screen. - Result Screen (Error): Android physical back replaces route with
/(tabs)(Home tab).
graph TD
Auth[Google Login / Email OTP] ──► Home[Home Dashboard]
Home ──► Scan[Scan Upload & Crop]
Home ──► Chat[Ayurvedic Consult Chat]
Home ──► Body[Body Insight Questionnaire]
Scan ──► AI[Backend AI/ML Gateway & HuggingFace Predict]
Chat ──► Gemini[Gemini streaming SSE]
Body ──► DB[PostgreSQL via Prisma persistence]
AI ──► DB
DB ──► History[User Diagnostic History & Chats]
- Active Chat Single Source of Truth:
The
activeChatIdinuseChatStoreis the only source of truth for the chat session currently being viewed. - No UI Hijacking / Isolation:
pending.chatIdinusePredictionStoreholds the target ID of a completed scan, but it never automatically changes the active chat session in the background or drives UI transitions without explicit user action (e.g., clicking "Discuss With SpandaVidya AI"). - Scan Origin Resolution:
- Home-origin scans: Do not have an active chat context. The backend
/v1/ai/predictendpoint creates a brand new chat session explicitly, returning its uniquechatId. - Chat-origin scans: Reuse the currently active chat's ID as
chatId, ensuring the result is appended to the same thread.
- Home-origin scans: Do not have an active chat context. The backend
- Auto-Consultation Safety Guard:
Ayurvedic AI consultations are triggered automatically if and only if
activeChatId === pending.chatIdandshouldAutoConsultistrue.
The /v1/ai/predict endpoint implements strict database-driven chat resolution:
chatIdprovided: The backend validates that the target chat exists and belongs to the authenticated user. On verification, it links theUploadandAiPredictionrecords to that chat. If not found or owned by another user, it fails withHTTP 404 Target chat session not found.chatIdomitted: The backend explicitly creates a new chat session titled "AI Health Consultation" for the user.- Strict Prohibition: The backend must never perform fallback lookups (such as finding the user's "most recent" chat session or using a
findFirstquery) to assign predictions.
sequenceDiagram
actor User
participant App as React Native App
participant BE as NestJS Backend
participant Gemini as Gemini AI Service
participant DB as PostgreSQL DB
User->>App: Send Message
App->>App: Optimistically append message to FlashList
App->>BE: POST /v1/chats/:chatId/messages
BE->>DB: Save User message in DB
BE->>Gemini: Request chat consultation (Gemini 2.5 Flash)
Gemini-->>BE: Stream response chunks (SSE)
BE-->>App: Stream token chunks via SSE
App->>App: Render incoming tokens in real-time
Note over App, BE: Connection closed upon completion
BE->>DB: Persist Assistant message to DB
graph TD
Start([Launch App]) --> Auth{Authenticated?}
Auth -->|No| Login[Auth Screen: Google / OTP]
Login --> Home
Auth -->|Yes| Home[Home Dashboard]
Home -->|Option 1| Chat[Ayurvedic Chat Consultation]
Home -->|Option 2| Scan[Cataract Scan Diagnostic]
Home -->|Option 3| Profile[Profile & Audit Logs]
Home -->|Option 4| BodyInsight[Ayurvedic Body Insight Questionnaire]
Scan --> Crop[Interactive Crop Screen]
Crop --> Analyze[AI Prediction Analysis]
Analyze --> Result[Outcome Screen]
Result --> Discuss{Tap Discuss with AI?}
Discuss -->|Yes| Chat
Discuss -->|No| Home
BodyInsight --> SaveReport[Save Body Constituent Result]
SaveReport --> Reports[View Reports / History]
Chat --> StreamChat[Receive Gemini Streaming Advice]
sequenceDiagram
participant UI as Chat Screen Component
participant Hook as useConsultationTrigger Hook
participant PS as usePredictionStore
participant CS as useChatStore
participant API as Backend Consultation API
UI->>Hook: Mounted / activeChatId changed
Hook->>PS: Get pending & shouldAutoConsult state
Hook->>CS: Get activeChatId
alt activeChatId === pending.chatId AND shouldAutoConsult === true
Hook->>PS: Set isConsultationTriggered = true (Prevent double triggers)
Hook->>API: POST /v1/chats/:chatId/consultation { prediction, confidence }
API-->>Hook: Stream Gemini SSE Response
Hook->>PS: clearPending() & clearWorkflow() (Reset states)
else Guards do not match
Hook->>Hook: No-op / Idle
end
graph TD
index.tsx[app/index.tsx <br/> Landing Screen] -->|Unauthenticated| login[app/login.tsx <br/> Shared AuthScreen]
index.tsx -->|Authenticated| tabs[app/(tabs)/_layout.tsx <br/> Tab Navigator]
subgraph Tabs [Tabs Group]
tabs --> tabIndex[app/(tabs)/index.tsx <br/> Home Dashboard]
tabs --> tabChat[app/(tabs)/chat.tsx <br/> Chat Consultation]
tabs --> tabReports[app/(tabs)/reports.tsx <br/> Reports History]
tabs --> tabExplore[app/(tabs)/explore.tsx <br/> Architecture Status]
tabs --> tabProfile[app/(tabs)/profile.tsx <br/> Profile & Settings]
end
tabIndex -->|Start Scan| scanUpload[app/scan-upload.tsx]
tabChat -->|Attach Scan| eyeCrop[app/eye-crop.tsx]
scanUpload --> eyeCrop
eyeCrop --> scanAnalysis[app/scan-analysis.tsx]
scanAnalysis --> scanResult[app/scan-result.tsx]
tabIndex --> bodyInsight[app/body-insight.tsx]
tabIndex --> dataCollection[app/data-collection.tsx]
scanResult -->|Discuss with AI| tabChat