+
+
+ {file['Doc Type'].toUpperCase()}
+
+
+ {fileName ? fileName.substring(0, 25).replace('temp_', '') : fileName}
+
+
+
+
+
+
+
+ );
+};
+
+export default FileCard;
diff --git a/cope2n-fe/src/pages/reviews2/api.ts b/cope2n-fe/src/pages/reviews2/api.ts
new file mode 100644
index 0000000..a8c1885
--- /dev/null
+++ b/cope2n-fe/src/pages/reviews2/api.ts
@@ -0,0 +1,79 @@
+import { baseURL } from 'request/api';
+
+export const fetchAllRequests = async (
+ filterDateRange,
+ filterSubsidiaries,
+ filterReviewState,
+ filterIncludeTests,
+ page = 1,
+ page_size = 20,
+ max_accuracy = 100
+) => {
+ const startDate =
+ filterDateRange && filterDateRange[0] ? filterDateRange[0] : '';
+ const endDate =
+ filterDateRange && filterDateRange[1] ? filterDateRange[1] : '';
+ let filterStr = '';
+ filterStr += `page=${page}&page_size=${page_size}&`;
+ if (filterSubsidiaries) {
+ filterStr += `subsidiary=${filterSubsidiaries}&`;
+ }
+ if (filterReviewState) {
+ filterStr += `is_reviewed=${filterReviewState}&`;
+ }
+ if (filterIncludeTests) {
+ filterStr += `includes_test=${filterIncludeTests}&`;
+ }
+ if (startDate && endDate) {
+ filterStr += `start_date=${startDate}&end_date=${endDate}&`;
+ }
+ filterStr += `max_accuracy=${max_accuracy}`
+ const token = localStorage.getItem('sbt-token') || '';
+ const data = await fetch(`${baseURL}/ctel/request_list/?${filterStr}`, {
+ method: 'GET',
+ headers: {
+ Authorization: `${JSON.parse(token)}`,
+ },
+ }).then(async (res) => {
+ const data = await res.json();
+ return data;
+ });
+ return data;
+};
+
+export const updateRevisedData = async (
+ requestID: any,
+ newRevisedData: any,
+) => {
+ // const requestID = ;
+ const token = localStorage.getItem('sbt-token') || '';
+ const result = await fetch(`${baseURL}/ctel/request/${requestID}/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `${JSON.parse(token)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ reviewed_result: newRevisedData,
+ }),
+ }).catch((error) => {
+ console.log(error);
+ throw error;
+ });
+ if (result.status != 200) {
+ throw new Error('Could not update revised data');
+ }
+};
+
+export const fetchRequest = async (id) => {
+ const token = localStorage.getItem('sbt-token') || '';
+ const response = await fetch(`${baseURL}/ctel/request/${id}/`, {
+ method: 'GET',
+ headers: {
+ Authorization: `${JSON.parse(token)}`,
+ },
+ });
+ return await (
+ await response.json()
+ ).subscription_requests[0];
+};
diff --git a/cope2n-fe/src/pages/reviews2/const.ts b/cope2n-fe/src/pages/reviews2/const.ts
new file mode 100644
index 0000000..e4b1f1d
--- /dev/null
+++ b/cope2n-fe/src/pages/reviews2/const.ts
@@ -0,0 +1,49 @@
+import { t } from "@lingui/macro";
+
+export const counter_measure_map = {
+ invalid_image: 'Remove this image from the evaluation report',
+ missing_information: 'Remove this image from the evaluation report',
+ too_blurry_text: 'Remove this image from the evaluation report',
+ too_small_text: 'Remove this image from the evaluation report',
+ ocr_cannot_extract: 'Improve OCR',
+ wrong_feedback: 'Update revised result and re-calculate accuracy',
+ handwritten: 'Remove this image from the evaluation report',
+ other: 'other',
+};
+
+export const REASON_BAD_QUALITY = [
+ { value: 'invalid_image', label: t`Invalid image` },
+ {
+ value: 'missing_information',
+ label: t`Missing information`,
+ },
+ { value: 'too_blurry_text', label: t`Too blurry text` },
+ { value: 'too_small_text', label: t`Too small text` },
+ { value: 'handwritten', label: t`Handwritten` },
+ { value: 'wrong_feedback', label: t`Wrong Feedback` },
+ { value: 'ocr_cannot_extract', label: t`Ocr cannot extract` },
+ { value: 'other', label: t`Other` },
+]
+
+export const SOLUTION_BAD_QUALITY =[
+ {
+ value: 'Remove this image from the evaluation report',
+ label: t`Remove this image from the evaluation report`,
+ },
+ { value: 'Improve OCR', label: t`Improve OCR` },
+ {
+ value: 'Update revised result and re-calculate accuracy',
+ label: t`Update revised result and re-calculate accuracy`,
+ },
+ { value: 'other', label: t`Other` },
+]
+
+export const SUBSIDIARIES = [
+ { value: 'SEAO', label: 'SEAO' },
+ { value: 'SEAU', label: 'SEAU' },
+ { value: 'SESP', label: 'SESP' },
+ { value: 'SME', label: 'SME' },
+ { value: 'SEPCO', label: 'SEPCO' },
+ { value: 'TSE', label: 'TSE' },
+ { value: 'SEIN', label: 'SEIN' },
+]
\ No newline at end of file
diff --git a/cope2n-fe/src/pages/reviews2/index.tsx b/cope2n-fe/src/pages/reviews2/index.tsx
new file mode 100644
index 0000000..46a11bf
--- /dev/null
+++ b/cope2n-fe/src/pages/reviews2/index.tsx
@@ -0,0 +1,1009 @@
+import {
+ ArrowLeftOutlined,
+ ArrowRightOutlined,
+ CheckCircleOutlined,
+ ClockCircleFilled,
+ CopyOutlined,
+ FullscreenExitOutlined,
+ FullscreenOutlined,
+} from '@ant-design/icons';
+import { t } from '@lingui/macro';
+import { Viewer } from '@react-pdf-viewer/core';
+import {
+ Button,
+ DatePicker,
+ Form,
+ Input,
+ InputNumber,
+ message,
+ Modal,
+ notification,
+ Select,
+ Spin,
+ Tag,
+} from 'antd';
+import { useEffect, useState } from 'react';
+import Lightbox from 'react-awesome-lightbox';
+import 'react-awesome-lightbox/build/style.css';
+import { useHotkeys } from 'react-hotkeys-hook';
+import { baseURL } from 'request/api';
+// Import the styles
+import '@react-pdf-viewer/core/lib/styles/index.css';
+
+import { badQualityReasonSubmit } from 'request';
+import { normalizeData } from 'utils/field-value-process';
+import { fetchAllRequests, fetchRequest } from './api';
+import {
+ counter_measure_map,
+ REASON_BAD_QUALITY,
+ SOLUTION_BAD_QUALITY,
+ SUBSIDIARIES,
+} from './const';
+import FileCard from './FileCard';
+
+const ReviewPage = () => {
+ const [loading, setLoading] = useState(false);
+ const [fullscreen, setFullscreen] = useState(false);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [isReasonModalOpen, setIsReasonModalOpen] = useState(false);
+ const [selectedFileId, setSelectedFileId] = useState(0);
+ const [selectedFileData, setSelectedFileData] = useState(null);
+ const [selectedFileName, setSelectedFileName] = useState(null);
+
+ // Default date range: 1 month ago to today
+ const [filterDateRange, setFilterDateRange] = useState(['', '']);
+
+ const [filterSubsidiaries, setFilterSubsidiaries] = useState('SEAO');
+ const [filterAccuracy, setFilterAccuracy] = useState(100);
+ const [filterReviewState, setFilterReviewState] = useState('all');
+ const [filterIncludeTests, setFilterIncludesTests] = useState('true');
+ // const [requests, setRequests] = useState([]);
+ const [currentRequest, setCurrentRequest] = useState(null);
+ const [currentRequestIndex, setCurrentRequestIndex] = useState(1);
+ const [hasNextRequest, setHasNextRequest] = useState(true);
+ const [totalRequests, setTotalPages] = useState(0);
+ const [dataSource, setDataSource] = useState([]);
+
+ const [pageIndexToGoto, setPageIndexToGoto] = useState(1);
+
+ const [reason, setReason] = useState('');
+ const [otherReason, setOtherReason] = useState('');
+ const [solution, setSolution] = useState('');
+ const [otherSolution, setOtherSolution] = useState('');
+ const [imageLoading, setImageLoading] = useState(false);
+
+ useEffect(() => {
+ if (reason) {
+ setSolution(counter_measure_map[reason]);
+ }
+ }, [reason]);
+
+ const setAndLoadSelectedFile = async (requestData, index) => {
+ setSelectedFileId(index);
+ if (!requestData['Files'][index]) {
+ setSelectedFileData('FAILED_TO_LOAD_FILE');
+ setImageLoading(false);
+ return;
+ }
+ const fileName = requestData['Files'][index]['File Name'];
+ const fileURL = requestData['Files'][index]['File URL'];
+ const response = await fetch(fileURL);
+ if (response.status === 200) {
+ setSelectedFileName(fileName);
+ setSelectedFileData(fileURL);
+ console.log('Loading file: ' + fileName);
+ console.log('URL: ' + fileURL);
+ } else {
+ setSelectedFileData('FAILED_TO_LOAD_FILE');
+ setImageLoading(false);
+ }
+ };
+
+ console.log(dataSource);
+ const loadCurrentRequest = (requestIndex) => {
+ setLoading(true);
+ setImageLoading(true);
+ fetchAllRequests(
+ filterDateRange,
+ filterSubsidiaries,
+ filterReviewState,
+ filterIncludeTests,
+ requestIndex,
+ 1,
+ filterAccuracy,
+ )
+ .then((data) => {
+ // setRequests(data?.subscription_requests);
+ // setHasNextRequest(data?.subscription_requests.length > 1);
+ setTotalPages(data?.page?.total_requests);
+ setHasNextRequest(requestIndex < data?.page?.total_requests);
+ const requestData = fetchRequest(
+ data?.subscription_requests[0].RequestID,
+ );
+ requestData
+ .then(async (data) => {
+ console.log('🚀 ~ .then ~ data:', data);
+ if (data) setCurrentRequest(data);
+ const predicted =
+ data && data['Predicted Result'] ? data['Predicted Result'] : {};
+ const submitted =
+ data && data['Feedback Result'] ? data['Feedback Result'] : {};
+ const revised =
+ data && data['Reviewed Result'] ? data['Reviewed Result'] : {};
+ const keys = Object.keys(predicted);
+ const tableRows = [];
+ for (let i = 0; i < keys.length; i++) {
+ let instance = {};
+ instance['key'] = keys[i];
+ instance['predicted'] = predicted[keys[i]];
+ instance['submitted'] = submitted[keys[i]];
+ instance['revised'] = revised[keys[i]];
+ tableRows.push(instance);
+ }
+ setDataSource(tableRows);
+ setAndLoadSelectedFile(data, 0);
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ })
+ .finally(() => {
+ setLoading(false);
+ });
+ };
+
+ const gotoNextRequest = () => {
+ if (currentRequestIndex >= totalRequests) {
+ return;
+ }
+ const nextRequestIndex = currentRequestIndex + 1;
+ setCurrentRequestIndex(nextRequestIndex);
+ loadCurrentRequest(nextRequestIndex);
+ };
+
+ const gotoPreviousRequest = () => {
+ if (currentRequestIndex === 1) {
+ return;
+ }
+ const previousRequestIndex = currentRequestIndex - 1;
+ setCurrentRequestIndex(previousRequestIndex);
+ loadCurrentRequest(previousRequestIndex);
+ };
+
+ const reloadFilters = () => {
+ setCurrentRequestIndex(1);
+ fetchAllRequests(
+ filterDateRange,
+ filterSubsidiaries,
+ filterReviewState,
+ filterIncludeTests,
+ 1,
+ 1,
+ filterAccuracy,
+ ).then((data) => {
+ setTotalPages(data?.page?.total_requests);
+ // setRequests(data?.subscription_requests);
+ // setHasNextRequest(data?.subscription_requests.length > 1);
+ setHasNextRequest(1 < data?.page?.total_requests);
+ const firstRequest = fetchRequest(
+ data?.subscription_requests[0].RequestID,
+ );
+ firstRequest.then(async (data) => {
+ if (data) setCurrentRequest(data);
+ setAndLoadSelectedFile(data, 0);
+ setTimeout(() => {
+ loadCurrentRequest(1);
+ }, 500);
+ });
+ });
+ };
+
+ useEffect(() => {
+ setCurrentRequestIndex(1);
+ fetchAllRequests(
+ filterDateRange,
+ filterSubsidiaries,
+ filterReviewState,
+ filterIncludeTests,
+ 1,
+ 1,
+ filterAccuracy,
+ ).then((data) => {
+ setTotalPages(data?.page?.total_requests);
+ // setRequests(data?.subscription_requests);
+ setHasNextRequest(1 < data?.page?.total_requests);
+ const firstRequest = fetchRequest(
+ data?.subscription_requests[0].RequestID,
+ );
+ firstRequest.then(async (data) => {
+ if (data) setCurrentRequest(data);
+ setAndLoadSelectedFile(data, 0);
+ });
+ });
+ }, []);
+
+ // "Key", "Accuracy", "Submitted", "Revised"
+ interface DataType {
+ key: string;
+ accuracy: number;
+ submitted: string;
+ revised: string;
+ }
+
+ const updateRevisedData = async (newRevisedData: any) => {
+ const requestID = currentRequest.RequestID;
+ const token = localStorage.getItem('sbt-token') || '';
+ const result = await fetch(`${baseURL}/ctel/request/${requestID}/`, {
+ method: 'POST',
+ headers: {
+ Authorization: `${JSON.parse(token)}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ reviewed_result: newRevisedData,
+ }),
+ }).catch((error) => {
+ console.log(error);
+ throw error;
+ });
+ if (result.status != 200) {
+ throw new Error('Could not update revised data');
+ }
+ };
+
+ const handleSave = (row: DataType) => {
+ const newData = [...dataSource];
+ const index = newData.findIndex((item) => row.key === item.key);
+ const item = newData[index];
+ newData.splice(index, 1, {
+ ...item,
+ ...row,
+ });
+ const newRevisedData = {};
+ for (let i = 0; i < newData.length; i++) {
+ newData[i].revised = normalizeData(newData[i].key, newData[i].revised);
+ newRevisedData[newData[i].key] = newData[i].revised;
+ }
+ updateRevisedData(newRevisedData)
+ .then(() => {
+ // "[Is Reviewed]" => true
+ setCurrentRequest({
+ ...currentRequest,
+ ['Is Reviewed']: true,
+ });
+ })
+ .then(() => {
+ setDataSource(newData);
+ })
+ .catch((error) => {
+ message.error(
+ 'Could not update revised data. Please check the format.',
+ );
+ });
+ };
+
+ const submitRevisedData = async () => {
+ const newData = [...dataSource];
+ const newRevisedData = {};
+ for (let i = 0; i < newData.length; i++) {
+ newData[i].revised = normalizeData(newData[i].key, newData[i].revised);
+ newRevisedData[newData[i].key] = newData[i].revised;
+ }
+ updateRevisedData(newRevisedData).then(() => {
+ // "[Is Reviewed]" => true
+ setCurrentRequest({
+ ...currentRequest,
+ ['Is Reviewed']: true,
+ });
+ });
+ };
+
+ // use left/right keys to navigate
+ useHotkeys('left', gotoPreviousRequest);
+ useHotkeys('right', gotoNextRequest);
+
+ const fileExtension = selectedFileName
+ ? selectedFileName.split('.').pop()
+ : '';
+
+ const [lightBox, setLightBox] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+ {totalRequests ? (
+ <>
+ Request ID: {currentRequest?.RequestID}
+ >
+ ) : (
+ ''
+ )}
+
+
+
+
+
+
+
+
+ {totalRequests > 0 && (
+
+ )}
+
+
+
+
+ {selectedFileData === 'FAILED_TO_LOAD_FILE' ? (
+ Failed to load file.
+ ) : fileExtension === 'pdf' ? (
+
+ ) : (
+ <>
+ setLightBox(true)}
+ onLoad={() => {
+ setImageLoading(false);
+ }}
+ />
+
+ {lightBox && (
+ setLightBox(false)}
+ >
+ )}
+ >
+ )}
+
+
+
+
+
+
+ {totalRequests
+ ? 'No: ' + currentRequestIndex + '/' + totalRequests
+ : 'No Request. Adjust your search criteria to see more results.'}
+
+ {currentRequest &&
+ (currentRequest['Is Reviewed'] ? (
+ }
+ color='success'
+ style={{ padding: '4px 16px' }}
+ >
+ Reviewed
+
+ ) : (
+ }
+ color='warning'
+ style={{ padding: '4px 16px' }}
+ >
+ Not Reviewed
+
+ ))}
+
+
+
+
+
+
{
+ if (pageIndexToGoto > totalRequests) {
+ message.error('RequestID is out of range.');
+ return;
+ }
+ if (pageIndexToGoto < 1) {
+ message.error('RequestID is out of range.');
+ return;
+ }
+ setCurrentRequestIndex(pageIndexToGoto);
+ loadCurrentRequest(pageIndexToGoto);
+ }}
+ >
+ Go to
+
+ }
+ value={pageIndexToGoto}
+ onChange={(e) => {
+ setPageIndexToGoto(parseInt(e.target.value));
+ }}
+ />
+
+
+
+
+
+
+ {dataSource?.map((data) => {
+ return (
+
+
+
{data.key}
+
}
+ size='small'
+ />
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
{
+ setIsModalOpen(false);
+ reloadFilters();
+ }}
+ onCancel={() => {
+ setIsModalOpen(false);
+ }}
+ >
+
+ {
+ setFilterDateRange(dateString);
+ }}
+ style={{ width: 200 }}
+ />
+
+
+
+
+
+
+
+ setFilterAccuracy(value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ // call submit api
+ if (!reason || !solution) {
+ notification.warning({
+ message: 'Please select a reason or a solution',
+ });
+ } else {
+ const params = {
+ request_id: currentRequest?.RequestID,
+ request_image_id: selectedFileName.replace(/\.[^/.]+$/, ''),
+ };
+
+ let submitReason = reason;
+ let submitSolution = solution;
+
+ if (reason === 'other') {
+ if (!otherReason) {
+ notification.warning({
+ message: 'Please input other reason',
+ });
+ return;
+ }
+ submitReason = otherReason;
+ }
+ if (solution === 'other') {
+ if (!otherSolution) {
+ notification.warning({
+ message: 'Please input other solution',
+ });
+ return;
+ }
+ submitSolution = otherSolution;
+ }
+
+ const res = await badQualityReasonSubmit(
+ params,
+ submitReason,
+ submitSolution,
+ );
+
+ if (res.message) {
+ notification.success({ message: 'Update reason success' });
+ setIsReasonModalOpen(false);
+ }
+ }
+ }}
+ onCancel={() => {
+ setIsReasonModalOpen(false);
+ }}
+ >
+
+
+
+
+
+ {reason === 'other' && (
+ {
+ setOtherReason(e.target.value);
+ }}
+ style={{
+ width: 200,
+ marginTop: 30,
+ marginBottom: 24,
+ marginLeft: 10,
+ }}
+ />
+ )}
+
+
+
+
+ {counter_measure_map[reason]}
+
+
+
+
+ {solution === 'other' && (
+ {
+ setOtherSolution(e.target.value);
+ }}
+ style={{
+ width: 200,
+ marginBottom: 24,
+ marginLeft: 10,
+ }}
+ />
+ )}
+
+
+
+ );
+};
+
+export default ReviewPage;
diff --git a/cope2n-fe/src/request/api.ts b/cope2n-fe/src/request/api.ts
index 2eecbfb..36cf3c5 100644
--- a/cope2n-fe/src/request/api.ts
+++ b/cope2n-fe/src/request/api.ts
@@ -11,7 +11,7 @@ const environment = process.env.NODE_ENV;
const AXIOS_TIMEOUT_MS = 30 * 60 * 1000; // This config sastified long-live upload file request
const EXPIRED_PASSWORD_SIGNAL = 'expired_password';
-export const baseURL = environment === 'development' ? 'http://107.120.133.27:9881/api' : '/api';
+export const baseURL = environment === 'development' ? 'http://107.120.133.27:9000/api' : '/api';
// export const baseURL = '/api';
diff --git a/cope2n-fe/src/routes/useAppRouter.tsx b/cope2n-fe/src/routes/useAppRouter.tsx
index 3e1419b..899824f 100644
--- a/cope2n-fe/src/routes/useAppRouter.tsx
+++ b/cope2n-fe/src/routes/useAppRouter.tsx
@@ -12,6 +12,7 @@ const DashboardPage = React.lazy(() => import('pages/dashboard'));
const InferencePage = React.lazy(() => import('pages/inference/index'));
const ReviewsPage = React.lazy(() => import('pages/reviews'));
+const ReviewsPage2 = React.lazy(() => import('pages/reviews2'));
const ReportsPage = React.lazy(() => import('pages/reports'));
const ReportDetailPage = React.lazy(
() => import('pages/reports/report_detail'),
@@ -65,6 +66,11 @@ export function useAppRouter() {
path: '/reviews',
element: