From 8507fbef186142c9fd4e5fca76ea5f7aca8598ad Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Fri, 25 Apr 2025 12:56:29 +0300 Subject: [PATCH 1/5] Refactor DocumentProcessorController to remove file upload handling and update report model to use errorMessage instead of debugMessage. Enhance report processing logic to ensure accurate status updates and error handling. --- .../document-processor.controller.ts | 478 +----------------- backend/src/reports/models/report.model.ts | 4 +- frontend/src/common/models/medicalReport.ts | 4 +- 3 files changed, 24 insertions(+), 462 deletions(-) diff --git a/backend/src/document-processor/controllers/document-processor.controller.ts b/backend/src/document-processor/controllers/document-processor.controller.ts index 59e3324..27a3b6c 100644 --- a/backend/src/document-processor/controllers/document-processor.controller.ts +++ b/backend/src/document-processor/controllers/document-processor.controller.ts @@ -1,25 +1,16 @@ import { Controller, Post, - UploadedFile, - UseInterceptors, Body, BadRequestException, Logger, Get, - Res, Req, UnauthorizedException, NotFoundException, InternalServerErrorException, } from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { - DocumentProcessorService, - ProcessedDocumentResult, -} from '../services/document-processor.service'; -import { Express } from 'express'; -import { Response } from 'express'; +import { DocumentProcessorService } from '../services/document-processor.service'; import { ReportsService } from '../../reports/reports.service'; import { RequestWithUser } from '../../auth/auth.middleware'; import { Readable } from 'stream'; @@ -37,83 +28,6 @@ export class DocumentProcessorController { private readonly configService: ConfigService, ) {} - @Post('upload') - @UseInterceptors(FileInterceptor('file')) - async uploadAndProcessReport( - @UploadedFile() file: Express.Multer.File, - @Body('userId') userId: string, - @Body('debug') debug?: string, - ): Promise { - if (!file) { - throw new BadRequestException('No file uploaded'); - } - - // Validate file type - const validMimeTypes = [ - 'image/jpeg', - 'image/png', - 'image/heic', - 'image/heif', - 'application/pdf', - ]; - - if (!validMimeTypes.includes(file.mimetype)) { - throw new BadRequestException( - `Invalid file type: ${file.mimetype}. Supported types: JPEG, PNG, HEIC, HEIF, and PDF.`, - ); - } - - // Validate file size (10MB max) - const maxSizeBytes = 10 * 1024 * 1024; - if (file.size > maxSizeBytes) { - throw new BadRequestException(`File size exceeds maximum allowed (10MB)`); - } - - // Use test userId if not provided - const effectiveUserId = userId || 'test-user-id'; - - // Check if debug mode is enabled - const debugMode = debug === 'true'; - - this.logger.log( - `Processing document: ${file.originalname} (${file.mimetype})${debugMode ? ' with DEBUG enabled' : ''}`, - ); - - try { - // Process the document - const result = await this.documentProcessorService.processDocument( - file.buffer, - effectiveUserId, - ); - - // If debug mode is enabled, include the raw responses from AWS services - if (debugMode) { - this.logger.debug('DEBUG: Document processing result', { - extractedTextRaw: result.extractedText, - analysisRaw: result.analysis, - }); - - // For debugging purposes, return the complete raw result - return { - ...result, - _debug: { - timestamp: new Date().toISOString(), - rawExtractedText: result.extractedText, - rawAnalysis: result.analysis, - rawSimplifiedExplanation: result.simplifiedExplanation, - }, - }; - } - - return { analysis: result.analysis, simplifiedExplanation: result.simplifiedExplanation }; - } catch (error: unknown) { - this.logger.error( - `Error processing document: ${error instanceof Error ? error.message : 'Unknown error'}`, - ); - throw error; - } - } - @Post('process-file') async processReport( @Body('reportId') reportId: string, @@ -191,6 +105,14 @@ export class DocumentProcessorController { try { this.logger.log(`Started async processing for report: ${reportId}`); + // Fetch the report again to ensure we have the latest version + const report = await this.reportsService.findOne(reportId, userId); + if (!report) { + this.logger.error(`Report ${reportId} not found during async processing`); + + return; + } + // Get the file from S3 let fileBuffer; try { @@ -215,10 +137,13 @@ export class DocumentProcessorController { return; } - // Fetch the report again to ensure we have the latest version - const report = await this.reportsService.findOne(reportId, userId); - if (!report) { - this.logger.error(`Report ${reportId} not found during async processing`); + if (!result.analysis.metadata.isMedicalReport) { + this.logger.log(`Report ${reportId} is not a medical report.`); + report.processingStatus = ProcessingStatus.FAILED; + report.errorMessage = 'Document is not a medical report'; + report.updatedAt = new Date().toISOString(); + await this.reportsService.updateReport(report); + return; } @@ -250,22 +175,22 @@ export class DocumentProcessorController { } /** - * Updates a report's processing status to FAILED and logs a debug message + * Updates a report's processing status to FAILED and logs a error message * @param reportId - ID of the report to update * @param userId - ID of the user who owns the report - * @param debugMessage - Optional debug message describing the failure + * @param errorMessage - Optional error message describing the failure */ private async failReport( reportId: string, userId: string, - debugMessage: string | undefined = undefined, + errorMessage: string | undefined = undefined, ): Promise { try { const report = await this.reportsService.findOne(reportId, userId); if (report) { report.processingStatus = ProcessingStatus.FAILED; report.updatedAt = new Date().toISOString(); - report.debugMessage = debugMessage; + report.errorMessage = errorMessage; await this.reportsService.updateReport(report); this.logger.log(`Updated status of report ${reportId} to FAILED`); } @@ -348,369 +273,6 @@ export class DocumentProcessorController { }); } - @Get('test') - getTestStatus(): { status: string } { - return { status: 'DocumentProcessorController is working' }; - } - - @Get('test-form') - getUploadForm(@Res() res: Response): void { - const html = ` - - - - Medical Document Processor Test - - - -

Medical Document Processor Test

-

Upload a medical document (PDF or image) to see the extracted text and analysis.

- -
-
- - -
- -
- -
- - -
- -
- -
- - -
-
- When enabled, the response will include the complete raw output from both AWS Textract and Bedrock services. - This is useful for troubleshooting but will produce a much larger response. -
-
- - -
- -
Processing document... This may take a minute.
- -
-

Result

-
-
- - - - - `; - - res.type('text/html').send(html); - } - @Get('report-status/:reportId') async getReportStatus(@Req() request: RequestWithUser): Promise { // Get reportId from path parameter diff --git a/backend/src/reports/models/report.model.ts b/backend/src/reports/models/report.model.ts index 45aff23..b3f2a25 100644 --- a/backend/src/reports/models/report.model.ts +++ b/backend/src/reports/models/report.model.ts @@ -75,6 +75,6 @@ export class Report { @ApiProperty({ description: 'Last update timestamp' }) updatedAt: string; - @ApiProperty({ description: 'Optional debug message for the report' }) - debugMessage?: string; + @ApiProperty({ description: 'Optional error message for the report' }) + errorMessage?: string; } diff --git a/frontend/src/common/models/medicalReport.ts b/frontend/src/common/models/medicalReport.ts index c2e05c1..0b0fb9b 100644 --- a/frontend/src/common/models/medicalReport.ts +++ b/frontend/src/common/models/medicalReport.ts @@ -52,11 +52,11 @@ export interface MedicalReport { labValues: LabValue[]; summary: string; confidence: number; - status: ReportStatus; filePath: string; originalFilename: string; fileSize: number; + status: ReportStatus; + errorMessage?: string; // Optional error message for the report createdAt: string; // ISO date string updatedAt: string; // ISO date string - debugMessage?: string; // Optional debug message for the report } From 843e639b4e629121577c50f39d02db8b40ec0f9a Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Fri, 25 Apr 2025 13:55:33 +0300 Subject: [PATCH 2/5] Refactor DocumentProcessorController and enhance report processing logic to include isMedicalReport flag. Update ProcessingPage and ProcessingError components for improved error handling and user feedback. Introduce new ProcessingMedicalReport component for non-medical report scenarios. Ensure all user-facing text is internationalized. --- .../document-processor.controller.ts | 31 ++++++++-------- backend/src/reports/models/report.model.ts | 3 ++ frontend/src/common/models/medicalReport.ts | 1 + .../src/pages/Processing/ProcessingPage.tsx | 35 +++++++++++++------ .../Processing/components/ProcessingError.tsx | 9 +++-- .../components/ProcessingMedicalReport.tsx | 24 +++++++++++++ 6 files changed, 77 insertions(+), 26 deletions(-) create mode 100644 frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx diff --git a/backend/src/document-processor/controllers/document-processor.controller.ts b/backend/src/document-processor/controllers/document-processor.controller.ts index 27a3b6c..e31f79d 100644 --- a/backend/src/document-processor/controllers/document-processor.controller.ts +++ b/backend/src/document-processor/controllers/document-processor.controller.ts @@ -38,10 +38,7 @@ export class DocumentProcessorController { } // Extract userId from the request (attached by auth middleware) - const userId = request.user?.sub; - if (!userId) { - throw new UnauthorizedException('User ID not found in request'); - } + const userId = this.extractUserId(request); this.logger.log(`Queueing document for processing, report ID: ${reportId}`); @@ -138,11 +135,9 @@ export class DocumentProcessorController { } if (!result.analysis.metadata.isMedicalReport) { - this.logger.log(`Report ${reportId} is not a medical report.`); - report.processingStatus = ProcessingStatus.FAILED; - report.errorMessage = 'Document is not a medical report'; - report.updatedAt = new Date().toISOString(); - await this.reportsService.updateReport(report); + const errorMessage = `Report ${reportId} is not a medical report.`; + this.logger.log(errorMessage); + await this.failReport(reportId, userId, errorMessage, false); return; } @@ -151,7 +146,7 @@ export class DocumentProcessorController { report.title = result.analysis.title; report.category = result.analysis.category; report.processingStatus = ProcessingStatus.PROCESSED; - + report.isMedicalReport = true; // Extract lab values report.labValues = result.analysis.labValues || []; @@ -184,6 +179,7 @@ export class DocumentProcessorController { reportId: string, userId: string, errorMessage: string | undefined = undefined, + isMedicalReport: boolean | undefined = undefined, ): Promise { try { const report = await this.reportsService.findOne(reportId, userId); @@ -191,6 +187,7 @@ export class DocumentProcessorController { report.processingStatus = ProcessingStatus.FAILED; report.updatedAt = new Date().toISOString(); report.errorMessage = errorMessage; + report.isMedicalReport = isMedicalReport; await this.reportsService.updateReport(report); this.logger.log(`Updated status of report ${reportId} to FAILED`); } @@ -283,10 +280,7 @@ export class DocumentProcessorController { } // Extract userId from the request (attached by auth middleware) - const userId = request.user?.sub; - if (!userId) { - throw new UnauthorizedException('User ID not found in request'); - } + const userId = this.extractUserId(request); try { // Fetch the associated report record from DynamoDB @@ -299,6 +293,7 @@ export class DocumentProcessorController { reportId: report.id, status: report.processingStatus, isComplete: report.processingStatus === ProcessingStatus.PROCESSED, + isMedicalReport: report.isMedicalReport, }; } catch (error: unknown) { this.logger.error( @@ -307,4 +302,12 @@ export class DocumentProcessorController { throw error; } } + + private extractUserId(request: RequestWithUser): string { + const userId = request.user?.sub; + if (!userId) { + throw new UnauthorizedException('User ID not found in request'); + } + return userId; + } } diff --git a/backend/src/reports/models/report.model.ts b/backend/src/reports/models/report.model.ts index b3f2a25..9fc327c 100644 --- a/backend/src/reports/models/report.model.ts +++ b/backend/src/reports/models/report.model.ts @@ -35,6 +35,9 @@ export class Report { }) processingStatus: ProcessingStatus; + @ApiProperty({ description: 'Optional flag to indicate if the report is a medical report' }) + isMedicalReport?: boolean; + @ApiProperty({ description: 'List of lab values' }) labValues: Array<{ name: string; diff --git a/frontend/src/common/models/medicalReport.ts b/frontend/src/common/models/medicalReport.ts index 0b0fb9b..a175338 100644 --- a/frontend/src/common/models/medicalReport.ts +++ b/frontend/src/common/models/medicalReport.ts @@ -57,6 +57,7 @@ export interface MedicalReport { fileSize: number; status: ReportStatus; errorMessage?: string; // Optional error message for the report + isMedicalReport?: boolean; // Optional flag to indicate if the report is a medical report createdAt: string; // ISO date string updatedAt: string; // ISO date string } diff --git a/frontend/src/pages/Processing/ProcessingPage.tsx b/frontend/src/pages/Processing/ProcessingPage.tsx index dccefa9..5ae6603 100644 --- a/frontend/src/pages/Processing/ProcessingPage.tsx +++ b/frontend/src/pages/Processing/ProcessingPage.tsx @@ -24,6 +24,7 @@ const ProcessingPage: React.FC = () => { // States to track processing const [isProcessing, setIsProcessing] = useState(true); const [processingError, setProcessingError] = useState(null); + const [errorHeading, setErrorHeading] = useState(null); const statusCheckIntervalRef = useRef(null); const lastTriggeredTime = useRef(null); @@ -42,6 +43,14 @@ const ProcessingPage: React.FC = () => { const checkReportStatus = async () => { if (!reportId) return; + const failedHeading = 'Processing Error'; + const failedMessage = + 'There was a problem processing your uploaded file. Please try again or upload another.'; + + const missingDataHeading = 'Missing Data'; + const missingDataMessage = + 'The system was unable to extract meaningful health data from your uploaded file. Please try again or upload another.'; + try { const response = await axios.get( `${API_URL}/api/document-processor/report-status/${reportId}`, @@ -50,27 +59,27 @@ const ProcessingPage: React.FC = () => { const data = response.data; - // If processing is complete, clear the interval and redirect to the report page if (data.isComplete) { setIsProcessing(false); - - // Clear the interval clearStatusCheckInterval(); console.log('Processing complete'); - // Redirect to report detail page history.push(`/tabs/reports/${reportId}`); + } else if (data.isMedicalReport === false) { + setIsProcessing(false); + clearStatusCheckInterval(); + setErrorHeading(missingDataHeading); + setProcessingError(missingDataMessage); } else if (data.status === 'failed') { - throw new Error('Processing failed'); + throw new Error(failedMessage); } } catch (error) { - // Clear the interval on error + setIsProcessing(false); clearStatusCheckInterval(); - console.error('Error checking report status:', error); - setProcessingError('An error occurred while processing the report. Please try again.'); - setIsProcessing(false); + setErrorHeading(failedHeading); + setProcessingError(error instanceof Error ? error.message : failedMessage); } }; @@ -148,7 +157,13 @@ const ProcessingPage: React.FC = () => { {isProcessing && } {/* Error state - shows when processing fails */} - {processingError && } + {processingError && errorHeading && ( + + )} diff --git a/frontend/src/pages/Processing/components/ProcessingError.tsx b/frontend/src/pages/Processing/components/ProcessingError.tsx index 5001c36..43e1921 100644 --- a/frontend/src/pages/Processing/components/ProcessingError.tsx +++ b/frontend/src/pages/Processing/components/ProcessingError.tsx @@ -5,6 +5,7 @@ import warning from '../../../assets/icons/warning.svg'; interface ProcessingErrorProps { errorMessage: string; + errorHeading: string; onRetry: () => void; } @@ -12,7 +13,11 @@ interface ProcessingErrorProps { * Component that displays processing error information and actions * Exactly matches the design from the provided screenshot */ -const ProcessingError: React.FC = ({ errorMessage, onRetry }) => { +const ProcessingError: React.FC = ({ + errorMessage, + errorHeading, + onRetry, +}) => { const history = useHistory(); return ( @@ -27,7 +32,7 @@ const ProcessingError: React.FC = ({ errorMessage, onRetry Warning Icon -

Processing Error

+

{errorHeading}

{errorMessage}

diff --git a/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx b/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx new file mode 100644 index 0000000..fb913eb --- /dev/null +++ b/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { IonButton } from '@ionic/react'; +import { useHistory } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; + +/** + * Component displayed when the uploaded document is not a medical report + */ +const ProcessingMedicalReport: React.FC = () => { + const history = useHistory(); + const { t } = useTranslation(); + + return ( +
+

{t('error.not_medical_report', { ns: 'processing' })}

+

{t('error.not_medical_report_message', { ns: 'processing' })}

+ history.push('/tabs/upload')} className="retry-button"> + {t('button.try_another', { ns: 'common' })} + +
+ ); +}; + +export default ProcessingMedicalReport; From f60546bd7f74d0bde4cef4694afbe7e91bbfb368 Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Fri, 25 Apr 2025 14:01:08 +0300 Subject: [PATCH 3/5] Remove ProcessingMedicalReport component as it is no longer needed. Ensure all user-facing text was previously internationalized. --- .../components/ProcessingMedicalReport.tsx | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx diff --git a/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx b/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx deleted file mode 100644 index fb913eb..0000000 --- a/frontend/src/pages/Processing/components/ProcessingMedicalReport.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { IonButton } from '@ionic/react'; -import { useHistory } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; - -/** - * Component displayed when the uploaded document is not a medical report - */ -const ProcessingMedicalReport: React.FC = () => { - const history = useHistory(); - const { t } = useTranslation(); - - return ( -
-

{t('error.not_medical_report', { ns: 'processing' })}

-

{t('error.not_medical_report_message', { ns: 'processing' })}

- history.push('/tabs/upload')} className="retry-button"> - {t('button.try_another', { ns: 'common' })} - -
- ); -}; - -export default ProcessingMedicalReport; From ae6b98b2b52baae844bdb083a81365c71266472c Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Fri, 25 Apr 2025 14:08:48 +0300 Subject: [PATCH 4/5] Refactor error handling in ProcessingPage to consolidate error state management using setError function. Ensure all user-facing text is internationalized. --- .../src/pages/Processing/ProcessingPage.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/pages/Processing/ProcessingPage.tsx b/frontend/src/pages/Processing/ProcessingPage.tsx index 5ae6603..da832ff 100644 --- a/frontend/src/pages/Processing/ProcessingPage.tsx +++ b/frontend/src/pages/Processing/ProcessingPage.tsx @@ -39,6 +39,12 @@ const ProcessingPage: React.FC = () => { } }; + const setError = (heading: string | null, message: string | null) => { + setErrorHeading(heading); + setProcessingError(message); + setIsProcessing(false); + }; + // Check the status of the report processing const checkReportStatus = async () => { if (!reportId) return; @@ -69,17 +75,14 @@ const ProcessingPage: React.FC = () => { } else if (data.isMedicalReport === false) { setIsProcessing(false); clearStatusCheckInterval(); - setErrorHeading(missingDataHeading); - setProcessingError(missingDataMessage); + setError(missingDataHeading, missingDataMessage); } else if (data.status === 'failed') { throw new Error(failedMessage); } } catch (error) { setIsProcessing(false); clearStatusCheckInterval(); - - setErrorHeading(failedHeading); - setProcessingError(error instanceof Error ? error.message : failedMessage); + setError(failedHeading, error instanceof Error ? error.message : failedMessage); } }; @@ -110,7 +113,7 @@ const ProcessingPage: React.FC = () => { statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 2000); } catch (error) { console.error('Error processing file:', error); - setProcessingError('Failed to process the file. Please try again.'); + setError('Processing Error', 'Failed to process the file. Please try again.'); setIsProcessing(false); } }; @@ -118,7 +121,7 @@ const ProcessingPage: React.FC = () => { // Handle retry attempt const execute = () => { // Reset error state and try processing the same file again - setProcessingError(null); + setError(null, null); setIsProcessing(true); lastTriggeredTime.current = Date.now(); processFile(); From 755798833f0fe8d648eb88acb93a56a1f9edabcd Mon Sep 17 00:00:00 2001 From: Adam Refaey Date: Fri, 25 Apr 2025 14:16:02 +0300 Subject: [PATCH 5/5] Refactor error handling in ProcessingPage to remove error message from thrown Error. Simplify error state management by using a default failed message in setError. Ensure all user-facing text is internationalized. --- frontend/src/pages/Processing/ProcessingPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/Processing/ProcessingPage.tsx b/frontend/src/pages/Processing/ProcessingPage.tsx index da832ff..282ee98 100644 --- a/frontend/src/pages/Processing/ProcessingPage.tsx +++ b/frontend/src/pages/Processing/ProcessingPage.tsx @@ -77,12 +77,12 @@ const ProcessingPage: React.FC = () => { clearStatusCheckInterval(); setError(missingDataHeading, missingDataMessage); } else if (data.status === 'failed') { - throw new Error(failedMessage); + throw new Error(); } - } catch (error) { + } catch { setIsProcessing(false); clearStatusCheckInterval(); - setError(failedHeading, error instanceof Error ? error.message : failedMessage); + setError(failedHeading, failedMessage); } };