diff --git a/cope2n-fe/package.json b/cope2n-fe/package.json index 7798b18..447642a 100644 --- a/cope2n-fe/package.json +++ b/cope2n-fe/package.json @@ -44,6 +44,7 @@ "pdfjs-dist": "^3.11.174", "process": "^0.11.10", "react": "^18.2.0", + "react-awesome-lightbox": "^1.8.1", "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-hotkeys-hook": "^4.5.0", @@ -51,6 +52,7 @@ "react-office-viewer": "^1.0.4", "react-router-dom": "^6.6.1", "styled-components": "^5.3.6", + "ts-node": "^10.9.2", "uuid": "^9.0.0" }, "devDependencies": { diff --git a/cope2n-fe/src/layouts/main-layout.tsx b/cope2n-fe/src/layouts/main-layout.tsx index c77c5e0..eee7337 100644 --- a/cope2n-fe/src/layouts/main-layout.tsx +++ b/cope2n-fe/src/layouts/main-layout.tsx @@ -119,7 +119,7 @@ export const MainLayout = ({ children }: { children: React.ReactNode }) => { style={{ height: '100%', overflow: 'auto', - padding: 32, + padding: 16, background: colorBgContainer, }} > diff --git a/cope2n-fe/src/pages/reviews/FileCard.tsx b/cope2n-fe/src/pages/reviews/FileCard.tsx index 363e5c6..1c3fd82 100644 --- a/cope2n-fe/src/pages/reviews/FileCard.tsx +++ b/cope2n-fe/src/pages/reviews/FileCard.tsx @@ -31,6 +31,7 @@ const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { > {file['Doc Type'].toUpperCase()} +
{ + const fileName = file['File Name']; + + return ( +
+
+

+ {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/consts.ts b/cope2n-fe/src/pages/reviews2/consts.ts new file mode 100644 index 0000000..6f5ddcf --- /dev/null +++ b/cope2n-fe/src/pages/reviews2/consts.ts @@ -0,0 +1,10 @@ +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', +}; diff --git a/cope2n-fe/src/pages/reviews2/index.tsx b/cope2n-fe/src/pages/reviews2/index.tsx new file mode 100644 index 0000000..6204de3 --- /dev/null +++ b/cope2n-fe/src/pages/reviews2/index.tsx @@ -0,0 +1,1394 @@ +import { + ArrowLeftOutlined, + ArrowRightOutlined, + CheckCircleOutlined, + ClockCircleFilled, + FullscreenExitOutlined, + FullscreenOutlined, +} from '@ant-design/icons'; +import { t } from '@lingui/macro'; +import { Viewer } from '@react-pdf-viewer/core'; +import type { GetRef } from 'antd'; +import { + Button, + DatePicker, + Form, + Input, + InputNumber, + message, + Modal, + notification, + Select, + Spin, + Table, + Tag, +} from 'antd'; +import React, { useContext, useEffect, useRef, 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 styled from 'styled-components'; +// 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 } from './consts'; +import FileCard from './FileCard'; + +const siderStyle: React.CSSProperties = { + backgroundColor: '#fafafa', + padding: 10, + flexBasis: 400, + flexShrink: 0, +}; + +const StyledTable = styled(Table)` + & .sbt-table-cell { + padding: 4px !important; + } +`; + +type InputRef = GetRef; +type FormInstance = GetRef>; + +const EditableContext = React.createContext | null>(null); + +interface Item { + key: string; + accuracy: number; + submitted: string; + revised: string; + action: string; +} + +interface EditableRowProps { + index: number; +} + +const EditableRow: React.FC = ({ index, ...props }) => { + const [form] = Form.useForm(); + return ( +
+ + + +
+ ); +}; + +interface EditableCellProps { + title: React.ReactNode; + editable: boolean; + children: React.ReactNode; + dataIndex: keyof Item; + record: Item; + handleSave: (record: Item) => void; +} + +const EditableCell: React.FC = ({ + title, + editable, + children, + dataIndex, + record, + handleSave, + ...restProps +}) => { + const [editing, setEditing] = useState(false); + const inputRef = useRef(null); + const form = useContext(EditableContext)!; + + useEffect(() => { + if (editing) { + inputRef.current!.focus(); + } + }, [editing]); + + const toggleEdit = () => { + setEditing(!editing); + form.setFieldsValue({ [dataIndex]: record[dataIndex] }); + }; + + const save = async () => { + try { + const values = await form.validateFields(); + + toggleEdit(); + handleSave({ ...record, ...values }); + } catch (errInfo) { + console.log('Save failed:', errInfo); + } + }; + + let childNode = children; + + if (editable) { + childNode = editing ? ( + + + + ) : ( +
+ {children} +
+ ); + } + + return {childNode}; +}; + +// type EditableTableProps = Parameters[0]; + +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(''); + + 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'); + 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'); + } + }; + + console.log(dataSource); + const loadCurrentRequest = (requestIndex) => { + setLoading(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); + setLoading(false); + 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); + }); + }); + }, []); + + const components = { + body: { + row: EditableRow, + cell: EditableCell, + }, + }; + + // "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, + }); + }); + }; + + const defaultColumns = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + width: 200, + }, + { + title: 'Predicted', + dataIndex: 'predicted', + key: 'predicted', + render: (text) => { + if (!text) return {''}; + const displayedContent = text; + if (typeof displayedContent === 'string') { + return {displayedContent}; + } else if (typeof displayedContent === 'object') { + if (displayedContent.length === 0) { + return {''}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ''; + } + } + return ( + + {displayedContent.join(', ')} + + ); + } + return {displayedContent}; + }, + }, + { + title: 'Submitted', + dataIndex: 'submitted', + key: 'submitted', + render: (text) => { + if (!text) return {''}; + const displayedContent = text; + if (typeof displayedContent === 'string') { + return {displayedContent}; + } else if (typeof displayedContent === 'object') { + if (displayedContent.length === 0) { + return {''}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ''; + } + } + return ( + + {displayedContent.join(', ')} + + ); + } + return {displayedContent}; + }, + }, + { + title: ( +
+ Revised   + + +
+ ), + dataIndex: 'revised', + key: 'revised', + editable: true, + render: (text) => { + if (!text) return {''}; + const displayedContent = text; + if (typeof displayedContent === 'string') { + return {displayedContent}; + } else if (typeof displayedContent === 'object') { + if (displayedContent.length === 0) { + return {''}; + } + // Set all empty values to "" + for (const key in displayedContent) { + if (!displayedContent[key]) { + displayedContent[key] = ''; + } + } + return ( + + {displayedContent.join(', ')} + + ); + } + return {displayedContent}; + }, + }, + ]; + + const columns = defaultColumns.map((col) => { + if (!col.editable) { + return col; + } + return { + ...col, + onCell: (record: DataType) => ({ + record, + editable: col.key != 'request_id' && col.editable, + dataIndex: col.dataIndex, + title: col.title, + handleSave, + }), + }; + }); + + // 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 && ( +
+
+

+ Files ({currentRequest?.Files?.length}) +

+ {currentRequest?.Files.map((file, index) => ( + { + setAndLoadSelectedFile(currentRequest, index); + }} + setIsReasonModalOpen={setIsReasonModalOpen} + /> + ))} +
+ {totalRequests > 0 && ( +
+ Request ID + + Redemption + + Uploaded date + + Request time + + Processing time + + Raw accuracy + +
+ )} +
+ )} +
+
+
+ {selectedFileData === 'FAILED_TO_LOAD_FILE' ? ( +

Failed to load file.

+ ) : fileExtension === 'pdf' ? ( + + ) : ( + <> + file setLightBox(true)} + /> + + {lightBox && ( + setLightBox(false)} + > + )} + + )} +
+ +
+
+

+ {totalRequests + ? 'Request: ' + 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)); + }} + /> +
+
+
+
+
+ {/* + + + */} + {/*
+

+ {totalRequests + ? 'Request: ' + 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} +

+ + + +
+ ); + })} +
+
+
+ { + setIsModalOpen(false); + reloadFilters(); + }} + onCancel={() => { + setIsModalOpen(false); + }} + > +
+ + { + setFilterDateRange(dateString); + }} + style={{ width: 200 }} + /> + + +
+ + + + + + + + {reason === 'other' && ( + { + setOtherReason(e.target.value); + }} + style={{ + width: 200, + marginTop: 30, + marginBottom: 24, + marginLeft: 10, + }} + /> + )} +
+
+
+ + + {counter_measure_map[reason]} + + { + setOtherSolution(e.target.value); + }} + style={{ + width: 200, + marginBottom: 24, + marginLeft: 10, + }} + /> + )} +
+
+ {/* {totalRequests > 0 && ( +
+ 'editable-row'} + bordered + dataSource={dataSource} + columns={columns} + /> +
+ )} */} +
+ ); +}; + +export default ReviewPage; 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: } />, }, + { + path: '/reviews2', + element: } />, + }, + { path: '/users', element: } />,