Yongbing chen/receipt predicting (#626)
* add receiptPredict * adjust scss * tslint * rename 'receipt' to 'prebuilt' * addd localization to prebuiltPredict * add prebuild settings to store * fix warning in test page * update inline icons * move predict button position in sidebar * add drebuilt type Dropdown * add textTablePage * textTablePage reflect, add filePicker, prebuiltSetting * move ocr code to IOcrHelper, move poll to utils.ts * page reflector, morge utils.ts * code refactor * add HomeProjectView, update homePage localization settings * change Misleading "Over the rate limitation..." * scss file refactor * remove comments code / imports * fix 'prebuilt' spelling * refactor predictResult * remove commented code * refactor predictPage, use DocumentFilePicker select file * Stew ro/use api version selected in project settings (#678) (#679) * refactor: change drawn region icon * fix: use api version selected in project settings * fix: change interpolate value for api version * fix: use existing git hash when not in git repository (#682) * Update script to executable * change prebuilt and layout route path, add strings.layoutPredict * change homePage OpenLocalProject style * fix: add margin for btns, rename models dropdown, renamed BussinessCard => Bussiness card * fix: add padding to prediction results list * update homepage description text, update projectSetingsPage style * add quickstartguid link url * update homePage styles * set quickstart default link * replace layout icon (#698) * add PreventLeaving in layoutPredictPage (#692) * Use prebuilt icon on prebuilt page (#707) * prevent error when open file (#694) * set uploadAssetToProject button display on onAddAssetToProject (#693) * Move open local project button (#708) * Remove API key copy button on prebuilt and layout pages (#710) * Extend prebuilt for type drop down to fit text (#711) * update homepage quickstartguide links (#709) * show table info in predictPage and prebuiltPredictPage (#706) * fix page crushed when add analyzed document to training set (#715) * show business card result (#713) * remain label data while deleting all labels (#716) * remain label data while deleting all labels * Do not clean auto label data for other actions expect training Co-authored-by: alex-krasn <64093224+alex-krasn@users.noreply.github.com> Co-authored-by: alex-krasn <v-alexkr@microsoft.com> Co-authored-by: stew-ro <60453211+stew-ro@users.noreply.github.com> Co-authored-by: Xinxing Liu <xinxl@microsoft.com> Co-authored-by: starain-pactera <73208113+starain-pactera@users.noreply.github.com>
This commit is contained in:
Родитель
32cfaea023
Коммит
e638cd8e3b
24
src/App.scss
24
src/App.scss
|
@ -118,7 +118,7 @@
|
|||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,6 +140,28 @@
|
|||
.ms-Icon {
|
||||
font-size: 1em;
|
||||
}
|
||||
.icon-2x {
|
||||
font-size: 2em;
|
||||
}
|
||||
.icon-3x {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
.icon-4x {
|
||||
font-size: 4em;
|
||||
}
|
||||
.icon-5x {
|
||||
font-size: 5em;
|
||||
}
|
||||
.icon-6x {
|
||||
font-size: 6em;
|
||||
}
|
||||
.icon-7x {
|
||||
font-size: 7em;
|
||||
}
|
||||
.icon-8x {
|
||||
font-size: 8em;
|
||||
}
|
||||
|
||||
.icon-9x {
|
||||
font-size: 9em;
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -49,4 +49,5 @@ export const constants = {
|
|||
return `https://fotts.azureedge.net/npm/pdfjs-dist/${version}/cmaps/`;
|
||||
},
|
||||
insightsKey: "",
|
||||
prebuiltServiceVersion: "v2.1-preview.2"
|
||||
};
|
||||
|
|
|
@ -38,6 +38,7 @@ export const english: IAppStrings = {
|
|||
newProject: "New Project",
|
||||
openLocalProject: {
|
||||
title: "Open Local Project",
|
||||
description: "Open Local Project",
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "Open Cloud Project",
|
||||
|
@ -57,6 +58,22 @@ export const english: IAppStrings = {
|
|||
messages: {
|
||||
deleteSuccess: "Successfully deleted ${project.name}",
|
||||
},
|
||||
homeProjectView: {
|
||||
title: "Train and use a model with labels"
|
||||
},
|
||||
prebuiltPredict: {
|
||||
title: "Use prebuilt model to get data",
|
||||
description: "Start with a pre-built model to extract data from your forms – Invoices, Receipts, Business cards and more. Submit your data and get results right away."
|
||||
},
|
||||
layoutPredict:{
|
||||
title:"Use Layout to get text and tables",
|
||||
description:"Try out the Form Recognizer Layout service to extract text, tables, selection marks and the structure of your document."
|
||||
},
|
||||
trainWithLabels:{
|
||||
title:"Train and use a model with labels",
|
||||
description:"You provide your own training data and do the learning. The model you create can train to your industry-specific forms."
|
||||
},
|
||||
quickStartGuide:"Quick start guide",
|
||||
},
|
||||
appSettings: {
|
||||
title: "Application Settings",
|
||||
|
@ -220,6 +237,18 @@ export const english: IAppStrings = {
|
|||
message: "Asset with name '${name}' exists in project, override?"
|
||||
}
|
||||
},
|
||||
prebuiltPredict: {
|
||||
title: "Prebuilt analyze",
|
||||
defaultLocalFileInput: "Browse for a file...",
|
||||
defaultURLInput: "Paste or type URL...",
|
||||
uploadFile: "Choose an image to analyze with",
|
||||
inProgress: "Analysis in progress...",
|
||||
anlayWithPrebuiltModels: "Analyze ${name} (preview)",
|
||||
},
|
||||
layoutPredict:{
|
||||
title: "Layout analyze",
|
||||
inProgress: "Analysis in progress...",
|
||||
},
|
||||
recentModelsView: {
|
||||
header: "Select a model to analyze with",
|
||||
checkboxAriaLabel: "Select model checkbox",
|
||||
|
|
|
@ -39,6 +39,7 @@ export const spanish: IAppStrings = {
|
|||
recentProjects: "Proyectos Recientes",
|
||||
openLocalProject: {
|
||||
title: "Abrir Proyecto Local",
|
||||
description: "Abrir Proyecto Local",
|
||||
},
|
||||
openCloudProject: {
|
||||
title: "Abrir Proyecto de la Nube",
|
||||
|
@ -57,6 +58,22 @@ export const spanish: IAppStrings = {
|
|||
messages: {
|
||||
deleteSuccess: "${project.name} eliminado correctamente",
|
||||
},
|
||||
homeProjectView: {
|
||||
title: "Entrene y use un modelo con etiquetas"
|
||||
},
|
||||
prebuiltPredict: {
|
||||
title: "Utilice un modelo prediseñado para obtener datos",
|
||||
description: "Comience con un modelo preconstruidos para extraer datos de sus formularios: facturas, recibos, tarjetas de visita y mucho más. Envíe sus datos y obtenga resultados de inmediato."
|
||||
},
|
||||
layoutPredict:{
|
||||
title:"Use Layout para obtener texto y tablas",
|
||||
description:"Pruebe el servicio Diseño del reconocedor de formularios para extraer texto, tablas, marcas de selección y la estructura del documento."
|
||||
},
|
||||
trainWithLabels:{
|
||||
title:"Entrene y use un modelo con etiquetas",
|
||||
description:"Tú proporcionas tus propios datos de entrenamiento y haces el aprendizaje. El modelo que cree puede adaptarse a los formularios específicos de su industria."
|
||||
},
|
||||
quickStartGuide:"Quick start guide",
|
||||
},
|
||||
appSettings: {
|
||||
title: "Configuración de Aplicación",
|
||||
|
@ -219,6 +236,18 @@ export const spanish: IAppStrings = {
|
|||
message: "El activo con el nombre '${name}' existe en el proyecto, ¿anularlo?"
|
||||
}
|
||||
},
|
||||
prebuiltPredict: {
|
||||
title: "Análisis preconstruido",
|
||||
defaultLocalFileInput: "Busca un archivo...",
|
||||
defaultURLInput: "Pegar o escribir URL...",
|
||||
uploadFile: "Elija una imagen para analizar con",
|
||||
inProgress: "Análisis en curso...",
|
||||
anlayWithPrebuiltModels: "Análisis ${name} (versión preliminar)",
|
||||
},
|
||||
layoutPredict:{
|
||||
title: "Análisis de diseño",
|
||||
inProgress: "Análisis en curso...",
|
||||
},
|
||||
recentModelsView: {
|
||||
header: "Seleccionar modelo para analizar con",
|
||||
checkboxAriaLabel: "Seleccione la casilla de verificación del modelo",
|
||||
|
|
|
@ -486,6 +486,7 @@ export default class MockFactory {
|
|||
loadAssetMetadata: jest.fn(() => Promise.resolve()),
|
||||
refreshAsset: jest.fn(() => Promise.resolve()),
|
||||
saveAssetMetadata: jest.fn(() => Promise.resolve()),
|
||||
saveAssetMetadataAndCleanEmptyLabel: jest.fn(()=> Promise.resolve()),
|
||||
updateProjectTag: jest.fn(() => Promise.resolve()),
|
||||
deleteProjectTag: jest.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import LocalizedStrings, { LocalizedStringsMethods } from "react-localization";
|
||||
import { english } from "./localization/en-us";
|
||||
import { spanish } from "./localization/es-cl";
|
||||
import LocalizedStrings, {LocalizedStringsMethods} from "react-localization";
|
||||
import {english} from "./localization/en-us";
|
||||
import {spanish} from "./localization/es-cl";
|
||||
|
||||
/**
|
||||
* Interface for all required strings in application
|
||||
|
@ -39,6 +39,7 @@ export interface IAppStrings {
|
|||
newProject: string,
|
||||
openLocalProject: {
|
||||
title: string,
|
||||
description: string,
|
||||
},
|
||||
openCloudProject: {
|
||||
title: string,
|
||||
|
@ -57,6 +58,22 @@ export interface IAppStrings {
|
|||
messages: {
|
||||
deleteSuccess: string,
|
||||
},
|
||||
homeProjectView: {
|
||||
title: string,
|
||||
},
|
||||
prebuiltPredict: {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
layoutPredict: {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
trainWithLabels: {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
quickStartGuide: string;
|
||||
};
|
||||
appSettings: {
|
||||
title: string,
|
||||
|
@ -218,6 +235,18 @@ export interface IAppStrings {
|
|||
message: string
|
||||
},
|
||||
};
|
||||
prebuiltPredict: {
|
||||
title: string;
|
||||
defaultLocalFileInput: string;
|
||||
defaultURLInput: string;
|
||||
uploadFile: string;
|
||||
inProgress: string;
|
||||
anlayWithPrebuiltModels: string;
|
||||
}
|
||||
layoutPredict:{
|
||||
title:string;
|
||||
inProgress: string;
|
||||
},
|
||||
recentModelsView: {
|
||||
header: string;
|
||||
checkboxAriaLabel: string;
|
||||
|
@ -616,7 +645,7 @@ interface IErrorMetadata {
|
|||
message: string,
|
||||
}
|
||||
|
||||
interface IStrings extends LocalizedStringsMethods, IAppStrings { }
|
||||
interface IStrings extends LocalizedStringsMethods, IAppStrings {}
|
||||
|
||||
export const strings: IStrings = new LocalizedStrings({
|
||||
en: english,
|
||||
|
@ -630,7 +659,7 @@ export const strings: IStrings = new LocalizedStrings({
|
|||
* @param json JSON object containing variable placeholders
|
||||
*/
|
||||
export function addLocValues(json: any) {
|
||||
return interpolateJson(json, { strings });
|
||||
return interpolateJson(json, {strings});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,8 @@ import Guard from "./guard";
|
|||
import { IProject, ISecurityToken, IProviderOptions, ISecureString, ITag } from "../models/applicationState";
|
||||
import { encryptObject, decryptObject, encrypt, decrypt } from "./crypto";
|
||||
import UTIF from "utif";
|
||||
import {constants} from "./constants";
|
||||
import _ from "lodash";
|
||||
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
const tagColors = require("../react/components/common/tagColors.json");
|
||||
|
@ -357,3 +359,56 @@ export function fixedEncodeURIComponent(str: string) {
|
|||
return '%' + c.charCodeAt(0).toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Poll function to repeatly check if request succeeded
|
||||
* @param func - function that will be called repeatly
|
||||
* @param timeout - timeout
|
||||
* @param interval - interval
|
||||
*/
|
||||
export function poll(func, timeout, interval): Promise<any> {
|
||||
const endTime = Number(new Date()) + (timeout || 10000);
|
||||
interval = interval || 100;
|
||||
|
||||
const checkSucceeded = (resolve, reject) => {
|
||||
const ajax = func();
|
||||
ajax.then((response) => {
|
||||
if (response.data.status.toLowerCase() === constants.statusCodeSucceeded) {
|
||||
resolve(response.data);
|
||||
} else if (response.data.status.toLowerCase() === constants.statusCodeFailed) {
|
||||
reject(_.get(
|
||||
response,
|
||||
"data.analyzeResult.errors[0].errorMessage",
|
||||
"Generic error during prediction"));
|
||||
} else if (Number(new Date()) < endTime) {
|
||||
// If the request isn't succeeded and the timeout hasn't elapsed, go again
|
||||
setTimeout(checkSucceeded, interval, resolve, reject);
|
||||
} else {
|
||||
// Didn't succeeded after too much time, reject
|
||||
reject("Timed out, please try other file.");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise(checkSucceeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* download data as json file
|
||||
* @param data
|
||||
* @param fileName
|
||||
* @param prefix
|
||||
*/
|
||||
export function downloadAsJsonFile(data: any, fileName: string, prefix?: string): void {
|
||||
const predictionData = JSON.stringify(data, null, 2);
|
||||
const fileURL = window.URL.createObjectURL(new Blob([predictionData]));
|
||||
const fileLink = document.createElement("a");
|
||||
const fileBaseName = fileName.split(".")[0];
|
||||
const downloadFileName = prefix + "Result-" + fileBaseName + ".json";
|
||||
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute("download", downloadFileName);
|
||||
document.body.appendChild(fileLink);
|
||||
fileLink.click();
|
||||
}
|
||||
|
|
|
@ -90,6 +90,10 @@
|
|||
"name": "Cloud",
|
||||
"unicode": "E753"
|
||||
},
|
||||
{
|
||||
"name": "ContactCard",
|
||||
"unicode": "EEBD"
|
||||
},
|
||||
{
|
||||
"name": "Copy",
|
||||
"unicode": "E8C8"
|
||||
|
@ -218,6 +222,10 @@
|
|||
"name": "Rename",
|
||||
"unicode": "E8AC"
|
||||
},
|
||||
{
|
||||
"name": "Rocket",
|
||||
"unicode": "F3B3"
|
||||
},
|
||||
{
|
||||
"name": "Rotate90Clockwise",
|
||||
"unicode": "F80D"
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ITrainRecordProps } from "../react/components/pages/train/trainRecord";
|
|||
* @member appError - error in the app if any
|
||||
*/
|
||||
export interface IApplicationState {
|
||||
prebuiltSettings?: IPrebuiltSettings;
|
||||
appSettings: IAppSettings,
|
||||
connections: IConnection[],
|
||||
recentProjects: IProject[],
|
||||
|
@ -170,6 +171,11 @@ export interface IAsset {
|
|||
mimeType?: string,
|
||||
}
|
||||
|
||||
export interface IPrebuiltSettings{
|
||||
serviceURI: string;
|
||||
apiKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name - Asset Metadata
|
||||
* @description - Format to store asset metadata for each asset within a project
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.document-file-picker {
|
||||
.sourceDropdown {
|
||||
width: 95px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
import {Dropdown, IDropdownOption, PrimaryButton, TextField} from '@fluentui/react';
|
||||
import React from 'react';
|
||||
import {strings} from '../../../../common/strings';
|
||||
import {getGreenWithWhiteBackgroundTheme, getPrimaryGreenTheme} from '../../../../common/themes';
|
||||
import "./documentFilePicker.scss";
|
||||
|
||||
interface IDocumentFilePickerProps {
|
||||
disabled: boolean;
|
||||
onFileChange?: (data: {
|
||||
file: File;
|
||||
fileLabel: string;
|
||||
fetchedFileURL: string;
|
||||
}) => void;
|
||||
onError?: (err: {alertTitle: string, alertMessage: string}) => void;
|
||||
onSelectSourceChange?: () => void;
|
||||
}
|
||||
|
||||
interface IDocumentFilePickerState {
|
||||
sourceOption: string;
|
||||
invalidFileFormat: boolean;
|
||||
inputedLocalFile: string;
|
||||
inputedFileURL: string;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
export class DocumentFilePicker extends React.Component<IDocumentFilePickerProps, IDocumentFilePickerState>{
|
||||
state = {
|
||||
sourceOption: "localFile",
|
||||
invalidFileFormat: false,
|
||||
isFetching: false,
|
||||
inputedFileURL: "",
|
||||
inputedLocalFile: strings.predict.defaultLocalFileInput,
|
||||
};
|
||||
|
||||
private fileInput: React.RefObject<HTMLInputElement> = React.createRef();
|
||||
|
||||
render() {
|
||||
const sourceOptions: IDropdownOption[] = [
|
||||
{key: "localFile", text: "Local file"},
|
||||
{key: "url", text: "URL"},
|
||||
];
|
||||
|
||||
let {disabled} = this.props;
|
||||
disabled = !!disabled;
|
||||
const urlInputDisabled = disabled || this.state.isFetching;
|
||||
const fetchDisabled: boolean =
|
||||
disabled ||
|
||||
this.state.isFetching ||
|
||||
this.state.inputedFileURL.length === 0 ||
|
||||
this.state.inputedFileURL === strings.prebuiltPredict.defaultURLInput;
|
||||
|
||||
return <>
|
||||
<div
|
||||
className="document-file-picker"
|
||||
style={{marginBottom: "3px"}}>Image source</div>
|
||||
<div className="container-space-between">
|
||||
<Dropdown
|
||||
className="sourceDropdown"
|
||||
selectedKey={this.state.sourceOption}
|
||||
options={sourceOptions}
|
||||
disabled={disabled}
|
||||
onChange={this.onSelectSourceChange}
|
||||
/>
|
||||
{this.state.sourceOption === "localFile" &&
|
||||
<input
|
||||
aria-hidden="true"
|
||||
type="file"
|
||||
accept="application/pdf, image/jpeg, image/png, image/tiff"
|
||||
id="hiddenInputFile"
|
||||
ref={this.fileInput}
|
||||
onChange={this.handleFileChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
{this.state.sourceOption === "localFile" &&
|
||||
<TextField
|
||||
className="mr-2 ml-2"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
style={{cursor: (disabled ? "default" : "pointer")}}
|
||||
onClick={this.handleDummyInputClick}
|
||||
readOnly={true}
|
||||
aria-label={strings.prebuiltPredict.uploadFile}
|
||||
value={this.state.inputedLocalFile}
|
||||
placeholder={strings.predict.uploadFile}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
{this.state.sourceOption === "localFile" &&
|
||||
<PrimaryButton
|
||||
className="keep-button-80px"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
text="Browse"
|
||||
allowDisabledFocus
|
||||
disabled={disabled}
|
||||
autoFocus={true}
|
||||
onClick={this.handleDummyInputClick}
|
||||
/>
|
||||
}
|
||||
{this.state.sourceOption === "url" &&
|
||||
<>
|
||||
<TextField
|
||||
className="mr-2 ml-2"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
onFocus={this.removeDefaultInputedFileURL}
|
||||
onChange={this.setInputedFileURL}
|
||||
aria-label={strings.prebuiltPredict.uploadFile}
|
||||
value={this.state.inputedFileURL}
|
||||
disabled={urlInputDisabled}
|
||||
placeholder={strings.predict.defaultURLInput}
|
||||
/>
|
||||
<PrimaryButton
|
||||
theme={getPrimaryGreenTheme()}
|
||||
className="keep-button-80px"
|
||||
text="Fetch"
|
||||
allowDisabledFocus
|
||||
disabled={fetchDisabled}
|
||||
autoFocus={true}
|
||||
onClick={this.getFileFromURL}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
private onSelectSourceChange = (event, option) => {
|
||||
if (option.key !== this.state.sourceOption) {
|
||||
this.setState({
|
||||
sourceOption: option.key,
|
||||
inputedFileURL: ""
|
||||
}, () => {
|
||||
if (this.props.onSelectSourceChange) {
|
||||
this.props.onSelectSourceChange();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private handleFileChange = () => {
|
||||
if (this.fileInput.current.value !== "") {
|
||||
this.setState({invalidFileFormat: false});
|
||||
const fileName = this.fileInput.current.value.split("\\").pop();
|
||||
if (fileName !== "") {
|
||||
this.setState({
|
||||
inputedLocalFile: fileName,
|
||||
}, () => {
|
||||
if (this.props.onFileChange) {
|
||||
this.props.onFileChange({
|
||||
file: this.fileInput.current.files[0],
|
||||
fileLabel: fileName,
|
||||
fetchedFileURL: ''
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
private handleDummyInputClick = () => {
|
||||
document.getElementById("hiddenInputFile").click();
|
||||
}
|
||||
|
||||
private removeDefaultInputedFileURL = () => {
|
||||
if (this.state.inputedFileURL === strings.prebuiltPredict.defaultURLInput) {
|
||||
this.setState({inputedFileURL: ""});
|
||||
}
|
||||
}
|
||||
|
||||
private setInputedFileURL = (event) => {
|
||||
this.setState({inputedFileURL: event.target.value});
|
||||
if (this.props.onFileChange) {
|
||||
this.props.onFileChange({
|
||||
file: null,
|
||||
fileLabel: "",
|
||||
fetchedFileURL: event.target.value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getFileFromURL = () => {
|
||||
this.setState({isFetching: true});
|
||||
fetch(this.state.inputedFileURL, {headers: {Accept: "application/pdf, image/jpeg, image/png, image/tiff"}})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
});
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Failed to fetch",
|
||||
alertMessage: response.status.toString() + " " + response.statusText,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const contentType = response.headers.get("Content-Type");
|
||||
if (!["application/pdf", "image/jpeg", "image/png", "image/tiff"].includes(contentType)) {
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
});
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Content-Type not supported",
|
||||
alertMessage: "Content-Type " + contentType + " not supported",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
response.blob().then((blob) => {
|
||||
const fileAsURL = new URL(this.state.inputedFileURL);
|
||||
const fileName = fileAsURL.pathname.split("/").pop();
|
||||
const file = new File([blob], fileName, {type: contentType});
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
}, () => {
|
||||
if (this.props.onFileChange) {
|
||||
this.props.onFileChange({
|
||||
file,
|
||||
fileLabel: fileName,
|
||||
fetchedFileURL: ""
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
});
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Invalid data",
|
||||
alertMessage: error
|
||||
});
|
||||
}
|
||||
return;
|
||||
});
|
||||
}).catch(() => {
|
||||
this.setState({
|
||||
isFetching: false,
|
||||
});
|
||||
if (this.props.onError) {
|
||||
this.props.onError({
|
||||
alertTitle: "Fetch failed",
|
||||
alertMessage: "Network error or Cross-Origin Resource Sharing (CORS) is not configured server-side",
|
||||
});
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { Feature, MapBrowserEvent, View } from "ol";
|
||||
import { Extent, getCenter } from "ol/extent";
|
||||
import { defaults as defaultInteractions, DragPan, Interaction, DragBox, Snap } from "ol/interaction.js";
|
||||
import {Feature, MapBrowserEvent, View} from "ol";
|
||||
import {Extent, getCenter} from "ol/extent";
|
||||
import {defaults as defaultInteractions, DragPan, Interaction, DragBox, Snap} from "ol/interaction.js";
|
||||
import PointerInteraction from 'ol/interaction/Pointer';
|
||||
import Draw from "ol/interaction/Draw.js";
|
||||
import Style from "ol/style/Style";
|
||||
import Collection from 'ol/Collection';
|
||||
import { shiftKeyOnly, never } from 'ol/events/condition';
|
||||
import { Modify } from "ol/interaction";
|
||||
import {shiftKeyOnly, never} from 'ol/events/condition';
|
||||
import {Modify} from "ol/interaction";
|
||||
import Polygon from "ol/geom/Polygon";
|
||||
import ImageLayer from "ol/layer/Image";
|
||||
import Layer from "ol/layer/Layer";
|
||||
|
@ -21,7 +21,7 @@ import VectorSource from "ol/source/Vector";
|
|||
import * as React from "react";
|
||||
import "./styles.css";
|
||||
import Utils from "./utils";
|
||||
import { FeatureCategory, IRegion, ImageMapParent } from "../../../../models/applicationState";
|
||||
import {FeatureCategory, IRegion} from "../../../../models/applicationState";
|
||||
|
||||
interface IImageMapProps {
|
||||
imageUri: string;
|
||||
|
@ -39,7 +39,9 @@ interface IImageMapProps {
|
|||
drawnRegionStyler?: (feature) => Style;
|
||||
modifyStyler?: () => Style;
|
||||
|
||||
parentPage?: string;
|
||||
initEditorMap?: boolean;
|
||||
initPredictMap?: boolean;
|
||||
initLayoutMap?: boolean;
|
||||
|
||||
enableFeatureSelection?: boolean;
|
||||
handleFeatureSelect?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void;
|
||||
|
@ -85,7 +87,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
private modify: Modify;
|
||||
private snap: Snap;
|
||||
|
||||
private drawnFeatures: Collection = new Collection([], { unique: true });
|
||||
private drawnFeatures: Collection = new Collection([], {unique: true});
|
||||
public modifyStartFeatureCoordinates: any = {};
|
||||
|
||||
private imageExtent: number[];
|
||||
|
@ -141,15 +143,18 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
if (this.props.parentPage === ImageMapParent.Editor) {
|
||||
if (this.props.initEditorMap) {
|
||||
this.initEditorMap();
|
||||
} else {
|
||||
} else if (this.props.initPredictMap) {
|
||||
this.initPredictMap();
|
||||
}
|
||||
else if (this.props.initLayoutMap) {
|
||||
this.initLayoutMap();
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: IImageMapProps) {
|
||||
if (this.props.parentPage === ImageMapParent.Editor) {
|
||||
if (this.props.initEditorMap || this.props.initLayoutMap) {
|
||||
if (this.props?.drawRegionMode) {
|
||||
this.removeInteraction(this.dragBox);
|
||||
this.initializeDraw();
|
||||
|
@ -199,7 +204,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
onMouseEnter={this.handlePointerEnterImageMap}
|
||||
className="map-wrapper"
|
||||
>
|
||||
<div style={{ cursor: this.getCursor() }} id="map" className="map" ref={(el) => this.mapElement = el} />
|
||||
<div style={{cursor: this.getCursor()}} id="map" className="map" ref={(el) => this.mapElement = el} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -469,7 +474,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
this.tableIconBorderVectorLayer?.getSource().clear();
|
||||
this.checkboxVectorLayer?.getSource().clear();
|
||||
this.labelVectorLayer?.getSource().clear();
|
||||
if (this.props.parentPage === ImageMapParent.Editor) {
|
||||
if (this.props.initEditorMap) {
|
||||
this.clearDrawnRegions();
|
||||
}
|
||||
}
|
||||
|
@ -478,7 +483,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
this.drawRegionVectorLayer?.getSource().clear();
|
||||
this.drawnLabelVectorLayer?.getSource().clear();
|
||||
|
||||
this.drawnFeatures = new Collection([], { unique: true });
|
||||
this.drawnFeatures = new Collection([], {unique: true});
|
||||
|
||||
this.drawRegionVectorLayer.getSource().on("addfeature", (evt) => {
|
||||
this.pushToDrawnFeatures(evt.feature, this.drawnFeatures);
|
||||
|
@ -584,6 +589,22 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
this.initializeDragPan();
|
||||
}
|
||||
|
||||
private initLayoutMap = () => {
|
||||
const projection = this.createProjection(this.imageExtent);
|
||||
const layers = this.initializeEditorLayers(projection);
|
||||
this.initializeMap(projection, layers);
|
||||
|
||||
this.map.on("pointerdown", this.handlePointerDown);
|
||||
this.map.on("pointermove", this.handlePointerMove);
|
||||
this.map.on("pointermove", this.handlePointerMoveOnTableIcon);
|
||||
this.map.on("pointerup", this.handlePointerUp);
|
||||
this.map.on("dblclick", this.handleDoubleClick);
|
||||
|
||||
this.initializeDefaultSelectionMode();
|
||||
this.initializeDragPan();
|
||||
|
||||
}
|
||||
|
||||
private setImage = (imageUri: string, imageExtent: number[]) => {
|
||||
const projection = this.createProjection(imageExtent);
|
||||
this.imageLayer.setSource(this.createImageSource(imageUri, projection, imageExtent));
|
||||
|
@ -1026,7 +1047,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
private initializeSnapCheck = () => {
|
||||
const snapCheck = new Interaction({
|
||||
handleEvent: (evt: MapBrowserEvent) => {
|
||||
if (!this.props.isVertexDragging) {
|
||||
if (!this.props.isVertexDragging && this.props.handleIsSnapped) {
|
||||
this.props.handleIsSnapped(this.snap.snapTo(evt.pixel, evt.coordinate, evt.map).snapped && this.props.isPointerOnImage)
|
||||
}
|
||||
return true;
|
||||
|
@ -1045,10 +1066,13 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
return true
|
||||
},
|
||||
this.imageLayerFilter);
|
||||
if (!Boolean(test) && this.props.isPointerOnImage) {
|
||||
this.props.handleIsPointerOnImage(false);
|
||||
} else if (!this.props.isPointerOnImage && Boolean(test)) {
|
||||
this.props.handleIsPointerOnImage(true);
|
||||
|
||||
if (this.props.handleIsPointerOnImage) {
|
||||
if (!Boolean(test) && this.props.isPointerOnImage) {
|
||||
this.props.handleIsPointerOnImage(false);
|
||||
} else if (!this.props.isPointerOnImage && Boolean(test)) {
|
||||
this.props.handleIsPointerOnImage(true);
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1057,7 +1081,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
}
|
||||
|
||||
private getCursor = () => {
|
||||
if (this.props.parentPage === ImageMapParent.Editor) {
|
||||
if (this.props.initEditorMap) {
|
||||
if (this.props.isVertexDragging) {
|
||||
return "grabbing";
|
||||
} else if (this.props.isSnapped) {
|
||||
|
@ -1077,11 +1101,13 @@ export class ImageMap extends React.Component<IImageMapProps> {
|
|||
}
|
||||
|
||||
private handlePonterLeaveImageMap = () => {
|
||||
if (this.props.parentPage === ImageMapParent.Editor) {
|
||||
if (this.props.initEditorMap) {
|
||||
if (this.props.isDrawing) {
|
||||
this.cancelDrawing();
|
||||
}
|
||||
this.props.handleIsPointerOnImage(false);
|
||||
if(this.props.handleIsPointerOnImage) {
|
||||
this.props.handleIsPointerOnImage(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
.prebuilt-setting {
|
||||
.apikeyContainer {
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
.apikey {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import {DefaultButton, FontIcon, TextField} from '@fluentui/react';
|
||||
import React from 'react';
|
||||
import {getGreenWithWhiteBackgroundTheme, getPrimaryGreyTheme} from '../../../../common/themes';
|
||||
import {IPrebuiltSettings} from '../../../../models/applicationState';
|
||||
import IAppPrebuiltSettingsActions from '../../../../redux/actions/prebuiltSettingsActions';
|
||||
import "./prebuiltSetting.scss";
|
||||
|
||||
interface IPrebuiltSettingProps {
|
||||
prebuiltSettings: IPrebuiltSettings;
|
||||
actions?: IAppPrebuiltSettingsActions;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
interface IPrebuiltSettingState {
|
||||
showInputedAPIKey: boolean;
|
||||
}
|
||||
|
||||
export class PrebuiltSetting extends React.Component<IPrebuiltSettingProps, IPrebuiltSettingState>{
|
||||
state = {
|
||||
showInputedAPIKey: false
|
||||
};
|
||||
|
||||
render() {
|
||||
const {disabled} = this.props;
|
||||
return <>
|
||||
<div className="p-3 prebuilt-setting" style={{marginTop: "8px"}}>
|
||||
<h5>Service configuration</h5>
|
||||
<div style={{marginBottom: "3px"}}>Form recognizer service endpoint</div>
|
||||
<TextField
|
||||
className="mb-1"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
value={this.props.prebuiltSettings?.serviceURI}
|
||||
onChange={this.setInputedServiceURI}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<div style={{marginBottom: "3px"}}>API key</div>
|
||||
<div className="apikeyContainer">
|
||||
<TextField
|
||||
className="apikey"
|
||||
theme={getGreenWithWhiteBackgroundTheme()}
|
||||
type={this.state.showInputedAPIKey ? "text" : "password"}
|
||||
value={this.props.prebuiltSettings?.apiKey}
|
||||
onChange={this.setInputedAPIKey}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<DefaultButton
|
||||
className="portected-input-margin"
|
||||
theme={getPrimaryGreyTheme()}
|
||||
title={this.state.showInputedAPIKey ? "Hide" : "Show"}
|
||||
disabled={disabled}
|
||||
onClick={this.toggleAPIKeyVisibility}
|
||||
>
|
||||
<FontIcon iconName={this.state.showInputedAPIKey ? "Hide3" : "View"} />
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
private setInputedServiceURI = (e: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
this.props.actions.update({...this.props.prebuiltSettings, serviceURI: newValue});
|
||||
}
|
||||
|
||||
private setInputedAPIKey = (e: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
|
||||
this.props.actions.update({...this.props.prebuiltSettings, apiKey: newValue});
|
||||
}
|
||||
private toggleAPIKeyVisibility = () => {
|
||||
this.setState({
|
||||
showInputedAPIKey: !this.state.showInputedAPIKey,
|
||||
});
|
||||
}
|
||||
|
||||
private async copyKey() {
|
||||
const clipboard = (navigator as any).clipboard;
|
||||
if (clipboard && clipboard.writeText && typeof clipboard.writeText === "function") {
|
||||
await clipboard.writeText(this.props.prebuiltSettings.apiKey);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -433,7 +433,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
|
|||
|
||||
if (onlyCurrentPageTags) {
|
||||
|
||||
const labels = this.props.labels.filter(item => item.value[ 0 ].page === this.props.pageNumber)
|
||||
const labels = this.props.labels.filter(item => item.value[ 0 ]?.page === this.props.pageNumber)
|
||||
.map(item => item.label);
|
||||
if (labels.length) {
|
||||
|
||||
|
|
|
@ -220,7 +220,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
|
|||
<div className="tag-item-label-container">
|
||||
{(confidence||revised) &&
|
||||
<div className="tag-item-label-container-item1">
|
||||
{confidence &&
|
||||
{!revised && confidence &&
|
||||
<div className="tag-item-confidence">
|
||||
{confidence}
|
||||
</div>
|
||||
|
@ -231,7 +231,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
|
|||
</div>
|
||||
}
|
||||
<div className="tag-item-label-container-item2">
|
||||
{ this.props.showOriginLabels && label.originValue &&
|
||||
{this.props.showOriginLabels && label.originValue &&
|
||||
<TagInputItemLabel
|
||||
label={label}
|
||||
isOrigin={true}
|
||||
|
@ -239,14 +239,14 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
|
|||
prefixText={strings.tags.preText.autoLabel}
|
||||
/>
|
||||
}
|
||||
<TagInputItemLabel
|
||||
{(label.originValue?.length > 0 || label.value?.length > 0) && <TagInputItemLabel
|
||||
label={label}
|
||||
value={label.value}
|
||||
isOrigin={false}
|
||||
onLabelEnter={this.props.onLabelEnter}
|
||||
onLabelLeave={this.props.onLabelLeave}
|
||||
prefixText={revised ? strings.tags.preText.revised : undefined}
|
||||
/>
|
||||
/>}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>);
|
||||
|
|
|
@ -20,7 +20,7 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
|
|||
public render() {
|
||||
const texts = [];
|
||||
let hasEmptyTextValue = false;
|
||||
this.props.value.forEach((formRegion: IFormRegion, idx) => {
|
||||
this.props.value?.forEach((formRegion: IFormRegion, idx) => {
|
||||
if (formRegion.text === "") {
|
||||
hasEmptyTextValue = true;
|
||||
} else {
|
||||
|
@ -35,7 +35,7 @@ export default class TagInputItemLabel extends React.Component<ITagInputItemLabe
|
|||
onMouseLeave={this.handleMouseLeave}
|
||||
>
|
||||
<div className="flex-center">
|
||||
{this.props.prefixText} {text}
|
||||
{text ? this.props.prefixText : undefined} {text}
|
||||
{hasEmptyTextValue &&
|
||||
<FontIcon className="pr-1 pl-1" iconName="FieldNotChanged" />
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
EditorMode, IAssetMetadata,
|
||||
IProject, IRegion, RegionType,
|
||||
AssetType, ILabelData, ILabel,
|
||||
ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, ImageMapParent, LabelType, AssetLabelingState, APIVersionPatches
|
||||
ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, LabelType, AssetLabelingState, APIVersionPatches
|
||||
} from "../../../../models/applicationState";
|
||||
import CanvasHelpers from "./canvasHelpers";
|
||||
import { AssetPreview } from "../../common/assetPreview/assetPreview";
|
||||
|
@ -273,10 +273,12 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
drawRegionMode={this.state.drawRegionMode}
|
||||
project={this.props.project}
|
||||
selectedAsset={this.props.selectedAsset}
|
||||
parentPage={strings.editorPage.title}
|
||||
showLayerMenu={true}
|
||||
showActionMenu={true}
|
||||
enableDrawRegion={true}
|
||||
/>
|
||||
<ImageMap
|
||||
parentPage={ImageMapParent.Editor}
|
||||
initEditorMap={true}
|
||||
ref={(ref) => this.imageMap = ref}
|
||||
imageUri={this.state.imageUri}
|
||||
imageWidth={this.state.imageWidth}
|
||||
|
@ -401,6 +403,13 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
errorMessage: `${err.message}, code ${err.code}`
|
||||
});
|
||||
}
|
||||
// catch(error){
|
||||
// this.setState({
|
||||
// isError: true,
|
||||
// errorTitle: error.title,
|
||||
// errorMessage: error.message,
|
||||
// });
|
||||
// }
|
||||
finally {
|
||||
this.setAutoLabelingStatus(AutoLabelingStatus.done);
|
||||
}
|
||||
|
@ -687,13 +696,14 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
currentAsset.labelData.labelingState = this.state.currentAsset.labelData.labelingState;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentAsset.labelData) {
|
||||
currentAsset.asset.labelingState = currentAsset.labelData.labelingState;
|
||||
} else if (currentAsset.asset.labelingState) {
|
||||
if(currentAsset.labelData?.labelingState!==AssetLabelingState.AutoLabeledAndAdjusted
|
||||
&&(!currentAsset.labelData||currentAsset.labelData.labels?.findIndex(label=>label.value.length>0)<0)){
|
||||
delete currentAsset.labelData?.labelingState;
|
||||
delete currentAsset.asset.labelingState;
|
||||
}
|
||||
|
||||
else {
|
||||
currentAsset.asset.labelingState = currentAsset.labelData.labelingState;
|
||||
}
|
||||
const isLabelChanged = this.compareLabelChanged(_.get(currentAsset, "labelData.labels", []) as ILabel[], _.get(this.state.currentAsset, "labelData.labels", []) as ILabel[]);
|
||||
this.setState({
|
||||
currentAsset,
|
||||
|
@ -1080,7 +1090,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
if (this.props.hoveredLabel) {
|
||||
const label = this.props.hoveredLabel;
|
||||
const id = feature.get("id");
|
||||
if (label.value.find((region) =>
|
||||
if (label.value?.find((region) =>
|
||||
id === this.createRegionIdFromBoundingBox(region.boundingBoxes[0], region.page))) {
|
||||
this.setFeatureProperty(feature, "highlighted", true);
|
||||
}
|
||||
|
@ -1440,9 +1450,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
if (relatedLabels && relatedLabels.confidence) {
|
||||
const originLabel = this.props.selectedAsset!.labelData?.labels?.find(a => a.label === relatedLabels.label);
|
||||
if (originLabel) {
|
||||
delete relatedLabels.confidence;
|
||||
relatedLabels.revised = true;
|
||||
relatedLabels.originValue = [...originLabel.value];
|
||||
if(!relatedLabels.originValue){
|
||||
relatedLabels.originValue = [...originLabel.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1461,9 +1472,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
if (label) {
|
||||
const originLabel = this.props.selectedAsset!.labelData?.labels?.find(a => a.label === tag);
|
||||
if (originLabel && label.confidence && region.changed) {
|
||||
delete label.confidence;
|
||||
label.revised = true;
|
||||
label.originValue = [...originLabel.value];
|
||||
if(!label.originValue){
|
||||
label.originValue = [...originLabel.value];
|
||||
}
|
||||
}
|
||||
if (originLabel && region.changed && label.labelType !== labelType) {
|
||||
label.labelType = labelType;
|
||||
|
@ -1489,12 +1501,10 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
}
|
||||
});
|
||||
});
|
||||
const newLabels = labels.filter(label => label.value.length > 0);
|
||||
const labelData: ILabelData = newLabels.length > 0 ?
|
||||
{
|
||||
document: decodeURIComponent(assetName).split("/").pop(),
|
||||
labels: newLabels,
|
||||
} : null;
|
||||
const labelData:ILabelData={
|
||||
document: decodeURIComponent(assetName).split("/").pop(),
|
||||
labels: [...labels],
|
||||
}
|
||||
return labelData;
|
||||
}
|
||||
|
||||
|
@ -1737,17 +1747,15 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
|
|||
|
||||
}
|
||||
private compareLabelChanged(newLabels: ILabel[], prevLabels: ILabel[]): boolean {
|
||||
if (newLabels.length !== prevLabels.length) {
|
||||
return true;
|
||||
} else if (newLabels.length > 0) {
|
||||
if (newLabels.length > 0) {
|
||||
const newFieldNames = newLabels.map((label) => label.label);
|
||||
const prevFieldNames = prevLabels.map((label) => label.label);
|
||||
if (_.isEqual(newFieldNames.sort(), prevFieldNames.sort())) {
|
||||
for (const name of newFieldNames) {
|
||||
const newValue = newLabels.find(label => label.label === name).value.map(region => region.boundingBoxes).join(",");
|
||||
const prevValue = prevLabels.find(label => label.label === name).value.map(region => region.boundingBoxes).join(",");
|
||||
const newValue = newLabels.find(label => label.label === name).value?.map(region => region.boundingBoxes).join(",");
|
||||
const prevValue = prevLabels.find(label => label.label === name).value?.map(region => region.boundingBoxes).join(",");
|
||||
if (newValue !== prevValue) {
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import * as React from "react";
|
||||
import { CommandBar, ICommandBarItemProps } from "@fluentui/react/lib/CommandBar";
|
||||
import { ICustomizations, Customizer } from "@fluentui/react/lib/Utilities";
|
||||
import { getDarkGreyTheme } from "../../../../common/themes";
|
||||
import { interpolate, strings } from '../../../../common/strings';
|
||||
import { ContextualMenuItemType } from "@fluentui/react";
|
||||
import { IProject, IAssetMetadata, AssetLabelingState } from "../../../../models/applicationState";
|
||||
import {CommandBar, ICommandBarItemProps} from "@fluentui/react/lib/CommandBar";
|
||||
import {ICustomizations, Customizer} from "@fluentui/react/lib/Utilities";
|
||||
import {getDarkGreyTheme} from "../../../../common/themes";
|
||||
import {strings} from '../../../../common/strings';
|
||||
import {ContextualMenuItemType} from "@fluentui/react";
|
||||
import {IProject, IAssetMetadata, AssetLabelingState} from "../../../../models/applicationState";
|
||||
import _ from "lodash";
|
||||
import "./canvasCommandBar.scss";
|
||||
import { constants } from "../../../../common/constants";
|
||||
|
||||
interface ICanvasCommandBarProps {
|
||||
handleZoomIn: () => void;
|
||||
|
@ -19,14 +21,16 @@ interface ICanvasCommandBarProps {
|
|||
handleLayerChange?: (layer: string) => void;
|
||||
handleToggleDrawRegionMode?: () => void;
|
||||
handleAssetDeleted?: () => void;
|
||||
project: IProject;
|
||||
project?: IProject;
|
||||
selectedAsset?: IAssetMetadata;
|
||||
handleRotateImage: (degrees: number) => void;
|
||||
|
||||
drawRegionMode?: boolean;
|
||||
connectionType?: string;
|
||||
layers?: any;
|
||||
parentPage: string;
|
||||
showLayerMenu?: boolean;
|
||||
showActionMenu?: boolean;
|
||||
enableDrawRegion?: boolean;
|
||||
}
|
||||
|
||||
export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> = (props: ICanvasCommandBarProps) => {
|
||||
|
@ -36,7 +40,7 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
},
|
||||
scopedSettings: {},
|
||||
};
|
||||
const disableAutoLabeling = !props.project.predictModelId;
|
||||
const disableAutoLabeling = !props.project?.predictModelId;
|
||||
let disableAutoLabelingCurrentAsset = disableAutoLabeling;
|
||||
if (!disableAutoLabeling) {
|
||||
const labelingState = _.get(props.selectedAsset, "labelData.labelingState");
|
||||
|
@ -46,18 +50,18 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
}
|
||||
|
||||
let commandBarItems: ICommandBarItemProps[] = [];
|
||||
if (props.parentPage === strings.editorPage.title) {
|
||||
commandBarItems = [{
|
||||
if (props.showLayerMenu) {
|
||||
const layerItem: ICommandBarItemProps = {
|
||||
key: "layers",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.text,
|
||||
iconProps: { iconName: "MapLayers" },
|
||||
iconProps: {iconName: "MapLayers"},
|
||||
subMenuProps: {
|
||||
items: [
|
||||
{
|
||||
key: "text",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.text,
|
||||
canCheck: true,
|
||||
iconProps: { iconName: "TextField" },
|
||||
iconProps: {iconName: "TextField"},
|
||||
isChecked: props.layers["text"],
|
||||
onClick: () => props.handleLayerChange("text"),
|
||||
},
|
||||
|
@ -65,7 +69,7 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
key: "table",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.tables,
|
||||
canCheck: true,
|
||||
iconProps: { iconName: "Table" },
|
||||
iconProps: {iconName: "Table"},
|
||||
isChecked: props.layers["tables"],
|
||||
onClick: () => props.handleLayerChange("tables"),
|
||||
},
|
||||
|
@ -73,42 +77,48 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
key: "selectionMark",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.selectionMarks,
|
||||
canCheck: true,
|
||||
iconProps: { iconName: "CheckboxComposite" },
|
||||
iconProps: {iconName: "CheckboxComposite"},
|
||||
isChecked: props.layers["checkboxes"],
|
||||
onClick: () => props.handleLayerChange("checkboxes"),
|
||||
},
|
||||
{
|
||||
key: "DrawnRegions",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.drawnRegions,
|
||||
canCheck: true,
|
||||
iconProps: { iconName: "FieldNotChanged" },
|
||||
isChecked: props.layers["drawnRegions"],
|
||||
className: props.drawRegionMode ? "disabled" : "",
|
||||
onClick: () => props.handleLayerChange("drawnRegions"),
|
||||
disabled: props.drawRegionMode
|
||||
key: "DrawnRegions",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.drawnRegions,
|
||||
canCheck: true,
|
||||
iconProps: {iconName: "FieldNotChanged"},
|
||||
isChecked: props.layers["drawnRegions"],
|
||||
className: props.drawRegionMode ? "disabled" : "",
|
||||
onClick: () => props.handleLayerChange("drawnRegions"),
|
||||
disabled: props.drawRegionMode
|
||||
},
|
||||
{
|
||||
key: "Label",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.labels,
|
||||
canCheck: true,
|
||||
iconProps: { iconName: "Label" },
|
||||
iconProps: {iconName: "Label"},
|
||||
isChecked: props.layers["label"],
|
||||
onClick: () => props.handleLayerChange("label"),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "drawRegion",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion,
|
||||
iconProps: { iconName: "FieldNotChanged" },
|
||||
toggle: true,
|
||||
checked: props.drawRegionMode,
|
||||
className: !props.layers["drawnRegions"] ? "disabled" : "",
|
||||
onClick: () => props.handleToggleDrawRegionMode(),
|
||||
disabled: !props.layers["drawnRegions"],
|
||||
}
|
||||
};
|
||||
commandBarItems = [
|
||||
layerItem,
|
||||
{
|
||||
key: "drawRegion",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion,
|
||||
iconProps: {iconName: "FieldNotChanged"},
|
||||
toggle: true,
|
||||
checked: props.drawRegionMode,
|
||||
className: !props.layers["drawnRegions"] ? "disabled" : "",
|
||||
onClick: () => props.handleToggleDrawRegionMode(),
|
||||
disabled: !props.layers["drawnRegions"],
|
||||
}
|
||||
];
|
||||
if (!props.enableDrawRegion) {
|
||||
layerItem.subMenuProps.items = layerItem.subMenuProps.items.filter(item => item.key !== "DrawnRegions");
|
||||
commandBarItems = [...commandBarItems.filter(item => item.key !== "drawRegion")];
|
||||
}
|
||||
}
|
||||
|
||||
const commandBarFarItems: ICommandBarItemProps[] = [
|
||||
|
@ -118,7 +128,7 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
// This needs an ariaLabel since it's icon-only
|
||||
ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.rotate.counterClockwise,
|
||||
iconOnly: true,
|
||||
iconProps: { iconName: "Rotate90CounterClockwise" },
|
||||
iconProps: {iconName: "Rotate90CounterClockwise"},
|
||||
onClick: () => props.handleRotateImage(-90),
|
||||
},
|
||||
{
|
||||
|
@ -127,8 +137,8 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
// This needs an ariaLabel since it's icon-only
|
||||
ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.rotate.clockwise,
|
||||
iconOnly: true,
|
||||
iconProps: { iconName: "Rotate90Clockwise" },
|
||||
style: { marginRight: "1rem" },
|
||||
iconProps: {iconName: "Rotate90Clockwise"},
|
||||
style: {marginRight: "1rem"},
|
||||
onClick: () => props.handleRotateImage(90),
|
||||
},
|
||||
{
|
||||
|
@ -137,7 +147,7 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
// This needs an ariaLabel since it's icon-only
|
||||
ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.zoom.zoomOut,
|
||||
iconOnly: true,
|
||||
iconProps: { iconName: "ZoomOut" },
|
||||
iconProps: {iconName: "ZoomOut"},
|
||||
onClick: () => props.handleZoomOut(),
|
||||
},
|
||||
{
|
||||
|
@ -146,11 +156,11 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
// This needs an ariaLabel since it's icon-only
|
||||
ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.zoom.zoomIn,
|
||||
iconOnly: true,
|
||||
iconProps: { iconName: "ZoomIn" },
|
||||
iconProps: {iconName: "ZoomIn"},
|
||||
onClick: () => props.handleZoomIn(),
|
||||
}
|
||||
]
|
||||
if (props.parentPage === strings.editorPage.title) {
|
||||
];
|
||||
if (props.showActionMenu) {
|
||||
commandBarFarItems.push({
|
||||
key: "additionalActions",
|
||||
text: "Actions",
|
||||
|
@ -165,21 +175,21 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
{
|
||||
key: "runOcrForCurrentDocument",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnCurrentDocument,
|
||||
iconProps: { iconName: "TextDocument" },
|
||||
onClick: () => { if (props.handleRunOcr) props.handleRunOcr(); },
|
||||
iconProps: {iconName: "TextDocument"},
|
||||
onClick: () => {if (props.handleRunOcr) props.handleRunOcr();},
|
||||
},
|
||||
{
|
||||
key: "runOcrForAllDocuments",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnAllDocuments,
|
||||
iconProps: { iconName: "Documentation" },
|
||||
onClick: () => { if (props.handleRunOcrForAllDocuments) props.handleRunOcrForAllDocuments(); },
|
||||
iconProps: {iconName: "Documentation"},
|
||||
onClick: () => {if (props.handleRunOcrForAllDocuments) props.handleRunOcrForAllDocuments();},
|
||||
},
|
||||
{
|
||||
key: "runAutoLabelingCurrentDocument",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingCurrentDocument,
|
||||
iconProps: { iconName: "Tag" },
|
||||
iconProps: {iconName: "Tag"},
|
||||
disabled: disableAutoLabelingCurrentAsset,
|
||||
title: props.project.predictModelId ? "" :
|
||||
title: props.project?.predictModelId ? "" :
|
||||
strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject,
|
||||
onClick: () => {
|
||||
if (props.handleRunAutoLabelingOnCurrentDocument) props.handleRunAutoLabelingOnCurrentDocument();
|
||||
|
@ -188,9 +198,9 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
{
|
||||
key: "runAutoLabelingOnMultipleUnlabeledDocuments",
|
||||
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnMultipleUnlabeledDocuments,
|
||||
iconProps: { iconName: "Tag" },
|
||||
iconProps: {iconName: "Tag"},
|
||||
disabled: disableAutoLabeling,
|
||||
title: props.project.predictModelId ? "" :
|
||||
title: props.project?.predictModelId ? "" :
|
||||
strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject,
|
||||
onClick: () => {
|
||||
if (props.handleRunAutoLabelingOnMultipleUnlabeledDocuments) props.handleRunAutoLabelingOnMultipleUnlabeledDocuments();
|
||||
|
@ -203,8 +213,8 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
|
|||
{
|
||||
key: "deleteAsset",
|
||||
text: strings.editorPage.asset.delete.title,
|
||||
iconProps: { iconName: "Delete" },
|
||||
onClick: () => { if (props.handleAssetDeleted) props.handleAssetDeleted(); },
|
||||
iconProps: {iconName: "Delete"},
|
||||
onClick: () => {if (props.handleAssetDeleted) props.handleAssetDeleted();},
|
||||
}
|
||||
],
|
||||
},
|
||||
|
|
|
@ -553,8 +553,9 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
|
|||
|
||||
const asset = { ...assetMetadata.asset };
|
||||
|
||||
if (this.isTaggableAssetType(asset)) {
|
||||
asset.state = _.get(assetMetadata, "labelData.labels.length", 0) > 0 ?
|
||||
if (this.isTaggableAssetType(asset)&&asset.state!==AssetState.Tagged) {
|
||||
asset.state = _.get(assetMetadata, "labelData.labels.length", 0) > 0
|
||||
&& assetMetadata.labelData.labels.findIndex(item=>item.value?.length>0)>=0 ?
|
||||
AssetState.Tagged :
|
||||
AssetState.Visited;
|
||||
} else if (asset.state === AssetState.NotVisited) {
|
||||
|
@ -729,7 +730,6 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
|
|||
const ocrService = new OCRService(project);
|
||||
if (this.state.assets) {
|
||||
this.setState({ isRunningOCRs: true });
|
||||
console.log({ isRunningOCRs: true });
|
||||
try {
|
||||
await throttle(
|
||||
constants.maxConcurrentServiceRequests,
|
||||
|
@ -758,7 +758,6 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
|
|||
);
|
||||
} finally {
|
||||
this.setState({ isRunningOCRs: false });
|
||||
console.log({ isRunningOCRs: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import '../../../../assets/sass/theme.scss';
|
||||
@import "../../../../assets/sass/theme.scss";
|
||||
|
||||
.app-homepage {
|
||||
flex-grow: 1;
|
||||
|
@ -13,25 +13,46 @@
|
|||
ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
margin: auto;
|
||||
flex-wrap: wrap;
|
||||
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
flex:1;
|
||||
list-style-type: none;
|
||||
text-align: center;
|
||||
|
||||
min-width: 24%;
|
||||
a {
|
||||
display: inline-block;
|
||||
.title {
|
||||
max-width: 12em;
|
||||
margin: auto;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
&.active, &:hover {
|
||||
&.active,
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-2;
|
||||
text-decoration: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.quickstart{
|
||||
line-height: 2em;
|
||||
.ms-Icon{
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: 1em;
|
||||
color: $lighter-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,4 +63,102 @@
|
|||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
&-open-cloud-project {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 10px 0;
|
||||
background-color: $lighter-1;
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-3;
|
||||
text-decoration: none;
|
||||
}
|
||||
.icon {
|
||||
margin: 10px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
.title {
|
||||
font-size: 1.2em;
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.homepage-modal {
|
||||
z-index: 10!important;
|
||||
.modal-container {
|
||||
height: 90vh !important;
|
||||
width: calc(100% - 32px) !important;
|
||||
max-width: calc(100% - 32px) !important;
|
||||
display: flex;
|
||||
padding: 0 !important;
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
.header {
|
||||
display: flex;
|
||||
.title {
|
||||
margin-left: 1em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
color: "white";
|
||||
}
|
||||
}
|
||||
.body {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
.modal-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
margin: auto;
|
||||
padding: 0;
|
||||
li {
|
||||
list-style-type: none;
|
||||
text-align: center;
|
||||
min-width: 24%;
|
||||
a {
|
||||
display: inline-block;
|
||||
.title {
|
||||
max-width: 12em;
|
||||
margin: auto;
|
||||
font-size: 1.3em;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
&.active,
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: $lighter-2;
|
||||
text-decoration: none;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: 1em;
|
||||
color: $lighter-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.modal-right {
|
||||
flex-basis: 20vw;
|
||||
min-width: 250px;
|
||||
max-width: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import React, { SyntheticEvent } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { FontIcon } from "@fluentui/react";
|
||||
import { strings, interpolate } from "../../../../common/strings";
|
||||
import { getPrimaryRedTheme } from "../../../../common/themes";
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
|
||||
import React, {SyntheticEvent} from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {RouteComponentProps} from "react-router-dom";
|
||||
import {bindActionCreators} from "redux";
|
||||
import {FontIcon} from "@fluentui/react";
|
||||
import {strings, interpolate} from "../../../../common/strings";
|
||||
import {getPrimaryRedTheme} from "../../../../common/themes";
|
||||
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
|
||||
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
|
||||
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
|
||||
import { CloudFilePicker } from "../../common/cloudFilePicker/cloudFilePicker";
|
||||
import {CloudFilePicker} from "../../common/cloudFilePicker/cloudFilePicker";
|
||||
import FilePicker from "../../common/filePicker/filePicker";
|
||||
import CondensedList from "../../common/condensedList/condensedList";
|
||||
import Confirm from "../../common/confirm/confirm";
|
||||
import "./homePage.scss";
|
||||
import RecentProjectItem from "./recentProjectItem";
|
||||
import { constants } from "../../../../common/constants";
|
||||
import {constants} from "../../../../common/constants";
|
||||
import {
|
||||
IApplicationState, IConnection, IProject,
|
||||
ErrorCode, AppError, IAppSettings,
|
||||
} from "../../../../models/applicationState";
|
||||
import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory";
|
||||
import { decryptProject } from "../../../../common/utils";
|
||||
import { toast } from "react-toastify";
|
||||
import { isElectron } from "../../../../common/hostProcess";
|
||||
import {StorageProviderFactory} from "../../../../providers/storage/storageProviderFactory";
|
||||
import {decryptProject} from "../../../../common/utils";
|
||||
import {toast} from "react-toastify";
|
||||
import {isElectron} from "../../../../common/hostProcess";
|
||||
import ProjectService from "../../../../services/projectService";
|
||||
import {HomeProjectView} from "./homeProjectView";
|
||||
|
||||
export interface IHomePageProps extends RouteComponentProps, React.Props<HomePage> {
|
||||
recentProjects: IProject[];
|
||||
|
@ -63,72 +66,105 @@ function mapDispatchToProps(dispatch) {
|
|||
export default class HomePage extends React.Component<IHomePageProps, IHomePageState> {
|
||||
|
||||
public state: IHomePageState = {
|
||||
cloudPickerOpen: false,
|
||||
cloudPickerOpen: false
|
||||
};
|
||||
|
||||
private homeProjectViewRef: React.RefObject<HomeProjectView> = React.createRef();
|
||||
private filePicker: React.RefObject<FilePicker> = React.createRef();
|
||||
private newProjectRef = React.createRef<HTMLAnchorElement>();
|
||||
private deleteConfirmRef = React.createRef<Confirm>();
|
||||
private cloudFilePickerRef = React.createRef<CloudFilePicker>();
|
||||
private importConfirmRef: React.RefObject<Confirm> = React.createRef();
|
||||
|
||||
public async componentDidMount() {
|
||||
this.props.appTitleActions.setTitle("Welcome");
|
||||
this.newProjectRef.current.focus();
|
||||
document.title = strings.homePage.title + " - " + strings.appName;
|
||||
}
|
||||
|
||||
public async componentDidUpdate() {
|
||||
this.newProjectRef.current.focus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="app-homepage" id="pageHome">
|
||||
<div className="app-homepage-main">
|
||||
<ul>
|
||||
<li>
|
||||
{/* eslint-disable-next-line */}
|
||||
<a ref={this.newProjectRef}
|
||||
id="home_newProject"
|
||||
href="#" onClick={this.createNewProject} className="p-5 new-project skipToMainContent" role="button">
|
||||
<FontIcon iconName="AddTo" className="icon-9x" />
|
||||
<div>{strings.homePage.newProject}</div>
|
||||
<li className="p-5">
|
||||
<a id="home_prebuilt"
|
||||
onClick={this.onPrebuiltClicked}
|
||||
className="p-2"
|
||||
role="button">
|
||||
<FontIcon iconName="ContactCard" className="icon-7x" />
|
||||
<div className="title">{strings.homePage.prebuiltPredict.title}</div>
|
||||
<div className="description">{strings.homePage.prebuiltPredict.description}</div>
|
||||
</a>
|
||||
<a className="quickstart"
|
||||
href="https://aka.ms/form-recognizer/pre-built"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<FontIcon iconName="Rocket" />{strings.homePage.quickStartGuide}</a>
|
||||
</li>
|
||||
<li className="p-5">
|
||||
<a onClick={this.onUseLayoutToGetTextAndTAblesClicked}
|
||||
className="p-2"
|
||||
role="button">
|
||||
<FontIcon iconName="KeyPhraseExtraction" className="icon-7x" />
|
||||
<div className="title">{strings.homePage.layoutPredict.title}</div>
|
||||
<div className="description">
|
||||
{strings.homePage.layoutPredict.description}
|
||||
</div>
|
||||
</a>
|
||||
<a className="quickstart"
|
||||
href="https://aka.ms/form-recognizer/layout"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
<FontIcon iconName="Rocket" />{strings.homePage.quickStartGuide}</a>
|
||||
</li>
|
||||
<li className="p-5">
|
||||
<a onClick={this.onTrainAndUseAModelWithLables}
|
||||
className="p-2"
|
||||
role="button">
|
||||
<FontIcon iconName="AddTo" className="icon-7x" />
|
||||
<div className="title">{strings.homePage.trainWithLabels.title}</div>
|
||||
<div className="description">
|
||||
{strings.homePage.trainWithLabels.description}
|
||||
</div>
|
||||
</a>
|
||||
<a className="quickstart"
|
||||
href="https://aka.ms/form-recognizer/custom"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
><FontIcon iconName="Rocket" />{strings.homePage.quickStartGuide}</a>
|
||||
</li>
|
||||
<CloudFilePicker
|
||||
ref={this.cloudFilePickerRef}
|
||||
connections={this.props.connections}
|
||||
onSubmit={(content, sharedToken?) => this.loadSelectedProject(JSON.parse(content), sharedToken)}
|
||||
fileExtension={constants.projectFileExtension}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
{(this.props.recentProjects && this.props.recentProjects.length > 0) &&
|
||||
<div className="app-homepage-recent bg-lighter-1">
|
||||
<div className="app-homepage-open-cloud-project" role="button"
|
||||
onClick={this.createNewProject}>
|
||||
<FontIcon iconName="AddTo" className="icon" />
|
||||
<span className="title">{strings.homePage.newProject}</span>
|
||||
</div>
|
||||
{isElectron() &&
|
||||
<li>
|
||||
<a href="#" className="p-5 file-upload"
|
||||
onClick={() => this.filePicker.current.upload()} >
|
||||
<FontIcon iconName="System" className="icon-9x" />
|
||||
<h6>{strings.homePage.openLocalProject.title}</h6>
|
||||
</a>
|
||||
<>
|
||||
<div className="app-homepage-open-cloud-project" role="button"
|
||||
onClick={() => this.filePicker.current.upload()}>
|
||||
<FontIcon iconName="System" className="icon" />
|
||||
<span className="title">{strings.homePage.openLocalProject.title}</span>
|
||||
</div>
|
||||
<FilePicker ref={this.filePicker}
|
||||
onChange={this.onProjectFileUpload}
|
||||
onError={this.onProjectFileUploadError}
|
||||
accept={[".fott"]}
|
||||
/>
|
||||
</li>
|
||||
</>
|
||||
}
|
||||
<li>
|
||||
{/*Open Cloud Project*/}
|
||||
{/* eslint-disable-next-line */}
|
||||
<a href="#" onClick={this.handleOpenCloudProjectClick}
|
||||
className="p-5 cloud-open-project" role="button">
|
||||
<FontIcon iconName="Cloud" className="icon-9x" />
|
||||
<div>{strings.homePage.openCloudProject.title}</div>
|
||||
</a>
|
||||
<CloudFilePicker
|
||||
ref={this.cloudFilePickerRef}
|
||||
connections={this.props.connections}
|
||||
onSubmit={(content, sharedToken?) => this.loadSelectedProject(JSON.parse(content), sharedToken)}
|
||||
fileExtension={constants.projectFileExtension}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{(this.props.recentProjects && this.props.recentProjects.length > 0) &&
|
||||
<div className="app-homepage-recent bg-lighter-1">
|
||||
<div className="app-homepage-open-cloud-project" role="button"
|
||||
onClick={this.onOpenCloudProjectClick}>
|
||||
<FontIcon iconName="Cloud" className="icon" />
|
||||
<span className="title">{strings.homePage.openCloudProject.title}</span>
|
||||
</div>
|
||||
<CondensedList
|
||||
title={strings.homePage.recentProjects}
|
||||
Component={RecentProjectItem}
|
||||
|
@ -142,6 +178,19 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
message={(project: IProject) => `${strings.homePage.deleteProject.confirmation} ${project.name}?`}
|
||||
confirmButtonTheme={getPrimaryRedTheme()}
|
||||
onConfirm={this.deleteProject} />
|
||||
|
||||
<HomeProjectView
|
||||
ref={this.homeProjectViewRef}
|
||||
recentProjects={this.props.recentProjects}
|
||||
connections={this.props.connections}
|
||||
createNewProject={this.createNewProject}
|
||||
onProjectFileUpload={this.onProjectFileUploadError}
|
||||
onProjectFileUploadError={this.onProjectFileUploadError}
|
||||
onOpenCloudProjectClick={this.onOpenCloudProjectClick}
|
||||
loadSelectedProject={this.loadSelectedProject}
|
||||
freshLoadSelectedProject={this.freshLoadSelectedProject}
|
||||
deleteProject={this.deleteProject}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -153,7 +202,20 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
e.preventDefault();
|
||||
}
|
||||
|
||||
private handleOpenCloudProjectClick = () => {
|
||||
private onPrebuiltClicked = () => {
|
||||
this.props.history.push("/prebuilts-analyze");
|
||||
}
|
||||
|
||||
private onUseLayoutToGetTextAndTAblesClicked = () => {
|
||||
this.props.history.push("/layout-analyze");
|
||||
}
|
||||
|
||||
private onTrainAndUseAModelWithLables = () => {
|
||||
this.homeProjectViewRef.current.open();
|
||||
}
|
||||
|
||||
private onOpenCloudProjectClick = () => {
|
||||
this.homeProjectViewRef.current.close();
|
||||
this.cloudFilePickerRef.current.open();
|
||||
}
|
||||
|
||||
|
@ -165,10 +227,10 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
}
|
||||
} catch (error) {
|
||||
if (error instanceof AppError && error.errorCode === ErrorCode.SecurityTokenNotFound) {
|
||||
toast.error(strings.errors.securityTokenNotFound.message, { autoClose: 5000 });
|
||||
toast.error(strings.errors.securityTokenNotFound.message, {autoClose: 5000});
|
||||
}
|
||||
if(error instanceof AppError && error.errorCode === ErrorCode.ProjectInvalidSecurityToken) {
|
||||
toast.error(strings.errors.projectInvalidSecurityToken.message, { autoClose: 5000 });
|
||||
if (error instanceof AppError && error.errorCode === ErrorCode.ProjectInvalidSecurityToken) {
|
||||
toast.error(strings.errors.projectInvalidSecurityToken.message, {autoClose: 5000});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -179,7 +241,7 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
.find((securityToken) => securityToken.name === project.securityToken);
|
||||
|
||||
if (!projectToken) {
|
||||
toast.error(strings.errors.securityTokenNotFound.message, { autoClose: 3000 });
|
||||
toast.error(strings.errors.securityTokenNotFound.message, {autoClose: 3000});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -204,12 +266,12 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
throw err;
|
||||
}
|
||||
}
|
||||
const selectedProject = { ...JSON.parse(projectStr), sourceConnection: project.sourceConnection };
|
||||
const selectedProject = {...JSON.parse(projectStr), sourceConnection: project.sourceConnection};
|
||||
await this.loadSelectedProject(selectedProject);
|
||||
} catch (err) {
|
||||
if (err instanceof AppError && err.errorCode === ErrorCode.BlobContainerIONotFound) {
|
||||
const reason = interpolate(strings.errors.projectNotFound.message, { file: `${project.name}${constants.projectFileExtension}`, container: project.sourceConnection.name });
|
||||
toast.error(reason, { autoClose: false });
|
||||
const reason = interpolate(strings.errors.projectNotFound.message, {file: `${project.name}${constants.projectFileExtension}`, container: project.sourceConnection.name});
|
||||
toast.error(reason, {autoClose: false});
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
|
@ -220,10 +282,10 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
|
|||
try {
|
||||
await this.props.actions.deleteProject(project);
|
||||
} catch (error) {
|
||||
if(error instanceof AppError && error.errorCode === ErrorCode.SecurityTokenNotFound){
|
||||
toast.error(error.message, {autoClose:false});
|
||||
if (error instanceof AppError && error.errorCode === ErrorCode.SecurityTokenNotFound) {
|
||||
toast.error(error.message, {autoClose: false});
|
||||
}
|
||||
else{
|
||||
else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import {Customizer, FontIcon, ICustomizations, Modal} from '@fluentui/react';
|
||||
import React, {SyntheticEvent} from 'react';
|
||||
import {ModalHeader} from 'reactstrap';
|
||||
import {isElectron} from "../../../../common/hostProcess";
|
||||
import {strings} from '../../../../common/strings';
|
||||
import {getDarkGreyTheme, getPrimaryRedTheme} from '../../../../common/themes';
|
||||
import {IConnection, IProject} from '../../../../models/applicationState';
|
||||
import CondensedList from '../../common/condensedList/condensedList';
|
||||
import Confirm from '../../common/confirm/confirm';
|
||||
import FilePicker from '../../common/filePicker/filePicker';
|
||||
import RecentProjectItem from './recentProjectItem';
|
||||
|
||||
interface IHomeProjectViewProps {
|
||||
recentProjects: IProject[];
|
||||
connections: IConnection[];
|
||||
|
||||
createNewProject(e: SyntheticEvent): void;
|
||||
onProjectFileUpload(e, project): void;
|
||||
onProjectFileUploadError(e, error: any): void;
|
||||
onOpenCloudProjectClick(): void;
|
||||
loadSelectedProject(project: IProject, sharedToken?: {}): void;
|
||||
freshLoadSelectedProject(project: IProject): void;
|
||||
deleteProject(project: IProject): void;
|
||||
}
|
||||
|
||||
interface IHomeProjectViewState {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export class HomeProjectView extends React.Component<Partial<IHomeProjectViewProps>, IHomeProjectViewState>{
|
||||
state = {isOpen: false};
|
||||
|
||||
open = () => {
|
||||
this.setState({isOpen: true});
|
||||
}
|
||||
close = () => {
|
||||
this.setState({isOpen: false});
|
||||
}
|
||||
|
||||
private filePicker: React.RefObject<FilePicker> = React.createRef();
|
||||
private newProjectRef = React.createRef<HTMLAnchorElement>();
|
||||
private deleteConfirmRef: React.RefObject<Confirm> = React.createRef();
|
||||
|
||||
|
||||
componentDidMount() {
|
||||
this.newProjectRef.current?.focus();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.newProjectRef.current?.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const dark: ICustomizations = {
|
||||
settings: {
|
||||
theme: getDarkGreyTheme(),
|
||||
},
|
||||
scopedSettings: {},
|
||||
};
|
||||
const closeBtn = <button className="close" onClick={this.close}>×</button>;
|
||||
return (
|
||||
<>
|
||||
<Customizer {...dark}>
|
||||
<Modal
|
||||
className="homepage-modal"
|
||||
isModeless={false}
|
||||
isOpen={this.state.isOpen}
|
||||
isBlocking={false}
|
||||
containerClassName="modal-container"
|
||||
scrollableContentClassName="modal-content"
|
||||
>
|
||||
<ModalHeader toggle={this.close} close={closeBtn}>
|
||||
{strings.homePage.homeProjectView.title}
|
||||
</ModalHeader>
|
||||
<div className="body">
|
||||
<div className="modal-left">
|
||||
<ul>
|
||||
<li>
|
||||
<a ref={this.newProjectRef}
|
||||
id="home_newProject"
|
||||
href="#" onClick={this.props.createNewProject} className="p-5 new-project skipToMainContent" role="button">
|
||||
<FontIcon iconName="AddTo" className="icon-9x" />
|
||||
<div className="title">{strings.homePage.newProject}</div>
|
||||
</a>
|
||||
</li>
|
||||
{isElectron() &&
|
||||
<li>
|
||||
<a href="#" className="p-5 file-upload"
|
||||
onClick={() => this.filePicker.current.upload()} >
|
||||
<FontIcon iconName="System" className="icon-9x" />
|
||||
<div className="title">{strings.homePage.openLocalProject.title}</div>
|
||||
</a>
|
||||
<FilePicker ref={this.filePicker}
|
||||
onChange={this.props.onProjectFileUpload}
|
||||
onError={this.props.onProjectFileUploadError}
|
||||
accept={[".fott"]}
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
{/*Open Cloud Project*/}
|
||||
{/* eslint-disable-next-line */}
|
||||
<a href="#" onClick={this.props.onOpenCloudProjectClick}
|
||||
className="p-5 cloud-open-project" role="button">
|
||||
<FontIcon iconName="Cloud" className="icon-9x" />
|
||||
<div className="title">{strings.homePage.openCloudProject.title}</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{(this.props.recentProjects && this.props.recentProjects.length > 0) &&
|
||||
<div className="modal-right bg-lighter-1">
|
||||
<CondensedList
|
||||
title={strings.homePage.recentProjects}
|
||||
Component={RecentProjectItem}
|
||||
items={this.props.recentProjects}
|
||||
onClick={this.props.freshLoadSelectedProject}
|
||||
onDelete={(project) => this.deleteConfirmRef.current.open(project)} />
|
||||
</div>
|
||||
|
||||
}
|
||||
</div>
|
||||
<Confirm title="Delete Project"
|
||||
ref={this.deleteConfirmRef as any}
|
||||
message={(project: IProject) => `${strings.homePage.deleteProject.confirmation} ${project.name}?`}
|
||||
confirmButtonTheme={getPrimaryRedTheme()}
|
||||
onConfirm={this.props.deleteProject} />
|
||||
</Modal>
|
||||
</Customizer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -172,7 +172,6 @@ h4 {
|
|||
}
|
||||
|
||||
.modal-confirm {
|
||||
//display: flex;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import { ViewSelection } from "./viewSelection";
|
|||
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
|
||||
import allSettled from "promise.allsettled";
|
||||
import { toast } from 'react-toastify';
|
||||
import Alert from "../../common/alert/alert";
|
||||
|
||||
export interface IModelComposePageProps extends RouteComponentProps, React.Props<ModelComposePage> {
|
||||
recentProjects: IProject[];
|
||||
|
@ -65,6 +66,10 @@ export interface IModelComposePageState {
|
|||
isLoading: boolean;
|
||||
refreshFlag: boolean;
|
||||
hasText: boolean;
|
||||
|
||||
isError?: boolean;
|
||||
errorTitle?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IModel {
|
||||
|
@ -344,6 +349,16 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
onComposeConfirm={this.onComposeConfirm}
|
||||
addToRecentModels={this.addToRecentModels}
|
||||
/>
|
||||
<Alert
|
||||
show={this.state.isError}
|
||||
title={this.state.errorTitle || "Error"}
|
||||
message={this.state.errorMessage}
|
||||
onClose={() => this.setState({
|
||||
isError: false,
|
||||
errorTitle: undefined,
|
||||
errorMessage: undefined,
|
||||
})}
|
||||
/>
|
||||
</Customizer>
|
||||
</Fabric>
|
||||
<PreventLeaving
|
||||
|
@ -428,7 +443,11 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorTitle: error.title,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -506,7 +525,11 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
this.setState({
|
||||
isError: true,
|
||||
errorTitle: error.title,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -548,7 +571,7 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
this.setState({
|
||||
isLoading: false,
|
||||
});
|
||||
ServiceHelper.handleServiceError(err);
|
||||
ServiceHelper.handleServiceError({...err, endpoint: baseURL});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -602,9 +625,8 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
} else if (b.modelName) {
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
})
|
||||
)
|
||||
} else {
|
||||
|
@ -774,7 +796,7 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
|
|||
this.props.project.apiKey as string,
|
||||
);
|
||||
} catch (err) {
|
||||
ServiceHelper.handleServiceError(err);
|
||||
ServiceHelper.handleServiceError({...err, endpoint: baseURL});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
import _ from "lodash";
|
||||
import pdfjsLib from "pdfjs-dist";
|
||||
import {constants} from "../../../../common/constants";
|
||||
import HtmlFileReader from "../../../../common/htmlFileReader";
|
||||
import {loadImageToCanvas, parseTiffData, renderTiffToCanvas} from "../../../../common/utils";
|
||||
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version);
|
||||
const cMapUrl = constants.pdfjsCMapUrl(pdfjsLib.version);
|
||||
|
||||
export interface ILoadFileResult {
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
imageUri: string,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
|
||||
shouldShowAlert: boolean;
|
||||
invalidFileFormat?: boolean;
|
||||
alertTitle: string;
|
||||
alertMessage: string;
|
||||
}
|
||||
export interface ILoadFileHelper {
|
||||
loadFile(file: File): Promise<Partial<ILoadFileResult>>;
|
||||
loadPage(pageNumber: number): Promise<Partial<ILoadFileResult>>;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export class LoadFileHelper implements ILoadFileHelper {
|
||||
currPdf: any;
|
||||
tiffImages: any[];
|
||||
|
||||
async loadFile(file: File): Promise<Partial<ILoadFileResult>> {
|
||||
if (!file) {
|
||||
// no file
|
||||
return;
|
||||
}
|
||||
this.reset();
|
||||
|
||||
// determine how to load file based on MIME type of the file
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
|
||||
switch (file.type) {
|
||||
case "image/jpeg":
|
||||
case "image/png":
|
||||
return await this.loadImageFile(file);
|
||||
case "image/tiff":
|
||||
return await this.loadTiffFile(file);
|
||||
case "application/pdf":
|
||||
return await this.loadPdfFile(file);
|
||||
default:
|
||||
// un-supported file type
|
||||
return {
|
||||
imageUri: "",
|
||||
shouldShowAlert: true,
|
||||
invalidFileFormat: true,
|
||||
alertTitle: "Not supported file type",
|
||||
alertMessage: "Sorry, we currently only support JPG/PNG/PDF files.",
|
||||
};
|
||||
}
|
||||
}
|
||||
private createObjectURL = (object: File) => {
|
||||
// generate a URL for the object
|
||||
return (window.URL) ? window.URL.createObjectURL(object) : "";
|
||||
}
|
||||
|
||||
private loadImageFile = async (file: File) => {
|
||||
const imageUri = this.createObjectURL(file);
|
||||
const canvas = await loadImageToCanvas(imageUri);
|
||||
return ({
|
||||
currentPage: 1,
|
||||
numPages: 1,
|
||||
imageUri: canvas.toDataURL(constants.convertedImageFormat, constants.convertedImageQuality),
|
||||
imageWidth: canvas.width,
|
||||
imageHeight: canvas.height,
|
||||
fileLoaded: true,
|
||||
});
|
||||
}
|
||||
|
||||
private loadTiffFile = async (file) => {
|
||||
const fileArrayBuffer = await HtmlFileReader.readFileAsArrayBuffer(file);
|
||||
this.tiffImages = parseTiffData(fileArrayBuffer);
|
||||
return this.loadTiffPage(1);
|
||||
}
|
||||
|
||||
public reset = () => {
|
||||
this.currPdf = null;
|
||||
this.tiffImages = [];
|
||||
}
|
||||
|
||||
public loadPage = async (pageNumber: number) => {
|
||||
if (this.currPdf) {
|
||||
return this.loadPdfPage(pageNumber);
|
||||
} else if (this.tiffImages?.length > 0) {
|
||||
return this.loadTiffPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private loadTiffPage = async (pageNumber: number) => {
|
||||
const tiffImage = this.tiffImages[pageNumber - 1];
|
||||
const canvas = renderTiffToCanvas(tiffImage);
|
||||
return ({
|
||||
currentPage: pageNumber,
|
||||
numPages: this.getPageCount(),
|
||||
imageUri: canvas.toDataURL(constants.convertedImageFormat, constants.convertedImageQuality),
|
||||
imageWidth: tiffImage.width,
|
||||
imageHeight: tiffImage.height,
|
||||
fileLoaded: true,
|
||||
});
|
||||
}
|
||||
|
||||
private loadPdfFile = async (file): Promise<Partial<ILoadFileResult>> => {
|
||||
const fileReader: FileReader = new FileReader();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileReader.onload = (e: any) => {
|
||||
const typedArray = new Uint8Array(e.target.result);
|
||||
const loadingTask = pdfjsLib.getDocument({data: typedArray, cMapUrl, cMapPacked: true});
|
||||
loadingTask.promise.then(async (pdf) => {
|
||||
this.currPdf = pdf;
|
||||
resolve(await this.loadPdfPage(1));
|
||||
}, (reason) => {
|
||||
resolve({
|
||||
shouldShowAlert: true,
|
||||
alertTitle: "Failed loading PDF",
|
||||
alertMessage: reason.toString(),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
||||
|
||||
public getPageCount() {
|
||||
if (this.currPdf !== null) {
|
||||
return _.get(this.currPdf, "numPages", 1);
|
||||
} else if (this.tiffImages.length !== 0) {
|
||||
return this.tiffImages.length;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
private loadPdfPage = async (pageNumber) => {
|
||||
const page = await this.currPdf.getPage(pageNumber);
|
||||
const defaultScale = 2;
|
||||
const viewport = page.getViewport({scale: defaultScale});
|
||||
|
||||
// Prepare canvas using PDF page dimensions
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d");
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
// Render PDF page into canvas context
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
};
|
||||
|
||||
const renderTask = page.render(renderContext);
|
||||
await renderTask.promise;
|
||||
return ({
|
||||
currentPage: pageNumber,
|
||||
numPages: this.getPageCount(),
|
||||
imageUri: canvas.toDataURL(constants.convertedImageFormat, constants.convertedImageQuality),
|
||||
imageWidth: canvas.width,
|
||||
imageHeight: canvas.height,
|
||||
fileLoaded: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {Feature} from "ol";
|
||||
import Polygon from "ol/geom/Polygon";
|
||||
import {ImageMap} from "../../common/imageMap/imageMap";
|
||||
|
||||
export interface ILayoutHelper {
|
||||
setImageMap(imageMap: ImageMap): void;
|
||||
setLayoutData(ocr: any): void;
|
||||
drawLayout(targetPage: number): void;
|
||||
reset(): void;
|
||||
getOcrResultForPage(targetPage: number): any;
|
||||
}
|
||||
|
||||
export class LayoutHelper implements ILayoutHelper {
|
||||
private imageMap: ImageMap;
|
||||
private layoutData: any;
|
||||
private regionOrders: Record<string, number>[] = [];
|
||||
private regionOrderById: string[][] = [];
|
||||
|
||||
setImageMap(imageMap: ImageMap) {
|
||||
this.imageMap = imageMap;
|
||||
}
|
||||
|
||||
setLayoutData(data: any) {
|
||||
this.layoutData = data;
|
||||
this.buildRegionOrders();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.layoutData = null;
|
||||
this.regionOrderById = [];
|
||||
this.regionOrders = [];
|
||||
this.imageMap?.removeAllFeatures();
|
||||
}
|
||||
|
||||
private buildRegionOrders() {
|
||||
// Build order index here instead of building it during 'drawOcr' for two reasons.
|
||||
// 1. Build order index for all pages at once. This allow us to support cross page
|
||||
// tagging if it's supported by FR service.
|
||||
// 2. Avoid rebuilding order index when users switch back and forth between pages.
|
||||
const ocrs = this.layoutData;
|
||||
const ocrReadResults = (ocrs.recognitionResults || (ocrs.analyzeResult && ocrs.analyzeResult.readResults));
|
||||
const ocrPageResults = (ocrs.recognitionResults || (ocrs.analyzeResult && ocrs.analyzeResult.pageResults));
|
||||
const imageExtent = this.imageMap.getImageExtent();
|
||||
ocrReadResults.forEach((ocr) => {
|
||||
const ocrExtent = [0, 0, ocr.width, ocr.height];
|
||||
const pageIndex = ocr.page - 1;
|
||||
this.regionOrders[pageIndex] = {};
|
||||
this.regionOrderById[pageIndex] = [];
|
||||
let order = 0;
|
||||
if (ocr.lines) {
|
||||
ocr.lines.forEach((line) => {
|
||||
if (line.words) {
|
||||
line.words.forEach((word) => {
|
||||
if (this.shouldDisplayOcrWord(word.text)) {
|
||||
const feature = this.createBoundingBoxVectorFeature(
|
||||
word.text, word.boundingBox, imageExtent, ocrExtent, ocr.page);
|
||||
this.regionOrders[pageIndex][feature.getId()] = order++;
|
||||
this.regionOrderById[pageIndex].push(feature.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
const checkboxes = ocr.selectionMarks
|
||||
|| (ocrPageResults && ocrPageResults[pageIndex] && ocrPageResults[pageIndex].checkboxes);
|
||||
if (checkboxes) {
|
||||
this.addCheckboxToRegionOrder(checkboxes, pageIndex, order, imageExtent, ocrExtent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public drawLayout(targetPage: number) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
|
||||
const ocrForCurrentPage = this.getOcrResultForPage(targetPage);
|
||||
const textFeatures = [];
|
||||
|
||||
const checkboxFeatures = [];
|
||||
const ocrReadResults = ocrForCurrentPage["readResults"];
|
||||
const ocrPageResults = ocrForCurrentPage["pageResults"];
|
||||
const imageExtent = this.imageMap.getImageExtent();
|
||||
if (ocrReadResults) {
|
||||
const ocrExtent = [0, 0, ocrReadResults.width, ocrReadResults.height];
|
||||
if (ocrReadResults.lines) {
|
||||
ocrReadResults.lines.forEach((line) => {
|
||||
if (line.words) {
|
||||
line.words.forEach((word) => {
|
||||
if (this.shouldDisplayOcrWord(word.text)) {
|
||||
textFeatures.push(this.createBoundingBoxVectorFeature(
|
||||
word.text, word.boundingBox, imageExtent, ocrExtent, ocrReadResults.page));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (ocrReadResults && ocrReadResults.selectionMarks) {
|
||||
ocrReadResults.selectionMarks.forEach((checkbox) => {
|
||||
checkboxFeatures.push(this.createBoundingBoxVectorFeature(
|
||||
checkbox.state, checkbox.boundingBox, imageExtent, ocrExtent, ocrReadResults.page));
|
||||
});
|
||||
} else if (ocrPageResults && ocrPageResults.checkboxes) {
|
||||
ocrPageResults.checkboxes.forEach((checkbox) => {
|
||||
checkboxFeatures.push(this.createBoundingBoxVectorFeature(
|
||||
checkbox.state, checkbox.boundingBox, imageExtent, ocrExtent, ocrPageResults.page));
|
||||
});
|
||||
}
|
||||
|
||||
if (textFeatures.length > 0) {
|
||||
this.imageMap.addFeatures(textFeatures);
|
||||
}
|
||||
if (checkboxFeatures.length > 0) {
|
||||
this.imageMap.addCheckboxFeatures(checkboxFeatures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getOcrResultForPage = (targetPage: number): any => {
|
||||
if (!this.layoutData) {
|
||||
return {};
|
||||
}
|
||||
if (this.layoutData.analyzeResult?.readResults) {
|
||||
// OCR schema with analyzeResult/readResults property
|
||||
const ocrResultsForCurrentPage = {};
|
||||
if (this.layoutData.analyzeResult.pageResults) {
|
||||
ocrResultsForCurrentPage["pageResults"] = this.layoutData.analyzeResult.pageResults[targetPage - 1];
|
||||
}
|
||||
ocrResultsForCurrentPage["readResults"] = this.layoutData.analyzeResult.readResults[targetPage - 1];
|
||||
return ocrResultsForCurrentPage;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private createBoundingBoxVectorFeature = (text, boundingBox, imageExtent, ocrExtent, page) => {
|
||||
const coordinates: any[] = [];
|
||||
const polygonPoints: number[] = [];
|
||||
const imageWidth = imageExtent[2] - imageExtent[0];
|
||||
const imageHeight = imageExtent[3] - imageExtent[1];
|
||||
const ocrWidth = ocrExtent[2] - ocrExtent[0];
|
||||
const ocrHeight = ocrExtent[3] - ocrExtent[1];
|
||||
|
||||
for (let i = 0; i < boundingBox.length; i += 2) {
|
||||
// An array of numbers representing an extent: [minx, miny, maxx, maxy]
|
||||
coordinates.push([
|
||||
Math.round((boundingBox[i] / ocrWidth) * imageWidth),
|
||||
Math.round((1 - (boundingBox[i + 1] / ocrHeight)) * imageHeight),
|
||||
]);
|
||||
polygonPoints.push(boundingBox[i] / ocrWidth);
|
||||
polygonPoints.push(boundingBox[i + 1] / ocrHeight);
|
||||
}
|
||||
|
||||
const featureId = this.createRegionIdFromBoundingBox(polygonPoints, page);
|
||||
const feature = new Feature({
|
||||
geometry: new Polygon([coordinates]),
|
||||
});
|
||||
feature.setProperties({
|
||||
id: featureId,
|
||||
text,
|
||||
boundingbox: boundingBox,
|
||||
highlighted: false,
|
||||
isOcrProposal: true,
|
||||
});
|
||||
feature.setId(featureId);
|
||||
|
||||
return feature;
|
||||
}
|
||||
|
||||
private createRegionIdFromBoundingBox = (boundingBox: number[], page: number): string => {
|
||||
return boundingBox.join(",") + ":" + page;
|
||||
}
|
||||
|
||||
|
||||
private shouldDisplayOcrWord = (text: string): boolean => {
|
||||
const regex = new RegExp(/^[_]+$/);
|
||||
return !text.match(regex);
|
||||
}
|
||||
|
||||
private addCheckboxToRegionOrder = (
|
||||
checkboxes: any[],
|
||||
pageIndex: number,
|
||||
order: number,
|
||||
imageExtent: number[],
|
||||
ocrExtent: any[]) => {
|
||||
checkboxes.forEach((checkbox) => {
|
||||
const checkboxFeature = this.createBoundingBoxVectorFeature(
|
||||
checkbox.state, checkbox.boundingBox, imageExtent, ocrExtent, pageIndex + 1);
|
||||
this.regionOrders[pageIndex][checkboxFeature.getId()] = order++;
|
||||
this.regionOrderById[pageIndex].push(checkboxFeature.getId());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,559 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {
|
||||
FontIcon,
|
||||
IconButton,
|
||||
ITooltipHostStyles,
|
||||
PrimaryButton,
|
||||
Spinner,
|
||||
SpinnerSize,
|
||||
TooltipHost
|
||||
} from "@fluentui/react";
|
||||
import Fill from "ol/style/Fill";
|
||||
import Icon from "ol/style/Icon";
|
||||
import Stroke from "ol/style/Stroke";
|
||||
import Style from "ol/style/Style";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {RouteComponentProps} from "react-router-dom";
|
||||
import {bindActionCreators} from "redux";
|
||||
import url from "url";
|
||||
import {constants} from "../../../../common/constants";
|
||||
import {interpolate, strings} from "../../../../common/strings";
|
||||
import {getPrimaryGreenTheme, getPrimaryWhiteTheme} from "../../../../common/themes";
|
||||
import {downloadAsJsonFile, poll} from "../../../../common/utils";
|
||||
import {
|
||||
ErrorCode,
|
||||
IApplicationState,
|
||||
IPrebuiltSettings
|
||||
} from "../../../../models/applicationState";
|
||||
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
|
||||
import IAppPrebuiltSettingsActions, * as appPrebuiltSettingsActions from "../../../../redux/actions/prebuiltSettingsActions";
|
||||
import ServiceHelper from "../../../../services/serviceHelper";
|
||||
import Alert from "../../common/alert/alert";
|
||||
import {DocumentFilePicker} from "../../common/documentFilePicker/documentFilePicker";
|
||||
import {ImageMap} from "../../common/imageMap/imageMap";
|
||||
import {PrebuiltSetting} from "../../common/prebuiltSetting/prebuiltSetting";
|
||||
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
|
||||
import {CanvasCommandBar} from "../editorPage/canvasCommandBar";
|
||||
import {TableView} from "../editorPage/tableView";
|
||||
import {ILayoutHelper, LayoutHelper} from "./layoutHelper";
|
||||
import {ILoadFileHelper, LoadFileHelper} from "./LoadFileHelper";
|
||||
import {ITableHelper, ITableState, TableHelper} from "./tableHelper";
|
||||
|
||||
interface ILayoutPredictPageProps extends RouteComponentProps {
|
||||
prebuiltSettings: IPrebuiltSettings;
|
||||
appTitleActions: IAppTitleActions;
|
||||
actions: IAppPrebuiltSettingsActions;
|
||||
}
|
||||
|
||||
interface ILayoutPredictPageState extends ITableState {
|
||||
layers: any;
|
||||
|
||||
imageUri: string;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
currentPage: number;
|
||||
numPages: number;
|
||||
|
||||
shouldShowAlert: boolean;
|
||||
alertTitle: string;
|
||||
alertMessage: string;
|
||||
invalidFileFormat?: boolean;
|
||||
|
||||
fileLabel: string;
|
||||
file?: File;
|
||||
isFetching?: boolean;
|
||||
fileLoaded?: boolean;
|
||||
|
||||
isAnalyzing: boolean;
|
||||
analyzationLoaded: boolean;
|
||||
fetchedFileURL: string;
|
||||
layoutData: any;
|
||||
imageAngle: number;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: IApplicationState) {
|
||||
return {
|
||||
prebuiltSettings: state.prebuiltSettings
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
appTitleActions: bindActionCreators(appTitleActions, dispatch),
|
||||
actions: bindActionCreators(appPrebuiltSettingsActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export class LayoutPredictPage extends React.Component<Partial<ILayoutPredictPageProps>, ILayoutPredictPageState>{
|
||||
private layoutHelper: ILayoutHelper = new LayoutHelper();
|
||||
private tableHelper: ITableHelper = new TableHelper(this);
|
||||
|
||||
state: ILayoutPredictPageState = {
|
||||
imageUri: null,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
currentPage: 1,
|
||||
numPages: 1,
|
||||
|
||||
shouldShowAlert: false,
|
||||
alertTitle: "",
|
||||
alertMessage: "",
|
||||
|
||||
fileLabel: "",
|
||||
|
||||
isAnalyzing: false,
|
||||
analyzationLoaded: false,
|
||||
fetchedFileURL: "",
|
||||
layoutData: null,
|
||||
imageAngle: 0,
|
||||
|
||||
layers: {text: true, tables: true, checkboxes: true, label: true, drawnRegions: true},
|
||||
|
||||
tableIconTooltip: {display: "none", width: 0, height: 0, top: 0, left: 0},
|
||||
hoveringFeature: null,
|
||||
tableToView: null,
|
||||
tableToViewId: null,
|
||||
};
|
||||
|
||||
private imageMap: ImageMap;
|
||||
private fileHelper: ILoadFileHelper = new LoadFileHelper();
|
||||
|
||||
componentDidMount() {
|
||||
document.title = strings.layoutPredict.title + " - " + strings.appName;
|
||||
this.props.appTitleActions.setTitle(strings.layoutPredict.title);
|
||||
}
|
||||
componentDidUpdate(_prevProps: ILayoutPredictPageProps, prevState: ILayoutPredictPageState) {
|
||||
if (this.state.file) {
|
||||
if (!this.state.fileLoaded && !this.state.isFetching) {
|
||||
this.loadFile(this.state.file);
|
||||
} else if (this.state.fileLoaded && prevState.currentPage !== this.state.currentPage) {
|
||||
this.fileHelper.loadPage(this.state.currentPage).then((res: any) => {
|
||||
if (res) {
|
||||
this.setState({...res});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadFile = (file: File) => {
|
||||
this.setState({isFetching: true});
|
||||
this.fileHelper.loadFile(file)
|
||||
.then((res: any) => {
|
||||
if (res) {
|
||||
this.setState({
|
||||
...res,
|
||||
isFetching: false,
|
||||
fileLoaded: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
const analyzeDisabled: boolean = this.state.isFetching || !this.state.file
|
||||
|| this.state.invalidFileFormat ||
|
||||
!this.state.fileLoaded ||
|
||||
this.state.isAnalyzing ||
|
||||
!this.props.prebuiltSettings?.apiKey ||
|
||||
!this.props.prebuiltSettings?.serviceURI;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="predict skipToMainContent"
|
||||
id="pagePredict"
|
||||
style={{display: "flex"}} >
|
||||
<div className="predict-main">
|
||||
{this.state.file && this.state.imageUri && this.renderImageMap()}
|
||||
{this.renderPrevPageButton()}
|
||||
{this.renderNextPageButton()}
|
||||
{this.renderPageIndicator()}
|
||||
</div>
|
||||
<div className="predict-sidebar bg-lighter-1">
|
||||
<div className="condensed-list">
|
||||
<h6 className="condensed-list-header bg-darker-2 p-2 flex-center">
|
||||
<FontIcon className="mr-1" iconName="KeyPhraseExtraction" />
|
||||
<span>Layout</span>
|
||||
</h6>
|
||||
<PrebuiltSetting prebuiltSettings={this.props.prebuiltSettings}
|
||||
disabled={this.state.isFetching || this.state.isAnalyzing}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<div className="p-3" style={{marginTop: "8px"}}>
|
||||
<h5>Upload file and run layout</h5>
|
||||
<DocumentFilePicker
|
||||
disabled={this.state.isFetching || this.state.isAnalyzing}
|
||||
onFileChange={(data) => this.onFileChange(data)}
|
||||
onSelectSourceChange={() => this.onSelectSourceChange()}
|
||||
onError={(err) => this.onFileLoadError(err)} />
|
||||
<div className="container-items-end predict-button">
|
||||
<PrimaryButton
|
||||
theme={getPrimaryWhiteTheme()}
|
||||
iconProps={{iconName: "KeyPhraseExtraction"}}
|
||||
text="Run Layout"
|
||||
aria-label={!this.state.analyzationLoaded ? strings.layoutPredict.inProgress : ""}
|
||||
allowDisabledFocus
|
||||
disabled={analyzeDisabled}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isFetching &&
|
||||
<div className="loading-container">
|
||||
<Spinner
|
||||
label="Fetching..."
|
||||
ariaLive="assertive"
|
||||
labelPosition="right"
|
||||
size={SpinnerSize.large}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.isAnalyzing &&
|
||||
<div className="loading-container">
|
||||
<Spinner
|
||||
label={strings.layoutPredict.inProgress}
|
||||
ariaLive="assertive"
|
||||
labelPosition="right"
|
||||
size={SpinnerSize.large}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.layoutData && !this.state.isAnalyzing &&
|
||||
<div className="container-items-center container-space-between results-container">
|
||||
<h5 className="results-header">Layout results</h5>
|
||||
<PrimaryButton
|
||||
className="align-self-end keep-button-80px"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
text="Download"
|
||||
allowDisabledFocus
|
||||
autoFocus={true}
|
||||
onClick={this.onDownloadClick}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
show={this.state.shouldShowAlert}
|
||||
title={this.state.alertTitle}
|
||||
message={this.state.alertMessage}
|
||||
onClose={() => this.setState({
|
||||
shouldShowAlert: false,
|
||||
alertTitle: "",
|
||||
alertMessage: "",
|
||||
analyzationLoaded: true
|
||||
})}
|
||||
/>
|
||||
<PreventLeaving
|
||||
when={this.state.isAnalyzing}
|
||||
message={"A prediction operation is currently in progress, are you sure you want to leave?"}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
onDownloadClick = () => {
|
||||
const {layoutData} = this.state;
|
||||
if (layoutData) {
|
||||
downloadAsJsonFile(layoutData, this.state.fileLabel, "Layout-");
|
||||
}
|
||||
}
|
||||
|
||||
onFileChange(data: {
|
||||
file: File,
|
||||
fileLabel: string,
|
||||
fetchedFileURL: string
|
||||
}): void {
|
||||
this.setState({
|
||||
currentPage: 1,
|
||||
layoutData: null,
|
||||
...data,
|
||||
analyzationLoaded: false,
|
||||
fileLoaded: false,
|
||||
}, () => {
|
||||
this.layoutHelper?.reset();
|
||||
});
|
||||
}
|
||||
|
||||
onSelectSourceChange(): void {
|
||||
this.setState({
|
||||
file: undefined,
|
||||
layoutData: null,
|
||||
analyzationLoaded: false,
|
||||
}, () => {
|
||||
this.layoutHelper.reset();
|
||||
});
|
||||
}
|
||||
|
||||
onFileLoadError(err: {alertTitle: string; alertMessage: string;}): void {
|
||||
this.setState({
|
||||
...err,
|
||||
shouldShowAlert: true,
|
||||
analyzationLoaded: false,
|
||||
});
|
||||
}
|
||||
|
||||
private renderImageMap = () => {
|
||||
const hostStyles: Partial<ITooltipHostStyles> = {
|
||||
root: {
|
||||
position: "absolute",
|
||||
top: this.state.tableIconTooltip.top,
|
||||
left: this.state.tableIconTooltip.left,
|
||||
width: this.state.tableIconTooltip.width,
|
||||
height: this.state.tableIconTooltip.height,
|
||||
display: this.state.tableIconTooltip.display,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%"}}>
|
||||
<CanvasCommandBar
|
||||
handleZoomIn={this.handleCanvasZoomIn}
|
||||
handleZoomOut={this.handleCanvasZoomOut}
|
||||
handleRotateImage={this.handleRotateCanvas}
|
||||
handleLayerChange={this.handleLayerChange}
|
||||
showLayerMenu={true}
|
||||
layers={this.state.layers}
|
||||
/>
|
||||
<ImageMap
|
||||
ref={(ref) => {
|
||||
this.imageMap = ref;
|
||||
this.layoutHelper.setImageMap(ref);
|
||||
this.tableHelper.setImageMap(ref);
|
||||
}}
|
||||
imageUri={this.state.imageUri || ""}
|
||||
imageWidth={this.state.imageWidth}
|
||||
imageHeight={this.state.imageHeight}
|
||||
imageAngle={this.state.imageAngle}
|
||||
initLayoutMap={true}
|
||||
hoveringFeature={this.state.hoveringFeature}
|
||||
onMapReady={this.noOp}
|
||||
featureStyler={this.featureStyler}
|
||||
tableBorderFeatureStyler={this.tableHelper.tableBorderFeatureStyler}
|
||||
tableIconFeatureStyler={this.tableHelper.tableIconFeatureStyler}
|
||||
tableIconBorderFeatureStyler={this.tableHelper.tableIconBorderFeatureStyler}
|
||||
handleTableToolTipChange={this.tableHelper.handleTableToolTipChange}
|
||||
/>
|
||||
<TooltipHost
|
||||
content={"rows: " + this.state.tableIconTooltip.rows +
|
||||
" columns: " + this.state.tableIconTooltip.columns}
|
||||
id="tableInfo"
|
||||
styles={hostStyles}
|
||||
>
|
||||
<div
|
||||
aria-describedby="tableInfo"
|
||||
className="tooltip-container"
|
||||
onClick={this.handleTableIconFeatureSelect}
|
||||
/>
|
||||
</TooltipHost>
|
||||
{this.state.tableToView !== null &&
|
||||
<TableView
|
||||
handleTableViewClose={this.handleTableViewClose}
|
||||
tableToView={this.state.tableToView}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleCanvasZoomIn = () => {
|
||||
this.imageMap.zoomIn();
|
||||
}
|
||||
|
||||
private handleCanvasZoomOut = () => {
|
||||
this.imageMap.zoomOut();
|
||||
}
|
||||
|
||||
private handleRotateCanvas = (degrees: number) => {
|
||||
this.setState({imageAngle: this.state.imageAngle + degrees});
|
||||
}
|
||||
|
||||
private handleLayerChange = (layer: string) => {
|
||||
switch (layer) {
|
||||
case "text":
|
||||
this.imageMap.toggleTextFeatureVisibility();
|
||||
break;
|
||||
case "tables":
|
||||
this.imageMap.toggleTableFeatureVisibility();
|
||||
break;
|
||||
case "checkboxes":
|
||||
this.imageMap.toggleCheckboxFeatureVisibility();
|
||||
break;
|
||||
case "label":
|
||||
this.imageMap.toggleLabelFeatureVisibility();
|
||||
break;
|
||||
case "drawnRegions":
|
||||
this.imageMap.toggleDrawnRegionsFeatureVisibility();
|
||||
break;
|
||||
}
|
||||
const newLayers = Object.assign({}, this.state.layers);
|
||||
newLayers[layer] = !newLayers[layer];
|
||||
this.setState({
|
||||
layers: newLayers,
|
||||
});
|
||||
}
|
||||
|
||||
private handleTableIconFeatureSelect = () => {
|
||||
if (this.state.hoveringFeature != null) {
|
||||
const tableState = this.imageMap.getTableBorderFeatureByID(this.state.hoveringFeature).get("state");
|
||||
if (tableState === "hovering" || tableState === "rest") {
|
||||
this.tableHelper.setTableToView(this.tableHelper.getTable(this.state.currentPage, this.state.hoveringFeature),
|
||||
this.state.hoveringFeature);
|
||||
} else {
|
||||
this.closeTableView("hovering");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTableViewClose = () => {
|
||||
this.closeTableView("rest");
|
||||
}
|
||||
|
||||
private closeTableView = (state: string) => {
|
||||
if (this.state.tableToView) {
|
||||
this.tableHelper.setTableState(this.state.tableToViewId, state);
|
||||
this.setState({
|
||||
tableToView: null,
|
||||
tableToViewId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private noOp = () => {
|
||||
// no operation
|
||||
}
|
||||
|
||||
private featureStyler = (feature) => {
|
||||
|
||||
// Unselected
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: "#fffc7f",
|
||||
width: 1,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "rgba(255, 252, 127, 0.2)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private renderPrevPageButton = () => {
|
||||
const prevPage = () => {
|
||||
this.goToPage(Math.max(1, this.state.currentPage - 1));
|
||||
};
|
||||
|
||||
return this.state.currentPage > 1 ?
|
||||
<IconButton
|
||||
className="toolbar-btn prev"
|
||||
title="Previous"
|
||||
iconProps={{iconName: "ChevronLeft"}}
|
||||
onClick={prevPage}
|
||||
/>
|
||||
: <div></div>;
|
||||
}
|
||||
|
||||
private renderNextPageButton = () => {
|
||||
const {numPages} = this.state;
|
||||
const nextPage = () => {
|
||||
this.goToPage(Math.min(this.state.currentPage + 1, numPages));
|
||||
};
|
||||
|
||||
return this.state.currentPage < numPages ?
|
||||
<IconButton
|
||||
className="toolbar-btn next"
|
||||
title="Next"
|
||||
onClick={nextPage}
|
||||
iconProps={{iconName: "ChevronRight"}}
|
||||
/>
|
||||
: <div></div>;
|
||||
}
|
||||
|
||||
private renderPageIndicator = () => {
|
||||
const {numPages} = this.state;
|
||||
return numPages > 1 ?
|
||||
<p className="page-number">
|
||||
Page {this.state.currentPage} of {numPages}
|
||||
</p> : <div></div>;
|
||||
}
|
||||
|
||||
private goToPage = async (targetPage: number) => {
|
||||
if (targetPage <= 0 || targetPage > this.state.numPages) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
currentPage: targetPage,
|
||||
}, () => {
|
||||
this.layoutHelper.drawLayout(targetPage);
|
||||
this.tableHelper.drawTables(targetPage);
|
||||
});
|
||||
}
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({analyzationLoaded: false, isAnalyzing: true});
|
||||
this.getAnalzation()
|
||||
.then((result) => {
|
||||
this.tableHelper.setAnalyzeResult(result?.analyzeResult);
|
||||
this.setState({
|
||||
isAnalyzing: false,
|
||||
analyzationLoaded: true,
|
||||
layoutData: result,
|
||||
}, () => {
|
||||
this.layoutHelper.setLayoutData(result);
|
||||
this.layoutHelper.drawLayout(this.state.currentPage);
|
||||
this.tableHelper.drawTables(this.state.currentPage);
|
||||
})
|
||||
}).catch((error) => {
|
||||
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 if (error.errorCode === ErrorCode.HttpStatusUnauthorized) {
|
||||
alertMessage = error.message;
|
||||
}
|
||||
else {
|
||||
alertMessage = interpolate(strings.errors.endpointConnectionError.message, {endpoint: "form recognizer backend URL"});
|
||||
}
|
||||
this.setState({
|
||||
shouldShowAlert: true,
|
||||
alertTitle: "Analyze Failed",
|
||||
alertMessage,
|
||||
isAnalyzing: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getAnalzation(): Promise<any> {
|
||||
const endpointURL = url.resolve(
|
||||
this.props.prebuiltSettings.serviceURI,
|
||||
`/formrecognizer/${constants.prebuiltServiceVersion}/layout/analyze`,
|
||||
);
|
||||
const apiKey = this.props.prebuiltSettings.apiKey;
|
||||
|
||||
const headers = {
|
||||
"Content-Type": this.state.file ? this.state.file.type : "application/json",
|
||||
"cache-control": "no-cache"
|
||||
};
|
||||
const body = this.state.file ?? ({source: this.state.fetchedFileURL});
|
||||
|
||||
// let response;
|
||||
try {
|
||||
const response = await ServiceHelper.postWithAutoRetry(
|
||||
endpointURL, body, {headers}, apiKey as string);
|
||||
const operationLocation = response.headers["operation-location"];
|
||||
|
||||
// Make the second REST API call and get the response.
|
||||
return poll(() => ServiceHelper.getWithAutoRetry(operationLocation, {headers}, apiKey as string), 120000, 500);
|
||||
} catch (err) {
|
||||
ServiceHelper.handleServiceError({...err, endpoint: endpointURL});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,759 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {
|
||||
Dropdown, FontIcon, IconButton, IDropdownOption,
|
||||
ITooltipHostStyles,
|
||||
PrimaryButton,
|
||||
Spinner, SpinnerSize, TooltipHost
|
||||
} from "@fluentui/react";
|
||||
import _ from "lodash";
|
||||
import {Feature} from "ol";
|
||||
import Polygon from "ol/geom/Polygon";
|
||||
import Fill from "ol/style/Fill";
|
||||
import Stroke from "ol/style/Stroke";
|
||||
import Style from "ol/style/Style";
|
||||
import React from "react";
|
||||
import {connect} from "react-redux";
|
||||
import {RouteComponentProps} from "react-router-dom";
|
||||
import {bindActionCreators} from "redux";
|
||||
import url from "url";
|
||||
import {constants} from "../../../../common/constants";
|
||||
import {interpolate, strings} from "../../../../common/strings";
|
||||
import {getPrimaryWhiteTheme} from "../../../../common/themes";
|
||||
import {poll} from "../../../../common/utils";
|
||||
import {ErrorCode, FieldFormat, FieldType, IApplicationState, IPrebuiltSettings, ITag} from "../../../../models/applicationState";
|
||||
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
|
||||
import IAppPrebuiltSettingsActions, * as appPrebuiltSettingsActions from "../../../../redux/actions/prebuiltSettingsActions";
|
||||
import ServiceHelper from "../../../../services/serviceHelper";
|
||||
import {getAppInsights} from "../../../../services/telemetryService";
|
||||
import Alert from "../../common/alert/alert";
|
||||
import {DocumentFilePicker} from "../../common/documentFilePicker/documentFilePicker";
|
||||
import {ImageMap} from "../../common/imageMap/imageMap";
|
||||
import {PrebuiltSetting} from "../../common/prebuiltSetting/prebuiltSetting";
|
||||
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 {ILoadFileHelper, ILoadFileResult, LoadFileHelper} from "./LoadFileHelper";
|
||||
import {ITableHelper, ITableState, TableHelper} from "./tableHelper";
|
||||
|
||||
interface IPrebuiltTypes {
|
||||
name: string;
|
||||
servicePath: string;
|
||||
}
|
||||
|
||||
export interface IPrebuiltPredictPageProps extends RouteComponentProps {
|
||||
prebuiltSettings: IPrebuiltSettings;
|
||||
appTitleActions: IAppTitleActions;
|
||||
actions: IAppPrebuiltSettingsActions;
|
||||
}
|
||||
|
||||
export interface IPrebuiltPredictPageState extends ILoadFileResult, ITableState {
|
||||
fileLabel: string;
|
||||
fileChanged: boolean;
|
||||
file?: File;
|
||||
isFetching?: boolean;
|
||||
fileLoaded?: boolean;
|
||||
|
||||
isPredicting: boolean;
|
||||
predictionLoaded: boolean;
|
||||
fetchedFileURL: string;
|
||||
analyzeResult: any;
|
||||
|
||||
tags?: ITag[];
|
||||
highlightedField?: string;
|
||||
imageAngle: number;
|
||||
currentPrebuiltType: IPrebuiltTypes;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: IApplicationState) {
|
||||
return {
|
||||
prebuiltSettings: state.prebuiltSettings
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
appTitleActions: bindActionCreators(appTitleActions, dispatch),
|
||||
actions: bindActionCreators(appPrebuiltSettingsActions, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
@connect(mapStateToProps, mapDispatchToProps)
|
||||
export class PrebuiltPredictPage extends React.Component<IPrebuiltPredictPageProps, IPrebuiltPredictPageState> {
|
||||
private appInsights: any = null;
|
||||
prebuiltTypes: IPrebuiltTypes[] = [
|
||||
{
|
||||
name: "Receipt",
|
||||
servicePath: "/prebuilt/receipt/analyze"
|
||||
},
|
||||
{
|
||||
name: "Invoice",
|
||||
servicePath: "/prebuilt/invoice/analyze"
|
||||
},
|
||||
{
|
||||
name: "Business card",
|
||||
servicePath: "/prebuilt/businessCard/analyze"
|
||||
},
|
||||
];
|
||||
|
||||
state: IPrebuiltPredictPageState = {
|
||||
imageUri: null,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
currentPage: 1,
|
||||
numPages: 1,
|
||||
|
||||
shouldShowAlert: false,
|
||||
alertTitle: "",
|
||||
alertMessage: "",
|
||||
|
||||
fileLabel: "",
|
||||
fileChanged: false,
|
||||
|
||||
isPredicting: false,
|
||||
predictionLoaded: false,
|
||||
fetchedFileURL: "",
|
||||
analyzeResult: null,
|
||||
|
||||
imageAngle: 0,
|
||||
currentPrebuiltType: this.prebuiltTypes[0],
|
||||
|
||||
tableIconTooltip: {display: "none", width: 0, height: 0, top: 0, left: 0},
|
||||
hoveringFeature: null,
|
||||
tableToView: null,
|
||||
tableToViewId: null,
|
||||
};
|
||||
|
||||
private fileHelper: ILoadFileHelper = new LoadFileHelper();
|
||||
|
||||
private tableHelper: ITableHelper = new TableHelper(this);
|
||||
|
||||
private imageMap: ImageMap;
|
||||
private tagColors = require("../../common/tagColors.json");
|
||||
|
||||
public async componentDidMount() {
|
||||
this.appInsights = getAppInsights();
|
||||
document.title = strings.prebuiltPredict.title + " - " + strings.appName;
|
||||
this.props.appTitleActions.setTitle(`${strings.prebuiltPredict.title}`);
|
||||
}
|
||||
|
||||
componentDidUpdate(_prevProps: IPrebuiltPredictPageProps, prevState: IPrebuiltPredictPageState) {
|
||||
if (this.state.file) {
|
||||
if (this.state.fileChanged && !this.state.isFetching) {
|
||||
this.loadFile(this.state.file);
|
||||
} else if (prevState.currentPage !== this.state.currentPage) {
|
||||
this.fileHelper.loadPage(this.state.currentPage).then((res: any) => {
|
||||
if (res) {
|
||||
this.setState({...res});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.getOcrFromAnalyzeResult(this.state.analyzeResult).length > 0 &&
|
||||
prevState.imageUri !== this.state.imageUri) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
this.drawPredictionResult();
|
||||
|
||||
}
|
||||
|
||||
if (prevState.highlightedField !== this.state.highlightedField) {
|
||||
this.setPredictedFieldHighlightStatus(this.state.highlightedField);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private loadFile = (file: File) => {
|
||||
this.setState({isFetching: true});
|
||||
this.fileHelper.loadFile(file).then((res: ILoadFileResult) => {
|
||||
if (res) {
|
||||
this.setState({
|
||||
...res,
|
||||
isFetching: false,
|
||||
fileChanged: false
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const predictDisabled: boolean = this.state.isPredicting || !this.state.file
|
||||
|| this.state.invalidFileFormat ||
|
||||
!this.state.fileLoaded ||
|
||||
!this.props.prebuiltSettings?.apiKey ||
|
||||
!this.props.prebuiltSettings?.serviceURI;
|
||||
|
||||
const predictions = this.getPredictionsFromAnalyzeResult(this.state.analyzeResult);
|
||||
|
||||
const onPrebuiltsPath: boolean = this.props.match.path.includes("prebuilts");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`predict skipToMainContent ${onPrebuiltsPath ? "" : "hidden"} `}
|
||||
id="pagePredict"
|
||||
style={{display: `${onPrebuiltsPath ? "flex" : "none"}`}} >
|
||||
<div className="predict-main">
|
||||
{this.state.file && this.state.imageUri && this.renderImageMap()}
|
||||
{this.renderPrevPageButton()}
|
||||
{this.renderNextPageButton()}
|
||||
{this.renderPageIndicator()}
|
||||
</div>
|
||||
<div className="predict-sidebar bg-lighter-1">
|
||||
<div className="condensed-list">
|
||||
<h6 className="condensed-list-header bg-darker-2 p-2 flex-center">
|
||||
<FontIcon className="mr-1" iconName="ContactCard" />
|
||||
<span>{interpolate(strings.prebuiltPredict.anlayWithPrebuiltModels, this.state.currentPrebuiltType)}</span>
|
||||
</h6>
|
||||
|
||||
<PrebuiltSetting prebuiltSettings={this.props.prebuiltSettings}
|
||||
disabled={this.state.isPredicting}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
<div className="p-3" style={{marginTop: "-3rem"}}>
|
||||
<div style={{marginBottom: "3px"}}>Form type</div>
|
||||
<Dropdown
|
||||
disabled={this.state.isPredicting}
|
||||
className="prebuilt-type-dropdown"
|
||||
options={this.prebuiltTypes.map(type => ({key: type.name, text: type.name}))}
|
||||
defaultSelectedKey={this.state.currentPrebuiltType.name}
|
||||
onChange={this.onPrebuiltTypeChange}></Dropdown>
|
||||
</div>
|
||||
<div className="p-3" style={{marginTop: "8px"}}>
|
||||
<h5>Upload file and run analysis</h5>
|
||||
<DocumentFilePicker
|
||||
disabled={this.state.isPredicting || this.state.isFetching}
|
||||
onFileChange={(data) => this.onFileChange(data)}
|
||||
onSelectSourceChange={() => this.onSelectSourceChange()}
|
||||
onError={(err) => this.onFileLoadError(err)} />
|
||||
<div className="container-items-end predict-button">
|
||||
<PrimaryButton
|
||||
theme={getPrimaryWhiteTheme()}
|
||||
iconProps={{iconName: "ContactCard"}}
|
||||
text="Run analysis"
|
||||
aria-label={!this.state.isPredicting ? strings.prebuiltPredict.inProgress : ""}
|
||||
allowDisabledFocus
|
||||
disabled={predictDisabled}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isFetching &&
|
||||
<div className="loading-container">
|
||||
<Spinner
|
||||
label="Fetching..."
|
||||
ariaLive="assertive"
|
||||
labelPosition="right"
|
||||
size={SpinnerSize.large}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{this.state.isPredicting &&
|
||||
<div className="loading-container">
|
||||
<Spinner
|
||||
label={strings.prebuiltPredict.inProgress}
|
||||
ariaLive="assertive"
|
||||
labelPosition="right"
|
||||
size={SpinnerSize.large}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{Object.keys(predictions).length > 0 &&
|
||||
<PredictResult
|
||||
predictions={predictions}
|
||||
analyzeResult={this.state.analyzeResult}
|
||||
page={this.state.currentPage}
|
||||
tags={this.state.tags}
|
||||
downloadPrefix={this.state.currentPrebuiltType.name}
|
||||
downloadResultLabel={this.state.fileLabel}
|
||||
onPredictionClick={this.onPredictionClick}
|
||||
onPredictionMouseEnter={this.onPredictionMouseEnter}
|
||||
onPredictionMouseLeave={this.onPredictionMouseLeave}
|
||||
/>
|
||||
}
|
||||
{
|
||||
(Object.keys(predictions).length === 0 && this.state.predictionLoaded) &&
|
||||
<div>No field can be extracted.</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Alert
|
||||
show={this.state.shouldShowAlert}
|
||||
title={this.state.alertTitle}
|
||||
message={this.state.alertMessage}
|
||||
onClose={() => this.setState({
|
||||
shouldShowAlert: false,
|
||||
alertTitle: "",
|
||||
alertMessage: "",
|
||||
predictionLoaded: true,
|
||||
})}
|
||||
/>
|
||||
<PreventLeaving
|
||||
when={this.state.isPredicting}
|
||||
message={"A prediction operation is currently in progress, are you sure you want to leave?"}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
onSelectSourceChange(): void {
|
||||
this.setState({
|
||||
file: undefined,
|
||||
analyzeResult: {},
|
||||
predictionLoaded: false,
|
||||
});
|
||||
if (this.imageMap) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
}
|
||||
}
|
||||
|
||||
onFileLoadError(err: {alertTitle: string; alertMessage: string;}): void {
|
||||
this.setState({
|
||||
...err,
|
||||
shouldShowAlert: true,
|
||||
isPredicting: false,
|
||||
});
|
||||
}
|
||||
onFileChange(data: {
|
||||
file: File,
|
||||
fileLabel: string,
|
||||
fetchedFileURL: string
|
||||
}): void {
|
||||
this.setState({
|
||||
currentPage: 1,
|
||||
analyzeResult: null,
|
||||
fileChanged: true,
|
||||
...data,
|
||||
predictionLoaded: false,
|
||||
fileLoaded: false,
|
||||
}, () => {
|
||||
if (this.imageMap) {
|
||||
this.imageMap.removeAllFeatures();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private onPrebuiltTypeChange = (e, option: IDropdownOption) => {
|
||||
const currentPrebuiltType = this.prebuiltTypes.find(type => type.name === option.key);
|
||||
if (currentPrebuiltType && this.state.currentPrebuiltType.name !== currentPrebuiltType.name) {
|
||||
this.setState({
|
||||
currentPrebuiltType,
|
||||
predictionLoaded: false,
|
||||
analyzeResult: {}
|
||||
}, () => {
|
||||
this.imageMap?.removeAllFeatures();
|
||||
});
|
||||
}
|
||||
}
|
||||
private prevPage = () => {
|
||||
this.setState((prevState) => ({
|
||||
currentPage: Math.max(1, prevState.currentPage - 1),
|
||||
}), () => {
|
||||
this.imageMap?.removeAllFeatures();
|
||||
});
|
||||
};
|
||||
private nextPage = () => {
|
||||
const {numPages} = this.state;
|
||||
this.setState((prevState) => ({
|
||||
currentPage: Math.min(prevState.currentPage + 1, numPages),
|
||||
}), () => {
|
||||
this.imageMap?.removeAllFeatures();
|
||||
});
|
||||
};
|
||||
private renderPrevPageButton = () => {
|
||||
return this.state.currentPage > 1 ?
|
||||
<IconButton
|
||||
className="toolbar-btn prev"
|
||||
title="Previous"
|
||||
iconProps={{iconName: "ChevronLeft"}}
|
||||
onClick={this.prevPage}
|
||||
/> : <div></div>;
|
||||
}
|
||||
|
||||
private renderNextPageButton = () => {
|
||||
return this.state.currentPage < this.state.numPages ?
|
||||
<IconButton
|
||||
className="toolbar-btn next"
|
||||
title="Next"
|
||||
onClick={this.nextPage}
|
||||
iconProps={{iconName: "ChevronRight"}}
|
||||
/> : <div></div>;
|
||||
}
|
||||
|
||||
private renderPageIndicator = () => {
|
||||
const {numPages} = this.state;
|
||||
return numPages > 1 ?
|
||||
<p className="page-number">
|
||||
Page {this.state.currentPage} of {numPages}
|
||||
</p> : <div></div>;
|
||||
}
|
||||
|
||||
private renderImageMap = () => {
|
||||
const hostStyles: Partial<ITooltipHostStyles> = {
|
||||
root: {
|
||||
position: "absolute",
|
||||
top: this.state.tableIconTooltip.top,
|
||||
left: this.state.tableIconTooltip.left,
|
||||
width: this.state.tableIconTooltip.width,
|
||||
height: this.state.tableIconTooltip.height,
|
||||
display: this.state.tableIconTooltip.display,
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%"}}>
|
||||
<CanvasCommandBar
|
||||
handleZoomIn={this.handleCanvasZoomIn}
|
||||
handleZoomOut={this.handleCanvasZoomOut}
|
||||
handleRotateImage={this.handleRotateCanvas}
|
||||
layers={{}}
|
||||
/>
|
||||
<ImageMap
|
||||
initPredictMap={true}
|
||||
initEditorMap={true}
|
||||
ref={(ref) => {
|
||||
this.imageMap = ref;
|
||||
this.tableHelper.setImageMap(ref);
|
||||
}}
|
||||
imageUri={this.state.imageUri || ""}
|
||||
imageWidth={this.state.imageWidth}
|
||||
imageHeight={this.state.imageHeight}
|
||||
imageAngle={this.state.imageAngle}
|
||||
featureStyler={this.featureStyler}
|
||||
onMapReady={this.noOp}
|
||||
tableBorderFeatureStyler={this.tableHelper.tableBorderFeatureStyler}
|
||||
tableIconFeatureStyler={this.tableHelper.tableIconFeatureStyler}
|
||||
tableIconBorderFeatureStyler={this.tableHelper.tableIconBorderFeatureStyler}
|
||||
handleTableToolTipChange={this.tableHelper.handleTableToolTipChange}
|
||||
/>
|
||||
<TooltipHost
|
||||
content={"rows: " + this.state.tableIconTooltip.rows +
|
||||
" columns: " + this.state.tableIconTooltip.columns}
|
||||
id="tableInfo"
|
||||
styles={hostStyles}
|
||||
>
|
||||
<div
|
||||
aria-describedby="tableInfo"
|
||||
className="tooltip-container"
|
||||
onClick={this.handleTableIconFeatureSelect}
|
||||
/>
|
||||
</TooltipHost>
|
||||
{this.state.tableToView !== null &&
|
||||
<TableView
|
||||
handleTableViewClose={this.handleTableViewClose}
|
||||
tableToView={this.state.tableToView}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private handleTableIconFeatureSelect = () => {
|
||||
if (this.state.hoveringFeature != null) {
|
||||
const tableState = this.imageMap.getTableBorderFeatureByID(this.state.hoveringFeature).get("state");
|
||||
if (tableState === "hovering" || tableState === "rest") {
|
||||
this.tableHelper.setTableToView(this.tableHelper.getTable(this.state.currentPage, this.state.hoveringFeature),
|
||||
this.state.hoveringFeature);
|
||||
} else {
|
||||
this.closeTableView("hovering");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTableViewClose = () => {
|
||||
this.closeTableView("rest");
|
||||
}
|
||||
|
||||
private closeTableView = (state: string) => {
|
||||
if (this.state.tableToView) {
|
||||
this.tableHelper.setTableState(this.state.tableToViewId, state);
|
||||
this.setState({
|
||||
tableToView: null,
|
||||
tableToViewId: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private handleTableToolTipChange = async (display: string, width: number, height: number, top: number,
|
||||
left: number, rows: number, columns: number, featureID: string) => {
|
||||
if (!this.imageMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (featureID !== null && this.imageMap.getTableBorderFeatureByID(featureID).get("state") !== "selected") {
|
||||
this.imageMap.getTableBorderFeatureByID(featureID).set("state", "hovering");
|
||||
this.imageMap.getTableIconFeatureByID(featureID).set("state", "hovering");
|
||||
} else if (featureID === null && this.state.hoveringFeature &&
|
||||
this.imageMap.getTableBorderFeatureByID(this.state.hoveringFeature).get("state") !== "selected") {
|
||||
this.imageMap.getTableBorderFeatureByID(this.state.hoveringFeature).set("state", "rest");
|
||||
this.imageMap.getTableIconFeatureByID(this.state.hoveringFeature).set("state", "rest");
|
||||
}
|
||||
const newTableIconTooltip = {
|
||||
display,
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
this.setState({
|
||||
tableIconTooltip: newTableIconTooltip,
|
||||
hoveringFeature: featureID,
|
||||
});
|
||||
}
|
||||
|
||||
private handleCanvasZoomIn = () => {
|
||||
this.imageMap.zoomIn();
|
||||
}
|
||||
|
||||
private handleCanvasZoomOut = () => {
|
||||
this.imageMap.zoomOut();
|
||||
}
|
||||
private handleZoomReset = () => {
|
||||
this.imageMap.resetZoom();
|
||||
}
|
||||
|
||||
private handleRotateCanvas = (degrees: number) => {
|
||||
this.setState({imageAngle: this.state.imageAngle + degrees});
|
||||
}
|
||||
|
||||
|
||||
private handleClick = () => {
|
||||
this.setState({predictionLoaded: false, isPredicting: true});
|
||||
this.getPrediction()
|
||||
.then((result) => {
|
||||
this.tableHelper.setAnalyzeResult(result?.analyzeResult);
|
||||
const tags = this.getTagsForPredictResults(this.getPredictionsFromAnalyzeResult(result?.analyzeResult));
|
||||
this.setState({
|
||||
tags,
|
||||
analyzeResult: result.analyzeResult,
|
||||
predictionLoaded: true,
|
||||
isPredicting: false,
|
||||
}, () => {
|
||||
this.drawPredictionResult();
|
||||
});
|
||||
}).catch((error) => {
|
||||
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,
|
||||
});
|
||||
});
|
||||
if (this.appInsights) {
|
||||
this.appInsights.trackEvent({name: "ANALYZE_EVENT"});
|
||||
}
|
||||
}
|
||||
|
||||
private getTagsForPredictResults(predictions) {
|
||||
const tags: ITag[] = [];
|
||||
Object.keys(predictions).forEach((key, index) => {
|
||||
tags.push({
|
||||
name: key,
|
||||
color: this.tagColors[index],
|
||||
// use default type
|
||||
type: FieldType.String,
|
||||
format: FieldFormat.NotSpecified,
|
||||
} as ITag);
|
||||
});
|
||||
this.setState({
|
||||
tags,
|
||||
});
|
||||
return tags;
|
||||
}
|
||||
|
||||
private async getPrediction(): Promise<any> {
|
||||
const endpointURL = url.resolve(
|
||||
this.props.prebuiltSettings.serviceURI,
|
||||
`/formrecognizer/${constants.prebuiltServiceVersion}${this.state.currentPrebuiltType.servicePath}`,
|
||||
);
|
||||
const apiKey = this.props.prebuiltSettings.apiKey;
|
||||
|
||||
const headers = {"Content-Type": this.state.file ? this.state.file.type : "application/json", "cache-control": "no-cache"};
|
||||
const body = this.state.file ?? {source: this.state.fetchedFileURL};
|
||||
let response;
|
||||
try {
|
||||
response = await ServiceHelper.postWithAutoRetry(
|
||||
endpointURL, body, {headers}, apiKey as string);
|
||||
} catch (err) {
|
||||
ServiceHelper.handleServiceError({...err, endpoint: endpointURL});
|
||||
}
|
||||
|
||||
const operationLocation = response.headers["operation-location"];
|
||||
|
||||
// Make the second REST API call and get the response.
|
||||
return poll(() => ServiceHelper.getWithAutoRetry(operationLocation, {headers}, apiKey as string), 120000, 500);
|
||||
}
|
||||
|
||||
private createBoundingBoxVectorFeature = (text, boundingBox, imageExtent, ocrExtent) => {
|
||||
const coordinates: number[][] = [];
|
||||
|
||||
// extent is int[4] to represent image dimentions: [left, bottom, right, top]
|
||||
const imageWidth = imageExtent[2] - imageExtent[0];
|
||||
const imageHeight = imageExtent[3] - imageExtent[1];
|
||||
const ocrWidth = ocrExtent[2] - ocrExtent[0];
|
||||
const ocrHeight = ocrExtent[3] - ocrExtent[1];
|
||||
|
||||
for (let i = 0; i < boundingBox.length; i += 2) {
|
||||
coordinates.push([
|
||||
Math.round((boundingBox[i] / ocrWidth) * imageWidth),
|
||||
Math.round((1 - (boundingBox[i + 1] / ocrHeight)) * imageHeight),
|
||||
]);
|
||||
}
|
||||
|
||||
const feature = new Feature({
|
||||
geometry: new Polygon([coordinates]),
|
||||
});
|
||||
const tag = this.state.tags.find((tag) => tag.name.toLocaleLowerCase() === text.toLocaleLowerCase());
|
||||
const isHighlighted = (text.toLocaleLowerCase() === this.state.highlightedField?.toLocaleLowerCase());
|
||||
feature.setProperties({
|
||||
color: _.get(tag, "color", "#333333"),
|
||||
fieldName: text,
|
||||
isHighlighted,
|
||||
});
|
||||
return feature;
|
||||
}
|
||||
|
||||
private featureStyler = (feature) => {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: feature.get("color"),
|
||||
width: feature.get("isHighlighted") ? 4 : 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "rgba(255, 255, 255, 0)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private drawPredictionResult = (): void => {
|
||||
this.imageMap.removeAllFeatures();
|
||||
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);
|
||||
}
|
||||
}
|
||||
this.imageMap.addFeatures(features);
|
||||
this.tableHelper.drawTables(this.state.currentPage);
|
||||
}
|
||||
|
||||
private getPredictionsFromAnalyzeResult(analyzeResult: any) {
|
||||
if (analyzeResult) {
|
||||
const predictions = _.get(analyzeResult, "documentResults[0].fields", {});
|
||||
const predictionsCopy = Object.assign({}, predictions);
|
||||
delete predictionsCopy.ReceiptType;
|
||||
|
||||
const extendPredictionItem = (key, field) => {
|
||||
const result = {};
|
||||
if (field.valueArray) {
|
||||
if (field.valueArray.length === 1) {
|
||||
const item = field.valueArray[0];
|
||||
const itemName = `${key}`;
|
||||
if (item.valueObject) {
|
||||
result[itemName] = item.valueObject.Name;
|
||||
if (item.valueObject.TotalPrice) {
|
||||
result[itemName + " price"] = item.valueObject.TotalPrice;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result[itemName] = item;
|
||||
}
|
||||
}
|
||||
else {
|
||||
field.valueArray.forEach((item, index) => {
|
||||
const itemName = `${key} ${index + 1}`;
|
||||
if (item.valueObject) {
|
||||
result[itemName] = item.valueObject.Name;
|
||||
if (item.valueObject.TotalPrice) {
|
||||
result[itemName + " price"] = item.valueObject.TotalPrice;
|
||||
}
|
||||
}
|
||||
else {
|
||||
result[itemName] = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
result[key] = field;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
let predictionResult = {};
|
||||
for (const key in predictionsCopy) {
|
||||
if (Object.prototype.hasOwnProperty.call(predictionsCopy, key)) {
|
||||
const item = predictionsCopy[key];
|
||||
if (item) {
|
||||
predictionResult = Object.assign({}, predictionResult, extendPredictionItem(key, item));
|
||||
}
|
||||
}
|
||||
}
|
||||
return predictionResult;
|
||||
} else {
|
||||
return _.get(analyzeResult, "documentResults[0].fields", {});
|
||||
}
|
||||
}
|
||||
|
||||
private getOcrFromAnalyzeResult(analyzeResult: any) {
|
||||
return _.get(analyzeResult, "readResults", []);
|
||||
}
|
||||
|
||||
private noOp = () => {
|
||||
// no operation
|
||||
}
|
||||
private onPredictionClick = (predictedItem: any) => {
|
||||
const targetPage = predictedItem.page;
|
||||
if (Number.isInteger(targetPage) && targetPage !== this.state.currentPage) {
|
||||
this.setState({
|
||||
currentPage: targetPage,
|
||||
highlightedField: predictedItem.fieldName ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onPredictionMouseEnter = (predictedItem: any) => {
|
||||
this.setState({
|
||||
highlightedField: predictedItem.fieldName ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
private onPredictionMouseLeave = (predictedItem: any) => {
|
||||
this.setState({
|
||||
highlightedField: "",
|
||||
});
|
||||
}
|
||||
|
||||
private setPredictedFieldHighlightStatus = (highlightedField: string) => {
|
||||
const features = this.imageMap.getAllFeatures();
|
||||
for (const feature of features) {
|
||||
if (feature.get("fieldName").toLocaleLowerCase() === highlightedField.toLocaleLowerCase()) {
|
||||
feature.set("isHighlighted", true);
|
||||
} else {
|
||||
feature.set("isHighlighted", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {Feature} from "ol";
|
||||
import Point from "ol/geom/Point";
|
||||
import Polygon from "ol/geom/Polygon";
|
||||
import Fill from "ol/style/Fill";
|
||||
import Icon from "ol/style/Icon";
|
||||
import Stroke from "ol/style/Stroke";
|
||||
import Style from "ol/style/Style";
|
||||
import {Component} from "react";
|
||||
import {ImageMap} from "../../common/imageMap/imageMap";
|
||||
|
||||
export interface ITableHelper {
|
||||
setImageMap(imageMap: ImageMap): void;
|
||||
setAnalyzeResult(analyzeResult: any): void;
|
||||
drawTables(targetPage: number): void;
|
||||
reset(): void;
|
||||
setTableState(viewedTableId, state): void;
|
||||
getTable(targetPage: number, hoveringFeature: string): any;
|
||||
|
||||
tableIconFeatureStyler(feature, resolution): Style;
|
||||
tableBorderFeatureStyler(feature): Style;
|
||||
tableIconBorderFeatureStyler(feature): Style;
|
||||
handleTableToolTipChange(display: string, width: number, height: number, top: number,
|
||||
left: number, rows: number, columns: number, featureID: string): void;
|
||||
|
||||
setTableToView(tableToView, tableToViewId): void;
|
||||
}
|
||||
|
||||
export interface ITableState {
|
||||
tableIconTooltip: any;
|
||||
hoveringFeature: string;
|
||||
tableToView: object;
|
||||
tableToViewId: string;
|
||||
}
|
||||
|
||||
export class TableHelper<TState extends ITableState> {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor(private component: Component<{}, TState>) {
|
||||
|
||||
}
|
||||
private imageMap: ImageMap;
|
||||
private analyzeResult: any;
|
||||
private tableIDToIndexMap: object;
|
||||
|
||||
public setImageMap = (imageMap: ImageMap) => {
|
||||
this.imageMap = imageMap;
|
||||
}
|
||||
public setAnalyzeResult = (analyzeResult: any) => {
|
||||
this.analyzeResult = analyzeResult;
|
||||
}
|
||||
|
||||
public reset = () => {
|
||||
this.analyzeResult = null;
|
||||
}
|
||||
|
||||
public setTableState = (viewedTableId, state) => {
|
||||
this.imageMap.getTableBorderFeatureByID(viewedTableId).set("state", state);
|
||||
this.imageMap.getTableIconFeatureByID(viewedTableId).set("state", state);
|
||||
}
|
||||
|
||||
public getTable = (targetPage: number, hoveringFeature: string) => {
|
||||
const pageOcrData = this.getOcrResultForPage(targetPage);
|
||||
return pageOcrData?.pageResults?.tables[this.tableIDToIndexMap[hoveringFeature]] ?? [];
|
||||
}
|
||||
|
||||
private getOcrResultForPage = (targetPage: number): any => {
|
||||
if (!this.analyzeResult) {
|
||||
return {};
|
||||
}
|
||||
if (this.analyzeResult?.readResults) {
|
||||
// OCR schema with analyzeResult/readResults property
|
||||
const ocrResultsForCurrentPage = {};
|
||||
if (this.analyzeResult.pageResults) {
|
||||
ocrResultsForCurrentPage["pageResults"] = this.analyzeResult.pageResults[targetPage - 1];
|
||||
}
|
||||
ocrResultsForCurrentPage["readResults"] = this.analyzeResult.readResults[targetPage - 1];
|
||||
return ocrResultsForCurrentPage;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
drawTables = (targetPage: number) => {
|
||||
const ocrForCurrentPage = this.getOcrResultForPage(targetPage);
|
||||
const tableBorderFeatures = [];
|
||||
const tableIconFeatures = [];
|
||||
const tableIconBorderFeatures = [];
|
||||
|
||||
const ocrReadResults = ocrForCurrentPage["readResults"];
|
||||
const ocrPageResults = ocrForCurrentPage["pageResults"];
|
||||
const imageExtent = this.imageMap.getImageExtent();
|
||||
|
||||
this.tableIDToIndexMap = {};
|
||||
if (ocrPageResults?.tables) {
|
||||
const ocrExtent = [0, 0, ocrReadResults.width, ocrReadResults.height];
|
||||
ocrPageResults.tables.forEach((table, index) => {
|
||||
|
||||
if (table.cells && table.columns && table.rows) {
|
||||
const tableBoundingBox = getTableBoundingBox(table.cells.map((cell) => cell.boundingBox));
|
||||
const createdTableFeatures = this.createBoundingBoxVectorTable(
|
||||
tableBoundingBox,
|
||||
imageExtent,
|
||||
ocrExtent,
|
||||
ocrPageResults.page,
|
||||
table.rows,
|
||||
table.columns,
|
||||
index);
|
||||
tableBorderFeatures.push(createdTableFeatures["border"]);
|
||||
tableIconFeatures.push(createdTableFeatures["icon"]);
|
||||
tableIconBorderFeatures.push(createdTableFeatures["iconBorder"]);
|
||||
}
|
||||
});
|
||||
if (tableBorderFeatures.length > 0 && tableBorderFeatures.length === tableIconFeatures.length
|
||||
&& tableBorderFeatures.length === tableIconBorderFeatures.length) {
|
||||
this.imageMap.addTableBorderFeatures(tableBorderFeatures);
|
||||
this.imageMap.addTableIconFeatures(tableIconFeatures);
|
||||
this.imageMap.addTableIconBorderFeatures(tableIconBorderFeatures);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createBoundingBoxVectorTable = (boundingBox, imageExtent, ocrExtent, page, rows, columns, index) => {
|
||||
const coordinates: any[] = [];
|
||||
const polygonPoints: number[] = [];
|
||||
const imageWidth = imageExtent[2] - imageExtent[0];
|
||||
const imageHeight = imageExtent[3] - imageExtent[1];
|
||||
const ocrWidth = ocrExtent[2] - ocrExtent[0];
|
||||
const ocrHeight = ocrExtent[3] - ocrExtent[1];
|
||||
|
||||
for (let i = 0; i < boundingBox.length; i += 2) {
|
||||
// An array of numbers representing an extent: [minx, miny, maxx, maxy]
|
||||
coordinates.push([
|
||||
Math.round((boundingBox[i] / ocrWidth) * imageWidth),
|
||||
Math.round((1 - (boundingBox[i + 1] / ocrHeight)) * imageHeight),
|
||||
]);
|
||||
|
||||
polygonPoints.push(boundingBox[i] / ocrWidth);
|
||||
polygonPoints.push(boundingBox[i + 1] / ocrHeight);
|
||||
}
|
||||
const tableID = createRegionIdFromBoundingBox(polygonPoints, page);
|
||||
this.tableIDToIndexMap[tableID] = index;
|
||||
const tableFeatures = {};
|
||||
tableFeatures["border"] = new Feature({
|
||||
geometry: new Polygon([coordinates]),
|
||||
id: tableID,
|
||||
state: "rest",
|
||||
boundingbox: boundingBox,
|
||||
});
|
||||
tableFeatures["icon"] = new Feature({
|
||||
geometry: new Point([coordinates[0][0] - 6.5, coordinates[0][1] - 4.5]),
|
||||
id: tableID,
|
||||
state: "rest",
|
||||
});
|
||||
|
||||
const iconTR = [coordinates[0][0] - 5, coordinates[0][1]];
|
||||
const iconTL = [iconTR[0] - 31.5, iconTR[1]];
|
||||
const iconBL = [iconTR[0], iconTR[1] - 29.5];
|
||||
const iconBR = [iconTR[0] - 31.5, iconTR[1] - 29.5];
|
||||
|
||||
tableFeatures["iconBorder"] = new Feature({
|
||||
geometry: new Polygon([[iconTR, iconTL, iconBR, iconBL]]),
|
||||
id: tableID,
|
||||
rows,
|
||||
columns,
|
||||
});
|
||||
|
||||
tableFeatures["border"].setId(tableID);
|
||||
tableFeatures["icon"].setId(tableID);
|
||||
tableFeatures["iconBorder"].setId(tableID);
|
||||
return tableFeatures;
|
||||
}
|
||||
|
||||
public tableIconFeatureStyler = (feature, resolution) => {
|
||||
if (feature.get("state") === "rest") {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
opacity: 0.3,
|
||||
scale: this.imageMap?.getResolutionForZoom(3) ?
|
||||
this.imageMap.getResolutionForZoom(3) / resolution : 1,
|
||||
anchor: [.95, 0.15],
|
||||
anchorXUnits: "fraction",
|
||||
anchorYUnits: "fraction",
|
||||
src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAAmCAYAAABZNrIjAAABhUlEQVRYR+1YQaqCUBQ9BYZOWkHQyEELSAJbQM7cQiMxmjTXkQtwEomjttAsF6AguoAGjQRX0CRRsI/yg/hlqV8w4b3xfe8ezn3nHN7rKYpy8zwP37o4jkNPkqSbaZrfihGSJHUQ5G63w2QyaZ3V0+mE1WqV43hi0rZt8DzfOkjHcTCfzzsMcr1eYzQatc5kGIbYbrevmWwd3QsA3VR3mXE/jiIT2WKxAEVRhUNIkgSWZSETQ7aq9qil7r/K03UdDMMUgrxer9hsNrgHRhkH+be6CcjfeRAmX13Mxu/k8XjEdDp9a5e+70MQhLxmuVxC0zTQNF24J4oiqKqK/X6f11Tt0U2fJIlTkwFi5nfiGld3ncgisVj3+UCyu0x2z2YzDIfDt2ZxuVzgum5eMx6PwbIs+v1+4Z40TXE+nxEEQV5TtQdJnJre/bTtickynwOPD3dRFCHLMgaDQSGmOI5hGAYOh0NeU7UHSRySOJ/+goiZlzHzqsprRd1NeVuT53Qncbrwsf8D9suXe5WWs/YAAAAASUVORK5CYII=",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
return new Style({
|
||||
image: new Icon({
|
||||
opacity: 1,
|
||||
scale: this.imageMap && this.imageMap.getResolutionForZoom(3) ?
|
||||
this.imageMap.getResolutionForZoom(3) / resolution : 1,
|
||||
anchor: [.95, 0.15],
|
||||
anchorXUnits: "fraction",
|
||||
anchorYUnits: "fraction",
|
||||
src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACkAAAAmCAYAAABZNrIjAAABhUlEQVRYR+1YQaqCUBQ9BYZOWkHQyEELSAJbQM7cQiMxmjTXkQtwEomjttAsF6AguoAGjQRX0CRRsI/yg/hlqV8w4b3xfe8ezn3nHN7rKYpy8zwP37o4jkNPkqSbaZrfihGSJHUQ5G63w2QyaZ3V0+mE1WqV43hi0rZt8DzfOkjHcTCfzzsMcr1eYzQatc5kGIbYbrevmWwd3QsA3VR3mXE/jiIT2WKxAEVRhUNIkgSWZSETQ7aq9qil7r/K03UdDMMUgrxer9hsNrgHRhkH+be6CcjfeRAmX13Mxu/k8XjEdDp9a5e+70MQhLxmuVxC0zTQNF24J4oiqKqK/X6f11Tt0U2fJIlTkwFi5nfiGld3ncgisVj3+UCyu0x2z2YzDIfDt2ZxuVzgum5eMx6PwbIs+v1+4Z40TXE+nxEEQV5TtQdJnJre/bTtickynwOPD3dRFCHLMgaDQSGmOI5hGAYOh0NeU7UHSRySOJ/+goiZlzHzqsprRd1NeVuT53Qncbrwsf8D9suXe5WWs/YAAAAASUVORK5CYII=",
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public tableBorderFeatureStyler(feature) {
|
||||
if (feature.get("state") === "rest") {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: "transparent",
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "transparent",
|
||||
}),
|
||||
});
|
||||
} else if (feature.get("state") === "hovering") {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
opacity: 0.75,
|
||||
color: "black",
|
||||
lineDash: [2, 6],
|
||||
width: 0.75,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "rgba(217, 217, 217, 0.1)",
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
color: "black",
|
||||
lineDash: [2, 6],
|
||||
width: 2,
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "rgba(217, 217, 217, 0.1)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public tableIconBorderFeatureStyler(_feature) {
|
||||
return new Style({
|
||||
stroke: new Stroke({
|
||||
width: 0,
|
||||
color: "transparent",
|
||||
}),
|
||||
fill: new Fill({
|
||||
color: "rgba(217, 217, 217, 0)",
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
setState = <K extends keyof TState>(state: ((prevState: Readonly<TState>) => (Pick<TState, K> | TState | null)) | (Pick<TState, K> | TState | null),
|
||||
callback?: () => void) => {
|
||||
this.component.setState(state, callback);
|
||||
}
|
||||
|
||||
public setTableToView = (tableToView: object, tableToViewId: string): void => {
|
||||
const {state} = this.component;
|
||||
if (state.tableToViewId) {
|
||||
this.setTableState(state.tableToViewId, "rest");
|
||||
}
|
||||
this.setTableState(tableToViewId, "selected");
|
||||
this.setState({
|
||||
tableToView,
|
||||
tableToViewId,
|
||||
});
|
||||
}
|
||||
|
||||
public handleTableToolTipChange = (display: string, width: number, height: number, top: number,
|
||||
left: number, rows: number, columns: number, featureID: string): void => {
|
||||
if (!this.imageMap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {state} = this.component;
|
||||
|
||||
if (featureID !== null && this.imageMap.getTableBorderFeatureByID(featureID).get("state") !== "selected") {
|
||||
this.imageMap.getTableBorderFeatureByID(featureID).set("state", "hovering");
|
||||
this.imageMap.getTableIconFeatureByID(featureID).set("state", "hovering");
|
||||
} else if (featureID === null && state.hoveringFeature &&
|
||||
this.imageMap.getTableBorderFeatureByID(state.hoveringFeature).get("state") !== "selected") {
|
||||
this.imageMap.getTableBorderFeatureByID(state.hoveringFeature).set("state", "rest");
|
||||
this.imageMap.getTableIconFeatureByID(state.hoveringFeature).set("state", "rest");
|
||||
}
|
||||
const newTableIconTooltip = {
|
||||
display,
|
||||
width,
|
||||
height,
|
||||
top,
|
||||
left,
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
this.setState({
|
||||
tableIconTooltip: newTableIconTooltip,
|
||||
hoveringFeature: featureID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getTableBoundingBox(lines: []) {
|
||||
const flattenedLines = [].concat(...lines);
|
||||
const xAxisValues = flattenedLines.filter((value, index) => index % 2 === 0);
|
||||
const yAxisValues = flattenedLines.filter((value, index) => index % 2 === 1);
|
||||
const left = Math.min(...xAxisValues);
|
||||
const top = Math.min(...yAxisValues);
|
||||
const right = Math.max(...xAxisValues);
|
||||
const bottom = Math.max(...yAxisValues);
|
||||
return ([left, top, right, top, right, bottom, left, bottom]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function createRegionIdFromBoundingBox(boundingBox: number[], page: number) {
|
||||
return boundingBox.join(",") + ":" + page;
|
||||
}
|
||||
|
||||
|
|
@ -27,6 +27,10 @@
|
|||
display: flex;
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
.prebuilt-type-dropdown {
|
||||
width: 130px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-disabled {
|
||||
|
@ -78,7 +82,7 @@
|
|||
}
|
||||
|
||||
.model-selection-container {
|
||||
padding-top: 8px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -6,7 +6,6 @@ import { ITag } from "../../../../models/applicationState";
|
|||
import "./predictResult.scss";
|
||||
import { getPrimaryGreenTheme } from "../../../../common/themes";
|
||||
import { PrimaryButton } from "@fluentui/react";
|
||||
import PredictModelInfo from './predictModelInfo';
|
||||
import { strings } from "../../../../common/strings";
|
||||
|
||||
export interface IAnalyzeModelInfo {
|
||||
|
@ -18,7 +17,7 @@ export interface IAnalyzeModelInfo {
|
|||
export interface IPredictResultProps {
|
||||
predictions: { [key: string]: any };
|
||||
analyzeResult: {};
|
||||
analyzeModelInfo: IAnalyzeModelInfo;
|
||||
downloadPrefix?: string;
|
||||
page: number;
|
||||
tags: ITag[];
|
||||
downloadResultLabel: string;
|
||||
|
@ -32,7 +31,7 @@ export interface IPredictResultState { }
|
|||
|
||||
export default class PredictResult extends React.Component<IPredictResultProps, IPredictResultState> {
|
||||
public render() {
|
||||
const { tags, predictions, analyzeModelInfo } = this.props;
|
||||
const { tags, predictions } = this.props;
|
||||
const tagsDisplayOrder = tags.map((tag) => tag.name);
|
||||
for (const name of Object.keys(predictions)) {
|
||||
const prediction = predictions[name];
|
||||
|
@ -51,10 +50,13 @@ export default class PredictResult extends React.Component<IPredictResultProps,
|
|||
|
||||
</div>
|
||||
<div className="container-items-center container-space-between">
|
||||
<PrimaryButton
|
||||
theme={getPrimaryGreenTheme()}
|
||||
onClick={this.onAddAssetToProject}
|
||||
text={strings.predict.editAndUploadToTrainingSet} />
|
||||
{this.props.onAddAssetToProject ?
|
||||
<PrimaryButton
|
||||
theme={getPrimaryGreenTheme()}
|
||||
onClick={this.onAddAssetToProject}
|
||||
text={strings.predict.editAndUploadToTrainingSet} />
|
||||
:<span></span>
|
||||
}
|
||||
<PrimaryButton
|
||||
className="align-self-end keep-button-80px"
|
||||
theme={getPrimaryGreenTheme()}
|
||||
|
@ -64,8 +66,8 @@ export default class PredictResult extends React.Component<IPredictResultProps,
|
|||
onClick={this.triggerDownload}
|
||||
/>
|
||||
</div>
|
||||
<PredictModelInfo modelInfo={analyzeModelInfo} />
|
||||
<div className="prediction-field-header">
|
||||
{this.props.children}
|
||||
<div className="prediction-field-header" style={{marginTop: 28}}>
|
||||
<h6 className="prediction-field-header-field"> Page # / Field name / Value</h6>
|
||||
<h6 className="prediction-field-header-confidence"> Confidence</h6>
|
||||
</div>
|
||||
|
@ -162,7 +164,7 @@ export default class PredictResult extends React.Component<IPredictResultProps,
|
|||
const fileURL = window.URL.createObjectURL(new Blob([predictionData]));
|
||||
const fileLink = document.createElement("a");
|
||||
const fileBaseName = this.props.downloadResultLabel.split(".")[0];
|
||||
const downloadFileName = "Result-" + fileBaseName + ".json";
|
||||
const downloadFileName = this.props.downloadPrefix + "Result-" + fileBaseName + ".json";
|
||||
|
||||
fileLink.href = fileURL;
|
||||
fileLink.setAttribute("download", downloadFileName);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
&-settings {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&-metrics {
|
||||
|
|
|
@ -207,7 +207,6 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
|
|||
await this.deleteOldProjectWhenRenamed(project, isNew);
|
||||
await this.props.applicationActions.ensureSecurityToken(project);
|
||||
await this.props.projectActions.saveProject(project, false, true);
|
||||
// removeStorageItem(constants.projectFormTempKey);
|
||||
|
||||
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
|
||||
|
||||
|
|
|
@ -1,35 +1,33 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {FontIcon, PrimaryButton, Spinner, SpinnerSize, TextField} from "@fluentui/react";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { RouteComponentProps } from "react-router-dom";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { FontIcon, PrimaryButton, Spinner, SpinnerSize, TextField } from "@fluentui/react";
|
||||
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
|
||||
import {connect} from "react-redux";
|
||||
import {RouteComponentProps} from "react-router-dom";
|
||||
import {bindActionCreators} from "redux";
|
||||
import url from "url";
|
||||
import {constants} from "../../../../common/constants";
|
||||
import {isElectron} from "../../../../common/hostProcess";
|
||||
import {interpolate, strings} from "../../../../common/strings";
|
||||
import {getGreenWithWhiteBackgroundTheme, getPrimaryGreenTheme} from "../../../../common/themes";
|
||||
import {AssetLabelingState, AssetState, FieldType, IApplicationState, IAppSettings, IAssetMetadata, IConnection, IProject, IRecentModel} from "../../../../models/applicationState";
|
||||
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
|
||||
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
|
||||
import {
|
||||
IApplicationState, IConnection, IProject, IAppSettings, FieldType, IRecentModel, AssetLabelingState, IAssetMetadata,
|
||||
} from "../../../../models/applicationState";
|
||||
import TrainChart from "./trainChart";
|
||||
import TrainPanel from "./trainPanel";
|
||||
import TrainTable from "./trainTable";
|
||||
import { ITrainRecordProps } from "./trainRecord";
|
||||
import "./trainPage.scss";
|
||||
import { strings, interpolate } from "../../../../common/strings";
|
||||
import { constants } from "../../../../common/constants";
|
||||
import _ from "lodash";
|
||||
import Alert from "../../common/alert/alert";
|
||||
import url from "url";
|
||||
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
|
||||
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
|
||||
import {AssetService} from "../../../../services/assetService";
|
||||
import ServiceHelper from "../../../../services/serviceHelper";
|
||||
import { getPrimaryGreenTheme, getGreenWithWhiteBackgroundTheme } from "../../../../common/themes";
|
||||
import { getAppInsights } from '../../../../services/telemetryService';
|
||||
import { AssetService } from "../../../../services/assetService";
|
||||
import Confirm from "../../common/confirm/confirm";
|
||||
import {getAppInsights} from '../../../../services/telemetryService';
|
||||
import UseLocalStorage from '../../../../services/useLocalStorage';
|
||||
import { isElectron } from "../../../../common/hostProcess";
|
||||
import Alert from "../../common/alert/alert";
|
||||
import Confirm from "../../common/confirm/confirm";
|
||||
import PreventLeaving from "../../common/preventLeaving/preventLeaving";
|
||||
import TrainChart from "./trainChart";
|
||||
import "./trainPage.scss";
|
||||
import TrainPanel from "./trainPanel";
|
||||
import {ITrainRecordProps} from "./trainRecord";
|
||||
import TrainTable from "./trainTable";
|
||||
|
||||
export interface ITrainPageProps extends RouteComponentProps, React.Props<TrainPage> {
|
||||
connections: IConnection[];
|
||||
|
@ -408,7 +406,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
this.setState({ modelUrl: result.headers.location });
|
||||
return result;
|
||||
} catch (err) {
|
||||
ServiceHelper.handleServiceError(err);
|
||||
ServiceHelper.handleServiceError({...err, endpoint: baseURL});
|
||||
}
|
||||
}
|
||||
private async cleanLabelData() {
|
||||
|
@ -417,7 +415,14 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
.filter(asset => asset.labelingState !== AssetLabelingState.Trained)
|
||||
.forEachAsync(async (asset) => {
|
||||
const assetMetadata: IAssetMetadata = { ...await this.props.actions.loadAssetMetadata(this.props.project, asset) };
|
||||
if (assetMetadata.asset.labelingState === AssetLabelingState.ManuallyLabeled
|
||||
let isUpdated=false;
|
||||
assetMetadata.labelData?.labels?.forEach((label,index)=>{
|
||||
if(label.value?.length===0){
|
||||
assetMetadata.labelData.labels.splice(index,1);
|
||||
isUpdated=true;
|
||||
}
|
||||
});
|
||||
if (!isUpdated&&assetMetadata.asset.labelingState === AssetLabelingState.ManuallyLabeled
|
||||
&& assetMetadata.labelData?.labels?.findIndex(label => label.confidence
|
||||
|| label.originValue
|
||||
|| label.revised
|
||||
|
@ -438,7 +443,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
delete item["confidence"];
|
||||
});
|
||||
});
|
||||
await this.props.actions.saveAssetMetadata(this.props.project,assetMetadata);
|
||||
await this.props.actions.saveAssetMetadataAndCleanEmptyLabel(this.props.project,assetMetadata);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -590,7 +595,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
|
|||
project.apiKey as string,
|
||||
).then(res => res.request.response);
|
||||
} catch (error) {
|
||||
ServiceHelper.handleServiceError(error);
|
||||
ServiceHelper.handleServiceError({...error, endpoint: baseURL});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Licensed under the MIT license.
|
||||
|
||||
import React from "react";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import {Switch, Route} from "react-router-dom";
|
||||
import HomePage from "../pages/homepage/homePage";
|
||||
import AppSettingsPage from "../pages/appSettings/appSettingsPage";
|
||||
import TrainPage from "../pages/train/trainPage";
|
||||
|
@ -10,7 +10,9 @@ import ConnectionPage from "../pages/connections/connectionsPage";
|
|||
import EditorPage from "../pages/editorPage/editorPage";
|
||||
import ProjectSettingsPage from "../pages/projectSettings/projectSettingsPage";
|
||||
import ModelComposePage from "../pages/modelCompose/modelCompose";
|
||||
import { PredictPageRoute } from './preditcPageRoute';
|
||||
import {PredictPageRoute} from './preditcPageRoute';
|
||||
import {PrebuiltPredictPage} from "../pages/prebuiltPredict/prebuiltPredictPage";
|
||||
import {LayoutPredictPage} from "../pages/prebuiltPredict/layoutPredictPage";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -32,6 +34,8 @@ export function MainContentRouter() {
|
|||
<Route path="/projects/:projectId/train" component={TrainPage} />
|
||||
<Route path="/projects/:projectId/predict" />
|
||||
<Route path="/projects/:projectId/settings" component={ProjectSettingsPage} />
|
||||
<Route path="/prebuilts-analyze" component={PrebuiltPredictPage} />
|
||||
<Route path="/layout-analyze" component={LayoutPredictPage} />
|
||||
<Route component={HomePage} />
|
||||
</Switch>
|
||||
<PredictPageRoute />
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
.prebuilt-demo-sidebar-item {
|
||||
position: relative;
|
||||
a {
|
||||
text-decoration: none;
|
||||
.demo-badge {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: .35rem;
|
||||
color:white;
|
||||
font-weight: 600;
|
||||
margin-top: -2px;
|
||||
margin-left: -1px;
|
||||
background-color: rgb(177, 7, 7);
|
||||
border-radius: 2px;
|
||||
text-decoration: none;
|
||||
padding: 1px 14px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import { NavLink } from "react-router-dom";
|
|||
import { FontIcon } from "@fluentui/react";
|
||||
import ConditionalNavLink from "../common/conditionalNavLink/conditionalNavLink";
|
||||
import { strings } from "../../../common/strings";
|
||||
import "./sidebar.scss";
|
||||
|
||||
/**
|
||||
* Side bar that remains visible throughout app experience
|
||||
|
@ -65,6 +66,16 @@ export function Sidebar({ project }) {
|
|||
<FontIcon iconName="Plug" />
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="prebuilt-demo-sidebar-item">
|
||||
<NavLink title={strings.prebuiltPredict.title} to={`/prebuilts-analyze`} role="button">
|
||||
<FontIcon iconName="ContactCard" />
|
||||
</NavLink>
|
||||
</li>
|
||||
<li className="prebuilt-demo-sidebar-item">
|
||||
<NavLink title={strings.layoutPredict.title} to={`/layout-analyze`} role="button">
|
||||
<FontIcon iconName="KeyPhraseExtraction" />
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="app-sidebar-fill"></div>
|
||||
<ul>
|
||||
|
|
|
@ -37,4 +37,6 @@ export enum ActionTypes {
|
|||
CLEAR_ERROR = "CLEAR_ERROR",
|
||||
|
||||
SET_TITLE = "SET_TITLE",
|
||||
|
||||
UPDATE_PREBUILT_SETTINGS = "UPDATE_PREBUILT_SETTINGS"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import {Dispatch} from "redux";
|
||||
import {IPrebuiltSettings} from "../../models/applicationState";
|
||||
import {createPayloadAction, IPayloadAction} from "./actionCreators";
|
||||
import {ActionTypes} from "./actionTypes";
|
||||
|
||||
|
||||
export default interface IAppPrebuiltSettingsActions {
|
||||
update(setting: IPrebuiltSettings): void;
|
||||
}
|
||||
|
||||
export function update(setting: IPrebuiltSettings): (dispatch: Dispatch) => void {
|
||||
return (dispatch: Dispatch) => {
|
||||
dispatch(updatePrebuiltSettingsAction(setting));
|
||||
}
|
||||
}
|
||||
|
||||
export interface IUpdatePrebuiltSettingsAction extends IPayloadAction<string, IPrebuiltSettings> {
|
||||
type: ActionTypes.UPDATE_PREBUILT_SETTINGS;
|
||||
}
|
||||
|
||||
const updatePrebuiltSettingsAction = createPayloadAction<IUpdatePrebuiltSettingsAction>(ActionTypes.UPDATE_PREBUILT_SETTINGS);
|
|
@ -35,6 +35,7 @@ export default interface IProjectActions {
|
|||
refreshAsset(project: IProject, assetName: string):Promise<void>;
|
||||
loadAssetMetadata(project: IProject, asset: IAsset): Promise<IAssetMetadata>;
|
||||
saveAssetMetadata(project: IProject, assetMetadata: IAssetMetadata): Promise<IAssetMetadata>;
|
||||
saveAssetMetadataAndCleanEmptyLabel(project: IProject, assetMetadata: IAssetMetadata): Promise<IAssetMetadata>;
|
||||
updateProjectTag(project: IProject, oldTag: ITag, newTag: ITag): Promise<IAssetMetadata[]>;
|
||||
deleteProjectTag(project: IProject, tagName): Promise<IAssetMetadata[]>;
|
||||
updateProjectTagsFromFiles(project: IProject, asset?: string): Promise<void>;
|
||||
|
@ -273,6 +274,19 @@ export function saveAssetMetadata(
|
|||
};
|
||||
}
|
||||
|
||||
export function saveAssetMetadataAndCleanEmptyLabel(
|
||||
project: IProject,
|
||||
assetMetadata: IAssetMetadata): (dispatch: Dispatch) => Promise<IAssetMetadata> {
|
||||
const newAssetMetadata = { ...assetMetadata, version: appInfo.version };
|
||||
|
||||
return async (dispatch: Dispatch) => {
|
||||
const assetService = new AssetService(project);
|
||||
const savedMetadata = await assetService.save(newAssetMetadata,true);
|
||||
dispatch(saveAssetMetadataAction(savedMetadata));
|
||||
|
||||
return { ...savedMetadata };
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Updates a project and all asset references from oldTagName to newTagName
|
||||
* @param project The project to update tags
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { combineReducers } from "redux";
|
||||
import {combineReducers} from "redux";
|
||||
import * as appSettings from "./applicationReducer";
|
||||
import * as connections from "./connectionsReducer";
|
||||
import * as currentProject from "./currentProjectReducer";
|
||||
import * as recentProjects from "./recentProjectsReducer";
|
||||
import * as appError from "./appErrorReducer";
|
||||
import * as appTitle from "./appTitleReducer";
|
||||
import * as prebuiltSettings from "./prebuiltSettingsReducer";
|
||||
|
||||
/**
|
||||
* All application reducers
|
||||
|
@ -24,4 +25,5 @@ export default combineReducers({
|
|||
currentProject: currentProject.reducer,
|
||||
appError: appError.reducer,
|
||||
appTitle: appTitle.reducer,
|
||||
prebuiltSettings: prebuiltSettings.reducer
|
||||
});
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import {IPrebuiltSettings} from "../../models/applicationState";
|
||||
import {ActionTypes} from "../actions/actionTypes";
|
||||
import {IUpdatePrebuiltSettingsAction} from "../actions/prebuiltSettingsActions";
|
||||
|
||||
type AnyAction = IUpdatePrebuiltSettingsAction;
|
||||
|
||||
/**
|
||||
* prebuilt setting Reducer
|
||||
* Actions handled:
|
||||
* UPDATE_PREBUILT_SETTING
|
||||
* @param {IPrebuiltSettings} state
|
||||
* @param {AnyAction} action
|
||||
* @returns {IPrebuiltSettings}
|
||||
*/
|
||||
export const reducer = (state: IPrebuiltSettings = {apiKey: "", serviceURI: ""}, action: AnyAction) :IPrebuiltSettings => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.UPDATE_PREBUILT_SETTINGS:
|
||||
return action.payload;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { IApplicationState } from "../../models/applicationState";
|
||||
import {IApplicationState} from "../../models/applicationState";
|
||||
|
||||
/**
|
||||
* Initial state of application
|
||||
|
@ -18,6 +18,7 @@ const initialState: IApplicationState = {
|
|||
recentProjects: [],
|
||||
currentProject: null,
|
||||
appError: null,
|
||||
prebuiltSettings: null
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -16,7 +16,7 @@ import { Env } from "../../common/environment";
|
|||
export default function createReduxStore(
|
||||
initialState?: IApplicationState,
|
||||
useLocalStorage: boolean = false): Store {
|
||||
const paths: string[] = ["appSettings", "connections", "recentProjects"];
|
||||
const paths: string[] = ["appSettings", "connections", "recentProjects", "prebuiltSettings"];
|
||||
|
||||
let middlewares = [thunk];
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license
|
||||
|
||||
import { registerIcons as registerIcons_ } from "@fluentui/react";
|
||||
import {registerIcons as registerIcons_} from "@fluentui/react";
|
||||
|
||||
export function registerIcons() {
|
||||
registerIcons_({
|
||||
|
@ -9,79 +9,81 @@ export function registerIcons() {
|
|||
fontFamily: "FabricMDL2Icons",
|
||||
},
|
||||
icons: {
|
||||
'Add': '\uE710',
|
||||
'AddField': '\uE4C7',
|
||||
'AddTo': '\uECC8',
|
||||
'AlertSolid': '\uF331',
|
||||
'AutoEnhanceOff': '\uE78E',
|
||||
'AutoEnhanceOn': '\uE78D',
|
||||
'AzureAPIManagement': '\uF37F',
|
||||
'BookAnswers': '\uF8A4',
|
||||
'BranchMerge': '\uF295',
|
||||
'Cancel': '\uE711',
|
||||
'CheckboxComposite': '\uE73A',
|
||||
'CheckMark': '\uE73E',
|
||||
'ChevronDown': '\uE70D',
|
||||
'ChevronLeft': '\uE76B',
|
||||
'ChevronRight': '\uE76C',
|
||||
'ChevronUp': '\uE70E',
|
||||
'ChromeMinimize': '\uE921',
|
||||
'ChromeRestore': '\uE923',
|
||||
'CircleRing': '\uEA3A',
|
||||
'ClearFilter': '\uEF8F',
|
||||
'Cloud': '\uE753',
|
||||
'Copy': '\uE8C8',
|
||||
'Delete': '\uE74D',
|
||||
'Documentation': '\uEC17',
|
||||
'DocumentManagement': '\uEFFC',
|
||||
'Down': '\uE74B',
|
||||
'Download': '\uE896',
|
||||
'Edit': '\uE70F',
|
||||
'FieldChanged': "\uF2C3",
|
||||
'FieldNotChanged': "\uF2C4",
|
||||
'Filter': '\uE71C',
|
||||
'GroupedList': '\uEF74',
|
||||
'GroupList': '\uF168',
|
||||
'Help': '\uE897',
|
||||
'Hide3': '\uF6AC',
|
||||
'Home': '\uE80F',
|
||||
'Info': '\uE946',
|
||||
'Insights': '\uE3AF',
|
||||
'KeyPhraseExtraction': '\uE395',
|
||||
'Label': '\uE932',
|
||||
'Link': '\uE71B',
|
||||
'MachineLearning': '\uE3B8',
|
||||
'MapLayers': '\uE81E',
|
||||
'Merge': '\uE7D5',
|
||||
'More': '\uE712',
|
||||
'OpenFolderHorizontal': '\uED25',
|
||||
'Plug': '\uF300',
|
||||
'PlugConnected': '\uF302',
|
||||
'ReceiptProcessing': '\uE496',
|
||||
'RectangleShape': '\uF1A9',
|
||||
'Refresh': '\uE72C',
|
||||
'Relationship': '\uF003',
|
||||
'Rename': '\uE8AC',
|
||||
'Rotate90Clockwise': '\uF80D',
|
||||
'Rotate90CounterClockwise': '\uF80E',
|
||||
'Search': '\uE721',
|
||||
'Settings': '\uE713',
|
||||
'Share': '\uE72D',
|
||||
'SortDown': '\uEE69',
|
||||
'SortUp': '\uEE68',
|
||||
'SquareShape': '\uF1A6',
|
||||
'StatusCircleCheckmark': '\uF13E',
|
||||
'System': '\uE770',
|
||||
'Table': '\uED86',
|
||||
'Tag': '\uE8EC',
|
||||
'TagGroup': '\uE3F6',
|
||||
'TextDocument': '\uF029',
|
||||
'TextField': '\uEDC3',
|
||||
'Up': '\uE74A',
|
||||
'View': '\uE890',
|
||||
'WarningSolid': '\uF736',
|
||||
'ZoomIn': '\uE8A3',
|
||||
'ZoomOut': '\uE71F',
|
||||
Add: "\uE710",
|
||||
AddField: "\uE4C7",
|
||||
AddTo: "\uECC8",
|
||||
AlertSolid: "\uF331",
|
||||
AutoEnhanceOff: "\uE78E",
|
||||
AutoEnhanceOn: "\uE78D",
|
||||
AzureAPIManagement: "\uF37F",
|
||||
BookAnswers: "\uF8A4",
|
||||
BranchMerge: "\uF295",
|
||||
Cancel: "\uE711",
|
||||
CheckboxComposite: "\uE73A",
|
||||
CheckMark: "\uE73E",
|
||||
ChevronDown: "\uE70D",
|
||||
ChevronLeft: "\uE76B",
|
||||
ChevronRight: "\uE76C",
|
||||
ChevronUp: "\uE70E",
|
||||
ChromeMinimize: "\uE921",
|
||||
ChromeRestore: "\uE923",
|
||||
CircleRing: "\uEA3A",
|
||||
ClearFilter: "\uEF8F",
|
||||
Cloud: "\uE753",
|
||||
ContactCard: "\uEEBD",
|
||||
Copy: "\uE8C8",
|
||||
Delete: "\uE74D",
|
||||
Documentation: "\uEC17",
|
||||
DocumentManagement: "\uEFFC",
|
||||
Down: "\uE74B",
|
||||
Download: "\uE896",
|
||||
Edit: "\uE70F",
|
||||
FieldChanged: "\uF2C3",
|
||||
FieldNotChanged: "\uF2C4",
|
||||
Filter: "\uE71C",
|
||||
GroupedList: "\uEF74",
|
||||
GroupList: "\uF168",
|
||||
Help: "\uE897",
|
||||
Hide3: "\uF6AC",
|
||||
Home: "\uE80F",
|
||||
Info: "\uE946",
|
||||
Insights: "\uE3AF",
|
||||
KeyPhraseExtraction: "\uE395",
|
||||
Label: "\uE932",
|
||||
Link: "\uE71B",
|
||||
MachineLearning: "\uE3B8",
|
||||
MapLayers: "\uE81E",
|
||||
Merge: "\uE7D5",
|
||||
More: "\uE712",
|
||||
OpenFolderHorizontal: "\uED25",
|
||||
Plug: "\uF300",
|
||||
PlugConnected: "\uF302",
|
||||
ReceiptProcessing: "\uE496",
|
||||
RectangleShape: "\uF1A9",
|
||||
Refresh: "\uE72C",
|
||||
Relationship: "\uF003",
|
||||
Rename: "\uE8AC",
|
||||
Rocket: "\uF3B3",
|
||||
Rotate90Clockwise: "\uF80D",
|
||||
Rotate90CounterClockwise: "\uF80E",
|
||||
Search: "\uE721",
|
||||
Settings: "\uE713",
|
||||
Share: "\uE72D",
|
||||
SortDown: "\uEE69",
|
||||
SortUp: "\uEE68",
|
||||
SquareShape: "\uF1A6",
|
||||
StatusCircleCheckmark: "\uF13E",
|
||||
System: "\uE770",
|
||||
Table: "\uED86",
|
||||
Tag: "\uE8EC",
|
||||
TagGroup: "\uE3F6",
|
||||
TextDocument: "\uF029",
|
||||
TextField: "\uEDC3",
|
||||
Up: "\uE74A",
|
||||
View: "\uE890",
|
||||
WarningSolid: "\uF736",
|
||||
ZoomIn: "\uE8A3",
|
||||
ZoomOut: "\uE71F"
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -363,15 +363,18 @@ export class AssetService {
|
|||
* Save metadata for asset
|
||||
* @param metadata - Metadata for asset
|
||||
*/
|
||||
public async save(metadata: IAssetMetadata): Promise<IAssetMetadata> {
|
||||
public async save(metadata: IAssetMetadata, needCleanEmptyLabel: boolean=false): Promise<IAssetMetadata> {
|
||||
Guard.null(metadata);
|
||||
|
||||
const labelFileName = decodeURIComponent(`${metadata.asset.name}${constants.labelFileExtension}`);
|
||||
if (metadata.labelData) {
|
||||
await this.storageProvider.writeText(labelFileName, JSON.stringify(metadata.labelData, null, 4));
|
||||
}
|
||||
|
||||
if (metadata.asset.state !== AssetState.Tagged) {
|
||||
let cleanLabel: boolean=false;
|
||||
if (needCleanEmptyLabel && !metadata.labelData?.labels?.find(label => label?.value?.length !== 0)) {
|
||||
cleanLabel = true;
|
||||
}
|
||||
if (cleanLabel || metadata.asset.state !== AssetState.Tagged) {
|
||||
// If the asset is no longer tagged, then it doesn't contain any regions
|
||||
// and the file is not required.
|
||||
try {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import _ from "lodash";
|
||||
import url from 'url';
|
||||
import { constants } from "../common/constants";
|
||||
import { interpolate, strings } from "../common/strings";
|
||||
import { AppError, ErrorCode, IProject } from "../models/applicationState";
|
||||
import {constants} from "../common/constants";
|
||||
import {interpolate, strings} from "../common/strings";
|
||||
import {AppError, ErrorCode, IProject} from "../models/applicationState";
|
||||
import ServiceHelper from "./serviceHelper";
|
||||
|
||||
export enum AutoLabelingStatus {
|
||||
|
@ -41,13 +41,13 @@ export class PredictService {
|
|||
|
||||
|
||||
} catch (err) {
|
||||
if (err.response.status === 404) {
|
||||
if (err.response?.status === 404) {
|
||||
throw new AppError(
|
||||
ErrorCode.ModelNotFound,
|
||||
interpolate(strings.errors.modelNotFound.message, { modelID })
|
||||
);
|
||||
} else {
|
||||
ServiceHelper.handleServiceError(err);
|
||||
ServiceHelper.handleServiceError({...err, endpoint: endpointURL});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
import { constants } from "../common/constants";
|
||||
import { AppError, ErrorCode } from "../models/applicationState";
|
||||
import { delay } from "../common/utils";
|
||||
import axios, { AxiosRequestConfig, AxiosPromise } from "axios";
|
||||
import { strings } from "../common/strings";
|
||||
import { toast } from "react-toastify";
|
||||
import axios, {AxiosPromise, AxiosRequestConfig} from "axios";
|
||||
import {toast} from "react-toastify";
|
||||
import {constants} from "../common/constants";
|
||||
import {interpolate, strings} from "../common/strings";
|
||||
import {delay} from "../common/utils";
|
||||
import {AppError, ErrorCode} from "../models/applicationState";
|
||||
|
||||
export default class ServiceHelper {
|
||||
public static handleServiceError = (err: any) => {
|
||||
|
@ -14,7 +14,7 @@ export default class ServiceHelper {
|
|||
if (err.response.status === 401) {
|
||||
const message = (err.response.data && err.response.data.error && err.response.data.error.message) || "Please make sure the API key is correct.";
|
||||
throw new AppError(ErrorCode.HttpStatusUnauthorized, message, "Permission Denied");
|
||||
} else if (err.response.status === 404) {
|
||||
} else if (err.response?.status === 404) {
|
||||
throw new AppError(
|
||||
ErrorCode.HttpStatusNotFound,
|
||||
"Please make sure the service endpoint is correct.",
|
||||
|
@ -48,6 +48,12 @@ export default class ServiceHelper {
|
|||
"An error occurred in the service. Please try again later.",
|
||||
"Error");
|
||||
}
|
||||
} else if (err.endpoint) {
|
||||
toast.warn(interpolate(strings.errors.endpointConnectionError.message, err), {autoClose: 10000})
|
||||
throw new AppError(
|
||||
ErrorCode.HttpStatusNotFound,
|
||||
interpolate(strings.errors.endpointConnectionError.message, err),
|
||||
strings.errors.endpointConnectionError.title);
|
||||
} else {
|
||||
// Network Error
|
||||
toast.warn("Over rate limitation, please try again later",{autoClose: 10000})
|
||||
|
@ -77,7 +83,7 @@ export default class ServiceHelper {
|
|||
...config,
|
||||
headers: {
|
||||
...config.headers,
|
||||
...(apiKey ? { [constants.apiKeyHeader]: apiKey } : {}),
|
||||
...(apiKey ? {[constants.apiKeyHeader]: apiKey} : {}),
|
||||
},
|
||||
})
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче