diff --git a/.gitignore b/.gitignore index 82e15428..6d9de335 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ secrets.sh es6-src/ report/ debug.log +src/git-commit-info.txt diff --git a/README.md b/README.md index a03b6f1e..4379d1d8 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,15 @@ Click on the Analyze icon on the left pane to open the Analyze page. Upload a fo Tip: You can also run the Analyze API with a REST call. To learn how to do this, see [Train with labels using Python](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/quickstarts/python-labeled-data). +#### Compose a model #### +Click the Compose icon on the left pane to open the Compose page. FoTT will display the first page of your models—by decending order of Model ID—in a list. Select multiple models you want to compose into one model and click the **Compose** button. Once the new model has been composed, it's ready to analyze with. + +![alt text](docs/images/compose.png "Compose") + +To load more of your models, click the **Load next page** button at the bottom of the list. This will load the next page of your models by decending order of model ID. + +You can sort the currently loaded models by clicking the column headers at the top of the list. Only the currently loaded models will be sorted. You will need to load all pages of your models first and then sort to view the complete sorted list of your models. + #### Save a project and resume later #### To resume your project at another time or in another browser, you need to save your project's security token and reenter it later. diff --git a/docs/images/compose.png b/docs/images/compose.png new file mode 100644 index 00000000..64a2f898 Binary files /dev/null and b/docs/images/compose.png differ diff --git a/package.json b/package.json index 23577e9b..8d8afeb6 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "redux": "^4.0.4", "redux-thunk": "^2.3.0", "rimraf": "^3.0.2", - "serialize-javascript": "^3.0.0", + "serialize-javascript": "^5.0.1", "shortid": "^2.2.15", "utif": "^3.1.0", "vott-react": "^0.2.12", @@ -52,8 +52,8 @@ "scripts": { "start": "env-cmd -f .env.electron nf start -p 3000", "compile": "tsc", - "build": "react-scripts build", - "react-start": "react-scripts start", + "build": "node ./scripts/dump_git_info.js && react-scripts build", + "react-start": "node ./scripts/dump_git_info.js && react-scripts start", "test": "react-scripts test --env=jsdom --silent", "eject": "react-scripts eject", "webpack:dev": "webpack --config ./config/webpack.dev.js", diff --git a/scripts/dump_git_info.js b/scripts/dump_git_info.js new file mode 100644 index 00000000..6ee59864 --- /dev/null +++ b/scripts/dump_git_info.js @@ -0,0 +1,21 @@ +spawn = require('child_process').spawn, +fs = require('fs'); + +git = spawn('git', ['log', '-1']), +buf = Buffer.alloc(0); + +git.stdout.on('data', (data) => { + buf = Buffer.concat([buf, data]) +}); + +git.stderr.on('data', (data) => { + console.log(data.toString()); +}); + +git.on('close', (code) => { + fs.writeFile("src/git-commit-info.txt", buf.toString(), (err, data) => { + if (err) { + console.log(err); + } + }); +}); \ No newline at end of file diff --git a/scripts/dump_git_info.sh b/scripts/dump_git_info.sh new file mode 100644 index 00000000..5cf20642 --- /dev/null +++ b/scripts/dump_git_info.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +git status && git log -1 > $DIR/../src/git-commit-info.txt || echo 'Not a Git repo. Continue...' diff --git a/src/App.scss b/src/App.scss index b27b5161..6ada4df2 100644 --- a/src/App.scss +++ b/src/App.scss @@ -148,6 +148,9 @@ .ms-Icon-18px { font-size: 18px; } +.ms-Icon-25px{ + font-size: 25px; +} .ms-Spinner-label { color: inherit; diff --git a/src/assets/sass/fabric-icons-inline.scss b/src/assets/sass/fabric-icons-inline.scss index a7699852..bd5ef2e9 100644 --- a/src/assets/sass/fabric-icons-inline.scss +++ b/src/assets/sass/fabric-icons-inline.scss @@ -3,7 +3,7 @@ */ @font-face { font-family: 'FabricMDL2Icons'; - src: url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,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') format('truetype'); } .ms-Icon { @@ -34,6 +34,7 @@ @mixin ms-Icon--ChromeMinimize { content: "\E921"; } @mixin ms-Icon--ChromeRestore { content: "\E923"; } @mixin ms-Icon--CircleRing { content: "\EA3A"; } +@mixin ms-Icon--ClearFilter { content: "\EF8F"; } @mixin ms-Icon--Cloud { content: "\E753"; } @mixin ms-Icon--Copy { content: "\E8C8"; } @mixin ms-Icon--Delete { content: "\E74D"; } @@ -43,6 +44,8 @@ @mixin ms-Icon--Download { content: "\E896"; } @mixin ms-Icon--Edit { content: "\E70F"; } @mixin ms-Icon--Filter { content: "\E71C"; } +@mixin ms-Icon--GroupedList { content: "\EF74"; } +@mixin ms-Icon--GroupList { content: "\F168"; } @mixin ms-Icon--Help { content: "\E897"; } @mixin ms-Icon--Hide3 { content: "\F6AC"; } @mixin ms-Icon--Home { content: "\E80F"; } @@ -103,6 +106,7 @@ .ms-Icon--ChromeMinimize:before { @include ms-Icon--ChromeMinimize } .ms-Icon--ChromeRestore:before { @include ms-Icon--ChromeRestore } .ms-Icon--CircleRing:before { @include ms-Icon--CircleRing } +.ms-Icon--ClearFilter:before { @include ms-Icon--ClearFilter } .ms-Icon--Cloud:before { @include ms-Icon--Cloud } .ms-Icon--Copy:before { @include ms-Icon--Copy } .ms-Icon--Delete:before { @include ms-Icon--Delete } @@ -112,6 +116,8 @@ .ms-Icon--Download:before { @include ms-Icon--Download } .ms-Icon--Edit:before { @include ms-Icon--Edit } .ms-Icon--Filter:before { @include ms-Icon--Filter } +.ms-Icon--GroupedList:before { @include ms-Icon--GroupedList } +.ms-Icon--GroupList:before { @include ms-Icon--GroupList } .ms-Icon--Help:before { @include ms-Icon--Help } .ms-Icon--Hide3:before { @include ms-Icon--Hide3 } .ms-Icon--Home:before { @include ms-Icon--Home } diff --git a/src/common/constants.ts b/src/common/constants.ts index 4fd6f9d4..8bd70b48 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -37,7 +37,9 @@ export const constants = { convertedThumbnailQuality: 0.2, recentModelRecordsCount: 5, apiModelsPath: `/formrecognizer/${apiVersion}/custom/models`, - autoLabelBatchSize: 10, + autoLabelBatchSizeMax: 10, + autoLabelBatchSizeMin: 3, + showOriginLabelsByDefault: true, pdfjsWorkerSrc(version: string) { return `https://fotts.azureedge.net/npm/pdfjs-dist/${version}/pdf.worker.js`; diff --git a/src/common/localization/en-us.ts b/src/common/localization/en-us.ts index 59dc6d5f..1ec1eced 100644 --- a/src/common/localization/en-us.ts +++ b/src/common/localization/en-us.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IAppStrings } from "../strings"; +import {IAppStrings} from "../strings"; /*eslint-disable no-template-curly-in-string, no-multi-str*/ @@ -41,8 +41,8 @@ export const english: IAppStrings = { }, openCloudProject: { title: "Open Cloud Project", - selectConnection: "Select a Connection", - pasteSharedUri: "Please paste shared project string here", + selectConnection: "Open cloud project", + pasteSharedUri: "Paste shared project token here", }, recentProjects: "Recent Projects", deleteProject: { @@ -131,7 +131,7 @@ export const english: IAppStrings = { downloadJson: "Download JSON file", trainConfirm: { title: "Labels not revised yet", - message: "You have label files not yet revised, do you want to train with those files?" + message: "There are newly auto-labeled files not yet revised by you, do you want to train with those files?" }, errors: { electron: { @@ -272,6 +272,10 @@ export const english: IAppStrings = { }, toolbar: { add: "Add new tag", + onlyShowCurrentPageTags: "Only show tags used in current page", + showAllTags: "Show all tags", + showOriginLabels:"Show origin labels", + hideOriginLabels:"Hide origin labels", contextualMenu: "Contextual Menu", delete: "Delete tag", edit: "Edit tag", @@ -449,10 +453,10 @@ export const english: IAppStrings = { additionalActions: { text: "Additional actions", subIMenuItems: { - runOcrOnCurrentDocument: "Run OCR on current document", - runOcrOnAllDocuments: "Run OCR on all documents", + runOcrOnCurrentDocument: "Run Layout on current document", + runOcrOnAllDocuments: "Run Layout on all documents", runAutoLabelingCurrentDocument: "Auto-label the current document", - runAutoLabelingOnNotLabelingDocuments: "Auto-label next ${batchSize} unlabeled documents", + runAutoLabelingOnMultipleUnlabeledDocuments: "Auto-label multiple unlabeled documents", noPredictModelOnProject: "Predict model not avaliable, please train the model first.", } } @@ -529,7 +533,7 @@ export const english: IAppStrings = { }, tips: { quickLabeling: { - name: "Lable with hot keys", + name: "Label with hot keys", description: "Hotkeys 1 through 0 and all letters are assigned to first 36 tags. After selecting one or multiple words, press tag's assigned hotkey.", }, renameTag: { @@ -700,13 +704,13 @@ export const english: IAppStrings = { shareProject: { name: "Share Project", errors: { - cannotDecodeString: "Cannot decode shared string! Please, check if your string has been modified.", + cannotDecodeString: "Cannot decode shared token. Check if shared token has been modified.", connectionNotFound: "Connection not found. Add shared project's connection to your connections.", - noConnections: "Connection is required for project sharing", + connectionRequirement: "Shared project's connection must be added before opening it", tokenNameExist: "Warning! You already have token with same name as in shared project. Please create a new token, and update the existing project which uses ''${sharedTokenName}'' with new token name." }, copy: { - success: "String for sharing your project has been saved to clipboard. In order to use it, paste it in appropriate section of the 'Open Cloud Project' popup.", + success: "Project token copied to clipboard and ready to share. Reciever of project token can click 'Open Cloud Project' from the Home page to use shared token.", } }, }; diff --git a/src/common/localization/es-cl.ts b/src/common/localization/es-cl.ts index 022d6ad8..dd3c067d 100644 --- a/src/common/localization/es-cl.ts +++ b/src/common/localization/es-cl.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { IAppStrings } from "../strings"; +import {IAppStrings} from "../strings"; /*eslint-disable no-template-curly-in-string, no-multi-str*/ @@ -42,7 +42,7 @@ export const spanish: IAppStrings = { }, openCloudProject: { title: "Abrir Proyecto de la Nube", - selectConnection: "Select a Connection", + selectConnection: "Proyecto de nube abierta", pasteSharedUri: "Pegue la cadena de proyecto compartida aquí", }, deleteProject: { @@ -132,7 +132,7 @@ export const spanish: IAppStrings = { downloadJson: "Descargar archivo JSON", trainConfirm: { title: "Etiquetas no revisadas todavía", - message: "Tiene archivos de etiquetas que aún no han sido revisados, ¿desea entrenar con esos archivos?" + message: "Hay archivos recientemente etiquetados automáticamente que aún no ha revisado, ¿desea entrenar con esos archivos?" }, errors: { electron: { @@ -271,6 +271,10 @@ export const spanish: IAppStrings = { }, toolbar: { add: "Agregar nueva etiqueta", + onlyShowCurrentPageTags: "Mostrar solo las etiquetas utilizadas en la página actual", + showAllTags: "Mostrar todas las etiquetas", + showOriginLabels: "Mostrar etiquetas de origen", + hideOriginLabels: "Ocultar etiquetas de origen", contextualMenu: "Menú contextual", delete: "Borrar etiqueta", edit: "Editar etiqueta", @@ -450,10 +454,10 @@ export const spanish: IAppStrings = { additionalActions: { text: "Acciones adicionales", subIMenuItems: { - runOcrOnCurrentDocument: "Ejecutar OCR en el documento actual", - runOcrOnAllDocuments: "Ejecute OCR en todos los documentos", + runOcrOnCurrentDocument: "Ejecutar Layout en el documento actual", + runOcrOnAllDocuments: "Ejecute Layout en todos los documentos", runAutoLabelingCurrentDocument: "Etiquetar automáticamente el documento actual", - runAutoLabelingOnNotLabelingDocuments: "Etiquetar automáticamente los siguientes ${batchSize} documentos sin etiquetar", + runAutoLabelingOnMultipleUnlabeledDocuments: "Etiquetar automáticamente varios documentos sin etiquetar", noPredictModelOnProject: "Predecir modelo no disponible, entrene el modelo primero.", } } @@ -701,13 +705,13 @@ export const spanish: IAppStrings = { shareProject: { name: "Compartir proyecto", errors: { - cannotDecodeString: "¡No se puede decodificar la cadena compartida! Por favor, verifique si su cadena ha sido modificada.", + cannotDecodeString: "No se puede decodificar el token compartido. Compruebe si se ha modificado el token compartido.", connectionNotFound: "Conexión no encontrada. Agregue la conexión del proyecto compartido a sus conexiones.", - noConnections: "Se requiere conexión para compartir proyectos", + connectionRequirement: "La conexión del proyecto compartido debe agregarse antes de abrirlo", tokenNameExist: "¡Advertencia! Ya tiene token con el mismo nombre que en el proyecto compartido. Cree un nuevo token y actualice el proyecto existente que usa ''${sharedTokenName}'' con el nuevo nombre del token.", }, copy: { - success: "La cadena para compartir su proyecto se ha guardado en el portapapeles. Para usarlo, péguelo en la sección correspondiente de la ventana emergente 'Open Cloud Project'.", + success: "Token de proyecto copiado al portapapeles y listo para compartir. El receptor del token del proyecto puede hacer clic en 'Abrir proyecto en la nube' desde la página de inicio para usar el token compartido.", } } }; diff --git a/src/common/strings.ts b/src/common/strings.ts index cc84dfb4..b0ca0b49 100644 --- a/src/common/strings.ts +++ b/src/common/strings.ts @@ -246,6 +246,10 @@ export interface IAppStrings { } toolbar: { add: string, + onlyShowCurrentPageTags:string, + showAllTags:string, + showOriginLabels: string + hideOriginLabels: string, contextualMenu: string, delete: string, edit: string, @@ -447,7 +451,7 @@ export interface IAppStrings { runOcrOnCurrentDocument: string, runOcrOnAllDocuments: string, runAutoLabelingCurrentDocument: string, - runAutoLabelingOnNotLabelingDocuments: string, + runAutoLabelingOnMultipleUnlabeledDocuments: string, noPredictModelOnProject: string, } } @@ -589,7 +593,7 @@ export interface IAppStrings { errors: { cannotDecodeString: string, connectionNotFound: string, - noConnections: string, + connectionRequirement: string, tokenNameExist: string, }, copy: { diff --git a/src/config/fabric-icons.json b/src/config/fabric-icons.json index 0dc44921..e8b0b81d 100644 --- a/src/config/fabric-icons.json +++ b/src/config/fabric-icons.json @@ -74,6 +74,10 @@ "name": "CircleRing", "unicode": "EA3A" }, + { + "name": "ClearFilter", + "unicode": "EF8F" + }, { "name": "Cloud", "unicode": "E753" @@ -110,6 +114,14 @@ "name": "Filter", "unicode": "E71C" }, + { + "name": "GroupedList", + "unicode": "EF74" + }, + { + "name": "GroupList", + "unicode": "F168" + }, { "name": "Help", "unicode": "E897" diff --git a/src/electron/providers/storage/localFileSystem.ts b/src/electron/providers/storage/localFileSystem.ts index 2a683b6a..55da5fe9 100644 --- a/src/electron/providers/storage/localFileSystem.ts +++ b/src/electron/providers/storage/localFileSystem.ts @@ -6,7 +6,7 @@ import path from "path"; import rimraf from "rimraf"; import { BrowserWindow, dialog } from "electron"; import { IStorageProvider } from "../../../providers/storage/storageProviderFactory"; -import { IAsset, AssetState, AssetType, StorageType } from "../../../models/applicationState"; +import { IAsset, AssetState, AssetType, StorageType, ILabelData, AssetLabelingState } from "../../../models/applicationState"; import { AssetService } from "../../../services/assetService"; import { constants } from "../../../common/constants"; import { strings } from "../../../common/strings"; @@ -180,6 +180,11 @@ export default class LocalFileSystem implements IStorageProvider { const ocrFileName = decodeURIComponent(`${file}${constants.ocrFileExtension}`); if (files.find((str) => str === labelFileName)) { asset.state = AssetState.Tagged; + const json = await this.readText(labelFileName); + const labelData = JSON.parse(json) as ILabelData; + if (labelData) { + asset.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled; + } } else if (files.find((str) => str === ocrFileName)) { asset.state = AssetState.Visited; } else { diff --git a/src/models/applicationState.ts b/src/models/applicationState.ts index ec248052..1a6c57c0 100644 --- a/src/models/applicationState.ts +++ b/src/models/applicationState.ts @@ -232,8 +232,10 @@ export interface ILabel { label: string, key?: IFormRegion[], value: IFormRegion[], + originValue?: IFormRegion[], labelType?: string, confidence?: number, + revised?: boolean; } /** diff --git a/src/react/components/common/assetPreview/assetPreview.tsx b/src/react/components/common/assetPreview/assetPreview.tsx index de209685..beae7c73 100644 --- a/src/react/components/common/assetPreview/assetPreview.tsx +++ b/src/react/components/common/assetPreview/assetPreview.tsx @@ -110,7 +110,7 @@ export class AssetPreview extends React.Component
- +
} diff --git a/src/react/components/common/cloudFilePicker/cloudFilePicker.scss b/src/react/components/common/cloudFilePicker/cloudFilePicker.scss index a4306859..c9b57ad6 100644 --- a/src/react/components/common/cloudFilePicker/cloudFilePicker.scss +++ b/src/react/components/common/cloudFilePicker/cloudFilePicker.scss @@ -1,11 +1,15 @@ .shared-string-input-container { padding: 1rem; .input-uri { - padding: 1rem; + padding: 1rem 1rem 0 1rem; .form-control{ font-size: 90%; } } + .error-message { + color: #db7272; + padding: 4px 1rem 1rem 1rem; + } } .separator { diff --git a/src/react/components/common/cloudFilePicker/cloudFilePicker.tsx b/src/react/components/common/cloudFilePicker/cloudFilePicker.tsx index 3590c517..8433edd5 100644 --- a/src/react/components/common/cloudFilePicker/cloudFilePicker.tsx +++ b/src/react/components/common/cloudFilePicker/cloudFilePicker.tsx @@ -3,13 +3,14 @@ import React from "react"; import { toast } from "react-toastify"; -import { Button, Modal, ModalBody, ModalFooter, ModalHeader, InputGroup, Input } from "reactstrap"; +import { Modal, ModalBody, ModalFooter, ModalHeader, InputGroup, Input } from "reactstrap"; import { strings, interpolate } from "../../../../common/strings"; import { IConnection, StorageType, ErrorCode, AppError, ISecurityToken } from "../../../../models/applicationState"; import { StorageProviderFactory } from "../../../../providers/storage/storageProviderFactory"; import CondensedList, { ListItem } from "../condensedList/condensedList"; import "./cloudFilePicker.scss" -import { Separator } from "@fluentui/react"; +import { PrimaryButton, Separator } from "@fluentui/react"; +import { getPrimaryGreenTheme, getPrimaryGreyTheme } from "../../../../common/themes"; /** * Properties for Cloud File Picker @@ -52,7 +53,6 @@ export interface ICloudFilePickerState { pastedUri: string; pasting: boolean; sharedStringData: ISharedStringData; - haveCloudConnections: boolean; } /** @@ -82,7 +82,6 @@ export class CloudFilePicker extends React.Component×; - return ( @@ -91,10 +90,10 @@ export class CloudFilePicker extends React.Component
-
Shared Project String
- {!this.state.haveCloudConnections && -
{strings.shareProject.errors.noConnections}
- } +
Shared project token
+
+ {strings.shareProject.errors.connectionRequirement} +
@@ -115,13 +113,28 @@ export class CloudFilePicker extends React.Component {this.state.selectedFile || ""} - + {!this.state.okDisabled && + + Open + + } {this.state.backDisabled && !this.state.pastedUri ? - : - + + Cancel + : + + Go Back + }
@@ -161,7 +174,6 @@ export class CloudFilePicker extends React.Component 0, }; } @@ -192,7 +204,7 @@ export class CloudFilePicker extends React.Component providerOptions["sas"].includes(sasFolder)); + const connection: IConnection = connections?.find(({ providerOptions }) => providerOptions["sas"]?.includes(sasFolder)); if (connection) { return connection; } @@ -263,7 +275,7 @@ export class CloudFilePicker extends React.Component { function createProps(tags?: ITag[], onChange?): ITagInputProps { return { tagsLoaded: true, + pageNumber: 1, tags: tags || MockFactory.createTestTags(), lockedTags: [], selectedRegions: [MockFactory.createTestRegion()], diff --git a/src/react/components/common/tagInput/tagInput.tsx b/src/react/components/common/tagInput/tagInput.tsx index e8270ead..c769527c 100644 --- a/src/react/components/common/tagInput/tagInput.tsx +++ b/src/react/components/common/tagInput/tagInput.tsx @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React, { KeyboardEvent } from "react"; import { ContextualMenu, ContextualMenuItemType, @@ -10,20 +9,22 @@ import { IContextualMenuItem, ICustomizations, Spinner, - SpinnerSize, + SpinnerSize } from "@fluentui/react"; -import { strings, interpolate } from "../../../../common/strings"; -import { getDarkTheme } from "../../../../common/themes"; -import { AlignPortal } from "../align/alignPortal"; -import { getNextColor } from "../../../../common/utils"; -import { IRegion, ITag, ILabel, FieldType, FieldFormat, FeatureCategory } from "../../../../models/applicationState"; -import { ColorPicker } from "../colorPicker"; -import "./tagInput.scss"; -import "../condensedList/condensedList.scss"; -import TagInputItem, { ITagInputItemProps, ITagClickProps } from "./tagInputItem"; -import TagInputToolbar from "./tagInputToolbar"; -import { toast } from "react-toastify"; import debounce from 'lodash/debounce'; +import React, {KeyboardEvent} from "react"; +import {toast} from "react-toastify"; +import {constants} from "../../../../common/constants"; +import {interpolate, strings} from "../../../../common/strings"; +import {getDarkTheme} from "../../../../common/themes"; +import {getNextColor} from "../../../../common/utils"; +import {FeatureCategory, FieldFormat, FieldType, ILabel, IRegion, ITag} from "../../../../models/applicationState"; +import {AlignPortal} from "../align/alignPortal"; +import {ColorPicker} from "../colorPicker"; +import "../condensedList/condensedList.scss"; +import "./tagInput.scss"; +import TagInputItem, {ITagClickProps, ITagInputItemProps} from "./tagInputItem"; +import TagInputToolbar from "./tagInputToolbar"; // tslint:disable-next-line:no-var-requires const tagColors = require("../../common/tagColors.json"); @@ -50,6 +51,8 @@ export interface ITagInputProps { selectedRegions?: IRegion[]; /** The labels in the canvas */ labels: ILabel[]; + /** The doc current page number */ + pageNumber: number; /** Tags that are currently locked for editing experience */ lockedTags?: string[]; /** Updates to locked tags */ @@ -83,6 +86,8 @@ export interface ITagInputState { tags: ITag[]; tagOperation: TagOperationMode; addTags: boolean; + onlyCurrentPageTags: boolean; + showOriginLabels: boolean; searchTags: boolean; searchQuery: string; selectedTag: ITag; @@ -109,7 +114,7 @@ function filterFormat(type: FieldType): FieldFormat[] { FieldFormat.YMD, ]; default: - return [FieldFormat.NotSpecified]; + return [ FieldFormat.NotSpecified ]; } } @@ -131,6 +136,8 @@ export class TagInput extends React.Component { searchTags: this.props.showSearchBox, searchQuery: "", selectedTag: null, + onlyCurrentPageTags: false, + showOriginLabels: constants.showOriginLabelsByDefault, }; private tagItemRefs: Map = new Map(); @@ -156,7 +163,6 @@ export class TagInput extends React.Component { }); } } - public render() { const dark: ICustomizations = { settings: { @@ -165,7 +171,7 @@ export class TagInput extends React.Component { scopedSettings: {}, }; - const { selectedTag, tagOperation } = this.state; + const {selectedTag, tagOperation} = this.state; const selectedTagRef = selectedTag ? this.tagItemRefs.get(selectedTag.name)?.getTagNameRef() : null; return ( @@ -174,7 +180,9 @@ export class TagInput extends React.Component { {strings.tags.title} this.setState({ addTags: !this.state.addTags })} + onAddTags={() => this.setState({addTags: !this.state.addTags})} + onOnlyCurrentPageTags={() => this.setState({onlyCurrentPageTags: !this.state.onlyCurrentPageTags})} + onShowOriginLabels = {(showOriginLabels: boolean) => this.setState({showOriginLabels})} onSearchTags={() => this.setState({ searchTags: !this.state.searchTags, searchQuery: "", @@ -186,7 +194,7 @@ export class TagInput extends React.Component { onReorder={this.onReOrder} /> - { this.props.tagsLoaded ? + {this.props.tagsLoaded ?
{ @@ -196,10 +204,10 @@ export class TagInput extends React.Component { className="tag-search-box" type="text" onKeyDown={this.onSearchKeyDown} - onChange={(e) => this.setState({ searchQuery: e.target.value })} + onChange={(e) => this.setState({searchQuery: e.target.value})} placeholder="Search tags" autoFocus={true} - onFocus={() => this.setState({ selectedTag: null, tagOperation: TagOperationMode.Rename })} + onFocus={() => this.setState({selectedTag: null, tagOperation: TagOperationMode.Rename})} />
@@ -243,13 +251,11 @@ export class TagInput extends React.Component {
); } - public triggerNewTagBlur() { if (this.inputRef.current) { this.inputRef.current.blur(); } } - private onRenameTag = (tag: ITag) => { const tagOperation = this.state.tagOperation === TagOperationMode.Rename ? TagOperationMode.None : TagOperationMode.Rename; @@ -262,7 +268,7 @@ export class TagInput extends React.Component { if (!tag) { return; } - let lockedTags = [...this.props.lockedTags]; + let lockedTags = [ ...this.props.lockedTags ]; if (lockedTags.find((str) => isNameEqual(tag.name, str))) { lockedTags = lockedTags.filter((str) => !isNameEqual(tag.name, str)); } else { @@ -279,7 +285,7 @@ export class TagInput extends React.Component { if (!tag) { return; } - const tags = [...this.state.tags]; + const tags = [ ...this.state.tags ]; const currentIndex = tags.indexOf(tag); const newIndex = currentIndex + displacement; if (newIndex < 0 || newIndex >= tags.length) { @@ -315,7 +321,7 @@ export class TagInput extends React.Component { return; } - const tags = [...this.state.tags, tag]; + const tags = [ ...this.state.tags, tag ]; this.setState({ tags, }, () => this.props.onChange(tags)); @@ -362,10 +368,10 @@ export class TagInput extends React.Component { } private getColorPickerPortal = () => { - const { selectedTag } = this.state; + const {selectedTag} = this.state; const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker; return ( - this.headerRef.current}> + this.headerRef.current}>
{ showColorPicker && @@ -395,6 +401,7 @@ export class TagInput extends React.Component { {...prop} key={prop.tag.name} labels={this.setTagLabels(prop.tag.name)} + showOriginLabels={this.state.showOriginLabels} ref={(item) => this.setTagItemRef(item, prop.tag)} onLabelEnter={this.props.onLabelEnter} onLabelLeave={this.props.onLabelLeave} @@ -413,9 +420,34 @@ export class TagInput extends React.Component { } private createTagItemProps = (): ITagInputItemProps[] => { - const { tags, selectedTag, tagOperation } = this.state; + const {tags, selectedTag, tagOperation, onlyCurrentPageTags} = this.state; const selectedRegionTagSet = this.getSelectedRegionTagSet(); + if (onlyCurrentPageTags) { + + const labels = this.props.labels.filter(item => item.value[ 0 ].page === this.props.pageNumber) + .map(item => item.label); + if (labels.length) { + + return tags.filter(tag => labels.find(a => a === tag.name)) + .map(tag => { + return { + tag, + index: tags.findIndex((t) => isNameEqual(t.name, tag.name)), + isLocked: this.props.lockedTags + && this.props.lockedTags.findIndex((str) => isNameEqual(tag.name, str)) > -1, + isRenaming: selectedTag && isNameEqual(selectedTag.name, tag.name) + && tagOperation === TagOperationMode.Rename, + isSelected: selectedTag && isNameEqual(selectedTag.name, tag.name), + appliedToSelectedRegions: selectedRegionTagSet.has(tag.name), + onClick: this.onTagItemClick, + onRename: this.onTagRename, + } as ITagInputItemProps; + }); + } + return []; + } + return tags.map((tag) => ( { tag, @@ -453,7 +485,7 @@ export class TagInput extends React.Component { tagOperation: TagOperationMode.Rename, }); } else if (props.clickedDropDown) { - const { selectedTag } = this.state; + const {selectedTag} = this.state; const showContextualMenu = !selectedTag || !isNameEqual(selectedTag.name, tag.name) || this.state.tagOperation !== TagOperationMode.ContextualMenu; const tagOperation = showContextualMenu ? TagOperationMode.ContextualMenu : TagOperationMode.None; @@ -462,7 +494,7 @@ export class TagInput extends React.Component { tagOperation, }); } else if (props.clickedColor) { - const { selectedTag, tagOperation } = this.state; + const {selectedTag, tagOperation} = this.state; const showColorPicker = tagOperation !== TagOperationMode.ColorPicker; const newTagOperation = showColorPicker ? TagOperationMode.ColorPicker : TagOperationMode.None; this.setState({ @@ -470,27 +502,27 @@ export class TagInput extends React.Component { tagOperation: newTagOperation, }); } else { // Select tag - const { selectedTag, tagOperation: oldTagOperation } = this.state; + const {selectedTag, tagOperation: oldTagOperation} = this.state; const selected = selectedTag && isNameEqual(selectedTag.name, tag.name); const tagOperation = selected ? oldTagOperation : TagOperationMode.None; let deselect = selected && oldTagOperation === TagOperationMode.None; // Only fire click event if a region is selected - const { selectedRegions, onTagClick, labels } = this.props; + const {selectedRegions, onTagClick, labels} = this.props; if (selectedRegions && selectedRegions.length && onTagClick) { - const { category } = selectedRegions[0]; - const { format, type, documentCount, name } = tag; + const {category} = selectedRegions[ 0 ]; + const {format, type, documentCount, name} = tag; const tagCategory = this.getTagCategory(type); const isTagLabelTypeDrawnRegion = this.labelAssignedDrawnRegion(labels, tag.name); const labelAssigned = this.labelAssigned(labels, name); if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) { if (isTagLabelTypeDrawnRegion) { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: category })); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: category})); } else if (tagCategory === FeatureCategory.Checkbox) { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Checkbox })); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: FeatureCategory.Checkbox})); } else { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Text })); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCatagory: FeatureCategory.Text})); } return; } else if (tagCategory === category || category === FeatureCategory.DrawnRegion || @@ -502,7 +534,7 @@ export class TagInput extends React.Component { onTagClick(tag); deselect = false; } else { - toast.warn(strings.tags.warnings.notCompatibleTagType, { autoClose: 7000 }); + toast.warn(strings.tags.warnings.notCompatibleTagType, {autoClose: 7000}); } } this.setState({ @@ -586,7 +618,7 @@ export class TagInput extends React.Component { format: FieldFormat.NotSpecified, documentCount: 0, }; - if (newTag.name.length && ![...this.state.tags, newTag].containsDuplicates((t) => t.name)) { + if (newTag.name.length && ![ ...this.state.tags, newTag ].containsDuplicates((t) => t.name)) { this.addTag(newTag); } else if (!newTag.name.length) { toast.warn(strings.tags.warnings.emptyName); @@ -611,7 +643,7 @@ export class TagInput extends React.Component { } private onHideContextualMenu = () => { - this.setState({ tagOperation: TagOperationMode.None }); + this.setState({tagOperation: TagOperationMode.None}); } private getContextualMenuItems = (): IContextualMenuItem[] => { diff --git a/src/react/components/common/tagInput/tagInputItem.test.tsx b/src/react/components/common/tagInput/tagInputItem.test.tsx index c3fa1465..7dc77670 100644 --- a/src/react/components/common/tagInput/tagInputItem.test.tsx +++ b/src/react/components/common/tagInput/tagInputItem.test.tsx @@ -21,6 +21,7 @@ describe("Tag Input Item", () => { onRename: jest.fn(), onLabelEnter: jest.fn(), onLabelLeave: jest.fn(), + showOriginLabels:false, }; } diff --git a/src/react/components/common/tagInput/tagInputItem.tsx b/src/react/components/common/tagInput/tagInputItem.tsx index 4d7e9de3..b35ab793 100644 --- a/src/react/components/common/tagInput/tagInputItem.tsx +++ b/src/react/components/common/tagInput/tagInputItem.tsx @@ -1,13 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React, { MouseEvent } from "react"; -import { FontIcon, IconButton } from "@fluentui/react"; -import { ITag, ILabel, FieldType, FieldFormat } from "../../../../models/applicationState"; -import { strings } from "../../../../common/strings"; -import TagInputItemLabel from "./tagInputItemLabel"; -import { tagIndexKeys } from "./tagIndexKeys"; +import {FontIcon, IconButton} from "@fluentui/react"; import _ from "lodash"; +import React, {Fragment, MouseEvent} from "react"; +import {strings} from "../../../../common/strings"; +import {FieldFormat, FieldType, ILabel, ITag} from "../../../../models/applicationState"; +import {tagIndexKeys} from "./tagIndexKeys"; +import TagInputItemLabel from "./tagInputItemLabel"; export interface ITagClickProps { ctrlKey?: boolean; @@ -27,6 +27,8 @@ export interface ITagInputItemProps { index: number; /** Labels owned by the tag */ labels: ILabel[]; + /** show Origin Labels or not */ + showOriginLabels: boolean; /** Tag is currently renaming */ isRenaming: boolean; /** Tag is currently locked for application */ @@ -42,7 +44,7 @@ export interface ITagInputItemProps { onLabelEnter: (label: ILabel) => void; onLabelLeave: (label: ILabel) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void; - onTagDoubleClick?: (label:ILabel) => void; + onTagDoubleClick?: (label: ILabel) => void; } export interface ITagInputItemState { @@ -81,14 +83,8 @@ export default class TagInputItem extends React.Component - {confidence && -
- {confidence} -
- }
{ @@ -136,7 +132,7 @@ export default class TagInputItem extends React.Component { @@ -144,12 +140,12 @@ export default class TagInputItem extends React.Component { + private onNameDoubleClick = (e: MouseEvent) => { e.stopPropagation(); - const { labels } = this.props; + const {labels} = this.props; if (labels.length > 0) { this.props.onTagDoubleClick(labels[0]); } @@ -206,7 +202,7 @@ export default class TagInputItem extends React.Component
@@ -214,15 +210,47 @@ export default class TagInputItem extends React.Component { + let confidence = _.get(this.props, "labels[0].confidence", null); + if (confidence > .995) { + confidence = 0.995; + } + const revised = _.get(this.props, "labels[0].revised", false); return this.props.labels.map((label, idx) => - ); + +
+ {(confidence||revised)&& +
+ {confidence && +
+ {confidence} +
+ } + {revised && + + } +
+ } +
+ { this.props.showOriginLabels && label.originValue && + + } + +
+
+
); } - private onInputRef = (element: HTMLInputElement) => { this.inputElement = element; if (element) { @@ -274,20 +302,20 @@ export default class TagInputItem extends React.Component { - const { tag } = this.props; + const {tag} = this.props; return (tag.type && tag.type !== FieldType.String) || (tag.format && tag.format !== FieldFormat.NotSpecified); } private handleMouseEnter = () => { - const { labels } = this.props; + const {labels} = this.props; if (labels.length > 0) { this.props.onLabelEnter(labels[0]); } } private handleMouseLeave = () => { - const { labels } = this.props; + const {labels} = this.props; if (labels.length > 0) { this.props.onLabelLeave(labels[0]); } diff --git a/src/react/components/common/tagInput/tagInputItemLabel.tsx b/src/react/components/common/tagInput/tagInputItemLabel.tsx index 04a44e94..6b804a56 100644 --- a/src/react/components/common/tagInput/tagInputItemLabel.tsx +++ b/src/react/components/common/tagInput/tagInputItemLabel.tsx @@ -2,13 +2,16 @@ // Licensed under the MIT license. import React from "react"; -import { ILabel, IFormRegion } from "../../../../models/applicationState"; -import { FontIcon } from "@fluentui/react"; +import {ILabel, IFormRegion} from "../../../../models/applicationState"; +import {FontIcon} from "@fluentui/react"; export interface ITagInputItemLabelProps { label: ILabel; - onLabelEnter: (label: ILabel) => void; - onLabelLeave: (label: ILabel) => void; + value: IFormRegion[]; + isOrigin: boolean; + onLabelEnter?: (label: ILabel) => void; + onLabelLeave?: (label: ILabel) => void; + prefixText?:string } export interface ITagInputItemLabelState {} @@ -17,7 +20,7 @@ export default class TagInputItemLabel extends React.Component { + this.props.value.forEach((formRegion: IFormRegion, idx) => { if (formRegion.text === "") { hasEmptyTextValue = true; } else { @@ -27,14 +30,14 @@ export default class TagInputItemLabel extends React.Component
- {text} + {this.props.prefixText} {text} {hasEmptyTextValue && - + }
@@ -42,10 +45,14 @@ export default class TagInputItemLabel extends React.Component { - this.props.onLabelEnter(this.props.label); + if (this.props.onLabelEnter) { + this.props.onLabelEnter(this.props.label); + } } private handleMouseLeave = () => { - this.props.onLabelLeave(this.props.label); + if (this.props.onLabelLeave) { + this.props.onLabelLeave(this.props.label); + } } } diff --git a/src/react/components/common/tagInput/tagInputSize.scss b/src/react/components/common/tagInput/tagInputSize.scss index 954abc45..110562f2 100644 --- a/src/react/components/common/tagInput/tagInputSize.scss +++ b/src/react/components/common/tagInput/tagInputSize.scss @@ -5,7 +5,7 @@ $tagInputWidth: 275px; $tagColorWidth: 8px; $tagLinkWidth: 22px; $tagItemWidth: $tagInputWidth - $tagColorWidth; -$tagTextWidth: $tagItemWidth - 55px; +$tagTextWidth: $tagItemWidth - 95px; $tagTextLinkedWidth: $tagTextWidth - $tagLinkWidth; $tagContextualMenuWidth: $tagItemWidth - 8px; $tagColorPickerHeight: 480px; diff --git a/src/react/components/common/tagInput/tagInputToolbar.tsx b/src/react/components/common/tagInput/tagInputToolbar.tsx index 664c407c..c034db33 100644 --- a/src/react/components/common/tagInput/tagInputToolbar.tsx +++ b/src/react/components/common/tagInput/tagInputToolbar.tsx @@ -2,9 +2,10 @@ // Licensed under the MIT license. import React from "react"; -import { IconButton } from "@fluentui/react"; -import { strings } from "../../../../common/strings"; -import { ITag } from "../../../../models/applicationState"; +import {IconButton} from "@fluentui/react"; +import {strings} from "../../../../common/strings"; +import {ITag} from "../../../../models/applicationState"; +import {constants} from "../../../../common/constants"; enum Categories { General, @@ -29,6 +30,8 @@ export interface ITagInputToolbarProps { onDelete: (tag: ITag) => void; /** Function to call when one of the re-order buttons is clicked */ onReorder: (tag: ITag, displacement: number) => void; + onOnlyCurrentPageTags: (onlyCurrentPageTags: boolean) => void; + onShowOriginLabels?: (showOrigin: boolean) => void; searchingTags: boolean; } @@ -40,7 +43,17 @@ interface ITagInputToolbarItemProps { accelerators?: string[]; } -export default class TagInputToolbar extends React.Component { +interface ITagInputToolbarItemState { + tagFilterToggled: boolean; + showOriginLabels: boolean; +} + +export default class TagInputToolbar extends React.Component { + state = { + tagFilterToggled: false, + showOriginLabels: constants.showOriginLabelsByDefault, + }; + public render() { return (
@@ -57,6 +70,18 @@ export default class TagInputToolbar extends React.Component { if (itemConfig.category === Categories.General) { return ( @@ -161,6 +186,19 @@ export default class TagInputToolbar extends React.Component { this.props.onAddTags(); } + private handleOnlyCurrentPageTags = () => { + this.setState({tagFilterToggled: !this.state.tagFilterToggled}, () => { + this.props.onOnlyCurrentPageTags(this.state.tagFilterToggled); + }); + } + + private handleShowOriginLabels = () => { + this.setState({showOriginLabels: !this.state.showOriginLabels}, () => { + if (this.props.onShowOriginLabels) { + this.props.onShowOriginLabels(this.state.showOriginLabels); + } + }); + } private handleSearch = () => { this.props.onSearchTags(); diff --git a/src/react/components/pages/editorPage/batchSizeModal.tsx b/src/react/components/pages/editorPage/batchSizeModal.tsx new file mode 100644 index 00000000..a659acb4 --- /dev/null +++ b/src/react/components/pages/editorPage/batchSizeModal.tsx @@ -0,0 +1,89 @@ +import {Customizer, ICustomizations, IModalStyles, Modal, PrimaryButton, Slider} from "@fluentui/react"; +import React from "react"; +import {ModalBody} from "reactstrap"; +import {constants} from "../../../../common/constants"; +import {getDarkGreyTheme, getPrimaryGreenTheme, getPrimaryGreyTheme} from "../../../../common/themes"; + +interface IBatchSizeModalProps { + onConfirm?: (batchSize: number) => void; +} + +interface IBatchSizeModalState { + showModal: boolean; + batchSize: number; +} + +export class BatchSizeModal extends React.Component{ + state = { + showModal: false, + batchSize: 3 + }; + + onConfirm = () => { + this.setState({showModal: false}); + if (this.props.onConfirm) { + this.props.onConfirm(this.state.batchSize); + } + } + onCancel = () => { + this.setState({showModal: false}); + } + + onBatchSizeChange = (value: number) => { + this.setState({ + batchSize: value + }); + } + + openModal() { + this.setState({ + batchSize: constants.autoLabelBatchSizeMin, + showModal: true, + }); + } + + render() { + const dark: ICustomizations = { + settings: { + theme: getDarkGreyTheme(), + }, + scopedSettings: {}, + }; + const styles: Partial = { + main: { + width: "400px!important", + } + }; + return ( + + +

Set Auto Labeling Batch Size

+ + + +
+ + Ok + + Cancel +
+
+
+ ); + } +} \ No newline at end of file diff --git a/src/react/components/pages/editorPage/canvas.scss b/src/react/components/pages/editorPage/canvas.scss index adc0e25e..ffd4e1ba 100644 --- a/src/react/components/pages/editorPage/canvas.scss +++ b/src/react/components/pages/editorPage/canvas.scss @@ -40,14 +40,14 @@ .prev { position: absolute; top: 50%; - left: 50px; + left: 0; margin-left: 10px; } .next { position: absolute; top: 50%; - right: 50px; + right: 0; margin-right: 10px; } diff --git a/src/react/components/pages/editorPage/canvas.tsx b/src/react/components/pages/editorPage/canvas.tsx index 96557377..5b4047b2 100644 --- a/src/react/components/pages/editorPage/canvas.tsx +++ b/src/react/components/pages/editorPage/canvas.tsx @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import React, { ReactElement } from "react"; +import React, { ReactElement, RefObject } from "react"; import { Spinner, SpinnerSize } from "@fluentui/react/lib/Spinner"; import { Label } from "@fluentui/react/lib/Label"; import { IconButton } from "@fluentui/react/lib/Button"; @@ -39,6 +39,7 @@ import { AutoLabelingStatus, PredictService } from "../../../../services/predict import { AssetService } from "../../../../services/assetService"; import { interpolate, strings } from "../../../../common/strings"; import { toast } from "react-toastify"; +import {BatchSizeModal} from "./batchSizeModal"; pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version); @@ -62,8 +63,9 @@ export interface ICanvasProps extends React.Props { onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void; runOcrForAllDocs?: (runForAllDocs: boolean) => void; - runAutoLabelingOnNextBatch?: () => Promise; + runAutoLabelingOnNextBatch?: (batchSize: number) => Promise; onAssetDeleted?: () => void; + onPageLoaded?: (pageNumber: number) => void; } export interface ICanvasState { @@ -81,7 +83,7 @@ export interface ICanvasState { errorTitle?: string; errorMessage: string; ocrStatus: OcrStatus; - autoLableingStatus: AutoLabelingStatus; + autoLabelingStatus: AutoLabelingStatus; layers: any; tableIconTooltip: any; hoveringFeature: string; @@ -141,7 +143,7 @@ export default class Canvas extends React.Component isError: false, errorMessage: undefined, ocrStatus: OcrStatus.done, - autoLableingStatus: AutoLabelingStatus.none, + autoLabelingStatus: AutoLabelingStatus.none, layers: { text: true, tables: true, checkboxes: true, label: true, drawnRegions: true }, tableIconTooltip: { display: "none", width: 0, height: 0, top: 0, left: 0 }, hoveringFeature: null, @@ -172,6 +174,8 @@ export default class Canvas extends React.Component private tableIDToIndexMap: object; + autoLabelingBatchSizeModal: RefObject = React.createRef(); + public componentDidMount = async () => { this.ocrService = new OCRService(this.props.project); const asset = this.state.currentAsset.asset; @@ -257,7 +261,7 @@ export default class Canvas extends React.Component handleAssetDeleted={this.props.onAssetDeleted} handleRunOcrForAllDocuments={this.runOcrForAllDocuments} handleRunAutoLabelingOnCurrentDocument={this.runAutoLabelingOnCurrentDocument} - handleRunAutoLabelingForRestDocuments={this.runAutoLabelingForRestDocuments} + handleRunAutoLabelingOnMultipleUnlabeledDocuments={this.runAutoLabelingOnMultipleUnlabeledDocuments} handleToggleDrawRegionMode={this.handleToggleDrawRegionMode} connectionType={this.props.project.sourceConnection.providerType} drawRegionMode={this.state.drawRegionMode} @@ -339,11 +343,11 @@ export default class Canvas extends React.Component
- +
} - {this.state.autoLableingStatus === AutoLabelingStatus.running && + {this.state.autoLabelingStatus === AutoLabelingStatus.running &&
@@ -361,6 +365,10 @@ export default class Canvas extends React.Component errorMessage: undefined, })} /> +
); } @@ -385,10 +393,13 @@ export default class Canvas extends React.Component this.setAutoLabelingStatus(AutoLabelingStatus.done); } } - private runAutoLabelingForRestDocuments = async () => { - this.setState({ autoLableingStatus: AutoLabelingStatus.running }); - await this.props.runAutoLabelingOnNextBatch(); - this.setState({ autoLableingStatus: AutoLabelingStatus.done }); + private runAutoLabelingOnMultipleUnlabeledDocuments = async () => { + this.autoLabelingBatchSizeModal.current.openModal(); + } + private confirmRunAutoLabelingOnMultipleUnlabeledDocuments = async (batchSize: number) => { + this.setState({ autoLabelingStatus: AutoLabelingStatus.running }); + await this.props.runAutoLabelingOnNextBatch(batchSize); + this.setState({ autoLabelingStatus: AutoLabelingStatus.done }); } public updateSize() { @@ -537,8 +548,8 @@ export default class Canvas extends React.Component } private deleteRegions = (regions: IRegion[]) => { - this.deleteRegionsFromSelectedRegionIds(regions); this.deleteRegionsFromAsset(regions); + this.deleteRegionsFromSelectedRegionIds(regions); this.deleteRegionsFromImageMap(regions); } @@ -1196,10 +1207,11 @@ export default class Canvas extends React.Component } }); } - private setAutoLabelingStatus = (autoLableingStatus: AutoLabelingStatus) => { - this.setState({ autoLableingStatus }, () => { + + private setAutoLabelingStatus = (autoLabelingStatus: AutoLabelingStatus) => { + this.setState({ autoLabelingStatus }, () => { if (this.props.onRunningAutoLabelingStatusChanged) { - this.props.onRunningAutoLabelingStatusChanged(autoLableingStatus === AutoLabelingStatus.running); + this.props.onRunningAutoLabelingStatusChanged(autoLabelingStatus === AutoLabelingStatus.running); } }) } @@ -1297,6 +1309,9 @@ export default class Canvas extends React.Component currentPage: pageNumber, pdfFile: pdf, }); + if (this.props.onPageLoaded) { + this.props.onPageLoaded(pageNumber); + } } } @@ -1370,7 +1385,19 @@ export default class Canvas extends React.Component && this.props.selectedAsset.labelData.labels.map(label => ({ ...label, value: [] }))) || []; - + const selectedRegions = this.getSelectedRegions(); + if (selectedRegions.length > 0) { + const intersectionResult = _.intersection(selectedRegions, regions); + if (intersectionResult.length === 0) { + const relatedLabels = labels.find(label => selectedRegions.find(sr => sr.tags.find(t => t === label.label))); + const originLabel = this.props.selectedAsset!.labelData!.labels.find(a => a.label === relatedLabels.label); + if (relatedLabels&&originLabel&&relatedLabels.confidence) { + delete relatedLabels.confidence; + relatedLabels.revised = true; + relatedLabels.originValue = [...originLabel.value]; + } + } + } regions.forEach((region) => { const labelType = this.getLabelType(region.category); const boundingBox = region.id.split(",").map(parseFloat); @@ -1382,8 +1409,11 @@ export default class Canvas extends React.Component region.tags.forEach((tag) => { const label = labels.find(label => label.label === tag); if (label) { + const originLabel = this.props.selectedAsset!.labelData!.labels.find(a=>a.label === tag); if (label.confidence && region.changed) { delete label.confidence; + label.revised = true; + label.originValue = [...originLabel.value]; } label.value.push(formRegion); } else { diff --git a/src/react/components/pages/editorPage/canvasCommandBar.tsx b/src/react/components/pages/editorPage/canvasCommandBar.tsx index e9557d83..21ceb938 100644 --- a/src/react/components/pages/editorPage/canvasCommandBar.tsx +++ b/src/react/components/pages/editorPage/canvasCommandBar.tsx @@ -15,7 +15,7 @@ interface ICanvasCommandBarProps { handleRunOcr?: () => void; handleRunOcrForAllDocuments?: () => void; handleRunAutoLabelingOnCurrentDocument?: () => void; - handleRunAutoLabelingForRestDocuments?: () => void; + handleRunAutoLabelingOnMultipleUnlabeledDocuments?: () => void; handleLayerChange?: (layer: string) => void; handleToggleDrawRegionMode?: () => void; handleAssetDeleted?: () => void; @@ -153,11 +153,9 @@ export const CanvasCommandBar: React.FunctionComponent = if (props.parentPage === strings.editorPage.title) { commandBarFarItems.push({ key: "additionalActions", + text: "Actions", title: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.text, - // This needs an ariaLabel since it's icon-only - ariaLabel: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.text, className: "additional-action-dropdown", - iconProps: { iconName: "More" }, subMenuProps: { items: [ { @@ -188,14 +186,14 @@ export const CanvasCommandBar: React.FunctionComponent = }, }, { - key: "runAutoLabelingForRestDocuments", - text: interpolate(strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnNotLabelingDocuments, { batchSize: constants.autoLabelBatchSize }), + key: "runAutoLabelingOnMultipleUnlabeledDocuments", + text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnMultipleUnlabeledDocuments, iconProps: { iconName: "Tag" }, disabled: disableAutoLabeling, title: props.project.predictModelId ? "" : strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject, onClick: () => { - if (props.handleRunAutoLabelingForRestDocuments) props.handleRunAutoLabelingForRestDocuments(); + if (props.handleRunAutoLabelingOnMultipleUnlabeledDocuments) props.handleRunAutoLabelingOnMultipleUnlabeledDocuments(); }, }, { diff --git a/src/react/components/pages/editorPage/editorPage.tsx b/src/react/components/pages/editorPage/editorPage.tsx index ce7f4850..f404f9c6 100644 --- a/src/react/components/pages/editorPage/editorPage.tsx +++ b/src/react/components/pages/editorPage/editorPage.tsx @@ -96,6 +96,7 @@ export interface IEditorPageState { errorMessage?: string; tableToView: object; tableToViewId: string; + pageNumber: number; } function mapStateToProps(state: IApplicationState) { @@ -132,6 +133,7 @@ export default class EditorPage extends React.Component; @@ -231,11 +233,11 @@ export default class EditorPage extends React.Component -
: "Run OCR on unvisited documents" +
: "Run Layout on unvisited documents" } } @@ -278,6 +280,7 @@ export default class EditorPage extends React.Component @@ -296,6 +299,7 @@ export default class EditorPage extends React.Component { const tag = this.getTagFromKeyboardEvent(event); - const selection = this.canvas.current.getSelectedRegions(); + const selection = this.canvas?.current?.getSelectedRegions(); if (tag && selection.length) { const { format, type, documentCount, name } = tag; @@ -627,6 +631,10 @@ export default class EditorPage extends React.Component { + this.setState({ pageNumber }); + } + private onLockedTagsChanged = (lockedTags: string[]) => { this.setState({ lockedTags }); } @@ -753,7 +761,7 @@ export default class EditorPage extends React.Component { + private runAutoLabelingOnNextBatch = async (batchSize: number) => { if (this.isBusy()) { return; } @@ -764,7 +772,7 @@ export default class EditorPage extends React.Component diff --git a/src/react/components/pages/modelCompose/modelCompose.tsx b/src/react/components/pages/modelCompose/modelCompose.tsx index 133f8d59..d101c13d 100644 --- a/src/react/components/pages/modelCompose/modelCompose.tsx +++ b/src/react/components/pages/modelCompose/modelCompose.tsx @@ -489,18 +489,13 @@ export default class ModelComposePage extends React.Component recentModelIds.indexOf(model.modelId) === -1); - const newList = currentList.concat(nextPageList); - - this.allModels = newList; - const newCols = this.state.columns; - newCols.forEach((ncol) => { - ncol.isSorted = false; - ncol.isSortedDescending = true; - }); + const appendedList = currentList.concat(nextPageList); + const currerntlySortedColumn: IColumn = this.state.columns.find((column) => column.isSorted); + const appendedAndSortedList = this.copyAndSort(appendedList, currerntlySortedColumn.fieldName!, currerntlySortedColumn.isSortedDescending); + this.allModels = appendedAndSortedList; this.setState({ - modelList: newList, + modelList: appendedAndSortedList, nextLink: nextPage.nextLink, - columns: newCols, }, () => { this.setState({ isLoading: false, @@ -557,7 +552,7 @@ export default class ModelComposePage extends React.Component, column: IColumn): void => { const {columns, modelList} = this.state; const newColumns: IColumn[] = columns.slice(); - const currColumn: IColumn = newColumns.filter((col) => column.key === col.key)[0]; + const currColumn: IColumn = newColumns.find((col) => column.key === col.key); newColumns.forEach((newCol: IColumn) => { if (newCol === currColumn) { currColumn.isSortedDescending = !currColumn.isSortedDescending; diff --git a/src/react/components/shell/statusBar.tsx b/src/react/components/shell/statusBar.tsx index 6e8cc572..233d31e5 100644 --- a/src/react/components/shell/statusBar.tsx +++ b/src/react/components/shell/statusBar.tsx @@ -2,16 +2,29 @@ // Licensed under the MIT license. import React from "react"; -import { FontIcon } from "@fluentui/react"; -import { constants } from "../../../common/constants"; +import {FontIcon} from "@fluentui/react"; +import {constants} from "../../../common/constants"; +import axios from 'axios'; import "./statusBar.scss"; import { IProject } from "../../../models/applicationState"; - export interface IStatusBarProps { project: IProject; } +interface IStatusBarState { + commitHash?: string; +} +export class StatusBar extends React.Component { + componentDidMount() { + const commitInfoUrl = require("../../../git-commit-info.txt"); + axios.get(commitInfoUrl).then(res => { + // match the git commit hash + const commitHash = /commit ([0-9a-fA-F]{7})/.exec(res?.data)[1]; + this.setState({ commitHash: commitHash || "" }); + }); + } -export class StatusBar extends React.Component { + +// export class StatusBar extends React.Component { public render() { return (
@@ -29,7 +42,7 @@ export class StatusBar extends React.Component {
  • - {constants.appVersionRaw}-1f33130 + {constants.appVersion}-{this.state?.commitHash}
  • diff --git a/src/registerIcons.ts b/src/registerIcons.ts index b75a7b87..ba3d177f 100644 --- a/src/registerIcons.ts +++ b/src/registerIcons.ts @@ -55,6 +55,7 @@ export function registerIcons() { StatusCircleCheckmark: "\uF13E", CircleRing: "\uEA3A", Filter: "\uE71C", + ClearFilter: "\uEF8F", Table: "\uED86", MapLayers: "\uE81E", BookAnswers: "\uF8A4", @@ -74,6 +75,9 @@ export function registerIcons() { RectangleShape: "\uF1A9", Rotate90CounterClockwise: "\uF80E", Rotate90Clockwise: "\uF80D", - AzureAPIManagement: "\uF37F", }, + AzureAPIManagement: "\uF37F", + GroupedList: "\uEF74", + GroupList: "\uF168", + }, }); } diff --git a/src/services/ocrService.ts b/src/services/ocrService.ts index affebef2..e62ff0d3 100644 --- a/src/services/ocrService.ts +++ b/src/services/ocrService.ts @@ -155,7 +155,7 @@ export class OCRService { setTimeout(checkSucceeded, interval, resolve, reject); } else { // Didn't succeeded after too much time, reject - reject(new Error("Timed out for getting OCR results")); + reject(new Error("Timed out for getting Layout results")); } }); }; diff --git a/yarn.lock b/yarn.lock index a032d80a..fa68709d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11753,13 +11753,20 @@ serialize-javascript@^2.1.2: resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== -serialize-javascript@^3.0.0, serialize-javascript@^3.1.0: +serialize-javascript@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.1.0.tgz#8bf3a9170712664ef2561b44b691eafe399214ea" integrity sha512-JIJT1DGiWmIKhzRsG91aS6Ze4sFUrYbltlkg2onR5OrnNM02Kl/hnY/T4FN2omvyeBbQmMJv+K4cPOpGzOTFBg== dependencies: randombytes "^2.1.0" +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239"