This commit is contained in:
phanphan 2024-05-03 09:52:15 +07:00
parent b4efb40978
commit 04c1551204
3 changed files with 90 additions and 453 deletions

View File

@ -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' },
]

View File

@ -1,10 +0,0 @@
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',
};

View File

@ -3,12 +3,12 @@ import {
ArrowRightOutlined, ArrowRightOutlined,
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleFilled, ClockCircleFilled,
CopyOutlined,
FullscreenExitOutlined, FullscreenExitOutlined,
FullscreenOutlined, FullscreenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Viewer } from '@react-pdf-viewer/core'; import { Viewer } from '@react-pdf-viewer/core';
import type { GetRef } from 'antd';
import { import {
Button, Button,
DatePicker, DatePicker,
@ -20,22 +20,25 @@ import {
notification, notification,
Select, Select,
Spin, Spin,
Table,
Tag, Tag,
} from 'antd'; } from 'antd';
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Lightbox from 'react-awesome-lightbox'; import Lightbox from 'react-awesome-lightbox';
import 'react-awesome-lightbox/build/style.css'; import 'react-awesome-lightbox/build/style.css';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { baseURL } from 'request/api'; import { baseURL } from 'request/api';
import styled from 'styled-components';
// Import the styles // Import the styles
import '@react-pdf-viewer/core/lib/styles/index.css'; import '@react-pdf-viewer/core/lib/styles/index.css';
import { badQualityReasonSubmit } from 'request'; import { badQualityReasonSubmit } from 'request';
import { normalizeData } from 'utils/field-value-process'; import { normalizeData } from 'utils/field-value-process';
import { fetchAllRequests, fetchRequest } from './api'; import { fetchAllRequests, fetchRequest } from './api';
import { counter_measure_map } from './consts'; import {
counter_measure_map,
REASON_BAD_QUALITY,
SOLUTION_BAD_QUALITY,
SUBSIDIARIES,
} from './const';
import FileCard from './FileCard'; import FileCard from './FileCard';
const siderStyle: React.CSSProperties = { const siderStyle: React.CSSProperties = {
@ -44,106 +47,6 @@ const siderStyle: React.CSSProperties = {
flexBasis: 400, flexBasis: 400,
flexShrink: 0, flexShrink: 0,
}; };
const StyledTable = styled(Table)`
& .sbt-table-cell {
padding: 4px !important;
}
`;
type InputRef = GetRef<typeof Input>;
type FormInstance<T> = GetRef<typeof Form<T>>;
const EditableContext = React.createContext<FormInstance<any> | null>(null);
interface Item {
key: string;
accuracy: number;
submitted: string;
revised: string;
action: string;
}
interface EditableRowProps {
index: number;
}
const EditableRow: React.FC<EditableRowProps> = ({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
};
interface EditableCellProps {
title: React.ReactNode;
editable: boolean;
children: React.ReactNode;
dataIndex: keyof Item;
record: Item;
handleSave: (record: Item) => void;
}
const EditableCell: React.FC<EditableCellProps> = ({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef<InputRef>(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 ? (
<Form.Item style={{ margin: 0 }} name={dataIndex}>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className='editable-cell-value-wrap'
style={{ paddingRight: 24 }}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
};
// type EditableTableProps = Parameters<typeof Table>[0]; // type EditableTableProps = Parameters<typeof Table>[0];
const ReviewPage = () => { const ReviewPage = () => {
@ -324,13 +227,6 @@ const ReviewPage = () => {
}); });
}, []); }, []);
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
// "Key", "Accuracy", "Submitted", "Revised" // "Key", "Accuracy", "Submitted", "Revised"
interface DataType { interface DataType {
key: string; key: string;
@ -407,159 +303,6 @@ const ReviewPage = () => {
}); });
}; };
const defaultColumns = [
{
title: 'Key',
dataIndex: 'key',
key: 'key',
width: 200,
},
{
title: 'Predicted',
dataIndex: 'predicted',
key: 'predicted',
render: (text) => {
if (!text) return <span style={{ color: '#888' }}>{'<empty>'}</span>;
const displayedContent = text;
if (typeof displayedContent === 'string') {
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
} else if (typeof displayedContent === 'object') {
if (displayedContent.length === 0) {
return <span style={{ color: '#888' }}>{'<empty>'}</span>;
}
// Set all empty values to "<empty>"
for (const key in displayedContent) {
if (!displayedContent[key]) {
displayedContent[key] = '<empty>';
}
}
return (
<span style={{ color: '#000000' }}>
{displayedContent.join(', ')}
</span>
);
}
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
},
},
{
title: 'Submitted',
dataIndex: 'submitted',
key: 'submitted',
render: (text) => {
if (!text) return <span style={{ color: '#888' }}>{'<empty>'}</span>;
const displayedContent = text;
if (typeof displayedContent === 'string') {
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
} else if (typeof displayedContent === 'object') {
if (displayedContent.length === 0) {
return <span style={{ color: '#888' }}>{'<empty>'}</span>;
}
// Set all empty values to "<empty>"
for (const key in displayedContent) {
if (!displayedContent[key]) {
displayedContent[key] = '<empty>';
}
}
return (
<span style={{ color: '#000000' }}>
{displayedContent.join(', ')}
</span>
);
}
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
},
},
{
title: (
<div
style={{
width: 120,
display: 'flex',
lineHeight: '32px',
marginLeft: 10,
}}
>
Revised&nbsp;&nbsp;
<Button
onClick={() => {
if (!dataSource || !dataSource.length) return;
setDataSource(
dataSource.map((item) => {
item.revised = item.predicted;
return item;
}),
);
setTimeout(() => {
submitRevisedData();
}, 1000);
}}
>
Copy Predicted
</Button>
<Button
onClick={() => {
if (!dataSource || !dataSource.length) return;
setDataSource(
dataSource.map((item) => {
item.revised = item.submitted;
return item;
}),
);
setTimeout(() => {
submitRevisedData();
}, 1000);
}}
>
Copy Submitted
</Button>
</div>
),
dataIndex: 'revised',
key: 'revised',
editable: true,
render: (text) => {
if (!text) return <span style={{ color: '#888' }}>{'<empty>'}</span>;
const displayedContent = text;
if (typeof displayedContent === 'string') {
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
} else if (typeof displayedContent === 'object') {
if (displayedContent.length === 0) {
return <span style={{ color: '#888' }}>{'<empty>'}</span>;
}
// Set all empty values to "<empty>"
for (const key in displayedContent) {
if (!displayedContent[key]) {
displayedContent[key] = '<empty>';
}
}
return (
<span style={{ color: '#000000' }}>
{displayedContent.join(', ')}
</span>
);
}
return <span style={{ color: '#000000' }}>{displayedContent}</span>;
},
},
];
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 // use left/right keys to navigate
useHotkeys('left', gotoPreviousRequest); useHotkeys('left', gotoPreviousRequest);
useHotkeys('right', gotoNextRequest); useHotkeys('right', gotoNextRequest);
@ -642,6 +385,7 @@ const ReviewPage = () => {
</div> </div>
<div style={{ display: 'flex', flexBasis: '400px' }}> <div style={{ display: 'flex', flexBasis: '400px' }}>
<Input <Input
size='small'
value={`Sub: ${filterSubsidiaries}, Date:${ value={`Sub: ${filterSubsidiaries}, Date:${
filterDateRange[0] filterDateRange[0]
? filterDateRange[0] + ' to ' + filterDateRange[1] ? filterDateRange[0] + ' to ' + filterDateRange[1]
@ -651,7 +395,7 @@ const ReviewPage = () => {
/> />
<Button <Button
type='primary' type='primary'
size='large' size='middle'
onClick={() => { onClick={() => {
setIsModalOpen(true); setIsModalOpen(true);
}} }}
@ -664,10 +408,9 @@ const ReviewPage = () => {
style={{ style={{
overflow: 'auto', overflow: 'auto',
width: '100%', width: '100%',
height: fullscreen ? 'calc(100% - 32px)' : 'calc(100% - 16px)', height: fullscreen ? 'calc(100% - 32px)' : 'calc(100% - 32px)',
maxWidth: '100%', maxWidth: '100%',
display: 'flex', display: 'flex',
padding: '8px',
background: '#efefef', background: '#efefef',
}} }}
> >
@ -691,7 +434,7 @@ const ReviewPage = () => {
flexDirection: 'column', flexDirection: 'column',
alignItems: 'start', alignItems: 'start',
flexGrow: 0, flexGrow: 0,
padding: '0 4px 0 0', padding: '0 4px',
}} }}
> >
<div <div
@ -793,7 +536,7 @@ const ReviewPage = () => {
</div> </div>
<div <div
style={{ style={{
border: '1px solid #ccc', borderLeft: '1px solid #ccc',
height: '100%', height: '100%',
// width: '300px', // width: '300px',
flexGrow: 1, flexGrow: 1,
@ -807,6 +550,8 @@ const ReviewPage = () => {
style={{ style={{
flexGrow: 1, flexGrow: 1,
textAlign: 'center', textAlign: 'center',
overflow: 'auto',
padding: '4px',
}} }}
> >
{selectedFileData === 'FAILED_TO_LOAD_FILE' ? ( {selectedFileData === 'FAILED_TO_LOAD_FILE' ? (
@ -838,7 +583,14 @@ const ReviewPage = () => {
)} )}
</div> </div>
<div style={{ flexGrow: 0 }}> <div
style={{
flexGrow: 0,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<div <div
style={{ style={{
display: 'flex', display: 'flex',
@ -847,11 +599,11 @@ const ReviewPage = () => {
margin: '8px 0 4px', margin: '8px 0 4px',
}} }}
> >
<h2 style={{ margin: '0 8px ' }}> <h3 style={{ margin: '0 8px ' }}>
{totalRequests {totalRequests
? 'Request: ' + currentRequestIndex + '/' + totalRequests ? 'No: ' + currentRequestIndex + '/' + totalRequests
: 'No Request. Adjust your search criteria to see more results.'} : 'No Request. Adjust your search criteria to see more results.'}
</h2> </h3>
{currentRequest && {currentRequest &&
(currentRequest['Is Reviewed'] ? ( (currentRequest['Is Reviewed'] ? (
<Tag <Tag
@ -887,7 +639,7 @@ const ReviewPage = () => {
}} }}
> >
<ArrowLeftOutlined /> <ArrowLeftOutlined />
Previous Prev
</Button> </Button>
<Button <Button
type='default' type='default'
@ -937,128 +689,21 @@ const ReviewPage = () => {
</div> </div>
</div> </div>
<div style={siderStyle}> <div style={siderStyle}>
{/* <Space.Compact style={{ width: '100%', marginBottom: 16 }}>
<Input
value={`Sub: ${filterSubsidiaries}, Date:${
filterDateRange[0]
? filterDateRange[0] + ' to ' + filterDateRange[1]
: 'All'
}, Reviewed: ${filterReviewState}, Tests: ${filterIncludeTests}`}
readOnly
/>
<Button
type='primary'
size='large'
onClick={() => {
setIsModalOpen(true);
}}
>
Filters
</Button>
</Space.Compact> */}
{/* <div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<h2 style={{ margin: '0 0 4px' }}>
{totalRequests
? 'Request: ' + currentRequestIndex + '/' + totalRequests
: 'No Request. Adjust your search criteria to see more results.'}
</h2>
{currentRequest &&
(currentRequest['Is Reviewed'] ? (
<Tag
icon={<CheckCircleOutlined />}
color='success'
style={{ padding: '4px 16px' }}
>
Reviewed
</Tag>
) : (
<Tag
icon={<ClockCircleFilled />}
color='warning'
style={{ padding: '4px 16px' }}
>
Not Reviewed
</Tag>
))}
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 8,
}}
>
<div>
<Button
type='default'
style={{ height: 38 }}
disabled={currentRequestIndex === 1}
onClick={() => {
gotoPreviousRequest();
}}
>
<ArrowLeftOutlined />
Previous
</Button>
<Button
type='default'
style={{ height: 38 }}
disabled={!hasNextRequest}
onClick={() => {
if (!hasNextRequest) {
return;
}
gotoNextRequest();
}}
>
Next
<ArrowRightOutlined />
</Button>
<Input
size='middle'
addonBefore='To'
style={{ marginBottom: '4px', marginLeft: '4px', width: 180 }}
defaultValue={currentRequestIndex}
addonAfter={
<Button
type='default'
onClick={() => {
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
</Button>
}
value={pageIndexToGoto}
onChange={(e) => {
setPageIndexToGoto(parseInt(e.target.value));
}}
/>
</div>
</div> */}
<div> <div>
{dataSource?.map((data) => { {dataSource?.map((data) => {
return ( return (
<div style={{ margin: '0 0 8px' }}> <div style={{ margin: '0 0 8px' }}>
<p style={{ fontWeight: 'bold', margin: '0 0 8px' }}> <div style={{ display: 'flex', justifyContent: "space-between", alignItems: "center", margin: '0 0 4px' }}>
{' '} <p style={{ fontWeight: 'bold', margin: 0 }}>
{data.key} {data.key}
</p> </p>
<Button
shape='round'
type="primary" ghost
icon={<CopyOutlined />}
size='small'
/>
</div>
<Input <Input
addonBefore='Feedback' addonBefore='Feedback'
size='small' size='small'
@ -1142,15 +787,7 @@ const ReviewPage = () => {
<Select <Select
placeholder='Select a subsidiary' placeholder='Select a subsidiary'
style={{ width: 200 }} style={{ width: 200 }}
options={[ options={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' },
]}
value={filterSubsidiaries} value={filterSubsidiaries}
defaultValue={filterSubsidiaries} defaultValue={filterSubsidiaries}
onChange={setFilterSubsidiaries} onChange={setFilterSubsidiaries}
@ -1294,19 +931,7 @@ const ReviewPage = () => {
<Select <Select
placeholder='Select a reason' placeholder='Select a reason'
style={{ width: 200 }} style={{ width: 200 }}
options={[ options={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` },
]}
onChange={setReason} onChange={setReason}
value={reason} value={reason}
/> />
@ -1337,18 +962,7 @@ const ReviewPage = () => {
<Select <Select
placeholder='Select a solution' placeholder='Select a solution'
style={{ width: 200 }} style={{ width: 200 }}
options={[ options={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` },
]}
onChange={setSolution} onChange={setSolution}
value={solution} value={solution}
// defaultValue={solution} // defaultValue={solution}
@ -1371,22 +985,6 @@ const ReviewPage = () => {
)} )}
</div> </div>
</Modal> </Modal>
{/* {totalRequests > 0 && (
<div
style={{
height: '25%',
overflowY: 'auto',
}}
>
<StyledTable
components={components}
rowClassName={() => 'editable-row'}
bordered
dataSource={dataSource}
columns={columns}
/>
</div>
)} */}
</div> </div>
); );
}; };