Skip to content
This repository was archived by the owner on Jun 2, 2025. It is now read-only.
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
2 changes: 1 addition & 1 deletion ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Routes, Route, useNavigate } from 'react-router';
import UploadPage from './pages/UploadPage';
import VerifyPage from './pages/VerifyPage';
import VerifyPage from './pages/VerifyPage/VerifyPage';
import DownloadPage from './pages/DownloadPage';
import NotSignedInPage from './pages/NotSignedInPage';
import SignInPage from './pages/SignInPage';
Expand Down
279 changes: 75 additions & 204 deletions ui/src/pages/VerifyPage.tsx → ui/src/pages/VerifyPage/VerifyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,214 +1,33 @@
import React, { useEffect, useState } from 'react';
import Layout from '../components/Layout';
import {
authorizedFetch,
FieldData,
GetDocumentResponse,
UpdateDocumentResponse,
} from '../utils/api';
import { useNavigate } from 'react-router';
import { shouldUseTextarea } from '../utils/formUtils';
import Layout from '../../components/Layout';
import { ExtractedData } from '../../utils/api';
import { shouldUseTextarea } from '../../utils/formUtils';
import { useVerifyPage } from './useVerifyPage.ts';

interface VerifyPageProps {
signOut: () => Promise<void>;
}

export default function VerifyPage({ signOut }: VerifyPageProps) {
const [documentId] = useState<string | null>(() =>
sessionStorage.getItem('documentId')
);
const [responseData, setResponseData] = useState<GetDocumentResponse | null>(
null
);
const [loading, setLoading] = useState<boolean>(true); // tracks if the page is loading
const [error, setError] = useState<boolean>(false); // tracks when there is an error

const navigate = useNavigate();

async function pollApiRequest(attempts = 30, delay = 2000) {
// Helper function to sleep for the specified delay
const sleep = () => new Promise((resolve) => setTimeout(resolve, delay));

if (!documentId) {
console.error('No documentId available for API request');
setLoading(false);
setError(true);
return;
}

for (let i = 0; i < attempts; i++) {
try {
const response = await authorizedFetch(`/api/document/${documentId}`, {
method: 'GET',
headers: {
Accept: 'application/json',
},
});

if (response.status === 401 || response.status === 403) {
alert('You are no longer signed in! Please sign in again.');
signOut();
return;
} else if (!response.ok) {
console.warn(`Attempt ${i + 1} failed: ${response.statusText}`);
await sleep();
continue;
}

const result = (await response.json()) as GetDocumentResponse; // parse response

if (result.status !== 'complete') {
console.info(
`Attempt ${i + 1} is not complete. Trying again in a little bit.`
);
await sleep();
continue;
}

setResponseData(result); // store API data in state
setLoading(false); // stop loading when data is received
setError(false); // clear any previous errors
return;
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
await sleep();
}
}

console.error('Attempt failed after max attempts');
setLoading(false);
setError(true);
}

async function handleVerifySubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();

if (!responseData || !responseData.extracted_data) {
console.log('no extracted data available');
return;
}

const formData = {
extracted_data: responseData.extracted_data,
};

try {
const apiUrl = `/api/document/${responseData.document_id}`;
const response = await authorizedFetch(apiUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});

if (response.ok) {
const result = (await response.json()) as UpdateDocumentResponse;
sessionStorage.setItem('verifiedData', JSON.stringify(result));
navigate('/download-document');
alert('Data saved successfully!');
} else if (response.status === 401 || response.status === 403) {
alert('You are no longer signed in! Please sign in again.');
signOut();
} else {
const result = await response.json();
alert('Failed to save data: ' + result.error);
}
} catch (error) {
console.error('Error submitting data:', error);
alert('An error occurred while saving.');
}
}

useEffect(() => {
if (!documentId) {
console.error('No documentId found in sessionStorage');
setLoading(false);
setError(true);
return;
}
pollApiRequest();
}, []); // runs only once when the component mounts

function displayFileName(): string {
return responseData?.document_key
? responseData?.document_key.replace('input/', '')
: ' ';
}

function handleInputChange(
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
key: string,
field: FieldData
const {
getDocumentResponseData,
loading,
error,
handleVerifySubmit,
handleInputChange,
displayFileName,
} = useVerifyPage(signOut);

function displayFilePreview(
base64_encoded_file: string,
document_key?: string
) {
setResponseData((prevData) => {
if (!prevData) return null;

return {
...prevData, // keep previous data
extracted_data: {
...prevData.extracted_data, // keep other fields the same
[key]: { ...field, value: event.target.value },
},
};
});
}

function displayExtractedData() {
if (!responseData?.extracted_data) {
console.warn('No extracted data found.');
return;
}

return Object.entries(responseData.extracted_data)
.sort(([keyA], [keyB]) =>
keyA.localeCompare(keyB, undefined, { numeric: true })
)
.map(([key, field]) => {
return (
<div key={key}>
<label className="usa-label" htmlFor={`field-${key}`}>
{key}{' '}
<span className="text-accent-cool-darker display-inline-block width-full padding-top-2px">
{field.confidence
? `(Confidence ${parseFloat(field.confidence).toFixed(2)})`
: 'Confidence'}
</span>
</label>
{shouldUseTextarea(field.value) ? (
<textarea
className="usa-textarea"
id={`field-${key}`}
name={`field-${key}`}
rows={2}
value={field.value || ''}
onChange={(event) => handleInputChange(event, key, field)}
/>
) : (
<input
className="usa-input"
id={`field-${key}`}
name={`field-${key}`}
value={field.value || ''}
onChange={(event) => handleInputChange(event, key, field)}
/>
)}
</div>
);
});
}

function displayFilePreview() {
if (!responseData || !responseData.base64_encoded_file) return null;

// get file extension
const fileExtension =
responseData.document_key?.split('.').pop()?.toLowerCase() || '';
const fileExtension = document_key?.split('.').pop()?.toLowerCase() || '';

const mimeType =
fileExtension === 'pdf' ? 'application/pdf' : `image/${fileExtension}`;
// Base64 URL to display image
const base64Src = `data:${mimeType};base64,${responseData.base64_encoded_file}`;
const base64Src = `data:${mimeType};base64,${base64_encoded_file}`;

return (
<div id="file-display-container">
Expand All @@ -230,7 +49,7 @@ export default function VerifyPage({ signOut }: VerifyPageProps) {
);
}

function displayStatusMessage() {
function displayStatusMessage(loading: boolean, error: boolean) {
if (loading) {
return (
<div className="loading-overlay">
Expand Down Expand Up @@ -263,6 +82,47 @@ export default function VerifyPage({ signOut }: VerifyPageProps) {
</div>
);
}

return <></>;
}

function displayExtractedData(extracted_data: ExtractedData) {
return Object.entries(extracted_data)
.sort(([keyA], [keyB]) =>
keyA.localeCompare(keyB, undefined, { numeric: true })
)
.map(([key, field]) => {
return (
<div key={key}>
<label className="usa-label" htmlFor={`field-${key}`}>
{key}{' '}
<span className="text-accent-cool-darker display-inline-block width-full padding-top-2px">
{field.confidence
? `(Confidence ${parseFloat(field.confidence).toFixed(2)})`
: 'Confidence'}
</span>
</label>
{shouldUseTextarea(field.value) ? (
<textarea
className="usa-textarea"
id={`field-${key}`}
name={`field-${key}`}
rows={2}
value={field.value || ''}
onChange={(event) => handleInputChange(event, key, field)}
/>
) : (
<input
className="usa-input"
id={`field-${key}`}
name={`field-${key}`}
value={field.value || ''}
onChange={(event) => handleInputChange(event, key, field)}
/>
)}
</div>
);
});
}

return (
Expand Down Expand Up @@ -301,7 +161,7 @@ export default function VerifyPage({ signOut }: VerifyPageProps) {
{/* End step indicator section */}
<div className="border-top-2px border-base-lighter">
<div className="grid-container position-relative">
{displayStatusMessage()}
{displayStatusMessage(loading, error)}
<div className="grid-row">
<div className="grid-col-12 tablet:grid-col-8">
{/* Start card section */}
Expand All @@ -310,8 +170,16 @@ export default function VerifyPage({ signOut }: VerifyPageProps) {
<div className="usa-card__container file-preview-col">
<div className="usa-card__body">
<div id="file-display-container"></div>
<div>{displayFilePreview()}</div>
<p>{displayFileName()}</p>
<div>
{getDocumentResponseData?.base64_encoded_file &&
displayFilePreview(
getDocumentResponseData.base64_encoded_file,
getDocumentResponseData.document_key
)}
</div>
<p>
{displayFileName(getDocumentResponseData?.document_key)}
</p>
</div>
</div>
</li>
Expand All @@ -325,7 +193,10 @@ export default function VerifyPage({ signOut }: VerifyPageProps) {
<li className="usa-card width-full">
<div className="usa-card__container verify-col">
<div className="usa-card__body overflow-y-scroll minh-mobile-lg maxh-mobile-lg">
{displayExtractedData()}
{getDocumentResponseData?.extracted_data &&
displayExtractedData(
getDocumentResponseData?.extracted_data
)}
</div>
<div className="usa-card__footer border-top-1px border-base-lighter">
<button
Expand Down
Loading