Spaces:
Sleeping
Sleeping
Hetansh Waghela commited on
Commit ·
88fafca
1
Parent(s): e560ae3
minor bug fixes
Browse files
backend/app/routes/ai_summary.py
CHANGED
|
@@ -9,6 +9,7 @@ from sqlalchemy import select
|
|
| 9 |
from typing import List
|
| 10 |
from uuid import UUID
|
| 11 |
import logging
|
|
|
|
| 12 |
|
| 13 |
from app.db import get_db
|
| 14 |
from app.security import get_current_user
|
|
@@ -53,22 +54,35 @@ async def list_reports_for_summary(
|
|
| 53 |
result = await db.execute(query)
|
| 54 |
reports = result.scalars().all()
|
| 55 |
|
| 56 |
-
response = []
|
| 57 |
for report in reports:
|
| 58 |
-
#
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
response.append(
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
| 72 |
|
| 73 |
return response
|
| 74 |
|
|
@@ -99,20 +113,25 @@ async def get_report_file(
|
|
| 99 |
if not os.path.exists(report.file_path):
|
| 100 |
raise HTTPException(status_code=404, detail="File not found on disk")
|
| 101 |
|
| 102 |
-
# Determine media type
|
|
|
|
|
|
|
| 103 |
media_types = {
|
| 104 |
-
'
|
| 105 |
-
'
|
| 106 |
-
'
|
| 107 |
-
'
|
| 108 |
-
'
|
| 109 |
}
|
| 110 |
-
|
|
|
|
| 111 |
|
|
|
|
| 112 |
return FileResponse(
|
| 113 |
report.file_path,
|
| 114 |
media_type=media_type,
|
| 115 |
-
filename=report.filename
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
|
|
|
|
| 9 |
from typing import List
|
| 10 |
from uuid import UUID
|
| 11 |
import logging
|
| 12 |
+
import os
|
| 13 |
|
| 14 |
from app.db import get_db
|
| 15 |
from app.security import get_current_user
|
|
|
|
| 54 |
result = await db.execute(query)
|
| 55 |
reports = result.scalars().all()
|
| 56 |
|
| 57 |
+
response: List[ReportForSummary] = []
|
| 58 |
for report in reports:
|
| 59 |
+
# Skip reports whose underlying file is missing on disk
|
| 60 |
+
if not report.file_path or not os.path.exists(report.file_path):
|
| 61 |
+
logger.warning(
|
| 62 |
+
"AI Summary: skipping report %s (%s) because file is missing at %s",
|
| 63 |
+
report.id,
|
| 64 |
+
report.filename,
|
| 65 |
+
report.file_path,
|
| 66 |
+
)
|
| 67 |
+
continue
|
| 68 |
+
|
| 69 |
+
# Build preview URL based on AI summary file endpoint
|
| 70 |
+
# Note: this route is defined on the /api/ai router (see get_report_file below)
|
| 71 |
+
preview_url = f"/api/ai/reports/{report.id}/file"
|
| 72 |
|
| 73 |
+
response.append(
|
| 74 |
+
ReportForSummary(
|
| 75 |
+
id=report.id,
|
| 76 |
+
filename=report.filename,
|
| 77 |
+
category=report.category,
|
| 78 |
+
doc_type=report.doc_type,
|
| 79 |
+
report_date=report.report_date,
|
| 80 |
+
uploaded_at=report.uploaded_at,
|
| 81 |
+
file_path=report.file_path,
|
| 82 |
+
file_type=report.file_type,
|
| 83 |
+
preview_url=preview_url,
|
| 84 |
+
)
|
| 85 |
+
)
|
| 86 |
|
| 87 |
return response
|
| 88 |
|
|
|
|
| 113 |
if not os.path.exists(report.file_path):
|
| 114 |
raise HTTPException(status_code=404, detail="File not found on disk")
|
| 115 |
|
| 116 |
+
# Determine media type. Note: in the new pipeline `file_type` is stored
|
| 117 |
+
# without the leading dot (e.g. "pdf"), while older records may still
|
| 118 |
+
# include the dot (".pdf"), so we normalize.
|
| 119 |
media_types = {
|
| 120 |
+
'pdf': 'application/pdf',
|
| 121 |
+
'png': 'image/png',
|
| 122 |
+
'jpg': 'image/jpeg',
|
| 123 |
+
'jpeg': 'image/jpeg',
|
| 124 |
+
'tiff': 'image/tiff',
|
| 125 |
}
|
| 126 |
+
normalized_type = (report.file_type or '').lstrip('.').lower()
|
| 127 |
+
media_type = media_types.get(normalized_type, 'application/octet-stream')
|
| 128 |
|
| 129 |
+
# Return inline so the browser renders in an <iframe> instead of forcing download
|
| 130 |
return FileResponse(
|
| 131 |
report.file_path,
|
| 132 |
media_type=media_type,
|
| 133 |
+
filename=report.filename,
|
| 134 |
+
content_disposition_type="inline",
|
| 135 |
)
|
| 136 |
|
| 137 |
|
backend/app/services/rag_service.py
CHANGED
|
@@ -33,6 +33,7 @@ class RAGService:
|
|
| 33 |
self._client = None
|
| 34 |
self._collection = None
|
| 35 |
self._embedding_model = None
|
|
|
|
| 36 |
|
| 37 |
def _get_client(self):
|
| 38 |
"""Lazy initialization of ChromaDB client."""
|
|
@@ -61,9 +62,21 @@ class RAGService:
|
|
| 61 |
|
| 62 |
def _get_embedding_model(self):
|
| 63 |
"""Lazy initialization of embedding model."""
|
|
|
|
|
|
|
|
|
|
| 64 |
if self._embedding_model is None:
|
| 65 |
-
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return self._embedding_model
|
| 68 |
|
| 69 |
def _embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
@@ -82,6 +95,7 @@ class RAGService:
|
|
| 82 |
return [list(e) for e in embeddings]
|
| 83 |
except Exception as e:
|
| 84 |
logger.error(f"Embedding failed: {e}")
|
|
|
|
| 85 |
raise ValueError(f"Failed to embed texts: {e}")
|
| 86 |
|
| 87 |
async def sync_user_reports(self, user_id: uuid.UUID, db: AsyncSession) -> int:
|
|
@@ -269,11 +283,20 @@ class RAGService:
|
|
| 269 |
"""
|
| 270 |
# First, ensure user data is synced (if db provided)
|
| 271 |
if db:
|
| 272 |
-
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
if not docs:
|
| 279 |
return "No health data available for this user yet."
|
|
|
|
| 33 |
self._client = None
|
| 34 |
self._collection = None
|
| 35 |
self._embedding_model = None
|
| 36 |
+
self._embedding_available = True # Disable gracefully on failure
|
| 37 |
|
| 38 |
def _get_client(self):
|
| 39 |
"""Lazy initialization of ChromaDB client."""
|
|
|
|
| 62 |
|
| 63 |
def _get_embedding_model(self):
|
| 64 |
"""Lazy initialization of embedding model."""
|
| 65 |
+
if not self._embedding_available:
|
| 66 |
+
raise RuntimeError("Embedding model is disabled due to previous initialization failure")
|
| 67 |
+
|
| 68 |
if self._embedding_model is None:
|
| 69 |
+
try:
|
| 70 |
+
from sentence_transformers import SentenceTransformer
|
| 71 |
+
logger.info(f"Initializing embedding model: {settings.EMBEDDING_MODEL}")
|
| 72 |
+
self._embedding_model = SentenceTransformer(settings.EMBEDDING_MODEL)
|
| 73 |
+
except Exception as e:
|
| 74 |
+
# If the model can't be loaded (no internet, HF issue, etc.),
|
| 75 |
+
# disable embeddings so the rest of the app can continue.
|
| 76 |
+
self._embedding_available = False
|
| 77 |
+
logger.error(f"Failed to initialize embedding model '{settings.EMBEDDING_MODEL}': {e}")
|
| 78 |
+
raise RuntimeError("Embedding model initialization failed") from e
|
| 79 |
+
|
| 80 |
return self._embedding_model
|
| 81 |
|
| 82 |
def _embed_texts(self, texts: List[str]) -> List[List[float]]:
|
|
|
|
| 95 |
return [list(e) for e in embeddings]
|
| 96 |
except Exception as e:
|
| 97 |
logger.error(f"Embedding failed: {e}")
|
| 98 |
+
# Propagate a clean error so callers can gracefully fall back
|
| 99 |
raise ValueError(f"Failed to embed texts: {e}")
|
| 100 |
|
| 101 |
async def sync_user_reports(self, user_id: uuid.UUID, db: AsyncSession) -> int:
|
|
|
|
| 283 |
"""
|
| 284 |
# First, ensure user data is synced (if db provided)
|
| 285 |
if db:
|
| 286 |
+
try:
|
| 287 |
+
await self.sync_user_reports(user_id, db)
|
| 288 |
+
await self.sync_user_observations(user_id, db)
|
| 289 |
+
except Exception as e:
|
| 290 |
+
# If syncing fails (embeddings/model/Chroma issues), log and continue.
|
| 291 |
+
logger.warning(f"RAG sync failed for user {user_id}: {e}")
|
| 292 |
|
| 293 |
+
try:
|
| 294 |
+
# Query for relevant documents
|
| 295 |
+
docs = await self.query(user_id, query)
|
| 296 |
+
except Exception as e:
|
| 297 |
+
# If RAG fails (e.g. embeddings unavailable, Chroma issue), fall back
|
| 298 |
+
logger.warning(f"RAG context unavailable for user {user_id}: {e}")
|
| 299 |
+
return "No health data available for this user yet."
|
| 300 |
|
| 301 |
if not docs:
|
| 302 |
return "No health data available for this user yet."
|
frontend/src/components/AuthShell.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { motion } from 'framer-motion'
|
| 2 |
import { Link } from 'react-router-dom'
|
|
|
|
| 3 |
import './AuthShell.css'
|
| 4 |
|
| 5 |
function AuthShell({ children, title, subtitle, footerText, footerLink, footerLinkText }) {
|
|
@@ -13,8 +14,7 @@ function AuthShell({ children, title, subtitle, footerText, footerLink, footerLi
|
|
| 13 |
transition={{ duration: 0.6 }}
|
| 14 |
>
|
| 15 |
<Link to="/" className="auth-logo">
|
| 16 |
-
<
|
| 17 |
-
<span className="logo-sub">GGW</span>
|
| 18 |
</Link>
|
| 19 |
</motion.div>
|
| 20 |
|
|
|
|
| 1 |
import { motion } from 'framer-motion'
|
| 2 |
import { Link } from 'react-router-dom'
|
| 3 |
+
import Logo from './ui/Logo'
|
| 4 |
import './AuthShell.css'
|
| 5 |
|
| 6 |
function AuthShell({ children, title, subtitle, footerText, footerLink, footerLinkText }) {
|
|
|
|
| 14 |
transition={{ duration: 0.6 }}
|
| 15 |
>
|
| 16 |
<Link to="/" className="auth-logo">
|
| 17 |
+
<Logo variant="header" />
|
|
|
|
| 18 |
</Link>
|
| 19 |
</motion.div>
|
| 20 |
|
frontend/src/components/AuthShell.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Link } from 'react-router-dom'
|
|
| 3 |
import { ReactNode } from 'react'
|
| 4 |
import './AuthShell.css'
|
| 5 |
|
|
|
|
| 6 |
interface AuthShellProps {
|
| 7 |
children: ReactNode
|
| 8 |
title: string
|
|
|
|
| 3 |
import { ReactNode } from 'react'
|
| 4 |
import './AuthShell.css'
|
| 5 |
|
| 6 |
+
|
| 7 |
interface AuthShellProps {
|
| 8 |
children: ReactNode
|
| 9 |
title: string
|
frontend/src/components/Hero.tsx
CHANGED
|
@@ -56,7 +56,7 @@ function Hero() {
|
|
| 56 |
>
|
| 57 |
{t('hero.tagline')}
|
| 58 |
<br />
|
| 59 |
-
|
| 60 |
className="highlight"
|
| 61 |
initial={{ scale: 0, opacity: 0 }}
|
| 62 |
animate={{ scale: 1, opacity: 1 }}
|
|
|
|
| 56 |
>
|
| 57 |
{t('hero.tagline')}
|
| 58 |
<br />
|
| 59 |
+
<motion.span
|
| 60 |
className="highlight"
|
| 61 |
initial={{ scale: 0, opacity: 0 }}
|
| 62 |
animate={{ scale: 1, opacity: 1 }}
|
frontend/src/i18n/locales/en.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
| 11 |
"cta": "Get Started"
|
| 12 |
},
|
| 13 |
"hero": {
|
| 14 |
-
"tagline": "
|
| 15 |
-
"taglineHighlight": "
|
| 16 |
"subtitle": "Track it. Understand it. Prevent it.",
|
| 17 |
"description": "Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Lumea helps you stay preventive—without becoming your doctor.",
|
| 18 |
"disclaimer": "Not medical advice. Always consult a licensed clinician for diagnosis/treatment.",
|
|
|
|
| 11 |
"cta": "Get Started"
|
| 12 |
},
|
| 13 |
"hero": {
|
| 14 |
+
"tagline": "Your health",
|
| 15 |
+
"taglineHighlight": "comes first",
|
| 16 |
"subtitle": "Track it. Understand it. Prevent it.",
|
| 17 |
"description": "Upload your lab reports, get them interpreted, and receive smart reminders for checkups + medicines. Lumea helps you stay preventive—without becoming your doctor.",
|
| 18 |
"disclaimer": "Not medical advice. Always consult a licensed clinician for diagnosis/treatment.",
|