Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions frontend/src/pages/Processing/ProcessingPage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,103 @@
}
}
}

&__error-container {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 2rem;
max-width: 500px;
margin: 0 auto;
}

&__error-header {
margin-bottom: 1.5rem;
}

&__error-oops {
font-size: 1.25rem;
color: var(--ion-color-medium);
margin-bottom: 0.5rem;
}

&__error-title {
font-size: 2rem;
font-weight: 600;
margin: 0;
color: var(--ion-color-dark);
}

&__error-icon {
margin: 1.5rem 0;
}

&__error-icon-circle {
width: 120px;
height: 120px;
background-color: rgba(196, 30, 58, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}

&__error-subheading {
font-size: 1.5rem;
font-weight: 600;
color: var(--ion-color-dark);
margin-bottom: 1rem;
}

&__error-message {
font-size: 1rem;
color: var(--ion-color-medium);
line-height: 1.5;
margin-bottom: 2rem;
max-width: 400px;
}

&__error-actions {
display: flex;
gap: 1rem;
width: 100%;
max-width: 400px;
}

&__retry-btn {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 2rem;
background: transparent;
border: 1px solid var(--ion-color-primary);
color: var(--ion-color-primary);
font-weight: 500;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;

&:hover {
background-color: var(--ion-color-primary-tint);
}
}

&__upload-btn {
flex: 1;
padding: 0.75rem 1rem;
border-radius: 2rem;
background: var(--ion-color-primary);
border: none;
color: white;
font-weight: 500;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;

&:hover {
background-color: var(--ion-color-primary-shade);
}
}
}

// Animation for the pulse effect
Expand Down
131 changes: 56 additions & 75 deletions frontend/src/pages/Processing/ProcessingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { useEffect, useState, useRef } from 'react';
import { useAxios } from '../../common/hooks/useAxios';
import './ProcessingPage.scss';
import { getAuthConfig } from 'common/api/reportService';
import ProcessingError from './components/ProcessingError';
import ProcessingAnimation from './components/ProcessingAnimation';

const API_URL = import.meta.env.VITE_BASE_URL_API || '';

/**
Expand All @@ -28,6 +31,13 @@ const ProcessingPage: React.FC = () => {
const location = useLocation<{ reportId: string }>();
const reportId = location.state?.reportId;

const clearStatusCheckInterval = () => {
if (statusCheckIntervalRef.current) {
window.clearInterval(statusCheckIntervalRef.current);
statusCheckIntervalRef.current = null;
}
};

// Check the status of the report processing
const checkReportStatus = async () => {
if (!reportId) return;
Expand All @@ -38,86 +48,79 @@ const ProcessingPage: React.FC = () => {
await getAuthConfig(),
);

console.log('Report status:', response.data);
const data = response.data;

// If processing is complete, clear the interval and redirect to the report page
if (response.data.isComplete) {
if (data.isComplete) {
setIsProcessing(false);

// Clear the interval
if (statusCheckIntervalRef.current) {
window.clearInterval(statusCheckIntervalRef.current);
statusCheckIntervalRef.current = null;
}
clearStatusCheckInterval();

console.log('Processing complete');

// Redirect to report detail page
history.push(`/tabs/reports/${reportId}`);
} else if (data.status === 'failed') {
throw new Error('Processing failed');
}
} catch (error) {
// Clear the interval on error
clearStatusCheckInterval();

console.error('Error checking report status:', error);
setProcessingError('Failed to check the status of the report. Please try again.');
setProcessingError('An error occurred while processing the report. Please try again.');
setIsProcessing(false);
}
};

// Clear the interval on error
if (statusCheckIntervalRef.current) {
window.clearInterval(statusCheckIntervalRef.current);
statusCheckIntervalRef.current = null;
}
// Process file function - moved outside useEffect to make it callable from buttons
const processFile = async () => {
if (!reportId) {
return;
}

try {
// Send POST request to backend API
await axios.post(
`${API_URL}/api/document-processor/process-file`,
{ reportId },
await getAuthConfig(),
);

// Start checking the status every 2 seconds
statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 2000);
} catch (error) {
console.error('Error processing file:', error);
setProcessingError('Failed to process the file. Please try again.');
setIsProcessing(false);
}
};

// Handle retry attempt
const handleRetry = () => {
// Reset error state and try processing the same file again
setProcessingError(null);
setIsProcessing(true);
hasInitiatedProcessing.current = false;
processFile();
};

// 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;
}

hasInitiatedProcessing.current = true; // Mark as initiated before fetching

const processFile = async () => {
try {
// Send POST request to backend API
const response = await axios.post(
`${API_URL}/api/document-processor/process-file`,
{ reportId },
await getAuthConfig(),
);

console.log('File processing started:', response.data);

// Start checking the status every 2 seconds
statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 2000);

// Run the first status check immediately
checkReportStatus();
} catch (error) {
console.error('Error processing file:', error);
setProcessingError('Failed to process the file. Please try again.');
setIsProcessing(false);
}
};

processFile();

// Clean up the interval when the component unmounts
return () => {
if (statusCheckIntervalRef.current) {
window.clearInterval(statusCheckIntervalRef.current);
}
};
return clearStatusCheckInterval;
}, [reportId, location, history]);


return (
<IonPage className="processing-page">
<IonContent fullscreen>
Expand All @@ -132,41 +135,19 @@ const ProcessingPage: React.FC = () => {
testid="processing-user-avatar"
/>
</div>

{/* Title section */}
<div className="processing-page__title">
<p className="processing-page__subtitle">
Just a few seconds{firstName && ', ' + firstName}!
</p>
<h1 className="processing-page__heading">
{processingError ? 'Processing Error' : 'Processing Data...'}
</h1>
{processingError && <p className="processing-page__error">{processingError}</p>}
</div>
</div>

{/* Animation circle */}
{isProcessing && (
<div className="processing-page__animation">
<div className="processing-page__animation-circle"></div>
</div>
)}
{/* Processing animation */}
{isProcessing && <ProcessingAnimation firstName={firstName} />}

{/* Error state - show retry button */}
{/* Error state - shows when processing fails */}
{processingError && (
<div className="processing-page__error-actions">
<button
className="processing-page__retry-btn"
onClick={() => history.push('/tabs/upload')}
>
Try Again
</button>
</div>
<ProcessingError errorMessage={processingError} onRetry={handleRetry} />
)}
</div>
</IonContent>
</IonPage>
);
};

export default ProcessingPage;
export default ProcessingPage;
26 changes: 26 additions & 0 deletions frontend/src/pages/Processing/components/ProcessingAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import '../ProcessingPage.scss';

interface ProcessingAnimationProps {
firstName?: string;
}

/**
* Component that displays processing animation and message
*/
const ProcessingAnimation: React.FC<ProcessingAnimationProps> = ({ firstName }) => {
return (
<>
<div className="processing-page__title">
<p className="processing-page__subtitle">
Just a few seconds{firstName && ', ' + firstName}!
</p>
<h1 className="processing-page__heading">Processing Data...</h1>
</div>
<div className="processing-page__animation">
<div className="processing-page__animation-circle"></div>
</div>
</>
);
};

export default ProcessingAnimation;
62 changes: 62 additions & 0 deletions frontend/src/pages/Processing/components/ProcessingError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useHistory } from 'react-router-dom';
import '../ProcessingPage.scss';

interface ProcessingErrorProps {
errorMessage: string;
onRetry: () => void;
}

/**
* Component that displays processing error information and actions
*/
const ProcessingError: React.FC<ProcessingErrorProps> = ({ errorMessage, onRetry }) => {
const history = useHistory();

return (
<div className="processing-page__error-container">
<div className="processing-page__error-header">
<p className="processing-page__error-oops">Oops! ...</p>
<h2 className="processing-page__error-title">Problem detected</h2>
</div>

<div className="processing-page__error-icon">
<div className="processing-page__error-icon-circle">
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M24 4C12.96 4 4 12.96 4 24C4 35.04 12.96 44 24 44C35.04 44 44 35.04 44 24C44 12.96 35.04 4 24 4ZM26 34H22V30H26V34ZM26 26H22V14H26V26Z"
fill="#C41E3A"
/>
</svg>
</div>
</div>

<h3 className="processing-page__error-subheading">Processing Error</h3>

<p className="processing-page__error-message">
{errorMessage ||
'There was a problem processing your uploaded file. Please try again or upload another.'}
</p>

<div className="processing-page__error-actions">
<button className="processing-page__retry-btn" onClick={onRetry}>
Try again
</button>

<button
className="processing-page__upload-btn"
onClick={() => history.push('/tabs/upload')}
>
New Upload
</button>
</div>
</div>
);
};

export default ProcessingError;