diff --git a/cope2n-fe/.env.development b/cope2n-fe/.env.development index 5150853..a27afc6 100644 --- a/cope2n-fe/.env.development +++ b/cope2n-fe/.env.development @@ -1,3 +1,3 @@ VITE_PORT=8080 -VITE_PROXY=https://107.120.133.22/ +VITE_PROXY=http://42.96.42.13:9881 VITE_KUBEFLOW_HOST=https://107.120.133.22:8085 \ No newline at end of file diff --git a/cope2n-fe/src/pages/inference/index.tsx b/cope2n-fe/src/pages/inference/index.tsx index 82f24a4..060e0a0 100644 --- a/cope2n-fe/src/pages/inference/index.tsx +++ b/cope2n-fe/src/pages/inference/index.tsx @@ -1,21 +1,226 @@ import { t } from '@lingui/macro'; -import { Button, message, Upload } from 'antd'; +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 { useHotkeys } from "react-hotkeys-hook"; +import { Viewer } from '@react-pdf-viewer/core'; import { SbtPageHeader } from 'components/page-header'; -import { useState } from 'react'; import { UploadOutlined } from '@ant-design/icons'; import type { GetProp, UploadFile, UploadProps } from 'antd'; -import { JsonView, allExpanded, defaultStyles } from 'react-json-view-lite'; import 'react-json-view-lite/dist/index.css'; -import { baseURL } from "request/api" type FileType = Parameters>[0]; +const ENABLE_REVIEW = false; + + +// Import the styles +import '@react-pdf-viewer/core/lib/styles/index.css'; + + +const siderStyle: React.CSSProperties = { + backgroundColor: '#fafafa', + padding: 10, + width: 200, +}; + + +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; + 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 FileCard = ({ file, isSelected, onClick, setIsReasonModalOpen }) => { + const fileName = file["File Name"]; + + return ( +
+
+ {file["Doc Type"].toUpperCase()} + + {fileName ? fileName.substring(0, 25).replace("temp_", "") : fileName} + +
+
+ + +
+
+ ); + +}; + +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([]); const [imei1Files, setImei1Files] = useState([]); const [imei2Files, setImei2Files] = useState([]); const [uploading, setUploading] = useState(false); - const [jsonData, setJsonData] = useState({}); + const [responseData, setResponseData] = useState(null); const [finishedProcessing, setFinishedProcessing] = useState(false); const handleUpload = () => { @@ -31,9 +236,7 @@ const InferencePage = () => { } formData.append('is_test_request', 'true'); setUploading(true); - setJsonData({ - "message": "Please wait..." - }) + setResponseData(null); const token = localStorage.getItem('sbt-token') || ''; fetch(`${baseURL}/ctel/images/process_sync/`, { method: 'POST', @@ -42,112 +245,443 @@ const InferencePage = () => { "Authorization": `${JSON.parse(token)}` } }) - .then(async(res) => { + .then(async (res) => { const data = await res.json(); - setJsonData(data); + if (data["status"] != "200") { + setResponseData(null); + return; + } + loadRequestById(data["request_id"]); setFinishedProcessing(true); return data; }) .then(() => { message.success('Upload successfully.'); }) - .catch(() => { + .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 [totalRequests, setTotalPages] = useState(0); + 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["Reviewed Result"]) ? data["Reviewed 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 = newRevisedData.request_id; + const token = localStorage.getItem('sbt-token') || ''; + 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); + message.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, + }); + setDataSource(newData); + const newRevisedData = {}; + for (let i = 0; i < newData.length; i++) { + newRevisedData[newData[i].key] = newData[i].revised; + } + updateRevisedData(newRevisedData).then(() => { + // "[Is Reviewed]" => true + setCurrentRequest({ + ...currentRequest, + ["Is Reviewed"]: true, + }) + }) + }; + + const submitRevisedData = async () => { + const newData = [...dataSource]; + const newRevisedData = {}; + for (let i = 0; i < newData.length; i++) { + newRevisedData[newData[i].key] = newData[i].revised; + } + console.log(currentRequest) + 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', + }, + { + title: (
Revised   + {ENABLE_REVIEW && } +
), + dataIndex: 'revised', + key: 'revised', + editable: ENABLE_REVIEW, + }, + ]; + + + 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 ( <> - -

- {t`Upload files to process. The requests here will not be used in accuracy or payment calculations.`} -

- { - if (finishedProcessing) return; - setInvoiceFiles([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setInvoiceFiles([file]) - return false; - }} - fileList={invoiceFiles} + +
+
+ { + if (finishedProcessing) return; + setInvoiceFiles([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setInvoiceFiles([file]) + return false; + }} + fileList={invoiceFiles} + > + Invoice: + +
+
+ { + if (finishedProcessing) return; + setImei1Files([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setImei1Files([file]) + return false; + }} + fileList={imei1Files} + > + IMEI 1: + +
+
+ { + if (finishedProcessing) return; + setImei2Files([]) + }} + beforeUpload={(file) => { + if (finishedProcessing) return; + setImei2Files([file]) + return false; + }} + fileList={imei2Files} + > + IMEI 2: + +
+
+ {!finishedProcessing && } + {finishedProcessing && } +
+
+
- Invoice File: - -
-
- { - if (finishedProcessing) return; - setImei1Files([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setImei1Files([file]) - return false; - }} - fileList={imei1Files} + +
+ + + {currentRequest?.Files?.length &&
+

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

+ {currentRequest?.Files.map((file, index) => ( + { + setAndLoadSelectedFile(currentRequest, index); + } + } setIsReasonModalOpen={setIsReasonModalOpen} /> + ))} +
} + {selectedFileData &&
+ {selectedFileData === "FAILED_TO_LOAD_FILE" ?

: (fileExtension === "pdf" ? () :
file
)} +
} +
+ +
+ + + + + +
+ {currentRequest && (currentRequest["Is Reviewed"] ? } color="success" style={{ padding: "4px 16px" }}> + Reviewed + : } color="warning" style={{ padding: "4px 16px" }}> + Not Reviewed + )} +
+
+
+
+ { + + } + } + onCancel={ + () => { + setIsReasonModalOpen(false); + } + } > - IMEI File 1: -
-
-
- { - if (finishedProcessing) return; - setImei2Files([]) - }} - beforeUpload={(file) => { - if (finishedProcessing) return; - setImei2Files([file]) - return false; - }} - fileList={imei2Files} - > - IMEI File 2: - -
- {!finishedProcessing && } - {finishedProcessing && } -
-

Result:

- +
+ +