diff --git a/backend/src/document-processor/controllers/document-processor.controller.ts b/backend/src/document-processor/controllers/document-processor.controller.ts index 59e33249..e31f79dd 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, @@ -124,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}`); @@ -191,6 +102,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 +134,11 @@ 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) { + const errorMessage = `Report ${reportId} is not a medical report.`; + this.logger.log(errorMessage); + await this.failReport(reportId, userId, errorMessage, false); + return; } @@ -226,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 || []; @@ -250,22 +170,24 @@ 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, + isMedicalReport: boolean | 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; + report.isMedicalReport = isMedicalReport; await this.reportsService.updateReport(report); this.logger.log(`Updated status of report ${reportId} to FAILED`); } @@ -348,369 +270,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 @@ -721,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 @@ -737,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( @@ -745,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 45aff23f..9fc327cb 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; @@ -75,6 +78,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 c2e05c15..a175338d 100644 --- a/frontend/src/common/models/medicalReport.ts +++ b/frontend/src/common/models/medicalReport.ts @@ -52,11 +52,12 @@ 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 + isMedicalReport?: boolean; // Optional flag to indicate if the report is a medical report createdAt: string; // ISO date string updatedAt: string; // ISO date string - debugMessage?: string; // Optional debug message for the report } diff --git a/frontend/src/pages/Processing/ProcessingPage.tsx b/frontend/src/pages/Processing/ProcessingPage.tsx index dccefa95..282ee98b 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); @@ -38,10 +39,24 @@ 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; + 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 +65,24 @@ 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(); + setError(missingDataHeading, missingDataMessage); } else if (data.status === 'failed') { - throw new Error('Processing failed'); + throw new Error(); } - } catch (error) { - // Clear the interval on error - clearStatusCheckInterval(); - - console.error('Error checking report status:', error); - setProcessingError('An error occurred while processing the report. Please try again.'); + } catch { setIsProcessing(false); + clearStatusCheckInterval(); + setError(failedHeading, failedMessage); } }; @@ -101,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); } }; @@ -109,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(); @@ -148,7 +160,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 5001c364..43e19210 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}