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:
Alex Chen 2020-11-09 10:49:23 +08:00 коммит произвёл GitHub
Родитель 32cfaea023
Коммит e638cd8e3b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
51 изменённых файлов: 3763 добавлений и 927 удалений

Просмотреть файл

@ -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}>&times;</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} : {}),
},
})