Compare commits

..

12 Commits

Author SHA1 Message Date
1a436dba8a Merge pull request #168 from SDSRV-IDP/feature/add_filter_review2
Feature/add filter review2
2024-12-06 10:01:41 +07:00
yagin
aaf028054c [yagin] change column reivew filters 2024-12-05 17:11:05 +07:00
ab1ec7a59d Merge pull request #167 from SDSRV-IDP/dev/search_content
Fix: misalign text in exclude reason
2024-12-05 16:48:10 +07:00
a106c96f2a Merge pull request #166 from SDSRV-IDP/yagin-hot-fix
Update DocumentCompareInfo.tsx
2024-12-05 16:47:45 +07:00
15a9f31eed Fix: misalign text in exclude reason 2024-12-05 16:46:40 +07:00
Nguyễn Duy Hiển
7bfb1c46b3 Update DocumentCompareInfo.tsx 2024-12-05 16:42:30 +07:00
yagin
53654197af [yagin] fix padding input 2024-12-05 16:36:55 +07:00
Nguyễn Duy Hiển
7767f37c52 Merge pull request #165 from SDSRV-IDP/dev/search_content
Indexing reason for the better perf of searching
2024-12-05 15:33:37 +07:00
d2e0c96ce2 Indexing reason for the better perf of searching 2024-12-05 15:04:13 +07:00
fe56fb91a4 Merge pull request #164 from SDSRV-IDP/feature/add_filter_review2
Feature/add filter review2
2024-12-05 15:01:44 +07:00
yagin
7841a775b7 [yagin] change style form review2 2024-12-05 14:44:49 +07:00
yagin
e51994958e [yagin] add bad reason filter and fix response 2024-12-05 10:30:37 +07:00
8 changed files with 284 additions and 118 deletions

View File

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from fwd_api.models import SubscriptionRequestFile
from fwd_api.models.SemiAutoCorrection import SemiAutoCorrection
from fwd_api.exception.exceptions import InvalidException
# Mapping dictionary for reasons
REASON_MAP = {
'Invalid image': 'invalid_image',
'Missing information': 'missing_information',
'Too blurry text': 'too_blurry_text',
'Too small text': 'too_small_text',
'Handwritten': 'handwritten',
'Recheck': 'recheck',
}
class Command(BaseCommand):
help = 'Replace the reason field in SubscriptionRequestFile and SemiAutoCorrection based on the provided mapping dictionary'
def handle(self, *args, **options):
# Process SubscriptionRequestFile instances
self.update_reasons(SubscriptionRequestFile, "SubscriptionRequestFile")
# Process SemiAutoCorrection instances
self.update_reasons(SemiAutoCorrection, "SemiAutoCorrection")
self.stdout.write(self.style.SUCCESS('All applicable reasons updated successfully!'))
def update_reasons(self, model, model_name):
instances = model.objects.exclude(reason__isnull=True).exclude(reason='').iterator()
for instance in tqdm(instances, desc=f"Updating reasons in {model_name}"):
try:
original_reason = instance.reason
new_reason = REASON_MAP.get(original_reason)
if new_reason is not None:
instance.reason = new_reason
instance.save()
self.stdout.write(self.style.SUCCESS(f"Updated reason for {model_name} ID {instance.id}: {original_reason} -> {new_reason}"))
except Exception as e:
self.stdout.write(self.style.ERROR(f"Updated reason failed for {model_name} ID {instance.id} due to {e}"))

View File

@ -23,7 +23,7 @@ class SubscriptionRequestFile(models.Model):
doc_type = models.CharField(max_length=10, default="") doc_type = models.CharField(max_length=10, default="")
index_in_request = models.IntegerField(default=0) # by doc_type index_in_request = models.IntegerField(default=0) # by doc_type
processing_time = models.FloatField(default=-1) # in milisecond processing_time = models.FloatField(default=-1) # in milisecond
reason = models.TextField(blank=True) reason = models.TextField(blank=True, db_index=True)
counter_measures = models.TextField(blank=True) counter_measures = models.TextField(blank=True)
is_reviewed = models.BooleanField(default=False) is_reviewed = models.BooleanField(default=False)
is_required = models.BooleanField(default=True) is_required = models.BooleanField(default=True)

View File

@ -40,6 +40,7 @@
"Remove this image from the evaluation report": "Remove this image from the evaluation report", "Remove this image from the evaluation report": "Remove this image from the evaluation report",
"Report Details": "Report Details", "Report Details": "Report Details",
"Report Filters": "Report Filters", "Report Filters": "Report Filters",
"Review Filters": "Review Filters",
"Report Type": "Report Type", "Report Type": "Report Type",
"Reports": "Reports", "Reports": "Reports",
"Retry": "Retry", "Retry": "Retry",

View File

@ -40,6 +40,7 @@
"Remove this image from the evaluation report": "", "Remove this image from the evaluation report": "",
"Report Details": "", "Report Details": "",
"Report Filters": "", "Report Filters": "",
"Review Filters": "",
"Report Type": "", "Report Type": "",
"Reports": "", "Reports": "",
"Retry": "Thử lại", "Retry": "Thử lại",

View File

@ -0,0 +1,69 @@
import React from 'react';
import { Button, Descriptions, Input } from 'antd';
import type { DescriptionsProps } from 'antd';
import { CopyOutlined } from '@ant-design/icons';
import { FEEDBACK_RESULT, PREDICTED_RESULT, REVIEWED_RESULT } from './const';
const DocumentCompareInfo = ({ key, data, selectedFileDataSource, updateRevisedByFeedback, handleUpdateFileInField, shouldRevised, disabledInput }) => {
const items: DescriptionsProps['items'] = [
{
key: selectedFileDataSource[data]?.[FEEDBACK_RESULT] || '1',
label: 'Feedback',
children: selectedFileDataSource[data]?.[FEEDBACK_RESULT],
labelStyle: { color: '#333', padding: '4px 16px' },
contentStyle: { padding: '4px 16px' },
span: 3
},
{
key: selectedFileDataSource[data]?.[PREDICTED_RESULT] || '2',
label: 'Predicted',
children: selectedFileDataSource[data]?.[PREDICTED_RESULT],
labelStyle: { color: '#333', padding: '4px 16px' },
contentStyle: { padding: '4px 16px' },
span: 3
},
{
key: selectedFileDataSource[data]?.[REVIEWED_RESULT] || '3',
label: 'Revised',
children: <Input
style={{ background: shouldRevised ? 'yellow' : '', padding: '0' }}
value={selectedFileDataSource[data]?.[REVIEWED_RESULT]}
size='small'
onChange={(e) =>
handleUpdateFileInField(data, e.target.value)
}
variant="borderless"
disabled={disabledInput === undefined || disabledInput === 0}
/>,
labelStyle: { color: '#333', padding: '4px 16px' },
contentStyle: { padding: '4px 16px' },
span: 3
},
];
return (
<div style={{ margin: '0 0 8px' }} className='file-input-group' key={key}>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
margin: '0 0 4px',
}}
>
<p style={{ fontWeight: 'bold', margin: 0 }}>{data}</p>
<Button
shape='round'
type='primary'
ghost
icon={<CopyOutlined />}
size='small'
onClick={() => updateRevisedByFeedback(data)}
disabled={disabledInput === undefined || disabledInput === 0}
/>
</div>
<Descriptions bordered items={items} layout="horizontal" size='small' contentStyle={{ height: '13px' }} />
</div>
)
}
export default DocumentCompareInfo;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Descriptions } from 'antd';
import type { DescriptionsProps } from 'antd';
const DocumentHeadInfo = ({ currentRequest }) => {
const items: DescriptionsProps['items'] = [
{
key: '1',
label: 'Request ID',
children: currentRequest?.RequestID,
span: 2,
labelStyle: { color: '#333', width: '200px' }
},
{
key: '2',
label: 'Raw accuracy',
children: currentRequest?.raw_accuracy,
labelStyle: { color: '#333', width: '200px' }
},
{
key: '3',
label: 'Redemption ID',
children: currentRequest?.RedemptionID,
span: 2,
labelStyle: { color: '#333', width: '200px' }
},
{
key: '4',
label: 'Processing time',
children: currentRequest?.['Server Processing Time (ms)'],
labelStyle: { color: '#333', width: '200px' }
}
];
return (
<Descriptions bordered items={items} size="small" />
)
}
export default DocumentHeadInfo;

View File

@ -10,6 +10,8 @@ export const fetchAllRequests = async (
filterFeedbackResult: string, filterFeedbackResult: string,
filterPredictResult: string, filterPredictResult: string,
filterReviewedResult: string, filterReviewedResult: string,
filterBadReason: string,
filterOtherReason: string,
page = 1, page = 1,
page_size = 20, page_size = 20,
max_accuracy = 100, max_accuracy = 100,
@ -42,6 +44,11 @@ export const fetchAllRequests = async (
if (filterReviewedResult) { if (filterReviewedResult) {
filterStr += `reviewed_result=${filterReviewedResult}&`; filterStr += `reviewed_result=${filterReviewedResult}&`;
} }
if (filterBadReason === 'other' && filterOtherReason.trim()) {
filterStr += `bad_reason=${filterOtherReason}&`;
} else if(filterBadReason !== 'other') {
filterStr += `bad_reason=${filterBadReason}&`;
}
// //
if (startDate && endDate) { if (startDate && endDate) {
filterStr += `start_date=${startDate}&end_date=${endDate}&`; filterStr += `start_date=${startDate}&end_date=${endDate}&`;
@ -105,7 +112,7 @@ export const fetchRequest = async (id: string) => {
}); });
return await ( return await (
await response.json() await response.json()
).subscription_requests?.[0] || null; ).subscription_requests?.[0];
}; };
export const addRecentRequest = ( export const addRecentRequest = (

View File

@ -3,7 +3,6 @@ import {
ArrowRightOutlined, ArrowRightOutlined,
CheckCircleOutlined, CheckCircleOutlined,
ClockCircleFilled, ClockCircleFilled,
CopyOutlined,
FullscreenExitOutlined, FullscreenExitOutlined,
FullscreenOutlined, FullscreenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@ -55,6 +54,8 @@ import {
import FileCard from './FileCard'; import FileCard from './FileCard';
import RecentRequest from './RecentRequest'; import RecentRequest from './RecentRequest';
import './style.css'; import './style.css';
import DocumentHeadInfo from './DocumentHeadInfo';
import DocumentCompareInfo from './DocumentCompareInfo';
const ReviewPage = () => { const ReviewPage = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -76,6 +77,9 @@ const ReviewPage = () => {
const [filterFeedbackResult, setFilterFeedbackResult] = useState(''); const [filterFeedbackResult, setFilterFeedbackResult] = useState('');
const [filterPredictResult, setFilterPredictResult] = useState(''); const [filterPredictResult, setFilterPredictResult] = useState('');
const [filterReviewedResult, setFilterReviewedResult] = useState(''); const [filterReviewedResult, setFilterReviewedResult] = useState('');
const [filterBadReason, setFilterBadReason] = useState('other');
const [filterOtherReason, setFilterOtherReason] = useState('');
// const [requests, setRequests] = useState([]); // const [requests, setRequests] = useState([]);
const [currentRequest, setCurrentRequest] = useState(null); const [currentRequest, setCurrentRequest] = useState(null);
const [currentRequestIndex, setCurrentRequestIndex] = useState(1); const [currentRequestIndex, setCurrentRequestIndex] = useState(1);
@ -118,6 +122,8 @@ const ReviewPage = () => {
filterFeedbackResult, filterFeedbackResult,
filterPredictResult, filterPredictResult,
filterReviewedResult, filterReviewedResult,
filterBadReason,
filterOtherReason,
1, 1,
1, 1,
filterAccuracy, filterAccuracy,
@ -125,7 +131,7 @@ const ReviewPage = () => {
setTotalPages(data?.page?.total_requests); setTotalPages(data?.page?.total_requests);
setHasNextRequest(1 < data?.page?.total_requests); setHasNextRequest(1 < data?.page?.total_requests);
const firstRequest = fetchRequest( const firstRequest = fetchRequest(
data?.subscription_requests[0].RequestID, data?.subscription_requests?.[0]?.RequestID,
); );
firstRequest.then(async (data) => { firstRequest.then(async (data) => {
if (data) setCurrentRequest(data); if (data) setCurrentRequest(data);
@ -231,6 +237,8 @@ const ReviewPage = () => {
filterFeedbackResult, filterFeedbackResult,
filterPredictResult, filterPredictResult,
filterReviewedResult, filterReviewedResult,
filterBadReason,
filterOtherReason,
requestIndex, requestIndex,
1, 1,
filterAccuracy, filterAccuracy,
@ -290,6 +298,8 @@ const ReviewPage = () => {
filterFeedbackResult, filterFeedbackResult,
filterPredictResult, filterPredictResult,
filterReviewedResult, filterReviewedResult,
filterBadReason,
filterOtherReason,
1, 1,
1, 1,
filterAccuracy, filterAccuracy,
@ -491,42 +501,10 @@ const ReviewPage = () => {
> >
{fullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />} {fullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</Button> </Button>
</div>
{totalRequests && ( {totalRequests && (
<div <DocumentHeadInfo currentRequest={currentRequest} />
style={{
flexGrow: 1,
display: 'grid',
gridTemplateColumns: '1fr 1fr 1fr',
marginLeft: '16px',
}}
>
<div style={{ gridColumn: 'span 2 / span 2' }}>
<b>Request ID: &nbsp;</b>
{currentRequest?.RequestID}
</div>{' '}
<div>
<b>Created at: &nbsp;</b>
{currentRequest?.created_at}
</div>{' '}
<div>
<b>Request time: &nbsp;</b>
{currentRequest?.['Client Request Time (ms)']}
</div>{' '}
<div style={{ gridColumn: 'span 2 / span 2' }}>
<b>Redemption ID: &nbsp;</b>
{currentRequest?.RedemptionID}
</div>{' '}
<div>
<b>Raw accuracy: &nbsp;</b>
{currentRequest?.raw_accuracy}
</div>{' '}
<div style={{ gridColumn: 'span 2 / span 2' }}>
<b>Processing time: &nbsp;</b>
{currentRequest?.['Server Processing Time (ms)']}
</div>{' '}
</div>
)} )}
</div>
{totalRequests > 0 && ( {totalRequests > 0 && (
<div <div
style={{ style={{
@ -787,47 +765,14 @@ const ReviewPage = () => {
} }
} catch (error) { } } catch (error) { }
return ( return (
<div style={{ margin: '0 0 8px' }} className='file-input-group' key={data}> <DocumentCompareInfo key={data}
<div data={data}
style={{ selectedFileDataSource={selectedFileDataSource}
display: 'flex', updateRevisedByFeedback={updateRevisedByFeedback}
justifyContent: 'space-between', handleUpdateFileInField={handleUpdateFileInField}
alignItems: 'center', shouldRevised={shouldRevised}
margin: '0 0 4px', disabledInput = {currentRequest?.Files?.length}
}}
>
<p style={{ fontWeight: 'bold', margin: 0 }}>{data}</p>
<Button
shape='round'
type='primary'
ghost
icon={<CopyOutlined />}
size='small'
onClick={() => updateRevisedByFeedback(data)}
/> />
</div>
<Input
addonBefore='Feedback'
size='small'
readOnly
value={selectedFileDataSource[data]?.[FEEDBACK_RESULT]}
/>
<Input
addonBefore='Predicted'
readOnly
size='small'
value={selectedFileDataSource[data]?.[PREDICTED_RESULT]}
/>
<Input
addonBefore='Revised'
style={{ background: shouldRevised ? 'yellow' : '' }}
size='small'
value={selectedFileDataSource[data]?.[REVIEWED_RESULT]}
onChange={(e) =>
handleUpdateFileInField(data, e.target.value)
}
/>
</div>
); );
})} })}
<b>{t`Bad image reason:`}</b> <b>{t`Bad image reason:`}</b>
@ -904,7 +849,7 @@ const ReviewPage = () => {
</div> </div>
</div> </div>
<Modal <Modal
title={t`Report Filters`} title={t`Review Filters`}
open={isModalOpen} open={isModalOpen}
width={700} width={700}
onOk={() => { onOk={() => {
@ -919,6 +864,16 @@ const ReviewPage = () => {
style={{ style={{
marginTop: 30, marginTop: 30,
}} }}
layout="vertical"
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginLeft: 0,
padding: 0,
}}
> >
<Form.Item <Form.Item
name='dateRange' name='dateRange'
@ -930,20 +885,54 @@ const ReviewPage = () => {
}, },
]} ]}
style={{ style={{
marginBottom: 24, flex: 1,
}} }}
> >
<DatePicker.RangePicker <DatePicker.RangePicker
onChange={(date, dateString) => { onChange={(date, dateString) => {
setFilterDateRange(dateString); setFilterDateRange(dateString);
}} }}
style={{ width: 200 }} style={{ width: 300 }}
/> />
</Form.Item> </Form.Item>
<div style={{
flex: 1,
}}>
<Form.Item
name='bad_reason'
label={t`Bad image reason`}
>
<Select
placeholder='Select a reason'
style={{ width: 300 }}
options={REASON_BAD_QUALITY}
value={filterBadReason}
defaultValue={filterBadReason}
onChange={setFilterBadReason}
/>
</Form.Item>
{filterBadReason === 'other' && (
<Form.Item
name='other_reason'
style={{
flex: 1,
}}
>
<Input
placeholder='Other reason'
value={filterOtherReason}
style={{ width: 300 }}
onChange={(e) => {
setFilterOtherReason(e.target.value);
}}
/>
</Form.Item>
)}
</div>
</div>
<div <div
style={{ style={{
marginTop: 10,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
marginLeft: 0, marginLeft: 0,
@ -959,10 +948,13 @@ const ReviewPage = () => {
message: 'Please select a subsidiary', message: 'Please select a subsidiary',
}, },
]} ]}
style={{
flex: 1,
}}
> >
<Select <Select
placeholder='Select a subsidiary' placeholder='Select a subsidiary'
style={{ width: 200 }} style={{ width: 300 }}
options={SUBSIDIARIES} options={SUBSIDIARIES}
value={filterSubsidiaries} value={filterSubsidiaries}
defaultValue={filterSubsidiaries} defaultValue={filterSubsidiaries}
@ -978,8 +970,12 @@ const ReviewPage = () => {
message: 'Please select max accuracy', message: 'Please select max accuracy',
}, },
]} ]}
style={{
flex: 1,
}}
> >
<InputNumber <InputNumber
style={{ width: 300 }}
min={1} min={1}
max={100} max={100}
defaultValue={filterAccuracy} defaultValue={filterAccuracy}
@ -989,7 +985,7 @@ const ReviewPage = () => {
</div> </div>
<div <div
style={{ style={{
marginTop: 10,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
marginLeft: 0, marginLeft: 0,
@ -1005,9 +1001,12 @@ const ReviewPage = () => {
message: 'Please select review status', message: 'Please select review status',
}, },
]} ]}
style={{
flex: 1,
}}
> >
<Select <Select
style={{ width: 200 }} style={{ width: 300 }}
options={[ options={[
{ label: 'All', value: 'all' }, { label: 'All', value: 'all' },
{ label: 'Reviewed', value: 'reviewed' }, { label: 'Reviewed', value: 'reviewed' },
@ -1027,10 +1026,10 @@ const ReviewPage = () => {
message: 'Please select test status', message: 'Please select test status',
}, },
]} ]}
style={{ marginLeft: 16 }} style={{ flex: 1 }}
> >
<Select <Select
style={{ width: 200 }} style={{ width: 300 }}
options={[ options={[
{ label: 'Include tests', value: 'true' }, { label: 'Include tests', value: 'true' },
{ label: 'Exclude tests', value: 'false' }, { label: 'Exclude tests', value: 'false' },
@ -1044,7 +1043,7 @@ const ReviewPage = () => {
{/* add 4 more filter fields */} {/* add 4 more filter fields */}
<div <div
style={{ style={{
marginTop: 10,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
marginLeft: 0, marginLeft: 0,
@ -1054,16 +1053,13 @@ const ReviewPage = () => {
<Form.Item <Form.Item
name='doc_type' name='doc_type'
label={t`Only type`} label={t`Only type`}
rules={[ style={{
{ flex: 1,
required: true, }}
message: 'Please select a document type',
},
]}
> >
<Select <Select
placeholder='Select a document type' placeholder='Select a document type'
style={{ width: 200 }} style={{ width: 300 }}
options={DOCTYPE} options={DOCTYPE}
value={filterDoctype} value={filterDoctype}
defaultValue={filterDoctype} defaultValue={filterDoctype}
@ -1073,8 +1069,12 @@ const ReviewPage = () => {
<Form.Item <Form.Item
name='feedback_result' name='feedback_result'
label={t`Feedback includes`} label={t`Feedback includes`}
style={{
flex: 1,
}}
> >
<Input <Input
style={{ width: 300 }}
defaultValue={filterFeedbackResult} defaultValue={filterFeedbackResult}
onChange={(e) => setFilterFeedbackResult(e.target.value)} onChange={(e) => setFilterFeedbackResult(e.target.value)}
/> />
@ -1082,7 +1082,7 @@ const ReviewPage = () => {
</div> </div>
<div <div
style={{ style={{
marginTop: 10,
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
marginLeft: 0, marginLeft: 0,
@ -1092,17 +1092,25 @@ const ReviewPage = () => {
<Form.Item <Form.Item
name='predict_result' name='predict_result'
label={t`Predict includes`} label={t`Predict includes`}
style={{
flex: 1,
}}
> >
<Input <Input
style={{ width: 300 }}
defaultValue={filterPredictResult} defaultValue={filterPredictResult}
onChange={(e) => setFilterPredictResult(e.target.value)} onChange={(e) => setFilterPredictResult(e.target.value)}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name='reviewed_result' name='reviewed_result'
label={t`Review inculdes`} label={t`Review includes`}
style={{
flex: 1,
}}
> >
<Input <Input
style={{ width: 300 }}
defaultValue={filterReviewedResult} defaultValue={filterReviewedResult}
onChange={(e) => setFilterReviewedResult(e.target.value)} onChange={(e) => setFilterReviewedResult(e.target.value)}
/> />