sbt-idp/cope2n-fe/src/pages/inference/index.tsx

735 lines
22 KiB
TypeScript

import { t } from '@lingui/macro';
import { Button, Input, Table, Tag, DatePicker, Form, Modal, Select, Spin, message, Upload } from 'antd';
import React, { useContext, useEffect, useRef, useState } from 'react';
import type { GetRef } from 'antd';
import { Layout } from 'antd';
import {
DownloadOutlined, CheckCircleOutlined,
ClockCircleFilled,
} from '@ant-design/icons';
import styled from 'styled-components';
const { Sider, Content } = Layout;
import { baseURL } from "request/api";
import { Viewer } from '@react-pdf-viewer/core';
import { SbtPageHeader } from 'components/page-header';
import { UploadOutlined } from '@ant-design/icons';
import type { GetProp, UploadFile, UploadProps } from 'antd';
import 'react-json-view-lite/dist/index.css';
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
const ENABLE_REVIEW = true;
// Import the styles
import '@react-pdf-viewer/core/lib/styles/index.css';
import { normalizeData } from 'utils/field-value-process';
const siderStyle: React.CSSProperties = {
backgroundColor: '#fafafa',
padding: 10,
width: 200,
};
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;
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];
const FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => {
const fileName = file["File Name"];
return (
<div style={{
border: '1px solid #ccc',
width: '200px',
backgroundColor: isSelected ? '#d4ecff' : '#fff',
padding: '4px 8px',
marginRight: '4px',
marginTop: '2px',
position: 'relative',
height: '100px',
overflow: 'hidden',
}} onClick={onClick}>
<div>
<span style={{
fontSize: '12px',
color: '#333',
fontWeight: 'bold',
padding: '4px 8px',
cursor: 'default',
}}>{file["Doc Type"].toUpperCase()}</span>
<span style={{
fontSize: '12px',
color: '#aaa',
fontWeight: 'bold',
padding: '4px 8px',
cursor: 'default',
maxWidth: '50px',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}>
{fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName}
</span>
</div>
<div style={{
padding: '4px',
position: 'absolute',
bottom: 2,
right: 2,
}}>
{/* <Button style={{
margin: '4px 2px',
}}
onClick={() => {
setIsReasonModalOpen(true);
}}
>
Review
</Button> */}
<Button style={{
margin: '4px 2px',
}} onClick={() => {
const downloadUrl = file["File URL"];
window.open(downloadUrl, '_blank');
}}>
<DownloadOutlined />
</Button>
</div>
</div>
);
};
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];
};
const InferencePage = () => {
const [invoiceFiles, setInvoiceFiles] = useState<UploadFile[]>([]);
const [imei1Files, setImei1Files] = useState<UploadFile[]>([]);
const [imei2Files, setImei2Files] = useState<UploadFile[]>([]);
const [uploading, setUploading] = useState(false);
const [responseData, setResponseData] = useState(null);
const [finishedProcessing, setFinishedProcessing] = useState(false);
const handleUpload = () => {
const formData = new FormData();
if (invoiceFiles.length > 0) {
formData.append('invoice_file', invoiceFiles[0] as FileType);
}
if (imei1Files.length > 0) {
formData.append('imei_files', imei1Files[0] as FileType);
}
if (imei2Files.length > 0) {
formData.append('imei_files', imei2Files[0] as FileType);
}
formData.append('is_test_request', 'true');
setUploading(true);
setResponseData(null);
const token = localStorage.getItem('sbt-token') || '';
fetch(`${baseURL}/ctel/images/process_sync/`, {
method: 'POST',
body: formData,
headers: {
"Authorization": `${JSON.parse(token)}`
}
})
.then(async (res) => {
const data = await res.json();
if (data["status"] != "200") {
setResponseData(null);
return;
}
setTimeout(() => {
loadRequestById(data["request_id"]);
}, 2000);
setFinishedProcessing(true);
return data;
})
.then(() => {
message.success('Upload successfully.');
})
.catch((e) => {
console.log(e);
message.error('Upload failed.');
})
.finally(() => {
setUploading(false);
});
};
const [loading, setLoading] = useState(false);
const [isReasonModalOpen, setIsReasonModalOpen] = useState(false);
const [selectedFileId, setSelectedFileId] = useState(0);
const [selectedFileData, setSelectedFileData] = useState(null);
const [selectedFileName, setSelectedFileName] = useState(null);
const [currentRequest, setCurrentRequest] = useState(null);
const [dataSource, setDataSource] = useState([]);
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");
}
};
const loadRequestById = (requestID) => {
setLoading(true);
const requestData = fetchRequest(requestID);
requestData.then(async (data) => {
if (data) setCurrentRequest(data);
const predicted = (data && data["Predicted Result"]) ? data["Predicted 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["revised"] = revised[keys[i]];
tableRows.push(instance);
}
setDataSource(tableRows);
setLoading(false);
setAndLoadSelectedFile(data, 0);
}).finally(() => {
setLoading(false);
});
};
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
// "Key", "Accuracy", "Revised"
interface DataType {
key: string;
accuracy: number;
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 <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;
{ENABLE_REVIEW && <Button
onClick={() => {
if (!dataSource || !dataSource.length) return;
setDataSource(dataSource.map(item => {
item.revised = item.predicted;
return item;
}));
setTimeout(() => {
submitRevisedData();
}, 1000);
}}
>
Copy Predicted
</Button>}
</div>),
dataIndex: 'revised',
key: 'revised',
editable: ENABLE_REVIEW,
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,
}),
};
});
const fileExtension = selectedFileName ? selectedFileName.split('.').pop() : '';
return (
<>
<div style={{
height: '100%',
position: 'relative',
}}>
<SbtPageHeader
title={t`Inference`}
/>
<div style={{
display: 'flex',
flexDirection: 'row',
}}>
<div style={{
paddingTop: "0.5rem",
padding: "0.5rem",
height: "80px",
}}>
<Upload
onRemove={(file) => {
if (finishedProcessing) return;
setInvoiceFiles([])
}}
beforeUpload={(file) => {
if (finishedProcessing) return;
setInvoiceFiles([file])
return false;
}}
fileList={invoiceFiles}
>
Invoice: <Button disabled={finishedProcessing} icon={<UploadOutlined />}>Select Image/PDF</Button>
</Upload>
</div>
<div style={{
paddingTop: "0.5rem",
padding: "0.5rem",
height: "80px",
}}>
<Upload
onRemove={(file) => {
if (finishedProcessing) return;
setImei1Files([])
}}
beforeUpload={(file) => {
if (finishedProcessing) return;
setImei1Files([file])
return false;
}}
fileList={imei1Files}
>
IMEI 1: <Button disabled={finishedProcessing} icon={<UploadOutlined />}>Select Image</Button>
</Upload>
</div>
<div style={{
paddingTop: "0.5rem",
padding: "0.5rem",
height: "80px",
}}>
<Upload
onRemove={(file) => {
if (finishedProcessing) return;
setImei2Files([])
}}
beforeUpload={(file) => {
if (finishedProcessing) return;
setImei2Files([file])
return false;
}}
fileList={imei2Files}
>
IMEI 2: <Button disabled={finishedProcessing} icon={<UploadOutlined />}>Select Image</Button>
</Upload>
</div>
<div style={{
paddingTop: "0.5rem",
padding: "0.5rem",
height: "80px",
display: 'flex',
flexDirection: 'row',
}}>
{!finishedProcessing && <Button
type="primary"
onClick={handleUpload}
disabled={imei1Files.length === 0 && imei2Files.length === 0}
loading={uploading}
>
{uploading ? 'Uploading' : 'Process Data'}
</Button>}
{finishedProcessing && <Button
type="primary"
onClick={() => {
setFinishedProcessing(false);
setResponseData(null);
setInvoiceFiles([]);
setImei1Files([]);
setImei2Files([]);
setCurrentRequest(null);
setDataSource([]);
setSelectedFileData(null);
setUploading(false);
}}
>
Reset
</Button>}
</div>
</div>
<div
style={{ height: '100%', position: 'absolute', top: 0, left: 0, width: '100%', background: "#00000033", zIndex: 1000, display: loading ? 'block' : 'none' }}
>
<Spin spinning={true} style={{
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12,
width: 24,
height: 24,
borderRadius: '50%',
}} size='large' />
</div>
<Layout style={{
overflow: 'auto',
width: '100%',
height: '100%',
maxWidth: '100%',
minHeight: '70%',
maxHeight: '70%',
display: 'flex',
padding: '8px',
}}>
<Content style={{
textAlign: 'center',
color: '#fff',
backgroundColor: '#efefef',
height: '100%',
display: 'flex',
flexDirection: 'row',
}}>
{currentRequest?.Files?.length && <div
style={{
width: "200px",
display: "flex",
flexDirection: "column",
flexGrow: 0,
}}>
<h2
style={{
color: "#333",
padding: 10,
fontWeight: 'bold',
fontSize: 18,
textTransform: 'uppercase'
}}
>Files ({currentRequest?.Files?.length})</h2>
{currentRequest?.Files.map((file, index) => (
<FileCard key={index} file={file} isSelected={index === selectedFileId} onClick={
() => {
setAndLoadSelectedFile(currentRequest, index);
}
} setIsReasonModalOpen={setIsReasonModalOpen} />
))}
</div>}
{selectedFileData && <div style={{
border: "1px solid #ccc",
flexGrow: 1,
height: '100%',
}}>
{selectedFileData === "FAILED_TO_LOAD_FILE" ? <p style={{ color: "#333" }}></p> : (fileExtension === "pdf" ? (<Viewer
fileUrl={selectedFileData}
/>) : <div style={{
height: "100%",
width: "100%",
overflow: "auto",
}}><img width={"100%"} src={selectedFileData} alt="file" /></div>)}
</div>}
</Content>
<Sider width="400px" style={siderStyle}>
<div>
<Input size='small' addonBefore="Request ID" style={{ marginBottom: "4px" }} readOnly value={currentRequest ? currentRequest.RequestID : ""} />
<Input size='small' addonBefore="Redemption" style={{ marginBottom: "4px" }} readOnly value={currentRequest?.RedemptionID ? currentRequest.RedemptionID : ""} />
<Input size='small' addonBefore="Uploaded date" style={{ marginBottom: "4px" }} readOnly value={currentRequest ? currentRequest.created_at : ""} />
<Input size='small' addonBefore="Request time" style={{ marginBottom: "4px" }} readOnly value={currentRequest ? currentRequest["Client Request Time (ms)"] : ""} />
<Input size='small' addonBefore="Processing time" style={{ marginBottom: "4px" }} readOnly value={currentRequest ? currentRequest["Server Processing Time (ms)"] : ""} />
<div style={{ marginBottom: "8px", marginTop: "8px", display: "flex" }}>
{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>
</Sider>
</Layout>
<Modal
title={t`Review`}
open={isReasonModalOpen}
width={700}
onOk={
() => {
}
}
onCancel={
() => {
setIsReasonModalOpen(false);
}
}
>
<Form
style={{
marginTop: 30,
}}
>
<Form.Item
name='reason'
label={t`Reason for bad quality:`}
style={{
marginBottom: 10,
}}
>
<Select
placeholder='Select a reason'
style={{ width: 200 }}
options={[
{ 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: 'recheck', label: t`Recheck` },
]}
/>
</Form.Item>
</Form>
</Modal>
<StyledTable components={components}
rowClassName={() => 'editable-row'}
bordered dataSource={dataSource} columns={columns}
/>
</div>
</>
);
};
export default InferencePage;