diff --git a/backend/src/document-processor/controllers/document-processor.controller.ts b/backend/src/document-processor/controllers/document-processor.controller.ts index 8a1b7c69..c705dc3a 100644 --- a/backend/src/document-processor/controllers/document-processor.controller.ts +++ b/backend/src/document-processor/controllers/document-processor.controller.ts @@ -201,11 +201,10 @@ export class DocumentProcessorController { // Extract lab values report.labValues = result.analysis.labValues || []; + report.confidence = result.analysis.metadata.confidence || 0; + // Create summary from simplified explanation or diagnoses - report.summary = - result.simplifiedExplanation || - result.analysis.diagnoses.map(d => d.condition).join(', ') || - 'No summary available'; + report.summary = result.simplifiedExplanation!; report.updatedAt = new Date().toISOString(); diff --git a/backend/src/reports/models/report.model.ts b/backend/src/reports/models/report.model.ts index ab2fb1ce..aab7bc7c 100644 --- a/backend/src/reports/models/report.model.ts +++ b/backend/src/reports/models/report.model.ts @@ -50,6 +50,9 @@ export class Report { @ApiProperty({ description: 'Summary of the report' }) summary: string; + @ApiProperty({ description: 'Confidence score of the analysis (0-100)' }) + confidence: number; + @ApiProperty({ description: 'Status of the report', enum: ReportStatus, @@ -60,6 +63,12 @@ export class Report { @ApiProperty({ description: 'File path of the report' }) filePath: string; + @ApiProperty({ description: 'Original filename of the uploaded file' }) + originalFilename: string; + + @ApiProperty({ description: 'File size in bytes' }) + fileSize: number; + @ApiProperty({ description: 'Creation timestamp' }) createdAt: string; diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index f4132029..17b21a8e 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -157,18 +157,28 @@ export class ReportsController { type: 'string', description: 'S3 file path for the report', }, + originalFilename: { + type: 'string', + description: 'Original filename of the uploaded file', + }, + fileSize: { + type: 'number', + description: 'Size of the file in bytes', + }, }, required: ['filePath'], }, - description: 'S3 file path for the report', + description: 'S3 file path and metadata for the report', }) @Post() async createReport( @Body('filePath') filePath: string, + @Body('originalFilename') originalFilename: string, + @Body('fileSize') fileSize: number, @Req() request: RequestWithUser, ): Promise { const userId = this.extractUserId(request); - return this.reportsService.saveReport(filePath, userId); + return this.reportsService.saveReport(filePath, userId, originalFilename, fileSize); } private extractUserId(request: RequestWithUser): string { diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index 9eb3f942..561b5e12 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -282,7 +282,20 @@ export class ReportsService { } } - async saveReport(filePath: string, userId: string): Promise { + /** + * Save a new report to DynamoDB + * @param filePath S3 object path of the uploaded file + * @param userId User ID of the report owner + * @param originalFilename Original filename of the uploaded file + * @param fileSize Size of the file in bytes + * @returns The saved report + */ + async saveReport( + filePath: string, + userId: string, + originalFilename: string = 'Unknown filename', + fileSize: number = 0, + ): Promise { if (!filePath) { throw new NotFoundException('File URL is required'); } @@ -296,12 +309,15 @@ export class ReportsService { id: uuidv4(), userId, filePath, + originalFilename, + fileSize, title: 'New Report', bookmarked: false, category: '', processingStatus: ProcessingStatus.UNPROCESSED, labValues: [], summary: '', + confidence: 0, status: ReportStatus.UNREAD, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), diff --git a/frontend/src/common/api/reportService.ts b/frontend/src/common/api/reportService.ts index b98007de..4b86f90f 100644 --- a/frontend/src/common/api/reportService.ts +++ b/frontend/src/common/api/reportService.ts @@ -75,6 +75,8 @@ export const uploadReport = async ( `${API_URL}/api/reports`, { filePath: s3Key, + originalFilename: file.name, + fileSize: file.size, }, config, ); diff --git a/frontend/src/common/components/Icon/Icon.tsx b/frontend/src/common/components/Icon/Icon.tsx index ef2dce81..75ca5c52 100644 --- a/frontend/src/common/components/Icon/Icon.tsx +++ b/frontend/src/common/components/Icon/Icon.tsx @@ -26,6 +26,8 @@ import { faArrowUpFromBracket, faHome, faFileLines as faSolidFileLines, + faFileAlt as faFileText, + faFilePdf, faUpload, faComment, faUserCircle, @@ -36,12 +38,14 @@ import { faChevronUp, faChevronDown, faVial, + faLightbulb as faSolidLightbulb, } from '@fortawesome/free-solid-svg-icons'; import { faFileLines as faRegularFileLines, faComment as faRegularComment, faUser as faRegularUser, faBookmark as faRegularBookmark, + faLightbulb as faRegularLightbulb, } from '@fortawesome/free-regular-svg-icons'; import classNames from 'classnames'; @@ -63,6 +67,8 @@ export type IconName = | 'comment' | 'envelope' | 'fileLines' + | 'fileText' + | 'filePdf' | 'home' | 'house' | 'link' @@ -86,7 +92,8 @@ export type IconName = | 'flask' | 'chevronUp' | 'chevronDown' - | 'vial'; + | 'vial' + | 'lightbulb'; /** * Properties for the `Icon` component. @@ -114,6 +121,8 @@ const solidIcons: Record = { comment: faComment, envelope: faEnvelope, fileLines: faSolidFileLines, + fileText: faFileText, + filePdf: faFilePdf, home: faHome, house: faHouse, link: faLink, @@ -138,6 +147,7 @@ const solidIcons: Record = { chevronUp: faChevronUp, chevronDown: faChevronDown, vial: faVial, + lightbulb: faSolidLightbulb, }; /** @@ -150,6 +160,7 @@ const regularIcons: Partial> = { user: faRegularUser, bookmark: faRegularBookmark, circleXmark: faCircleXmark, + lightbulb: faRegularLightbulb, }; /** diff --git a/frontend/src/common/components/Router/TabNavigation.tsx b/frontend/src/common/components/Router/TabNavigation.tsx index 4113edc9..ff3733af 100644 --- a/frontend/src/common/components/Router/TabNavigation.tsx +++ b/frontend/src/common/components/Router/TabNavigation.tsx @@ -18,7 +18,7 @@ import ChatPage from 'pages/Chat/ChatPage'; import UploadPage from 'pages/Upload/UploadPage'; import ReportsListPage from 'pages/Reports/ReportsListPage'; import ReportDetailPage from 'pages/Reports/ReportDetailPage'; -import Processing from 'pages/Processing/Processing'; +import ProcessingPage from 'pages/Processing/ProcessingPage'; /** * The `TabNavigation` component provides a router outlet for all of the @@ -92,7 +92,7 @@ const TabNavigation = (): JSX.Element => { - + diff --git a/frontend/src/common/models/medicalReport.ts b/frontend/src/common/models/medicalReport.ts index 7c8c806a..1ee0d711 100644 --- a/frontend/src/common/models/medicalReport.ts +++ b/frontend/src/common/models/medicalReport.ts @@ -22,6 +22,7 @@ export enum ProcessingStatus { PROCESSED = 'processed', UNPROCESSED = 'unprocessed', IN_PROGRESS = 'in_progress', + FAILED = 'failed', } /** @@ -50,8 +51,11 @@ export interface MedicalReport { processingStatus: ProcessingStatus; labValues: LabValue[]; summary: string; + confidence: number; status: ReportStatus; filePath: string; + originalFilename: string; + fileSize: number; createdAt: string; // ISO date string updatedAt: string; // ISO date string } diff --git a/frontend/src/common/utils/i18n/resources/en/common.json b/frontend/src/common/utils/i18n/resources/en/common.json index 0d7f60b6..83ddf15c 100644 --- a/frontend/src/common/utils/i18n/resources/en/common.json +++ b/frontend/src/common/utils/i18n/resources/en/common.json @@ -70,6 +70,9 @@ "required-path": "{{path}} is required. ", "url": "Must be a URL. " }, + "loading": { + "report": "Loading report..." + }, "no": "no", "updated": "updated", "welcome": "Welcome", @@ -79,4 +82,4 @@ "title": "AI Assistant" } } -} +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/en/errors.json b/frontend/src/common/utils/i18n/resources/en/errors.json index 60cf9f5e..501c3503 100644 --- a/frontend/src/common/utils/i18n/resources/en/errors.json +++ b/frontend/src/common/utils/i18n/resources/en/errors.json @@ -4,5 +4,9 @@ }, "ai": { "content_filtered": "I couldn't find an answer. Please try rephrasing your question or consult your healthcare provider." - } -} \ No newline at end of file + }, + "loading": { + "report": "Error loading the report. Please try again." + }, + "no-report-data": "No report data available." +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/en/index.ts b/frontend/src/common/utils/i18n/resources/en/index.ts index 7ff2dd6a..323720ba 100644 --- a/frontend/src/common/utils/i18n/resources/en/index.ts +++ b/frontend/src/common/utils/i18n/resources/en/index.ts @@ -4,6 +4,7 @@ import common from './common.json'; import errors from './errors.json'; import home from './home.json'; import report from './report.json'; +import reportDetail from './reportDetail.json'; import user from './user.json'; -export default { account, auth, common, errors, home, report, user }; +export default { account, auth, common, errors, home, report, reportDetail, user }; diff --git a/frontend/src/common/utils/i18n/resources/en/report.json b/frontend/src/common/utils/i18n/resources/en/report.json index dcfe8027..5d71168a 100644 --- a/frontend/src/common/utils/i18n/resources/en/report.json +++ b/frontend/src/common/utils/i18n/resources/en/report.json @@ -1,58 +1,58 @@ { - "detail": { - "title": "Report Detail", - "loading": "Loading report...", - "errorLoading": "Error loading report. Please try again later.", - "aiInsights": "AI Insights", - "testResults": "Test Results", - "test": "Test", - "results": "Results", - "refRange": "Ref. Range", - "reportDate": "Report Date", - "medicalComments": "Medical Comments", - "hemoglobin": "Hemoglobin", - "ldl": "LDL Cholesterol", - "glucose": "Fasting Blood Glucose", - "alt": "ALT (Liver Enzyme)", - "wbc": "WBC (White Blood Cells)", - "vitaminD": "Vitamin D (25-OH)", - "cholesterol": "Total Cholesterol", - "bookmarkAdded": "Report added to bookmarks", - "bookmarkRemoved": "Report removed from bookmarks", - "aiInsightsContent": "Based on the blood test results, our AI has identified several points of interest that may require attention or further discussion with your healthcare provider.", - "insight1Title": "Hemoglobin Level", - "insight1Content": "Your hemoglobin level is slightly below the reference range. This could indicate mild anemia, which may cause fatigue and weakness.", - "insight2Title": "Cholesterol Levels", - "insight2Content": "Both your LDL cholesterol and total cholesterol are elevated, which may increase your risk for cardiovascular disease.", - "insight3Title": "Blood Glucose", - "insight3Content": "Your fasting blood glucose is elevated, potentially indicating prediabetes. Lifestyle modifications may help improve this value.", - "hemoglobinComment": "The patient's hemoglobin level is 12.5 g/dL, which falls within the lower end of the normal reference range for most adults. While this value may still be considered acceptable, it is important to assess it in the context of the patient's age, sex, clinical symptoms, and medical history.", - "emergencyWarning": "Please contact your doctor or seek emergency care immediately.", - "flaggedValues": "Flagged values", - "highLdl": "High LDL Cholesterol", - "lowHemoglobin": "Low Hemoglobin (10.1 g/dL)", - "conclusion": "Conclusion:", - "suggestions": "Suggestions:", - "ldlConclusion": "Elevated LDL (bad cholesterol) increases your risk of cardiovascular disease", - "ldlSuggestion1": "Consider a heart-healthy diet (e.g., Mediterranean).", - "ldlSuggestion2": "Increase physical activity.", - "ldlSuggestion3": "Discuss statin therapy with your doctor if not already on one.", - "hemoglobinConclusion": "This level suggests anemia, which may cause fatigue and weakness.", - "hemoglobinSuggestion1": "Test for iron, B12, and folate deficiency.", - "hemoglobinSuggestion2": "Consider iron-rich foods or supplements after medical consultation already on one." - }, - "list": { - "title": "Reports", - "emptyState": "No reports found", - "uploadPrompt": "Upload a medical report to get started", - "filterAll": "All", - "filterBookmarked": "Bookmarked", - "noBookmarksTitle": "No Bookmarked Reports", - "noBookmarksMessage": "Bookmark reports to find them quickly here", - "sortButton": "Sort reports", - "filterButton": "Filter reports", - "categoryGeneral": "General", - "categoryBrain": "Brain", - "categoryHeart": "Heart" - } -} + "detail": { + "title": "Report Detail", + "loading": "Loading report...", + "errorLoading": "Error loading report. Please try again later.", + "aiInsights": "AI Insights", + "testResults": "Test Results", + "test": "Test", + "results": "Results", + "refRange": "Ref. Range", + "reportDate": "Report Date", + "medicalComments": "Medical Comments", + "hemoglobin": "Hemoglobin", + "ldl": "LDL Cholesterol", + "glucose": "Fasting Blood Glucose", + "alt": "ALT (Liver Enzyme)", + "wbc": "WBC (White Blood Cells)", + "vitaminD": "Vitamin D (25-OH)", + "cholesterol": "Total Cholesterol", + "bookmarkAdded": "Report added to bookmarks", + "bookmarkRemoved": "Report removed from bookmarks", + "aiInsightsContent": "Based on the blood test results, our AI has identified several points of interest that may require attention or further discussion with your healthcare provider.", + "insight1Title": "Hemoglobin Level", + "insight1Content": "Your hemoglobin level is slightly below the reference range. This could indicate mild anemia, which may cause fatigue and weakness.", + "insight2Title": "Cholesterol Levels", + "insight2Content": "Both your LDL cholesterol and total cholesterol are elevated, which may increase your risk for cardiovascular disease.", + "insight3Title": "Blood Glucose", + "insight3Content": "Your fasting blood glucose is elevated, potentially indicating prediabetes. Lifestyle modifications may help improve this value.", + "hemoglobinComment": "The patient's hemoglobin level is 12.5 g/dL, which falls within the lower end of the normal reference range for most adults. While this value may still be considered acceptable, it is important to assess it in the context of the patient's age, sex, clinical symptoms, and medical history.", + "emergencyWarning": "Please contact your doctor or seek emergency care immediately.", + "flaggedValues": "Flagged values", + "highLdl": "High LDL Cholesterol", + "lowHemoglobin": "Low Hemoglobin (10.1 g/dL)", + "conclusion": "Conclusion:", + "suggestions": "Suggestions:", + "ldlConclusion": "Elevated LDL (bad cholesterol) increases your risk of cardiovascular disease", + "ldlSuggestion1": "Consider a heart-healthy diet (e.g., Mediterranean).", + "ldlSuggestion2": "Increase physical activity.", + "ldlSuggestion3": "Discuss statin therapy with your doctor if not already on one.", + "hemoglobinConclusion": "This level suggests anemia, which may cause fatigue and weakness.", + "hemoglobinSuggestion1": "Test for iron, B12, and folate deficiency.", + "hemoglobinSuggestion2": "Consider iron-rich foods or supplements after medical consultation already on one." + }, + "list": { + "title": "Reports", + "emptyState": "No reports found", + "uploadPrompt": "Upload a medical report to get started", + "filterAll": "All", + "filterBookmarked": "Bookmarked", + "noBookmarksTitle": "No Bookmarked Reports", + "noBookmarksMessage": "Bookmark reports to find them quickly here", + "sortButton": "Sort reports", + "filterButton": "Filter reports", + "generalCategory": "General", + "brainCategory": "Brain", + "heartCategory": "Heart" + } +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/en/reportDetail.json b/frontend/src/common/utils/i18n/resources/en/reportDetail.json new file mode 100644 index 00000000..87ace9ad --- /dev/null +++ b/frontend/src/common/utils/i18n/resources/en/reportDetail.json @@ -0,0 +1,91 @@ +{ + "detail": { + "title": "Report Detail", + "loading": "Loading report...", + "errorLoading": "Error loading report. Please try again later.", + "aiInsights": "AI Insights", + "testResults": "Test Results", + "test": "Test", + "results": "Results", + "refRange": "Ref. Range", + "reportDate": "Report Date", + "medicalComments": "Medical Comments", + "hemoglobin": "Hemoglobin", + "ldl": "LDL Cholesterol", + "glucose": "Fasting Blood Glucose", + "alt": "ALT (Liver Enzyme)", + "wbc": "WBC (White Blood Cells)", + "vitaminD": "Vitamin D (25-OH)", + "cholesterol": "Total Cholesterol", + "bookmarkAdded": "Report added to bookmarks", + "bookmarkRemoved": "Report removed from bookmarks", + "aiInsightsContent": "Based on the blood test results, our AI has identified several points of interest that may require attention or further discussion with your healthcare provider.", + "insight1Title": "Hemoglobin Level", + "insight1Content": "Your hemoglobin level is slightly below the reference range. This could indicate mild anemia, which may cause fatigue and weakness.", + "insight2Title": "Cholesterol Levels", + "insight2Content": "Both your LDL cholesterol and total cholesterol are elevated, which may increase your risk for cardiovascular disease.", + "insight3Title": "Blood Glucose", + "insight3Content": "Your fasting blood glucose is elevated, potentially indicating prediabetes. Lifestyle modifications may help improve this value.", + "hemoglobinComment": "The patient's hemoglobin level is 12.5 g/dL, which falls within the lower end of the normal reference range for most adults. While this value may still be considered acceptable, it is important to assess it in the context of the patient's age, sex, clinical symptoms, and medical history.", + "emergencyWarning": "Please contact your doctor or seek emergency care immediately.", + "flaggedValues": "Flagged values", + "highLdl": "High LDL Cholesterol", + "lowHemoglobin": "Low Hemoglobin (10.1 g/dL)", + "conclusion": "Conclusion:", + "suggestions": "Suggestions:", + "ldlConclusion": "Elevated LDL (bad cholesterol) increases your risk of cardiovascular disease", + "ldlSuggestion1": "Consider a heart-healthy diet (e.g., Mediterranean).", + "ldlSuggestion2": "Increase physical activity.", + "ldlSuggestion3": "Discuss statin therapy with your doctor if not already on one.", + "hemoglobinConclusion": "This level suggests anemia, which may cause fatigue and weakness.", + "hemoglobinSuggestion1": "Test for iron, B12, and folate deficiency.", + "hemoglobinSuggestion2": "Consider iron-rich foods or supplements after medical consultation already on one." + }, + "list": { + "title": "Reports", + "emptyState": "No reports found", + "uploadPrompt": "Upload a medical report to get started", + "filterAll": "All", + "filterBookmarked": "Bookmarked", + "noBookmarksTitle": "No Bookmarked Reports", + "noBookmarksMessage": "Bookmark reports to find them quickly here", + "sortButton": "Sort reports", + "filterButton": "Filter reports", + "generalCategory": "General", + "brainCategory": "Brain", + "heartCategory": "Heart" + }, + "report": { + "analysis": { + "title": "Results Analysis" + }, + "emergency": { + "message": "Please contact your doctor or seek emergency care immediately." + }, + "flagged-values": { + "title": "Flagged values" + }, + "normal-values": { + "title": "Normal values" + }, + "conclusion": { + "title": "Conclusion" + }, + "suggestions": { + "title": "Suggestions" + }, + "doctor-note": "With all interpretations, these results should be discussed with your doctor.", + "ai-help": { + "title": "Still need further clarifications?", + "action": "Ask our AI Assistant" + }, + "action": { + "discard": "Discard", + "new-upload": "New Upload" + }, + "tabs": { + "ai-insights": "AI Insights", + "original-report": "Original Report" + } + } +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/es/common.json b/frontend/src/common/utils/i18n/resources/es/common.json index 5d9a041f..20aa3280 100644 --- a/frontend/src/common/utils/i18n/resources/es/common.json +++ b/frontend/src/common/utils/i18n/resources/es/common.json @@ -64,9 +64,12 @@ "min": "Debe tener al menos {{min}} caracteres. ", "oneOf": "Debe ser uno de: {{values}} ", "required": "Requerido. ", - "required-path": "{{path}} es obligatorio. ", + "required-path": "{{path}} es requerido. ", "url": "Debe ser una URL. " }, + "loading": { + "report": "Cargando informe..." + }, "no": "no", "updated": "actualizado", "welcome": "Bienvenido", @@ -79,4 +82,4 @@ "app": { "name": "MEDReport AI" } -} +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/es/errors.json b/frontend/src/common/utils/i18n/resources/es/errors.json index d815d34d..97ad65ce 100644 --- a/frontend/src/common/utils/i18n/resources/es/errors.json +++ b/frontend/src/common/utils/i18n/resources/es/errors.json @@ -4,5 +4,9 @@ }, "ai": { "content_filtered": "No pude encontrar una respuesta. Intenta reformular tu pregunta o consulta a tu proveedor de salud." - } -} \ No newline at end of file + }, + "loading": { + "report": "Error al cargar el informe. Por favor, inténtelo de nuevo." + }, + "no-report-data": "No hay datos de informe disponibles." +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/es/errors.json.new b/frontend/src/common/utils/i18n/resources/es/errors.json.new new file mode 100644 index 00000000..97ad65ce --- /dev/null +++ b/frontend/src/common/utils/i18n/resources/es/errors.json.new @@ -0,0 +1,12 @@ +{ + "chat": { + "general": "Lo siento, algo salió mal. Por favor, inténtalo de nuevo." + }, + "ai": { + "content_filtered": "No pude encontrar una respuesta. Intenta reformular tu pregunta o consulta a tu proveedor de salud." + }, + "loading": { + "report": "Error al cargar el informe. Por favor, inténtelo de nuevo." + }, + "no-report-data": "No hay datos de informe disponibles." +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/es/index.ts b/frontend/src/common/utils/i18n/resources/es/index.ts index 010bab1f..cf5c5119 100644 --- a/frontend/src/common/utils/i18n/resources/es/index.ts +++ b/frontend/src/common/utils/i18n/resources/es/index.ts @@ -3,6 +3,7 @@ import auth from './auth.json'; import common from './common.json'; import errors from './errors.json'; import home from './home.json'; +import reportDetail from './reportDetail.json'; import user from './user.json'; -export default { account, auth, common, errors, home, user }; +export default { account, auth, common, errors, home, reportDetail, user }; diff --git a/frontend/src/common/utils/i18n/resources/es/reportDetail.json b/frontend/src/common/utils/i18n/resources/es/reportDetail.json new file mode 100644 index 00000000..7f681812 --- /dev/null +++ b/frontend/src/common/utils/i18n/resources/es/reportDetail.json @@ -0,0 +1,35 @@ +{ + "report": { + "analysis": { + "title": "Análisis de resultados" + }, + "emergency": { + "message": "Por favor contacte a su médico o busque atención de emergencia inmediatamente." + }, + "flagged-values": { + "title": "Valores marcados" + }, + "normal-values": { + "title": "Valores normales" + }, + "conclusion": { + "title": "Conclusión" + }, + "suggestions": { + "title": "Sugerencias" + }, + "doctor-note": "Con todas las interpretaciones, estos resultados deben ser discutidos con su médico.", + "ai-help": { + "title": "¿Todavía necesita más aclaraciones?", + "action": "Pregunte a nuestro Asistente de IA" + }, + "action": { + "discard": "Descartar", + "new-upload": "Nueva carga" + }, + "tabs": { + "ai-insights": "Información de IA", + "original-report": "Informe original" + } + } +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/fr/common.json b/frontend/src/common/utils/i18n/resources/fr/common.json index 04cdc4b9..a819e56b 100644 --- a/frontend/src/common/utils/i18n/resources/fr/common.json +++ b/frontend/src/common/utils/i18n/resources/fr/common.json @@ -60,13 +60,16 @@ }, "validation": { "email": "Doit être une adresse e-mail. ", - "max": "Doit contenir au maximum {{max}} caractères. ", - "min": "Doit contenir au moins {{min}} caractères. ", - "oneOf": "Doit être l'un des: {{values}} ", - "required": "Requis. ", + "max": "Doit être au maximum {{max}} caractères. ", + "min": "Doit être au moins {{min}} caractères. ", + "oneOf": "Doit être l'un des suivants : {{values}} ", + "required": "Obligatoire. ", "required-path": "{{path}} est obligatoire. ", "url": "Doit être une URL. " }, + "loading": { + "report": "Chargement du rapport..." + }, "no": "non", "updated": "mis à jour", "welcome": "Bienvenu", @@ -79,4 +82,4 @@ "app": { "name": "MEDReport AI" } -} +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/fr/errors.json b/frontend/src/common/utils/i18n/resources/fr/errors.json index 27ff1be3..af9046d7 100644 --- a/frontend/src/common/utils/i18n/resources/fr/errors.json +++ b/frontend/src/common/utils/i18n/resources/fr/errors.json @@ -3,6 +3,10 @@ "general": "Désolé, une erreur s'est produite. Veuillez réessayer." }, "ai": { - "content_filtered": "Je n'ai pas pu trouver de réponse. Veuillez reformuler votre question ou consulter votre prestataire de soins de santé." - } -} \ No newline at end of file + "content_filtered": "Je n'ai pas pu trouver de réponse. Veuillez reformuler votre question ou consulter votre médecin." + }, + "loading": { + "report": "Erreur lors du chargement du rapport. Veuillez réessayer." + }, + "no-report-data": "Aucune donnée de rapport disponible." +} \ No newline at end of file diff --git a/frontend/src/common/utils/i18n/resources/fr/index.ts b/frontend/src/common/utils/i18n/resources/fr/index.ts index 010bab1f..cf5c5119 100644 --- a/frontend/src/common/utils/i18n/resources/fr/index.ts +++ b/frontend/src/common/utils/i18n/resources/fr/index.ts @@ -3,6 +3,7 @@ import auth from './auth.json'; import common from './common.json'; import errors from './errors.json'; import home from './home.json'; +import reportDetail from './reportDetail.json'; import user from './user.json'; -export default { account, auth, common, errors, home, user }; +export default { account, auth, common, errors, home, reportDetail, user }; diff --git a/frontend/src/common/utils/i18n/resources/fr/reportDetail.json b/frontend/src/common/utils/i18n/resources/fr/reportDetail.json new file mode 100644 index 00000000..54485d9a --- /dev/null +++ b/frontend/src/common/utils/i18n/resources/fr/reportDetail.json @@ -0,0 +1,35 @@ +{ + "report": { + "analysis": { + "title": "Analyse des résultats" + }, + "emergency": { + "message": "Veuillez contacter votre médecin ou consulter immédiatement un service d'urgence." + }, + "flagged-values": { + "title": "Valeurs signalées" + }, + "normal-values": { + "title": "Valeurs normales" + }, + "conclusion": { + "title": "Conclusion" + }, + "suggestions": { + "title": "Suggestions" + }, + "doctor-note": "Pour toute interprétation, ces résultats doivent être discutés avec votre médecin.", + "ai-help": { + "title": "Besoin de plus de clarifications?", + "action": "Demandez à notre assistant IA" + }, + "action": { + "discard": "Annuler", + "new-upload": "Nouveau téléchargement" + }, + "tabs": { + "ai-insights": "Analyses IA", + "original-report": "Rapport original" + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/Home/components/ReportItem/ReportItem.tsx b/frontend/src/pages/Home/components/ReportItem/ReportItem.tsx index 6b1ae960..9e6d415f 100644 --- a/frontend/src/pages/Home/components/ReportItem/ReportItem.tsx +++ b/frontend/src/pages/Home/components/ReportItem/ReportItem.tsx @@ -24,7 +24,7 @@ const ReportItem: React.FC = ({ report, onClick, onToggleBookmark, - showBookmarkButton = false + showBookmarkButton = false, }) => { const { t } = useTranslation(['common', 'report']); const { title, category, createdAt, status, bookmarked } = report; @@ -39,13 +39,13 @@ const ReportItem: React.FC = ({ // Get category translation key based on category value const getCategoryTranslationKey = () => { if (categoryStr === ReportCategory.GENERAL.toLowerCase()) { - return 'list.categoryGeneral'; + return 'list.generalCategory'; } else if (categoryStr === ReportCategory.BRAIN.toLowerCase()) { - return 'list.categoryBrain'; + return 'list.brainCategory'; } else if (categoryStr === ReportCategory.HEART.toLowerCase()) { - return 'list.categoryHeart'; + return 'list.heartCategory'; } - return 'list.categoryGeneral'; // Default to general if not found + return 'list.generalCategory'; // Default to general if not found }; // Get the appropriate icon for the category @@ -100,7 +100,9 @@ const ReportItem: React.FC = ({
)} diff --git a/frontend/src/pages/Processing/Processing.scss b/frontend/src/pages/Processing/ProcessingPage.scss similarity index 100% rename from frontend/src/pages/Processing/Processing.scss rename to frontend/src/pages/Processing/ProcessingPage.scss diff --git a/frontend/src/pages/Processing/Processing.tsx b/frontend/src/pages/Processing/ProcessingPage.tsx similarity index 90% rename from frontend/src/pages/Processing/Processing.tsx rename to frontend/src/pages/Processing/ProcessingPage.tsx index 6af0edf0..fa02b8d5 100644 --- a/frontend/src/pages/Processing/Processing.tsx +++ b/frontend/src/pages/Processing/ProcessingPage.tsx @@ -4,7 +4,7 @@ import Avatar from '../../common/components/Icon/Avatar'; import { useLocation, useHistory } from 'react-router-dom'; import { useEffect, useState, useRef } from 'react'; import { useAxios } from '../../common/hooks/useAxios'; -import './Processing.scss'; +import './ProcessingPage.scss'; import { getAuthConfig } from 'common/api/reportService'; const API_URL = import.meta.env.VITE_BASE_URL_API || ''; @@ -12,7 +12,7 @@ const API_URL = import.meta.env.VITE_BASE_URL_API || ''; * Processing page that shows while the system analyzes uploaded documents * This page automatically displays after a successful upload */ -const Processing: React.FC = () => { +const ProcessingPage: React.FC = () => { const currentUser = useCurrentUser(); const firstName = currentUser?.name?.split(' ')[0]; const axios = useAxios(); @@ -22,11 +22,11 @@ const Processing: React.FC = () => { const [isProcessing, setIsProcessing] = useState(true); const [processingError, setProcessingError] = useState(null); const statusCheckIntervalRef = useRef(null); + const hasInitiatedProcessing = useRef(false); // Get the location state which may contain the reportId (previously filePath) const location = useLocation<{ reportId: string }>(); const reportId = location.state?.reportId; - const [isFetching, setIsFetching] = useState(false); // Check the status of the report processing const checkReportStatus = async () => { @@ -70,19 +70,21 @@ const Processing: React.FC = () => { // Send the API request when component mounts useEffect(() => { + // Use ref to ensure this effect runs only once for the core logic + if (hasInitiatedProcessing.current) { + return; + } + if (!reportId) { setProcessingError('No report ID provided'); setIsProcessing(false); + hasInitiatedProcessing.current = true; // Mark as initiated even on error return; } - if (isFetching) { - return; - } + hasInitiatedProcessing.current = true; // Mark as initiated before fetching const processFile = async () => { - setIsFetching(true); - try { // Send POST request to backend API const response = await axios.post( @@ -93,8 +95,8 @@ const Processing: React.FC = () => { console.log('File processing started:', response.data); - // Start checking the status every 5 seconds - statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 5000); + // Start checking the status every 2 seconds + statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 2000); // Run the first status check immediately checkReportStatus(); @@ -113,7 +115,8 @@ const Processing: React.FC = () => { window.clearInterval(statusCheckIntervalRef.current); } }; - }, [reportId, axios, history]); + }, [reportId, location, history]); + return ( @@ -166,4 +169,4 @@ const Processing: React.FC = () => { ); }; -export default Processing; +export default ProcessingPage; \ No newline at end of file diff --git a/frontend/src/pages/Reports/ReportDetailPage.scss b/frontend/src/pages/Reports/ReportDetailPage.scss index e21d6e7b..0ad9d557 100644 --- a/frontend/src/pages/Reports/ReportDetailPage.scss +++ b/frontend/src/pages/Reports/ReportDetailPage.scss @@ -1,12 +1,13 @@ .report-detail-page { - --padding-horizontal: 20px; + --padding-horizontal: 16px; + --page-background: radial-gradient(circle, rgba(250, 250, 255, 0.83) 0%, rgba(249, 252, 255, 1) 100%); ion-content { - --background: #ffffff; + --background: var(--page-background); } &__header { - padding: 16px var(--padding-horizontal) 8px; + padding: 20px var(--padding-horizontal) 16px; position: relative; } @@ -14,119 +15,112 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; + } - h1 { - font-size: 22px; - font-weight: 600; - margin: 0; - } + &__title { + font-family: 'Inter', sans-serif; + font-size: 22px; + font-weight: 600; + margin: 0; + color: #313E4C; } - &__header-actions { + &__close-button { + border: none; + background: transparent; + color: #435FF0; + font-size: 24px; + padding: 0; + cursor: pointer; display: flex; - gap: 8px; + align-items: center; + justify-content: center; + } - .close-button { - --padding-start: 4px; - --padding-end: 4px; - --border-radius: 50%; - width: 32px; - height: 32px; - margin: 0; - } + &__close-icon { + font-size: 24px; } - &__category { + &__category-wrapper { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 2px; /* Reduced from 4px to 2px */ - } - - &__category-text { - color: #4765ff; - font-size: 14px; - font-weight: 500; + margin-top: 16px; + margin-bottom: 4px; } - &__report-title { - font-size: 20px; + &__category { + font-family: 'Inter', sans-serif; + font-size: 12px; font-weight: 600; - margin: 0 0 8px 0; - color: #333333; + color: #435FF0; + letter-spacing: 0.24px; } - .bookmark-button { - --padding-start: 0; - --padding-end: 0; - margin: 0; - height: 32px; - color: #aaaaaa; - - &.active { - color: #5970ef; - } - } - - &__bookmark-container { + &__bookmark-button { width: 34px; height: 34px; + border-radius: 50%; background-color: white; - border-radius: 100px; + border: none; display: flex; align-items: center; justify-content: center; + color: #838B94; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + } + + &__subtitle { + font-family: 'Inter', sans-serif; + font-size: 18px; + font-weight: 600; + margin: 4px 0 16px; + color: #313E4C; } &__tabs { + margin: 0 var(--padding-horizontal) 24px; display: flex; - margin: 0 var(--padding-horizontal) 16px; - background-color: #f2f2f2; + background-color: #F2F4F7; border-radius: 100px; padding: 4px; + height: 40px; } &__tab { - padding: 8px 16px; flex: 1; - text-align: center; - font-size: 13px; - color: #666666; - position: relative; - border-radius: 100px; - cursor: pointer; display: flex; align-items: center; justify-content: center; - gap: 6px; + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 600; + color: #838B94; + padding: 6px 0; + border-radius: 100px; + cursor: pointer; + letter-spacing: 0.26px; &--active { - color: #4765ff; - font-weight: 600; - background-color: #ffffff; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + background-color: white; + color: #313E4C; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } } - &__tab-icon { - display: inline-flex; - align-items: center; - justify-content: center; - - &--ai { - transform: translateY(-1px); - } + &__tab-icon-chevron { + margin-right: 6px; } &__emergency { border: 1px solid #c93a54; - border-radius: 12px; + border-radius: 16px; display: flex; - padding: 16px; - margin: 0 var(--padding-horizontal) 16px; - align-items: center; - background-color: white; + padding: 14px 16px; + margin: 0 16px 16px; + align-items: flex-start; + background-color: rgba(201, 58, 84, 0.08); gap: 12px; } @@ -134,50 +128,56 @@ color: #c93a54; min-width: 24px; display: flex; + margin-right: 10px; } &__emergency-text { margin: 0; color: #c93a54; font-weight: 400; - font-size: 13px; + font-size: 14px; line-height: 1.5; } &__section { - margin-bottom: 16px; - padding: 0 var(--padding-horizontal); + margin: 0 16px 20px; + padding: 0; + border-radius: 16px; + background-color: #fff; + border: 1px solid #EBEEF8; + overflow: hidden; } &__section-header { display: flex; align-items: center; - padding: 12px; + padding: 16px; cursor: pointer; - border-bottom: none; /* Removed border under Flagged values, Normal Values */ + background-color: rgba(235, 238, 248, 0.5); } &__section-icon { - margin-right: 10px; + margin-right: 12px; display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; - background-color: #ebeef8; - border-radius: 100px; + background-color: #FFF; + border-radius: 50%; + color: #313E4C; } &__section-title { flex-grow: 1; margin: 0; - font-size: 16px; - font-weight: 500; - color: #313e4c; + font-size: 18px; + font-weight: 600; + color: #313E4C; } &__section-toggle { - color: #838b94; + color: #838B94; display: flex; align-items: center; } @@ -188,7 +188,7 @@ padding: 0; margin-top: 12px; margin-bottom: 8px; - border: 1px solid #ebeef8; + border-top: 1px solid #EBEEF8; overflow: hidden; } @@ -196,42 +196,44 @@ display: flex; flex-wrap: wrap; align-items: center; - padding: 16px; - background-color: #fafafa; + padding: 12px 16px; + background-color: #FFF; &--high { - background-color: #fff1f3; + background-color: rgba(201, 58, 84, 0.08); } &--low { - background-color: #fff8f5; + background-color: rgba(108, 99, 255, 0.08); } } &__item-name { - font-weight: 600; - font-size: 13px; - margin-right: auto; + font-weight: 500; + font-size: 15px; + margin-right: 12px; flex: 1; letter-spacing: 0.26px; line-height: 15.7px; + color: #313E4C; } &__item-level { font-size: 12px; - padding: 4px 12px; - border-radius: 50px; - margin-right: 12px; - font-weight: 500; + padding: 2px 6px; + border-radius: 4px; + margin-right: 16px; + font-weight: 600; + text-transform: uppercase; &--high { - background-color: rgba(175, 27, 63, 0.8); - color: #ffffff; + background-color: rgba(201, 58, 84, 0.1); + color: #c93a54; } &--low { - background-color: rgba(254, 173, 127, 0.8); - color: #ffffff; + background-color: rgba(108, 99, 255, 0.1); + color: #435FF0; } } @@ -239,27 +241,34 @@ font-weight: 600; font-size: 15px; white-space: nowrap; + color: #313E4C; } &__item-details { color: #444; - padding: 0 16px 16px; + padding: 10px 16px 16px; + background-color: rgba(235, 238, 248, 0.3); } &__item-section { - margin-bottom: 14px; + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } h4 { - font-size: 13px; - color: #666; + font-size: 14px; + color: #313E4C; margin: 0 0 4px 0; - font-weight: 500; + font-weight: 600; } p { margin: 0; - font-size: 13px; - line-height: 1.5; + font-size: 14px; + line-height: 1.4; + color: #5C6D80; } } @@ -277,15 +286,15 @@ &__actions { display: flex; - gap: 12px; - padding: 0 var(--padding-horizontal) 24px; + gap: 16px; + padding: 0 16px 32px; margin-top: 32px; } &__action-button { flex: 1; padding: 14px 0; - border-radius: 11px; + border-radius: 12px; font-weight: 600; font-size: 18px; border: none; @@ -296,11 +305,12 @@ align-items: center; justify-content: center; letter-spacing: 0.36px; + font-family: 'Inter', sans-serif; &--discard { - background-color: #ffffff; - color: #c93a54; - border: 1px solid #c93a54; + background-color: #FFF; + color: #AF1B3F; + border: 1px solid #AF1B3F; &:hover { background-color: rgba(201, 58, 84, 0.05); @@ -308,8 +318,8 @@ } &--upload { - background-color: #4765ff; - color: white; + background-color: #435FF0; + color: #FFF; &:hover { background-color: #3a54c4; @@ -318,49 +328,394 @@ } &__ai-help { - margin: 20px var(--padding-horizontal) 32px; - padding: 16px; + margin: 20px 16px 32px; + padding: 0 16px 16px; border-radius: 16px; background-color: #faf8fd; border: 1px solid rgba(137, 117, 211, 0.2); text-align: center; + display: flex; + align-items: center; + justify-content: space-between; } &__ai-help-title { - color: #4765ff; - font-size: 13px; + color: #435FF0; + font-size: 14px; font-weight: 600; letter-spacing: 0.26px; margin-bottom: 4px; } &__ai-help-action { - color: #4765ff; - font-size: 13px; + color: #838B94; + font-size: 14px; font-weight: 600; letter-spacing: 0.26px; cursor: pointer; } &__info-card { - margin: 24px var(--padding-horizontal) 16px; - background-color: rgba(67, 95, 240, 0.05); + margin: 0 16px 16px; + background-color: #F6F8FC; border-radius: 16px; - padding: 16px; + padding: 14px 16px; display: flex; gap: 8px; align-items: flex-start; } &__info-icon { - color: #4765ff; + color: #435FF0; font-size: 24px; margin-top: 2px; + margin-right: 10px; } &__info-text { + color: #5C6D80; + font-size: 14px; + line-height: 20px; + } + + // Original Report Tab Styles + &__original-report { + padding: 0 16px; + margin: 16px 16px 20px; + } + + &__label-container { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding-left: 8px; + } + + &__label-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background-color: #f2f4f7; + border-radius: 50%; + } + + &__label-text { + font-family: 'Inter', sans-serif; + font-weight: 600; + font-size: 18px; + color: #313e4c; + margin: 0; + } + + &__results-table { + background-color: #fff; + border-radius: 16px; + border: 1px solid #ebeef8; + margin-bottom: 16px; + overflow: hidden; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); + } + + &__results-header { + display: flex; + background-color: #f2f4f7; + padding: 8px 16px; + } + + &__results-cell { + font-family: 'Inter', sans-serif; + font-size: 12px; + font-weight: 600; + color: #5c6d80; + + &--test { + flex: 1; + text-align: left; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #313e4c; + font-weight: 400; + } + + &--value { + width: 100px; + text-align: right; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #313e4c; + font-weight: 600; + padding-right: 10px; + } + + &--ref { + width: 80px; + text-align: right; + font-family: 'Inter', sans-serif; + font-size: 12px; + color: #5c6d80; + font-weight: 400; + padding-left: 10px; + } + } + + &__critical-label { + display: flex; + align-items: center; + padding: 14px 16px; + gap: 10px; + border-bottom: 1px solid #ebeef8; + + span { + font-family: 'Inter', sans-serif; + font-weight: 600; + font-size: 13px; + color: #313e4c; + } + } + + &__critical-icon { + display: flex; + align-items: center; + color: #C93A54; + } + + &__primary-sample { + display: flex; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid #ebeef8; + background-color: rgba(242, 244, 247, 0.1); + } + + &__primary-sample-label { + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #667091; + font-weight: 400; + } + + &__primary-sample-value { + font-family: 'Inter', sans-serif; + font-size: 13px; + font-weight: 600; color: #313e4c; + } + + &__results-row { + display: flex; + padding: 12px 16px; + border-bottom: 1px solid #ebeef8; + + &--flagged { + background-color: rgba(201, 58, 84, 0.02); + } + + // Make sure the last row doesn't have a bottom border + &:last-child { + border-bottom: none; + } + + .report-detail-page__results-cell { + &--test { + flex: 1; + text-align: left; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #313e4c; + font-weight: 400; + } + + &--value { + width: 100px; + text-align: right; + font-family: 'Inter', sans-serif; + font-size: 13px; + color: #313e4c; + font-weight: 600; + padding-right: 10px; + } + + &--ref { + width: 80px; + text-align: right; + font-family: 'Inter', sans-serif; + font-size: 12px; + color: #5c6d80; + font-weight: 400; + padding-left: 10px; + } + } + } + + &__comments-section { + background-color: #F0F2FF; + border-radius: 16px; + padding: 24px; + margin-bottom: 16px; + } + + &__comments-title { + font-family: 'Inter', sans-serif; + font-size: 18px; + font-weight: 600; + color: #313E4C; + margin: 0 0 12px 0; + letter-spacing: 0.26px; + } + + &__comments-text { + font-family: 'Inter', sans-serif; + font-size: 16px; + color: #313E4C; + line-height: 24px; + margin: 0; + font-weight: 400; + } + + &__uploaded-file { + background-color: #fff; + border: 1px solid #ebeef8; + border-radius: 16px; + padding: 16px; + margin-bottom: 16px; + } + + &__uploaded-file-title { + font-family: 'Inter', sans-serif; font-size: 13px; + font-weight: 600; + color: #313e4c; + margin: 0 0 16px 0; + letter-spacing: 0.26px; + } + + &__file-container { + display: flex; + gap: 8px; + margin-bottom: 12px; + } + + &__file-icon { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background-color: #f0f0f0; + border-radius: 6px; + color: #435ff0; + } + + &__file-details { + display: flex; + flex-direction: column; + justify-content: center; + } + + &__file-name { + font-family: 'Inter', sans-serif; + font-size: 12px; + font-weight: 500; + color: #313e4c; + margin-bottom: 2px; + } + + &__file-info { + display: flex; + align-items: center; + gap: 4px; + font-family: 'Inter', sans-serif; + font-size: 12px; + color: #5c6d80; + } + + &__file-separator { + color: #abbccd; + } + + &__file-date { + color: #667091; + } + + &__file-progress { + height: 6px; + background-color: #f0f0f0; + border-radius: 100px; + overflow: hidden; + } + + &__file-progress-bar { + height: 100%; + width: 64%; + background-color: #435ff0; + border-radius: 100px; + } + + // Low Confidence Notice styles + .low-confidence-notice { + margin: 0 16px 20px; + padding: 14px 16px; + display: flex; + align-items: flex-start; + border: 1px solid #dce0e9; + border-radius: 16px; + background-color: #fff; + } + + .notice-icon { + color: #667091; + margin-right: 12px; + font-size: 24px; + display: flex; + align-items: center; + min-width: 24px; + } + + .notice-text { + color: #313E4C; + font-size: 14px; line-height: 20px; + font-family: 'Inter', sans-serif; + font-weight: 400; + } +} + +// Media Queries for responsiveness +@media (min-width: 768px) { + .report-detail-page { + &__actions { + max-width: 600px; + margin: 32px auto 0; + } + + &__section, + &__emergency, + &__info-card, + &__original-report { + max-width: 600px; + margin-left: auto; + margin-right: auto; + } + + &__tabs { + max-width: 600px; + margin-left: auto; + margin-right: auto; + } + + &__ai-help { + max-width: 600px; + margin: 20px auto 32px; + padding: 0 16px 16px; + } + + .low-confidence-notice { + max-width: 600px; + margin-left: auto; + margin-right: auto; + } } } \ No newline at end of file diff --git a/frontend/src/pages/Reports/ReportDetailPage.tsx b/frontend/src/pages/Reports/ReportDetailPage.tsx index 687da614..04103054 100644 --- a/frontend/src/pages/Reports/ReportDetailPage.tsx +++ b/frontend/src/pages/Reports/ReportDetailPage.tsx @@ -1,25 +1,79 @@ -import { IonPage, IonContent, IonButton } from '@ionic/react'; +import { IonPage, IonContent } from '@ionic/react'; import { useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import Icon from '../../common/components/Icon/Icon'; +import { useHistory, useParams } from 'react-router-dom'; import './ReportDetailPage.scss'; +import { useQuery } from '@tanstack/react-query'; +import axios from 'axios'; +import { MedicalReport } from '../../common/models/medicalReport'; +import { useTranslation } from 'react-i18next'; +import { getAuthConfig } from 'common/api/reportService'; + +// Import components +import ReportHeader from './components/ReportHeader'; +import ReportTabs from './components/ReportTabs'; +import OriginalReportTab from './components/OriginalReportTab'; +import InfoCard from './components/InfoCard'; +import ActionButtons from './components/ActionButtons'; +import AiAnalysisTab from './components/AiAnalysisTab'; + +const API_URL = import.meta.env.VITE_BASE_URL_API || ''; + +// Function to fetch report by ID +const fetchReportById = async (id: string): Promise => { + const response = await axios.get( + `${API_URL}/api/reports/${id}`, + await getAuthConfig(), + ); + return response.data; +}; /** * Page component for displaying detailed medical report analysis. * This shows AI insights and original report data with flagged values. */ const ReportDetailPage: React.FC = () => { -// const { reportId } = useParams<{ reportId: string }>(); + const { reportId } = useParams<{ reportId: string }>(); const history = useHistory(); + const { t } = useTranslation(); + + // Fetch report data using react-query + const { data, isLoading, error } = useQuery({ + queryKey: ['report', reportId], + queryFn: () => fetchReportById(reportId!), + enabled: !!reportId, + }); // State to track expanded sections - const [flaggedValuesExpanded, setFlaggedValuesExpanded] = useState(true); - const [normalValuesExpanded, setNormalValuesExpanded] = useState(true); const [activeTab, setActiveTab] = useState<'ai' | 'original'>('ai'); - // Toggle expanded state of sections - const toggleFlaggedValues = () => setFlaggedValuesExpanded(!flaggedValuesExpanded); - const toggleNormalValues = () => setNormalValuesExpanded(!normalValuesExpanded); + // Handle loading and error states + if (isLoading) { + return ; + } + + if (error) { + return ( + + +
+ {t('error.loading.report', { ns: 'errors', errorMessage: (error as Error).message })} +
+
+
+ ); + } + + if (!data) { + return ( + + +
{t('error.no-report-data', { ns: 'errors' })}
+
+
+ ); + } + + const reportData = data; // Handle tab selection const handleTabChange = (tab: 'ai' | 'original') => { @@ -40,271 +94,31 @@ const ReportDetailPage: React.FC = () => { history.push('/tabs/upload'); }; - // Hardcoded data for now, will be replaced with real API data later - const reportData = { - title: 'Blood Test', - category: 'General', - flaggedValues: [ - { - name: 'High LDL Cholesterol', - level: 'High', - value: '165 mg/dL', - conclusion: 'Elevated LDL (bad cholesterol) increases your risk of cardiovascular disease', - suggestions: [ - 'Consider a heart-healthy diet (e.g., Mediterranean).', - 'Increase physical activity.', - 'Visit the nearest emergency room.', - ], - }, - { - name: 'Low Hemoglobin (10.1 g/dL)', - level: 'Low', - value: '10.1 g/dL', - conclusion: 'This level suggests anemia, which may cause fatigue and weakness.', - suggestions: [ - 'Test for iron, B12, and folate deficiency.', - 'Consider iron-rich foods or supplements after medical consultation.already on one.', - ], - }, - ], - normalValues: [ - { - name: 'White Blood Cell Count', - value: '6,800 /µL', - conclusion: 'Normal WBC count; your immune system is functioning well', - suggestions: ['Keep up a balanced diet, manage stress, and get adequate rest.'], - }, - { - name: 'Vitamin D', - value: '35 ng/mL', - conclusion: 'Adequate levels for bone health and immunity.', - suggestions: ['Maintain outdoor exposure and dietary intake.'], - }, - ], - hasEmergency: true, - }; - return ( - {/* Header section */} -
-
-

Results Analysis

-
- - - -
-
- - {/* Category & Title */} -
- {reportData.category} -
- -
-
-

{reportData.title}

-
+ {/* Header component */} + {/* Tab selector for AI Insights vs Original Report */} -
-
handleTabChange('ai')} - > - - - - - - AI Insights -
-
handleTabChange('original')} - > - Original Report -
-
- - {/* Emergency alert if needed */} - {reportData.hasEmergency && ( -
-
- - - - - -
-

- Please contact your doctor or seek emergency care immediately. -

-
+ + + {/* Content based on active tab */} + {activeTab === 'ai' ? ( + + ) : ( + /* Original Report Tab Content */ + )} - {/* Flagged values section */} -
-
-
- -
-

Flagged values

-
- -
-
- - {flaggedValuesExpanded && - reportData.flaggedValues.map((item, index) => ( -
-
-
{item.name}
-
- {item.level} -
-
{item.value}
-
-
-
-

Conclusion:

-

{item.conclusion}

-
-
-

Suggestions:

-
    - {item.suggestions.map((suggestion, idx) => ( -
  • {suggestion}
  • - ))} -
-
-
-
- ))} -
- - {/* Normal values section */} -
-
-
- -
-

Normal values

-
- -
-
- - {normalValuesExpanded && - reportData.normalValues.map((item, index) => ( -
-
-
{item.name}
-
{item.value}
-
-
-
-

Conclusion:

-

{item.conclusion}

-
-
-

Suggestions:

-
    - {item.suggestions.map((suggestion, idx) => ( -
  • {suggestion}
  • - ))} -
-
-
-
- ))} -
- {/* Doctor information note */} -
-
- -
-
- With all interpretations, these results should be discussed with your doctor. -
-
- - {/* AI Assistant help section */} -
-
- Still need further clarifications? -
-
Ask our AI Assistant >
-
+ {/* Action buttons at the bottom */} -
- - -
+
); }; -export default ReportDetailPage; \ No newline at end of file +export default ReportDetailPage; diff --git a/frontend/src/pages/Reports/components/ActionButtons.tsx b/frontend/src/pages/Reports/components/ActionButtons.tsx new file mode 100644 index 00000000..0e4c93db --- /dev/null +++ b/frontend/src/pages/Reports/components/ActionButtons.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +interface ActionButtonsProps { + onDiscard: () => void; + onNewUpload: () => void; +} + +const ActionButtons: React.FC = ({ onDiscard, onNewUpload }) => { + const { t } = useTranslation(); + + return ( +
+ + +
+ ); +}; + +export default ActionButtons; diff --git a/frontend/src/pages/Reports/components/AiAnalysisTab.tsx b/frontend/src/pages/Reports/components/AiAnalysisTab.tsx new file mode 100644 index 00000000..df0dd196 --- /dev/null +++ b/frontend/src/pages/Reports/components/AiAnalysisTab.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { MedicalReport, LabValue } from '../../../common/models/medicalReport'; +import EmergencyAlert from './EmergencyAlert'; +import FlaggedValuesSection from './FlaggedValuesSection'; +import NormalValuesSection from './NormalValuesSection'; +import LowConfidenceNotice from './LowConfidenceNotice'; + +interface AiAnalysisTabProps { + reportData: MedicalReport; + isEmergencyAlertVisible?: boolean; +} + +const AiAnalysisTab: React.FC = ({ + reportData, + isEmergencyAlertVisible = true, +}) => { + // State to track expanded sections + const [flaggedValuesExpanded, setFlaggedValuesExpanded] = React.useState(true); + const [normalValuesExpanded, setNormalValuesExpanded] = React.useState(true); + + // Toggle expanded state of sections + const toggleFlaggedValues = () => setFlaggedValuesExpanded(!flaggedValuesExpanded); + const toggleNormalValues = () => setNormalValuesExpanded(!normalValuesExpanded); + + // Process lab values data + const hasEmergency = reportData.labValues.some((value) => value.isCritical); + const flaggedValues: LabValue[] = reportData.labValues.filter( + (value) => value.status !== 'normal', + ); + const normalValues: LabValue[] = reportData.labValues.filter( + (value) => value.status === 'normal', + ); + + // Format confidence score for display + const confidenceScore = reportData.confidence; + + const isLowConfidence = confidenceScore < 0.75; + + return ( +
+ {/* Emergency alert if needed */} + {isEmergencyAlertVisible && hasEmergency && } + + {/* Low confidence notice */} + {isLowConfidence && } + + {/* Flagged values section */} + + + {/* Normal values section */} + +
+ ); +}; + +export default AiAnalysisTab; diff --git a/frontend/src/pages/Reports/components/EmergencyAlert.tsx b/frontend/src/pages/Reports/components/EmergencyAlert.tsx new file mode 100644 index 00000000..56dae4c7 --- /dev/null +++ b/frontend/src/pages/Reports/components/EmergencyAlert.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +const EmergencyAlert: React.FC = () => { + const { t } = useTranslation(); + + return ( +
+
+ + + + + +
+

+ {t('report.emergency.message', { ns: 'reportDetail' })} +

+
+ ); +}; + +export default EmergencyAlert; diff --git a/frontend/src/pages/Reports/components/FlaggedValuesSection.tsx b/frontend/src/pages/Reports/components/FlaggedValuesSection.tsx new file mode 100644 index 00000000..973bb2e8 --- /dev/null +++ b/frontend/src/pages/Reports/components/FlaggedValuesSection.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Icon from '../../../common/components/Icon/Icon'; +import { LabValue } from '../../../common/models/medicalReport'; +import LabValueItem from './LabValueItem'; + +interface FlaggedValuesSectionProps { + flaggedValues: LabValue[]; + isExpanded: boolean; + onToggle: () => void; +} + +const FlaggedValuesSection: React.FC = ({ + flaggedValues, + isExpanded, + onToggle, +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+ +
+

+ {t('report.flagged-values.title', { ns: 'reportDetail' })} +

+
+ +
+
+ + {isExpanded && flaggedValues.map((item, index) => )} +
+ ); +}; + +export default FlaggedValuesSection; diff --git a/frontend/src/pages/Reports/components/InfoCard.tsx b/frontend/src/pages/Reports/components/InfoCard.tsx new file mode 100644 index 00000000..a61eb71b --- /dev/null +++ b/frontend/src/pages/Reports/components/InfoCard.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Icon from '../../../common/components/Icon/Icon'; + +const InfoCard: React.FC = () => { + const { t } = useTranslation(); + + return ( +
+
+ +
+
+ {t('report.doctor-note', { ns: 'reportDetail' })} +
+
+ ); +}; + +export default InfoCard; diff --git a/frontend/src/pages/Reports/components/LabValueItem.tsx b/frontend/src/pages/Reports/components/LabValueItem.tsx new file mode 100644 index 00000000..8746a273 --- /dev/null +++ b/frontend/src/pages/Reports/components/LabValueItem.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { LabValue } from '../../../common/models/medicalReport'; + +interface LabValueItemProps { + item: LabValue; +} + +const LabValueItem: React.FC = ({ item }) => { + const { t } = useTranslation(); + + return ( +
+
+
{item.name}
+ {item.status !== 'normal' && ( +
+ {item.status} +
+ )} +
+ {item.value} {item.unit} +
+
+
+
+

{t('report.conclusion.title', { ns: 'reportDetail' })}:

+

{item.conclusion}

+
+
+

{t('report.suggestions.title', { ns: 'reportDetail' })}:

+

{item.suggestions}

+
+
+
+ ); +}; + +export default LabValueItem; diff --git a/frontend/src/pages/Reports/components/LowConfidenceNotice.tsx b/frontend/src/pages/Reports/components/LowConfidenceNotice.tsx new file mode 100644 index 00000000..88aad533 --- /dev/null +++ b/frontend/src/pages/Reports/components/LowConfidenceNotice.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { IonIcon } from '@ionic/react'; +import { informationCircleOutline } from 'ionicons/icons'; + +/** + * Component to display a notice when the confidence level is low + */ +const LowConfidenceNotice: React.FC = () => { + const { t } = useTranslation(); + + return ( +
+
+ +
+
+ {t('reports.lowConfidence.message', { + defaultValue: + 'Please note that this diagnosis is uncertain due to an incomplete report. For a more accurate interpretation, we recommend uploading another report for processing.', + })} +
+
+ ); +}; + +export default LowConfidenceNotice; diff --git a/frontend/src/pages/Reports/components/NormalValuesSection.tsx b/frontend/src/pages/Reports/components/NormalValuesSection.tsx new file mode 100644 index 00000000..8151fed0 --- /dev/null +++ b/frontend/src/pages/Reports/components/NormalValuesSection.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import Icon from '../../../common/components/Icon/Icon'; +import { LabValue } from '../../../common/models/medicalReport'; +import LabValueItem from './LabValueItem'; + +interface NormalValuesSectionProps { + normalValues: LabValue[]; + isExpanded: boolean; + onToggle: () => void; +} + +const NormalValuesSection: React.FC = ({ + normalValues, + isExpanded, + onToggle, +}) => { + const { t } = useTranslation(); + + return ( +
+
+
+ +
+

+ {t('report.normal-values.title', { ns: 'reportDetail' })} +

+
+ +
+
+ + {isExpanded && normalValues.map((item, index) => )} +
+ ); +}; + +export default NormalValuesSection; diff --git a/frontend/src/pages/Reports/components/OriginalReport.tsx b/frontend/src/pages/Reports/components/OriginalReport.tsx new file mode 100644 index 00000000..a9df4735 --- /dev/null +++ b/frontend/src/pages/Reports/components/OriginalReport.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { format } from 'date-fns'; +import Icon from '../../../common/components/Icon/Icon'; +import { MedicalReport } from '../../../common/models/medicalReport'; + +interface OriginalReportTabProps { + reportData: MedicalReport; +} + +const OriginalReportTab: React.FC = ({ reportData }) => { + // Function to format file size in KB or MB + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + else if (bytes < 1048576) return Math.round(bytes / 1024) + ' KB'; + else return (bytes / 1048576).toFixed(1) + ' MB'; + }; + + // Get filename from originalFilename or fall back to file path + const filename = + reportData.originalFilename || reportData.filePath.split('/').pop() || 'Unknown file'; + + // Format file size if available + const fileSize = reportData.fileSize ? formatFileSize(reportData.fileSize) : 'Unknown size'; + + return ( +
+ {/* Test results table */} +
+
+
+ Test +
+
+ Results +
+
+ Ref. +
+
+ + {/* Test Results Rows */} + {reportData.labValues.map((labValue, index) => ( +
+
+ {labValue.name} +
+
+ {labValue.value} {labValue.unit} +
+
+ {labValue.normalRange} +
+
+ ))} +
+ + {/* Medical Comments Section */} +
+

Medical Comments:

+
{reportData.summary}
+
+ + {/* Uploaded File Section */} +
+

Uploaded file

+
+
+ +
+
+
{filename}
+
+ {fileSize} + + + Uploaded ({format(new Date(reportData.createdAt), 'MM/dd/yyyy')}) + +
+
+
+
+
+ ); +}; + +export default OriginalReportTab; diff --git a/frontend/src/pages/Reports/components/OriginalReportTab.tsx b/frontend/src/pages/Reports/components/OriginalReportTab.tsx new file mode 100644 index 00000000..a9df4735 --- /dev/null +++ b/frontend/src/pages/Reports/components/OriginalReportTab.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { format } from 'date-fns'; +import Icon from '../../../common/components/Icon/Icon'; +import { MedicalReport } from '../../../common/models/medicalReport'; + +interface OriginalReportTabProps { + reportData: MedicalReport; +} + +const OriginalReportTab: React.FC = ({ reportData }) => { + // Function to format file size in KB or MB + const formatFileSize = (bytes: number): string => { + if (bytes < 1024) return bytes + ' B'; + else if (bytes < 1048576) return Math.round(bytes / 1024) + ' KB'; + else return (bytes / 1048576).toFixed(1) + ' MB'; + }; + + // Get filename from originalFilename or fall back to file path + const filename = + reportData.originalFilename || reportData.filePath.split('/').pop() || 'Unknown file'; + + // Format file size if available + const fileSize = reportData.fileSize ? formatFileSize(reportData.fileSize) : 'Unknown size'; + + return ( +
+ {/* Test results table */} +
+
+
+ Test +
+
+ Results +
+
+ Ref. +
+
+ + {/* Test Results Rows */} + {reportData.labValues.map((labValue, index) => ( +
+
+ {labValue.name} +
+
+ {labValue.value} {labValue.unit} +
+
+ {labValue.normalRange} +
+
+ ))} +
+ + {/* Medical Comments Section */} +
+

Medical Comments:

+
{reportData.summary}
+
+ + {/* Uploaded File Section */} +
+

Uploaded file

+
+
+ +
+
+
{filename}
+
+ {fileSize} + + + Uploaded ({format(new Date(reportData.createdAt), 'MM/dd/yyyy')}) + +
+
+
+
+
+ ); +}; + +export default OriginalReportTab; diff --git a/frontend/src/pages/Reports/components/ReportHeader.tsx b/frontend/src/pages/Reports/components/ReportHeader.tsx new file mode 100644 index 00000000..a8a7d038 --- /dev/null +++ b/frontend/src/pages/Reports/components/ReportHeader.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import Icon from '../../../common/components/Icon/Icon'; +import { useTranslation } from 'react-i18next'; +import { MedicalReport } from '../../../common/models/medicalReport'; + +interface ReportHeaderProps { + reportData: MedicalReport; + onClose: () => void; +} + +const ReportHeader: React.FC = ({ reportData, onClose }) => { + const { t } = useTranslation(); + + return ( +
+
+

Results Analysis

+ +
+ + {/* Category & Title */} +
+ + {t(`list.${reportData.category}Category`, { ns: 'report' })} + + +
+

{reportData.title}

+
+ ); +}; + +export default ReportHeader; diff --git a/frontend/src/pages/Reports/components/ReportTabs.tsx b/frontend/src/pages/Reports/components/ReportTabs.tsx new file mode 100644 index 00000000..02337a95 --- /dev/null +++ b/frontend/src/pages/Reports/components/ReportTabs.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +interface ReportTabsProps { + activeTab: 'ai' | 'original'; + onTabChange: (tab: 'ai' | 'original') => void; +} + +const ReportTabs: React.FC = ({ activeTab, onTabChange }) => { + return ( +
+
onTabChange('ai')} + > + + + + AI Insights +
+
onTabChange('original')} + > + Original Report +
+
+ ); +}; + +export default ReportTabs;