diff --git a/src/react/components/common/regionalTable/regionalTable.tsx b/src/react/components/common/regionalTable/regionalTable.tsx new file mode 100644 index 00000000..a4d3c54f --- /dev/null +++ b/src/react/components/common/regionalTable/regionalTable.tsx @@ -0,0 +1,176 @@ +import React from "react"; + +export interface IRegionalTableState { } + +export interface IRegionalTableProps { + regionalTableToView?: any; + tableTagColor: string; + onMouseEnter: (rowName: string, columnName: string) => void; + onMouseLeave: () => void; +} + +export default class RegionalTable extends React.Component { + makeOnMouseEnter = (rowName, columnName) => () => { + this.props.onMouseEnter(rowName, columnName); + } + + onMouseLeave = () => { + this.props.onMouseLeave(); + } + + private displayRegionalTable = (regionalTableToView) => { + const tableBody = []; + if (regionalTableToView?.type === "array") { + const columnHeaderRow = []; + const colKeys = Object.keys(regionalTableToView?.valueArray?.[0]?.valueObject || {}); + if (colKeys.length === 0) { + return ( +
+
+ Table name: {regionalTableToView.fieldName} +
+
+ + + Empty table + +
+
+
+ ); + } + for (let i = 0; i < colKeys.length + 1; i++) { + if (i === 0) { + columnHeaderRow.push( + + ); + } else { + columnHeaderRow.push( + + {colKeys[i - 1]} + + ); + } + } + tableBody.push({columnHeaderRow}); + regionalTableToView?.valueArray?.forEach((row, rowIndex) => { + const rowName = `#${rowIndex}`; + const tableRow = []; + tableRow.push( + + {rowName} + + ); + Object.keys(row?.valueObject).forEach((columnName, columnIndex) => { + const tableCell = row?.valueObject?.[columnName]; + tableRow.push( + + {tableCell ? tableCell.text : null} + + ); + }) + tableBody.push({tableRow}); + }) + } else { + const columnHeaderRow = []; + const colKeys = Object.keys(regionalTableToView?.valueObject?.[Object.keys(regionalTableToView?.valueObject)?.[0]]?.valueObject || {}); + if (colKeys.length === 0) { + return ( +
+
+ Table name: {regionalTableToView.fieldName} +
+
+ + + Empty table + +
+
+
+ ); + } + for (let i = 0; i < colKeys.length + 1; i++) { + if (i === 0) { + columnHeaderRow.push( + + ); + } else { + columnHeaderRow.push( + + {colKeys[i - 1]} + + ); + } + } + tableBody.push({columnHeaderRow}); + Object.keys(regionalTableToView?.valueObject).forEach((rowName, index) => { + const tableRow = []; + tableRow.push( + + {rowName} + + ); + if (regionalTableToView?.valueObject?.[rowName]) { + Object.keys(regionalTableToView?.valueObject?.[rowName]?.valueObject)?.forEach((columnName, index) => { + const tableCell = regionalTableToView?.valueObject?.[rowName]?.valueObject?.[columnName]; + tableRow.push( + { + this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName }) + }} + onMouseLeave={() => { + this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null }) + }} + > + {tableCell ? tableCell.text : null} + + ); + }); + } else { + colKeys.forEach((columnName, index) => { + tableRow.push( + + {null} + + ); + }) + } + tableBody.push({tableRow}); + }); + } + + return ( +
+
+ Table name: {regionalTableToView.fieldName} +
+
+ + + {tableBody} + +
+
+
+ ); + } + + render() { + return ( + <> + {this.displayRegionalTable(this.props.regionalTableToView)} + + ); + } +} diff --git a/src/react/components/pages/editorPage/tableView.tsx b/src/react/components/pages/editorPage/tableView.tsx index 172d3d52..76dd5ecb 100644 --- a/src/react/components/pages/editorPage/tableView.tsx +++ b/src/react/components/pages/editorPage/tableView.tsx @@ -2,13 +2,40 @@ import * as React from "react"; import { ICustomizations, Customizer, ContextualMenu, IDragOptions, Modal, FontIcon } from "@fluentui/react"; import { getDarkGreyTheme } from "../../../../common/themes"; import "./tableView.scss"; +import { TooltipHost, TooltipDelay, DirectionalHint, ITooltipProps, ITooltipHostStyles } from "@fluentui/react"; +import { useId } from '@uifabric/react-hooks'; + +function Tooltip({ children, content }) { + const makeTooltipProps = (content: object): ITooltipProps => ({ + onRenderContent: () => ( + + ), + }); + const hostStyles: Partial = { root: { display: 'inline-block' } }; + const tooltipId = useId('tooltip'); + const tooltipProps = makeTooltipProps(content); + return ( + + {children} + + ) +} interface ITableViewProps { handleTableViewClose: () => any; tableToView: object; + showToolTips?: boolean; } -export const TableView: React.FunctionComponent = (props) => { +export const TableView: React.FunctionComponent = ({ handleTableViewClose, tableToView, showToolTips = false }) => { const dark: ICustomizations = { settings: { theme: getDarkGreyTheme(), @@ -20,49 +47,52 @@ export const TableView: React.FunctionComponent = (props) => { moveMenuItemText: "Move", closeMenuItemText: "Close", menu: ContextualMenu, - }; + }; function getTableBody() { - const table = props.tableToView; + const table = tableToView; let tableBody = null; if (table !== null) { tableBody = []; const rows = table["rows"]; - // const columns = table["columns"]; for (let i = 0; i < rows; i++) { const tableRow = []; tableBody.push({tableRow}); } - table["cells"].forEach((cell) => { - const rowIndex = cell["rowIndex"]; - const columnIndex = cell["columnIndex"]; - const rowSpan = cell["rowSpan"]; - const colSpan = cell["columnSpan"]; - tableBody[rowIndex]["props"]["children"][columnIndex] = + table["cells"].forEach(({ rowIndex, columnIndex, rowSpan, colSpan, text, confidence }) => { + const content = { confidence: confidence || null }; + const hasContentValue = Object.values(content).reduce((hasValue, value) => value || hasValue, false); + tableBody[rowIndex]["props"]["children"][columnIndex] = ( - {cell["text"]} - ; + {showToolTips && hasContentValue ? ( + + {text} + + ) : ( + {text} + )} + + ) }); } return tableBody; } - return ( -
diff --git a/src/react/components/pages/prebuiltPredict/prebuiltPredictPage.tsx b/src/react/components/pages/prebuiltPredict/prebuiltPredictPage.tsx index a8b28715..299d767f 100644 --- a/src/react/components/pages/prebuiltPredict/prebuiltPredictPage.tsx +++ b/src/react/components/pages/prebuiltPredict/prebuiltPredictPage.tsx @@ -38,7 +38,7 @@ import PreventLeaving from "../../common/preventLeaving/preventLeaving"; import { CanvasCommandBar } from "../editorPage/canvasCommandBar"; import { TableView } from "../editorPage/tableView"; import "../predict/predictPage.scss"; -import PredictResult from "../predict/predictResult"; +import PredictResult, { ITableResultItem } from "../predict/predictResult"; import { ILoadFileHelper, ILoadFileResult, LoadFileHelper } from "./LoadFileHelper"; import "./prebuiltPredictPage.scss"; import { ITableHelper, ITableState, TableHelper } from "./tableHelper"; @@ -83,6 +83,10 @@ export interface IPrebuiltPredictPageState extends ILoadFileResult, ITableState predictionEndpointUrl: string; liveMode: boolean; + + viewRegionalTable?: boolean; + regionalTableToView?: any; + tableTagColor?: string; } function mapStateToProps(state: IApplicationState) { @@ -158,6 +162,10 @@ export class PrebuiltPredictPage extends React.Component } { (Object.keys(predictions).length === 0 && this.state.predictionLoaded) &&
{strings.prebuiltPredict.noFieldCanBeExtracted}
} + {this.state.viewRegionalTable && + + } JSON.parse(content as string)) + .then(({ content }) => JSON.parse(content as string)) .then(result => this.setState({ currentPage: 1, analyzeResult: null, @@ -470,7 +486,7 @@ export class PrebuiltPredictPage extends React.Component new Promise(() => this.handlePredictionResult(result)) .catch(this.handlePredictionError))) - } + } } handleLiveModeToggleChange = (event, checked: boolean) => { @@ -689,22 +705,22 @@ export class PrebuiltPredictPage extends React.Component { - let alertMessage = ""; - if (error.response) { - alertMessage = error.response.data; - } else if (error.errorCode === ErrorCode.PredictWithoutTrainForbidden) { - alertMessage = strings.errors.predictWithoutTrainForbidden.message; - } else if (error.errorCode === ErrorCode.ModelNotFound) { - alertMessage = error.message; - } else { - alertMessage = interpolate(strings.errors.endpointConnectionError.message, { endpoint: "form recognizer backend URL" }); - } - this.setState({ - shouldShowAlert: true, - alertTitle: "Prediction Failed", - alertMessage, - isPredicting: false, - }); + let alertMessage = ""; + if (error.response) { + alertMessage = error.response.data; + } else if (error.errorCode === ErrorCode.PredictWithoutTrainForbidden) { + alertMessage = strings.errors.predictWithoutTrainForbidden.message; + } else if (error.errorCode === ErrorCode.ModelNotFound) { + alertMessage = error.message; + } else { + alertMessage = interpolate(strings.errors.endpointConnectionError.message, { endpoint: "form recognizer backend URL" }); + } + this.setState({ + shouldShowAlert: true, + alertTitle: "Prediction Failed", + alertMessage, + isPredicting: false, + }); } private handleClick = () => { @@ -810,72 +826,89 @@ export class PrebuiltPredictPage extends React.Component { // Comment this line to prevent clear OCR boundary boxes. // this.imageMap.removeAllFeatures(); + const createFeature = (fieldName, field) => { + if (Array.isArray(field)) { + field.forEach(field => createFeature(fieldName, field)) + } else { + if (_.get(field, "page", null) === this.state.currentPage) { + const text = fieldName; + const boundingbox = _.get(field, "boundingBox", []); + const feature = this.createBoundingBoxVectorFeature(text, boundingbox, imageExtent, ocrExtent); + features.push(feature); + } + } + } const features = []; const imageExtent = [0, 0, this.state.imageWidth, this.state.imageHeight]; const ocrForCurrentPage: any = this.getOcrFromAnalyzeResult(this.state.analyzeResult)[this.state.currentPage - 1]; const ocrExtent = [0, 0, ocrForCurrentPage.width, ocrForCurrentPage.height]; - const predictions = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult); - for (const fieldName of Object.keys(predictions)) { - const field = predictions[fieldName]; - if (_.get(field, "page", null) === this.state.currentPage) { - const text = fieldName; - const boundingbox = _.get(field, "boundingBox", []); - const feature = this.createBoundingBoxVectorFeature(text, boundingbox, imageExtent, ocrExtent); - features.push(feature); - } + const predictions = this.flatFields(this.getPredictionsFromAnalyzeResult(this.state.analyzeResult)); + for (const [fieldName, field] of Object.entries(predictions)) { + createFeature(fieldName, field); } this.imageMap.addFeatures(features); this.tableHelper.drawTables(this.state.currentPage); } - private getPredictionsFromAnalyzeResult(analyzeResult: any) { - if (analyzeResult) { - const documentResults = _.get(analyzeResult, "documentResults", []); - const isSupportField = fieldName => { - // Define list of unsupported field names. - const blockedFieldNames = ["ReceiptType"]; - return blockedFieldNames.indexOf(fieldName) === -1; - } - const isRootItemObject = obj => obj.hasOwnProperty("text"); - // flat fieldProps of type "array" and "object", and extract root level field props in "object" type - const flattedFields = {}; - const flatFields = (fields = {}) => { - const flatFieldProps = (displayName, fieldProps) => { - if (isSupportField(displayName)) { - switch (_.get(fieldProps, "type", "")) { - case "array": { - const valueArray = _.get(fieldProps, "valueArray", []); - for (const [index, valueArrayItem] of valueArray.entries()) { - flatFieldProps(`${displayName} ${index + 1}`, valueArrayItem); - } - break; - } - case "object": { - // root level field props - const { type, valueObject, ...restProps } = fieldProps; - if (isRootItemObject(restProps)) { - flatFieldProps(displayName, restProps); - } - for (const [fieldName, objFieldProps] of Object.entries(fieldProps.valueObject)) { - flatFieldProps(`${displayName}: ${fieldName}`, objFieldProps); - } - break; - } - default: { - flattedFields[displayName] = fieldProps; + private flatFields = (fields: object = {}): { [key: string]: (object[] | object) } => { + /** + * @param fields: primitive types, object types likes array, object, and root level field + * @return flattenfields, a field props or an array of field props + */ + const flattedFields = {}; + const isSupportField = fieldName => { + // Define list of unsupported field names. + const blockedFieldNames = ["ReceiptType"]; + return blockedFieldNames.indexOf(fieldName) === -1; + } + const isRootItemObject = obj => obj.hasOwnProperty("text"); + // flat fieldProps of type "array" and "object", and extract root level field props in "object" type + const flatFieldProps = (fieldName, fieldProps) => { + if (isSupportField(fieldName)) { + switch (_.get(fieldProps, "type", "")) { + case "array": { + const valueArray = _.get(fieldProps, "valueArray", []); + for (const arrayItem of valueArray) { + flatFieldProps(fieldName, arrayItem); + } + break; + } + case "object": { + // root level field props + const { type, valueObject, ...restProps } = fieldProps; + if (isRootItemObject(restProps)) { + flatFieldProps(fieldName, restProps); + } + for (const objFieldProps of Object.values(fieldProps.valueObject)) { + if (objFieldProps) { + flatFieldProps(fieldName, objFieldProps); } } + break; + } + default: { + if (flattedFields[fieldName] == null) { + flattedFields[fieldName] = fieldProps; + } + else if (Array.isArray(flattedFields[fieldName])) { + flattedFields[fieldName].push(fieldProps) + } else { + flattedFields[fieldName] = [flattedFields[fieldName], fieldProps]; + } } } - for (const [fieldName, fieldProps] of Object.entries(fields)) { - flatFieldProps(fieldName, fieldProps); - } } - for (const documentResult of documentResults) { - const fields = documentResult["fields"]; - flatFields(fields); - } - return flattedFields; + } + for (const [fieldName, fieldProps] of Object.entries(fields)) { + flatFieldProps(fieldName, fieldProps); + } + return flattedFields; + } + + private getPredictionsFromAnalyzeResult(analyzeResult: any) { + if (analyzeResult) { + const documentResults = _.get(analyzeResult, "documentResults", []); + return documentResults.reduce((accFields, documentResult) => ({ ...accFields, ...documentResult.fields }), {}); } else { return {}; } @@ -997,4 +1030,45 @@ export class PrebuiltPredictPage extends React.Component { + const makeTable = (clickedFieldName) => { + function Cell(rowIndex, columnIndex, text = null, confidence = null) { + this.rowIndex = rowIndex; + this.columnIndex = columnIndex; + this.text = text; + this.confidence = confidence; + } + + const valueArray = clickedFieldName.valueArray || []; + const columnNames = Object.keys(valueArray[0].valueObject); + const columnHeaders = function makeColumnHeaders() { + const indexColumn = new Cell(0, 0, ""); + const contentColumns = columnNames.map((columnName, columnIndex) => new Cell(0, columnIndex + 1, columnName)); + return [indexColumn, ...contentColumns]; + }() + const matrix: any[] = [columnHeaders]; + for (let i = 0; i < valueArray.length; i++) { + const valueObject = valueArray[i].valueObject || {}; + const indexColumn = new Cell(i + 1, 0, `#${i + 1}`); + const contentColumns = columnNames.map((columnName, columnIndex) => { + const { text, confidence } = valueObject[columnName] || {}; + return new Cell(i + 1, columnIndex + 1, text, confidence); + }); + matrix.push([indexColumn, ...contentColumns]); + } + const flattenCells = matrix.reduce((cells, row) => cells = [...cells, ...row], []); + return { cells: flattenCells, columns: matrix[0].length, rows: matrix.length, fieldName: clickedField, tagColor }; + } + + const predictions = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult) + const clickedFieldName = predictedItem?.fieldName; + const clickedField = predictions[clickedFieldName]; + const regionalTableToView = makeTable(clickedField); + this.setState({ viewRegionalTable: true, regionalTableToView }); + } + + private onTablePredictionClose = () => { + this.setState({ viewRegionalTable: false, regionalTableToView: null }) + } } diff --git a/src/react/components/pages/predict/predictPage.tsx b/src/react/components/pages/predict/predictPage.tsx index 47750482..c5397d01 100644 --- a/src/react/components/pages/predict/predictPage.tsx +++ b/src/react/components/pages/predict/predictPage.tsx @@ -44,6 +44,7 @@ import "./predictPage.scss"; import PredictResult, { IAnalyzeModelInfo, ITableResultItem } from "./predictResult"; import RecentModelsView from "./recentModelsView"; import { UploadToTrainingSetView } from "./uploadToTrainingSetView"; +import RegionalTable from "../../common/regionalTable/regionalTable"; pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version); @@ -441,7 +442,12 @@ export default class PredictPage extends React.Component

View analyzed Table

- {this.displayRegionalTable(this.state.regionalTableToView)} + { - - const tableBody = []; - if (regionalTableToView?.type === "array") { - const columnHeaderRow = []; - const colKeys = Object.keys(regionalTableToView?.valueArray?.[0]?.valueObject || {}); - if (colKeys.length === 0) { - return ( -
-
- Table name: {regionalTableToView.fieldName} -
-
-
- - Empty table - -
-
- - ); - } - for (let i = 0; i < colKeys.length + 1; i++) { - if (i === 0) { - columnHeaderRow.push( - - ); - } else { - columnHeaderRow.push( - - {colKeys[i - 1]} - - ); - } - } - tableBody.push({columnHeaderRow}); - regionalTableToView?.valueArray?.forEach((row, rowIndex) => { - const tableRow = []; - tableRow.push( - - {"#" + rowIndex} - - ); - Object.keys(row?.valueObject).forEach((columnName, columnIndex) => { - const tableCell = row?.valueObject?.[columnName]; - tableRow.push( - { - this.setState({ highlightedTableCellRowKey: "#" + rowIndex, highlightedTableCellColumnKey: columnName }) - }} - onMouseLeave={() => { - this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null }) - }} - > - {tableCell ? tableCell.text : null} - - ); - }) - tableBody.push({tableRow}); - }) - } else { - const columnHeaderRow = []; - const colKeys = Object.keys(regionalTableToView?.valueObject?.[Object.keys(regionalTableToView?.valueObject)?.[0]]?.valueObject || {}); - if (colKeys.length === 0) { - return ( -
-
- Table name: {regionalTableToView.fieldName} -
-
- - - Empty table - -
-
-
- ); - } - for (let i = 0; i < colKeys.length + 1; i++) { - if (i === 0) { - columnHeaderRow.push( - - ); - } else { - columnHeaderRow.push( - - {colKeys[i - 1]} - - ); - } - } - tableBody.push({columnHeaderRow}); - Object.keys(regionalTableToView?.valueObject).forEach((rowName, index) => { - const tableRow = []; - tableRow.push( - - {rowName} - - ); - if (regionalTableToView?.valueObject?.[rowName]) { - Object.keys(regionalTableToView?.valueObject?.[rowName]?.valueObject)?.forEach((columnName, index) => { - const tableCell = regionalTableToView?.valueObject?.[rowName]?.valueObject?.[columnName]; - tableRow.push( - { - this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName }) - }} - onMouseLeave={() => { - this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null }) - }} - > - {tableCell ? tableCell.text : null} - - ); - }); - } else { - colKeys.forEach((columnName, index) => { - tableRow.push( - - {null} - - ); - }) - } - tableBody.push({tableRow}); - }); - } - - return ( -
-
- Table name: {regionalTableToView.fieldName} -
-
- - - {tableBody} - -
-
-
- ); - } - private onPredictionMouseEnter = (predictedItem: any) => { this.setState({ highlightedField: predictedItem.fieldName ?? "", @@ -1376,4 +1230,12 @@ export default class PredictPage extends React.Component { + this.setState({ highlightedTableCellRowKey: rowName, highlightedTableCellColumnKey: columnName }); + } + + private onMouseLeave = () => { + this.setState({ highlightedTableCellRowKey: null, highlightedTableCellColumnKey: null }); + } }