From 34627adaa015a2aeb3db9866da925dc78f095f81 Mon Sep 17 00:00:00 2001 From: alex-krasn <64093224+alex-krasn@users.noreply.github.com> Date: Fri, 16 Oct 2020 12:18:36 -0700 Subject: [PATCH] merge master into RTL branch (#647) * merge master into RTL branch * fix an API icon --- .env.release => .env.electron | 3 +- CHANGELOG.md | 38 +- README.md | 2 +- package.json | 6 +- public/updateChangelog.py | 6 +- src/App.tsx | 2 +- src/assets/sass/fabric-icons-inline.scss | 233 +++++----- src/common/constants.ts | 5 +- src/common/localization/en-us.ts | 34 +- src/common/localization/es-cl.ts | 30 +- src/common/mockFactory.ts | 1 + src/common/strings.ts | 10 + src/config/fabric-icons.json | 416 +++++++++--------- .../providers/storage/localFileSystem.ts | 4 + src/models/applicationState.ts | 40 +- src/providers/storage/azureBlobStorage.ts | 17 +- src/providers/storage/localFileSystemProxy.ts | 9 + .../storage/storageProviderFactory.test.ts | 3 + .../storage/storageProviderFactory.ts | 1 + .../apiVersionPicker/apiVersionPicker.tsx | 46 ++ .../common/assetPreview/assetPreview.tsx | 10 +- .../common/condensedList/condensedList.scss | 3 + .../common/condensedList/condensedList.tsx | 13 +- .../components/common/imageMap/imageMap.tsx | 50 ++- .../components/common/tagInput/tagInput.scss | 41 +- .../components/common/tagInput/tagInput.tsx | 57 ++- .../common/tagInput/tagInputItem.tsx | 50 ++- .../common/tagInput/tagInputToolbar.tsx | 45 +- .../components/pages/editorPage/canvas.scss | 6 +- .../components/pages/editorPage/canvas.tsx | 248 ++++++++--- .../pages/editorPage/canvasCommandBar.tsx | 86 ++-- .../pages/editorPage/editorPage.scss | 12 + .../pages/editorPage/editorPage.tsx | 161 +++++-- .../pages/editorPage/editorSideBar.tsx | 12 +- .../components/pages/homepage/homePage.tsx | 11 +- .../pages/modelCompose/modelCompose.tsx | 10 +- .../pages/predict/predictModelInfo.tsx | 8 +- .../components/pages/predict/predictPage.tsx | 10 +- .../pages/predict/predictResult.tsx | 2 +- .../projectSettings/editProjectForm.json | 5 + .../pages/projectSettings/newProjectForm.json | 5 + .../pages/projectSettings/projectForm.tsx | 8 + .../pages/projectSettings/projectForm.ui.json | 3 + .../projectSettings/projectSettingsPage.scss | 21 +- .../projectSettings/projectSettingsPage.tsx | 65 ++- .../components/pages/train/trainPage.tsx | 117 +++-- .../components/pages/train/trainRecord.tsx | 2 +- .../components/pages/train/trainTable.tsx | 2 +- src/react/components/shell/statusBar.test.tsx | 2 +- src/react/components/shell/statusBar.tsx | 17 +- src/redux/actions/projectActions.ts | 6 +- src/redux/reducers/currentProjectReducer.ts | 4 +- src/redux/reducers/recentProjectsReducer.ts | 4 + src/registerIcons.ts | 2 +- src/services/assetService.ts | 175 ++++---- src/services/ocrService.ts | 15 +- src/services/predictService.ts | 5 +- src/services/projectService.ts | 9 +- yarn.lock | 7 +- 59 files changed, 1465 insertions(+), 750 deletions(-) rename .env.release => .env.electron (89%) create mode 100644 src/react/components/common/apiVersionPicker/apiVersionPicker.tsx diff --git a/.env.release b/.env.electron similarity index 89% rename from .env.release rename to .env.electron index c91b04eb..bb8d7082 100644 --- a/.env.release +++ b/.env.electron @@ -2,4 +2,5 @@ # relative to index.html # without it, you'll see error like this # Failed to load resource: net::ERR_FILE_NOT_FOUND /favicon.ico:1 -PUBLIC_URL= \ No newline at end of file +PUBLIC_URL= +BROWSER=none diff --git a/CHANGELOG.md b/CHANGELOG.md index d07e445e..7783600e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,44 @@ # FoTT Changelog +## What's new in Form Recognizer? +Click [here](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/whats-new) to see what's new in Form Recognizer. + ## Released conatiner's currently referenced commit -2.1-Preview's released container image (mcr.microsoft.com/azure-cognitive-services/custom-form/labeltool:2.1.012970002-amd64-preview) currently references **2.1-preview.1-0633507 (09-14-2020)** +2.1-Preview's released container image, tracked by the `latest-preview` image tag in our [docker hub repository](https://hub.docker.com/_/microsoft-azure-cognitive-services-custom-form-labeltool), currently references **2.1-preview.1-1f33130 (10-09-2020)** ## Commit history +### 2.1-preview.1-1f33130 (10-09-2020) +* fix: support image map interactions for container releases([#639](https://github.com/microsoft/OCR-Form-Tools/commit/1f33130e3b6ad8a876f18fc1c05f82c4a14d36fa)) + +### 2.1-preview.1-6d4e93b (10-07-2020) +* Fix: use file type library for mime type validation ([#636](https://github.com/microsoft/OCR-Form-Tools/commit/6d4e93bca8a4e3d677c765ed5596bde502766e2e)) + +### 2.1-preview.1-355ca0b (09-30-2020) +* feat: add spinner in saving project, can avoid multiple commit ([#617](https://github.com/microsoft/OCR-Form-Tools/commit/355ca0b156b2d44aafd2eaaccf2fc52385c7f5f8)) + +### 2.1-preview.1-53044f7 (09-29-2020) +* fix: refresh currentProjects when load project ([#615](https://github.com/microsoft/OCR-Form-Tools/commit/53044f72dd9c9c72557c74c00605ba05ee50205d)) +* sync related region color when tag color changed ([#598](https://github.com/microsoft/OCR-Form-Tools/commit/3044cc51a9166877bb4f01f28753171b82c04ccd)) +* feat: add current list item style ([#601](https://github.com/microsoft/OCR-Form-Tools/commit/3e503e75513e44e6a90bd013d8dd15c3096cd7e9)) +* fix: remove project from app if security token does not exist ([#468](https://github.com/microsoft/OCR-Form-Tools/commit/730e1963a06f038a4efa9750fcef4be6f15a8460)) + +### 2.1-preview.1-d859d38 (09-27-2020) +* fix ,update document state when preview (#317) ([#471](https://github.com/microsoft/OCR-Form-Tools/commit/d859d38ecc1f96b194ffa130a1840f5a7d9b1a9b)) +* refactor: change the confidence value format to percentage ([#461](https://github.com/microsoft/OCR-Form-Tools/commit/e806b4e0dfcc68e6408e2130a46a318637a482a8)) + +### 2.1-preview.1-7a3f7a7 (09-25-2020) +* security: upgrade node-forge ([#622](https://github.com/microsoft/OCR-Form-Tools/commit/7a3f7a773c8b01f443afaad89d7974a5bbb0b869)) +* fix: disable move tag and support renaming when searching ([#618](https://github.com/microsoft/OCR-Form-Tools/commit/cac1e8e6cfb2805a6540f9e80d564a0ff8be81c7)) + +### 2.1-preview.1-4163edc (09-23-2020) +* docs: add latest tag reference to changelog ([#608](https://github.com/microsoft/OCR-Form-Tools/commit/4163edc18bc65234e263703fc829d2f297953385)) +* fix: use region instead of drawnRegion for labelType in label file ([#582](https://github.com/microsoft/OCR-Form-Tools/commit/ffafc200249a1c47698fedb279b4b55cef0190ba)) +* docs: update readme with docker hub info ([#604](https://github.com/microsoft/OCR-Form-Tools/commit/63bbea076d598d0286095fa0eca48d8c9d0ed706)) +* fix: remove opening browser for yarn start ([#605](https://github.com/microsoft/OCR-Form-Tools/commit/f6c4dc3585df71d09252a28f65e835a594389118)) +* fix: update changelog updater script ([#607](https://github.com/microsoft/OCR-Form-Tools/commit/7c4848c3a72259562c0461f0e2eadfb4a660fa64)) + +### 2.1-preview.1-f2db74e (09-17-2020) +* docs: udpate changlog with docker image reference ([#590](https://github.com/microsoft/OCR-Form-Tools/commit/f2db74e322c32338eba3b2df06c01a51cfb7ebc1)) + ### 2.1-preview.1-1a6b78e (09-16-2020) * fix: normalize folder path starting with a period ([#592](https://github.com/microsoft/OCR-Form-Tools/commit/1a6b78e054235da3188aafbe65636a8c18b439bf)) * fix: change label folder uri title ([#588](https://github.com/microsoft/OCR-Form-Tools/commit/7e4233e568d94817e23dda5ef5513b9ee7475d11)) diff --git a/README.md b/README.md index 20f2226f..a03b6f1e 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Form Labeling Tool requires [NodeJS (>= 10.x, Dubnium) and NPM](https://github.c ### Set up this tool with Docker -Please see instructions [here](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/quickstarts/label-tool#set-up-the-sample-labeling-tool) +Please see instructions [here](https://docs.microsoft.com/en-us/azure/cognitive-services/form-recognizer/quickstarts/label-tool#set-up-the-sample-labeling-tool), and view our docker hub repository [here](https://hub.docker.com/_/microsoft-azure-cognitive-services-custom-form-labeltool?tab=description) for the latest container image info. The `latest-preview` and `latest` docker image tags track the preview and general availability releases of FOTT. ### Run as web application diff --git a/package.json b/package.json index d7b4b2f6..37f0201b 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "yarn": "^1.22.4" }, "scripts": { - "start": "nf start -p 3000", + "start": "env-cmd -f .env.electron nf start -p 3000", "compile": "tsc", "build": "react-scripts build", "react-start": "react-scripts start", @@ -64,7 +64,7 @@ "electron:start:dev": "yarn electron-start", "electron:start:prod": "yarn webpack:prod && yarn electron-start", "electron-start": "node src/electron/start", - "release": "env-cmd -f .env.release yarn build && yarn webpack:prod && yarn electron-builder", + "release": "env-cmd -f .env.electron yarn build && yarn webpack:prod && yarn electron-builder", "tslint": "./node_modules/.bin/tslint 'src/**/*.ts*'", "tslintfix": "./node_modules/.bin/tslint 'src/**/*.ts*' --fix" }, @@ -108,7 +108,9 @@ "foreman": "^3.0.1", "jquery": "^3.5.0", "kind-of": "^6.0.3", + "mime": "^2.4.6", "minimist": "^1.2.2", + "node-forge": "^0.10.0", "node-sass": "^4.14.1", "pdfjs-dist": "^2.4.456", "react-scripts": "3.4.1", diff --git a/public/updateChangelog.py b/public/updateChangelog.py index cd695ffc..c9768a6a 100644 --- a/public/updateChangelog.py +++ b/public/updateChangelog.py @@ -37,15 +37,15 @@ repo = git.Repo("../") commits = list(repo.iter_commits("master")) for commit in commits: commitHex = commit.hexsha[:7] - if commitHex == lastChanglogCommit: - print("found last change log commit") - break commitDate = commit.committed_datetime.strftime("%m-%d-%Y") if currentCommitDate != commitDate: if currentCommitDate is not None: insterIntoChanglogContents("\n") currentCommitDate = commitDate insterIntoChanglogContents("### " + appVersion + "-" + commitHex + " (" + commitDate + ")\n") + if commitHex == lastChanglogCommit: + print("found last change log commit") + break commitMessage = commit.message.partition('\n')[0] commitMessageRegex = re.compile("(.*)\(\#(\d+)\)\s*$") match = commitMessageRegex.search(commitMessage) diff --git a/src/App.tsx b/src/App.tsx index bd161b47..c9e6d6c3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -95,7 +95,7 @@ export default class App extends React.Component { - + diff --git a/src/assets/sass/fabric-icons-inline.scss b/src/assets/sass/fabric-icons-inline.scss index 8a14795f..463afb51 100644 --- a/src/assets/sass/fabric-icons-inline.scss +++ b/src/assets/sass/fabric-icons-inline.scss @@ -3,8 +3,8 @@ */ @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 { -moz-osx-font-smoothing: grayscale; @@ -17,67 +17,69 @@ } // Mixins -@mixin ms-Icon--Table { content: "\ED86"; } -@mixin ms-Icon--TextField { content: "\EDC3"; } -@mixin ms-Icon--OpenFolderHorizontal { content: "\ED25"; } -@mixin ms-Icon--Documentation { content: "\EC17"; } -@mixin ms-Icon--AddTo { content: "\ECC8"; } -@mixin ms-Icon--SortUp { content: "\EE68"; } -@mixin ms-Icon--SortDown { content: "\EE69"; } -@mixin ms-Icon--Info { content: "\E946"; } -@mixin ms-Icon--ChromeMinimize { content: "\E921"; } -@mixin ms-Icon--ChromeRestore { content: "\E923"; } -@mixin ms-Icon--Label { content: "\E932"; } -@mixin ms-Icon--Copy { content: "\E8C8"; } -@mixin ms-Icon--Rename { content: "\E8AC"; } -@mixin ms-Icon--Download { content: "\E896"; } -@mixin ms-Icon--Help { content: "\E897"; } -@mixin ms-Icon--ZoomIn { content: "\E8A3"; } -@mixin ms-Icon--Tag { content: "\E8EC"; } -@mixin ms-Icon--CircleRing { content: "\EA3A"; } -@mixin ms-Icon--SquareShape { content: "\F1A6"; } -@mixin ms-Icon--RectangleShape { content: "\F1A9"; } -@mixin ms-Icon--DocumentManagement { content: "\EFFC"; } -@mixin ms-Icon--Relationship { content: "\F003"; } -@mixin ms-Icon--TextDocument { content: "\F029"; } -@mixin ms-Icon--StatusCircleCheckmark { content: "\F13E"; } -@mixin ms-Icon--PlugConnected { content: "\F302"; } -@mixin ms-Icon--Plug { content: "\F300"; } -@mixin ms-Icon--AlertSolid { content: "\F331"; } -@mixin ms-Icon--BranchMerge { content: "\F295"; } -@mixin ms-Icon--View { content: "\E890"; } -@mixin ms-Icon--ReceiptProcessing { content: "\E496"; } -@mixin ms-Icon--AddField { content: "\E4C7"; } -@mixin ms-Icon--TagGroup { content: "\E3F6"; } -@mixin ms-Icon--Insights { content: "\E3AF"; } -@mixin ms-Icon--MachineLearning { content: "\E3B8"; } -@mixin ms-Icon--Merge { content: "\E7D5"; } -@mixin ms-Icon--MapLayers { content: "\E81E"; } -@mixin ms-Icon--Home { content: "\E80F"; } -@mixin ms-Icon--ZoomOut { content: "\E71F"; } -@mixin ms-Icon--Search { content: "\E721"; } -@mixin ms-Icon--Refresh { content: "\E72C"; } -@mixin ms-Icon--Share { content: "\E72D"; } -@mixin ms-Icon--Link { content: "\E71B"; } -@mixin ms-Icon--ChevronDown { content: "\E70D"; } -@mixin ms-Icon--ChevronUp { content: "\E70E"; } -@mixin ms-Icon--Edit { content: "\E70F"; } @mixin ms-Icon--Add { content: "\E710"; } +@mixin ms-Icon--AddField { content: "\E4C7"; } +@mixin ms-Icon--AddTo { content: "\ECC8"; } +@mixin ms-Icon--AlertSolid { content: "\F331"; } +@mixin ms-Icon--AzureAPIManagement { content: "\F37F"; } +@mixin ms-Icon--BookAnswers { content: "\F8A4"; } +@mixin ms-Icon--BranchMerge { content: "\F295"; } @mixin ms-Icon--Cancel { content: "\E711"; } -@mixin ms-Icon--More { content: "\E712"; } -@mixin ms-Icon--Settings { content: "\E713"; } -@mixin ms-Icon--Filter { content: "\E71C"; } -@mixin ms-Icon--ChevronLeft { content: "\E76B"; } -@mixin ms-Icon--ChevronRight { content: "\E76C"; } -@mixin ms-Icon--System { content: "\E770"; } @mixin ms-Icon--CheckboxComposite { content: "\E73A"; } @mixin ms-Icon--CheckMark { content: "\E73E"; } -@mixin ms-Icon--Down { content: "\E74B"; } -@mixin ms-Icon--Delete { content: "\E74D"; } +@mixin ms-Icon--ChevronDown { content: "\E70D"; } +@mixin ms-Icon--ChevronLeft { content: "\E76B"; } +@mixin ms-Icon--ChevronRight { content: "\E76C"; } +@mixin ms-Icon--ChevronUp { content: "\E70E"; } +@mixin ms-Icon--ChromeMinimize { content: "\E921"; } +@mixin ms-Icon--ChromeRestore { content: "\E923"; } +@mixin ms-Icon--CircleRing { content: "\EA3A"; } @mixin ms-Icon--Cloud { content: "\E753"; } -@mixin ms-Icon--Up { content: "\E74A"; } -@mixin ms-Icon--KeyPhraseExtraction { content: "\E395"; } +@mixin ms-Icon--Copy { content: "\E8C8"; } +@mixin ms-Icon--Delete { content: "\E74D"; } +@mixin ms-Icon--Documentation { content: "\EC17"; } +@mixin ms-Icon--DocumentManagement { content: "\EFFC"; } +@mixin ms-Icon--Down { content: "\E74B"; } +@mixin ms-Icon--Download { content: "\E896"; } +@mixin ms-Icon--Edit { content: "\E70F"; } +@mixin ms-Icon--Filter { content: "\E71C"; } +@mixin ms-Icon--Help { content: "\E897"; } @mixin ms-Icon--Hide3 { content: "\F6AC"; } +@mixin ms-Icon--Home { content: "\E80F"; } +@mixin ms-Icon--Info { content: "\E946"; } +@mixin ms-Icon--Insights { content: "\E3AF"; } +@mixin ms-Icon--KeyPhraseExtraction { content: "\E395"; } +@mixin ms-Icon--Label { content: "\E932"; } +@mixin ms-Icon--Link { content: "\E71B"; } +@mixin ms-Icon--MachineLearning { content: "\E3B8"; } +@mixin ms-Icon--MapLayers { content: "\E81E"; } +@mixin ms-Icon--Merge { content: "\E7D5"; } +@mixin ms-Icon--More { content: "\E712"; } +@mixin ms-Icon--OpenFolderHorizontal { content: "\ED25"; } +@mixin ms-Icon--Plug { content: "\F300"; } +@mixin ms-Icon--PlugConnected { content: "\F302"; } +@mixin ms-Icon--ReceiptProcessing { content: "\E496"; } +@mixin ms-Icon--RectangleShape { content: "\F1A9"; } +@mixin ms-Icon--Refresh { content: "\E72C"; } +@mixin ms-Icon--Relationship { content: "\F003"; } +@mixin ms-Icon--Rename { content: "\E8AC"; } +@mixin ms-Icon--Rotate90Clockwise { content: "\F80D"; } +@mixin ms-Icon--Rotate90CounterClockwise { content: "\F80E"; } +@mixin ms-Icon--Search { content: "\E721"; } +@mixin ms-Icon--Settings { content: "\E713"; } +@mixin ms-Icon--Share { content: "\E72D"; } +@mixin ms-Icon--SortDown { content: "\EE69"; } +@mixin ms-Icon--SortUp { content: "\EE68"; } +@mixin ms-Icon--SquareShape { content: "\F1A6"; } +@mixin ms-Icon--StatusCircleCheckmark { content: "\F13E"; } +@mixin ms-Icon--System { content: "\E770"; } +@mixin ms-Icon--Table { content: "\ED86"; } +@mixin ms-Icon--Tag { content: "\E8EC"; } +@mixin ms-Icon--TagGroup { content: "\E3F6"; } +@mixin ms-Icon--TextDocument { content: "\F029"; } +@mixin ms-Icon--TextField { content: "\EDC3"; } +@mixin ms-Icon--Up { content: "\E74A"; } +@mixin ms-Icon--View { content: "\E890"; } @mixin ms-Icon--WarningSolid { content: "\F736"; } @mixin ms-Icon--BookAnswers { content: "\F8A4"; } @mixin ms-Icon--ChromeRestore { content: "\E923"; } @@ -93,67 +95,72 @@ @mixin ms-Icon--FixedColumnWidth { content: "\E3EA"; } @mixin ms-Icon--Rotate90Clockwise { content: "\F80D"; } @mixin ms-Icon--Rotate90CounterClockwise { content: "\F80E"; } +@mixin ms-Icon--ZoomIn { content: "\E8A3"; } +@mixin ms-Icon--ZoomOut { content: "\E71F"; } // Classes -.ms-Icon--Table:before { @include ms-Icon--Table } -.ms-Icon--TextField:before { @include ms-Icon--TextField } -.ms-Icon--OpenFolderHorizontal:before { @include ms-Icon--OpenFolderHorizontal } -.ms-Icon--Documentation:before { @include ms-Icon--Documentation } -.ms-Icon--AddTo:before { @include ms-Icon--AddTo } -.ms-Icon--SortUp:before { @include ms-Icon--SortUp } -.ms-Icon--SortDown:before { @include ms-Icon--SortDown } -.ms-Icon--Info:before { @include ms-Icon--Info } -.ms-Icon--ChromeMinimize:before { @include ms-Icon--ChromeMinimize } -.ms-Icon--ChromeRestore:before { @include ms-Icon--ChromeRestore } -.ms-Icon--Label:before { @include ms-Icon--Label } -.ms-Icon--Copy:before { @include ms-Icon--Copy } -.ms-Icon--Rename:before { @include ms-Icon--Rename } -.ms-Icon--Download:before { @include ms-Icon--Download } -.ms-Icon--Help:before { @include ms-Icon--Help } -.ms-Icon--ZoomIn:before { @include ms-Icon--ZoomIn } -.ms-Icon--Tag:before { @include ms-Icon--Tag } -.ms-Icon--CircleRing:before { @include ms-Icon--CircleRing } -.ms-Icon--SquareShape:before { @include ms-Icon--SquareShape } -.ms-Icon--RectangleShape:before { @include ms-Icon--RectangleShape } -.ms-Icon--DocumentManagement:before { @include ms-Icon--DocumentManagement } -.ms-Icon--Relationship:before { @include ms-Icon--Relationship } -.ms-Icon--TextDocument:before { @include ms-Icon--TextDocument } -.ms-Icon--StatusCircleCheckmark:before { @include ms-Icon--StatusCircleCheckmark } -.ms-Icon--PlugConnected:before { @include ms-Icon--PlugConnected } -.ms-Icon--Plug:before { @include ms-Icon--Plug } -.ms-Icon--AlertSolid:before { @include ms-Icon--AlertSolid } -.ms-Icon--BranchMerge:before { @include ms-Icon--BranchMerge } -.ms-Icon--View:before { @include ms-Icon--View } -.ms-Icon--ReceiptProcessing:before { @include ms-Icon--ReceiptProcessing } -.ms-Icon--AddField:before { @include ms-Icon--AddField } -.ms-Icon--TagGroup:before { @include ms-Icon--TagGroup } -.ms-Icon--Insights:before { @include ms-Icon--Insights } -.ms-Icon--MachineLearning:before { @include ms-Icon--MachineLearning } -.ms-Icon--Merge:before { @include ms-Icon--Merge } -.ms-Icon--MapLayers:before { @include ms-Icon--MapLayers } -.ms-Icon--Home:before { @include ms-Icon--Home } -.ms-Icon--ZoomOut:before { @include ms-Icon--ZoomOut } -.ms-Icon--Search:before { @include ms-Icon--Search } -.ms-Icon--Refresh:before { @include ms-Icon--Refresh } -.ms-Icon--Share:before { @include ms-Icon--Share } -.ms-Icon--Link:before { @include ms-Icon--Link } -.ms-Icon--ChevronDown:before { @include ms-Icon--ChevronDown } -.ms-Icon--ChevronUp:before { @include ms-Icon--ChevronUp } -.ms-Icon--Edit:before { @include ms-Icon--Edit } .ms-Icon--Add:before { @include ms-Icon--Add } +.ms-Icon--AddField:before { @include ms-Icon--AddField } +.ms-Icon--AddTo:before { @include ms-Icon--AddTo } +.ms-Icon--AlertSolid:before { @include ms-Icon--AlertSolid } +.ms-Icon--AzureAPIManagement:before { @include ms-Icon--AzureAPIManagement } +.ms-Icon--BookAnswers:before { @include ms-Icon--BookAnswers } +.ms-Icon--BranchMerge:before { @include ms-Icon--BranchMerge } .ms-Icon--Cancel:before { @include ms-Icon--Cancel } -.ms-Icon--More:before { @include ms-Icon--More } -.ms-Icon--Settings:before { @include ms-Icon--Settings } -.ms-Icon--Filter:before { @include ms-Icon--Filter } -.ms-Icon--ChevronLeft:before { @include ms-Icon--ChevronLeft } -.ms-Icon--ChevronRight:before { @include ms-Icon--ChevronRight } -.ms-Icon--System:before { @include ms-Icon--System } .ms-Icon--CheckboxComposite:before { @include ms-Icon--CheckboxComposite } .ms-Icon--CheckMark:before { @include ms-Icon--CheckMark } -.ms-Icon--Down:before { @include ms-Icon--Down } -.ms-Icon--Delete:before { @include ms-Icon--Delete } +.ms-Icon--ChevronDown:before { @include ms-Icon--ChevronDown } +.ms-Icon--ChevronLeft:before { @include ms-Icon--ChevronLeft } +.ms-Icon--ChevronRight:before { @include ms-Icon--ChevronRight } +.ms-Icon--ChevronUp:before { @include ms-Icon--ChevronUp } +.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--Cloud:before { @include ms-Icon--Cloud } +.ms-Icon--Copy:before { @include ms-Icon--Copy } +.ms-Icon--Delete:before { @include ms-Icon--Delete } +.ms-Icon--Documentation:before { @include ms-Icon--Documentation } +.ms-Icon--DocumentManagement:before { @include ms-Icon--DocumentManagement } +.ms-Icon--Down:before { @include ms-Icon--Down } +.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--Help:before { @include ms-Icon--Help } +.ms-Icon--Hide3:before { @include ms-Icon--Hide3 } +.ms-Icon--Home:before { @include ms-Icon--Home } +.ms-Icon--Info:before { @include ms-Icon--Info } +.ms-Icon--Insights:before { @include ms-Icon--Insights } +.ms-Icon--KeyPhraseExtraction:before { @include ms-Icon--KeyPhraseExtraction } +.ms-Icon--Label:before { @include ms-Icon--Label } +.ms-Icon--Link:before { @include ms-Icon--Link } +.ms-Icon--MachineLearning:before { @include ms-Icon--MachineLearning } +.ms-Icon--MapLayers:before { @include ms-Icon--MapLayers } +.ms-Icon--Merge:before { @include ms-Icon--Merge } +.ms-Icon--More:before { @include ms-Icon--More } +.ms-Icon--OpenFolderHorizontal:before { @include ms-Icon--OpenFolderHorizontal } +.ms-Icon--Plug:before { @include ms-Icon--Plug } +.ms-Icon--PlugConnected:before { @include ms-Icon--PlugConnected } +.ms-Icon--ReceiptProcessing:before { @include ms-Icon--ReceiptProcessing } +.ms-Icon--RectangleShape:before { @include ms-Icon--RectangleShape } +.ms-Icon--Refresh:before { @include ms-Icon--Refresh } +.ms-Icon--Relationship:before { @include ms-Icon--Relationship } +.ms-Icon--Rename:before { @include ms-Icon--Rename } +.ms-Icon--Rotate90Clockwise:before { @include ms-Icon--Rotate90Clockwise } +.ms-Icon--Rotate90CounterClockwise:before { @include ms-Icon--Rotate90CounterClockwise } +.ms-Icon--Search:before { @include ms-Icon--Search } +.ms-Icon--Settings:before { @include ms-Icon--Settings } +.ms-Icon--Share:before { @include ms-Icon--Share } +.ms-Icon--SortDown:before { @include ms-Icon--SortDown } +.ms-Icon--SortUp:before { @include ms-Icon--SortUp } +.ms-Icon--SquareShape:before { @include ms-Icon--SquareShape } +.ms-Icon--StatusCircleCheckmark:before { @include ms-Icon--StatusCircleCheckmark } +.ms-Icon--System:before { @include ms-Icon--System } +.ms-Icon--Table:before { @include ms-Icon--Table } +.ms-Icon--Tag:before { @include ms-Icon--Tag } +.ms-Icon--TagGroup:before { @include ms-Icon--TagGroup } +.ms-Icon--TextDocument:before { @include ms-Icon--TextDocument } +.ms-Icon--TextField:before { @include ms-Icon--TextField } .ms-Icon--Up:before { @include ms-Icon--Up } .ms-Icon--KeyPhraseExtraction:before { @include ms-Icon--KeyPhraseExtraction } .ms-Icon--Hide3:before { @include ms-Icon--Hide3 } @@ -172,8 +179,8 @@ .ms-Icon--FixedColumnWidth:before { @include ms-Icon--FixedColumnWidth } .ms-Icon--KeyPhraseExtraction:before { @include ms-Icon--KeyPhraseExtraction } .ms-Icon--Hide3:before { @include ms-Icon--Hide3 } +.ms-Icon--View:before { @include ms-Icon--View } .ms-Icon--WarningSolid:before { @include ms-Icon--WarningSolid } -.ms-Icon--BookAnswers:before { @include ms-Icon--BookAnswers } -.ms-Icon--Rotate90Clockwise:before { @include ms-Icon--Rotate90Clockwise } -.ms-Icon--Rotate90CounterClockwise:before { @include ms-Icon--Rotate90CounterClockwise } +.ms-Icon--ZoomIn:before { @include ms-Icon--ZoomIn } +.ms-Icon--ZoomOut:before { @include ms-Icon--ZoomOut } diff --git a/src/common/constants.ts b/src/common/constants.ts index 64cc4572..4fd6f9d4 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -3,7 +3,8 @@ import { appInfo } from "./appInfo" -const appVersionArr = appInfo.version.split("."); +const appVersionRaw = appInfo.version +const appVersionArr = appVersionRaw.split("."); appVersionArr[1] = appVersionArr[1] + "-preview"; const appVersion = appVersionArr.join("."); @@ -14,6 +15,7 @@ const apiVersion = "v2.1-preview.1"; */ export const constants = { version: "pubpreview_1.0", + appVersionRaw, appVersion, apiVersion, projectFormTempKey: "projectForm", @@ -35,6 +37,7 @@ export const constants = { convertedThumbnailQuality: 0.2, recentModelRecordsCount: 5, apiModelsPath: `/formrecognizer/${apiVersion}/custom/models`, + autoLabelBatchSize: 10, 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 633d4d7c..fc115d1d 100644 --- a/src/common/localization/en-us.ts +++ b/src/common/localization/en-us.ts @@ -129,6 +129,10 @@ export const english: IAppStrings = { backEndNotAvailable: "Checkbox feature will work in future version of Form Recognizer service, please stay tuned.", addName: "Add a model name...", 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?" + }, errors: { electron: { cantAccessFiles: "Cannot access files in '${folderUri}' for training. Please check if specified folder URI is correct." @@ -145,7 +149,7 @@ export const english: IAppStrings = { composing: "Model is composing, please wait...", column: { icon: { - name:"Composed Icon", + name: "Composed Icon", }, id: { headerName: "Model Id", @@ -209,7 +213,7 @@ export const english: IAppStrings = { defaultURLInput: "Paste or type URL...", editAndUploadToTrainingSet: "Edit & upload to training set", editAndUploadToTrainingSetNotify: "by clicking on this button, this form will be added to this project, where you can edit these labels.", - editAndUploadToTrainingSetNotify2: "We are adding this file to your training set, where you could edit the labels and re-train the model.", + editAndUploadToTrainingSetNotify2: "We are adding this file to your training set, where you can edit the labels and re-train the model.", uploadInPrgoress: "Upload in progress...", confirmDuplicatedAssetName: { title: "Asset name exists", @@ -264,7 +268,7 @@ export const english: IAppStrings = { unknownTagName: "Unknown", notCompatibleTagType: "Tag type is not compatible with this feature. If you want to change type of this tag, please remove or reassign all labels which using this tag in your project.", checkboxPerTagLimit: "Cannot assign more than one checkbox per tag", - notCompatibleWithDrawnRegionTag: "drawnRegion and ${otherCategory} values cannot both be assigned to the same document's tag", + notCompatibleWithDrawnRegionTag: "Drawn regions and ${otherCatagory} values cannot both be assigned to the same document's tag", }, regionTableTags: { configureTag: { @@ -476,10 +480,14 @@ export const english: IAppStrings = { subIMenuItems: { runOcrOnCurrentDocument: "Run OCR on current document", runOcrOnAllDocuments: "Run OCR on all documents", - runAutoLabelingCurrentDocument: "Run AutoLabeling on current document", + runAutoLabelingCurrentDocument: "Auto-label the current document", + runAutoLabelingOnNotLabelingDocuments: "Auto-label next ${batchSize} unlabeled documents", noPredictModelOnProject: "Predict model not avaliable, please train the model first.", } } + }, + warings: { + drawRegionUnsupportedAPIVersion: "Region labeling is not supported with API ${apiVersion}. It will be supported with the release of v2.1-preview.3", } } }, @@ -509,7 +517,7 @@ export const english: IAppStrings = { keys: { lessThan: "<", greaterThan: ">", - }, + }, description: { prevPage: "Go to previous page", nextPage: "Go to next page", @@ -520,7 +528,7 @@ export const english: IAppStrings = { minus: "-", plus: "=", slash: "/", - }, + }, description: { in: "Zoom in", out: "Zoom out", @@ -531,11 +539,11 @@ export const english: IAppStrings = { keys: { delete: "Delete", backSpace: "Backspace", - }, + }, description: { delete: "Remove selection and delete labels of selected words", backSpace: "Remove selection and delete labels of selected words", - }, + }, }, drawnRegions: { keys: { @@ -551,7 +559,7 @@ export const english: IAppStrings = { tips: { quickLabeling: { name: "Lable 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.", + 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: { name: "Rename tag", @@ -604,6 +612,10 @@ export const english: IAppStrings = { message: `An error occured while deleting the project. Validate the project file and security token exist and try again`, }, + projectDeleteErrorSecurityTokenNotFound: { + title: "Security token not found when delete project", + message: "Security Token Not Found. Project [${project.name}] has been removed from FoTT tool." + }, projectNotFound: { title: "Error loading project", message: "We couldn't find the project file ${file} at the target blob container ${container}.\ @@ -701,6 +713,10 @@ export const english: IAppStrings = { title: "Model not found", message: "Model \"${modelID}\" not found. Please use another model.", }, + connectionNotExistError: { + title: "Connection doesn't exist", + message: "Connection doesn't exist." + }, getOcrError: { title: "Cannot load OCR file", message: "Failed to load from OCR file. Please check your connection or network settings.", diff --git a/src/common/localization/es-cl.ts b/src/common/localization/es-cl.ts index b675810d..b7314720 100644 --- a/src/common/localization/es-cl.ts +++ b/src/common/localization/es-cl.ts @@ -130,6 +130,10 @@ export const spanish: IAppStrings = { backEndNotAvailable: "La función de casilla de verificación funcionará en la versión futura del servicio de reconocimiento de formularios, manténgase atento.", addName: "Agregar nombre de modelo ...", 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?" + }, errors: { electron: { cantAccessFiles: "No se puede acceder a los archivos en '${folderUri}' para entrenamiento. Compruebe si el URI de la carpeta especificada es correcto." @@ -451,7 +455,7 @@ export const spanish: IAppStrings = { }, canvasCommandBar: { items: { - layers:{ + layers: { text: "Capas", subMenuItems: { text: "Texto", @@ -477,10 +481,14 @@ export const spanish: IAppStrings = { subIMenuItems: { runOcrOnCurrentDocument: "Ejecutar OCR en el documento actual", runOcrOnAllDocuments: "Ejecute OCR en todos los documentos", - runAutoLabelingCurrentDocument: "Ejecutar AutoLabeling en el documento actual", + runAutoLabelingCurrentDocument: "Etiquetar automáticamente el documento actual", + runAutoLabelingOnNotLabelingDocuments: "Etiquetar automáticamente los siguientes ${batchSize} documentos sin etiquetar", noPredictModelOnProject: "Predecir modelo no disponible, entrene el modelo primero.", } } + }, + warings: { + drawRegionUnsupportedAPIVersion: "Las regiones de dibujo no son compatibles con la versión de API ${apiVersion}. Será compatible con el lanzamiento de v2.1-preview.3", } } }, @@ -510,7 +518,7 @@ export const spanish: IAppStrings = { keys: { lessThan: "<", greaterThan: ">", - }, + }, description: { prevPage: "Ir a la página anterior en documentos de varias páginas", nextPage: "Ir a la página siguiente en documentos de varias páginas", @@ -521,7 +529,7 @@ export const spanish: IAppStrings = { minus: "-", plus: "=", slash: "/", - }, + }, description: { in: "Acercarse", out: "Disminuir el zoom", @@ -532,11 +540,11 @@ export const spanish: IAppStrings = { keys: { delete: "Delete", backSpace: "Backspace", - }, + }, description: { delete: "Eliminar selección del mapa del documento o clave de selección de una etiqueta", backSpace: "Eliminar selección del mapa del documento o clave de selección de una etiqueta", - }, + }, }, drawnRegions: { keys: { @@ -552,7 +560,7 @@ export const spanish: IAppStrings = { tips: { quickLabeling: { name: "Etiquetado rápido", - description: "Las teclas de acceso rápido de 1 a 0 y todas las letras se asignan a las primeras 36 etiquetas, después de seleccionar una o varias palabras de los elementos de texto resaltados, al presionar estas teclas de acceso rápido, puede etiquetar las palabras seleccionadas.", + description: "Las teclas de acceso rápido de 1 a 0 y todas las letras se asignan a las primeras 36 etiquetas, después de seleccionar una o varias palabras de los elementos de texto resaltados, al presionar estas teclas de acceso rápido, puede etiquetar las palabras seleccionadas.", }, renameTag: { name: "Rename Tag", @@ -604,6 +612,10 @@ export const spanish: IAppStrings = { message: `Se ha producido un error al eliminar el proyecto. Validar el archivo de proyecto y el token de seguridad existen e inténtelo de nuevo`, }, + projectDeleteErrorSecurityTokenNotFound: { + title: 'No se encontró el token de seguridad al eliminar el proyecto', + message: "Token de seguridad no encontrado. El proyecto [$ {project.name}] se ha eliminado de la herramienta FoTT." + }, projectNotFound: { title: "", message: "", @@ -701,6 +713,10 @@ export const spanish: IAppStrings = { title: "Modelo no encontrado", message: "Modelo \"${modelID}\" no encontrado. Por favor use otro modelo.", }, + connectionNotExistError: { + title: "La conexión no existe", + message: "La conexión no existe." + }, getOcrError: { title: "No se puede cargar el archivo OCR", message: "Error al cargar desde el archivo OCR. Verifique su conexión o configuración de red." diff --git a/src/common/mockFactory.ts b/src/common/mockFactory.ts index 6986d9dc..fa4fb8a8 100644 --- a/src/common/mockFactory.ts +++ b/src/common/mockFactory.ts @@ -319,6 +319,7 @@ export default class MockFactory { createContainer: jest.fn(), deleteContainer: jest.fn(), getAssets: jest.fn(), + isFileExists: jest.fn(), }; } diff --git a/src/common/strings.ts b/src/common/strings.ts index bbd13ddf..3406a628 100644 --- a/src/common/strings.ts +++ b/src/common/strings.ts @@ -129,6 +129,10 @@ export interface IAppStrings { backEndNotAvailable: string, addName: string, downloadJson: string; + trainConfirm: { + title: string; + message: string; + }, errors: { electron: { cantAccessFiles: string; @@ -472,9 +476,13 @@ export interface IAppStrings { runOcrOnCurrentDocument: string, runOcrOnAllDocuments: string, runAutoLabelingCurrentDocument: string, + runAutoLabelingOnNotLabelingDocuments: string, noPredictModelOnProject: string, } } + }, + warings: { + drawRegionUnsupportedAPIVersion: string, } }, }, @@ -576,6 +584,7 @@ export interface IAppStrings { projectInvalidSecurityToken: IErrorMetadata, projectUploadError: IErrorMetadata, projectDeleteError: IErrorMetadata, + projectDeleteErrorSecurityTokenNotFound: IErrorMetadata, projectNotFound: IErrorMetadata, genericRenderError: IErrorMetadata, securityTokenNotFound: IErrorMetadata, @@ -600,6 +609,7 @@ export interface IAppStrings { modelCountLimitExceeded: IErrorMetadata, requestSendError: IErrorMetadata, modelNotFound: IErrorMetadata, + connectionNotExistError: IErrorMetadata, getOcrError: IErrorMetadata, }; shareProject: { diff --git a/src/config/fabric-icons.json b/src/config/fabric-icons.json index 16c65749..0dc44921 100644 --- a/src/config/fabric-icons.json +++ b/src/config/fabric-icons.json @@ -7,217 +7,37 @@ "hashFontFileName": true, "glyphs": [ { - "name": "Table", - "unicode": "ED86" - }, - { - "name": "TextField", - "unicode": "EDC3" - }, - { - "name": "OpenFolderHorizontal", - "unicode": "ED25" - }, - { - "name": "Documentation", - "unicode": "EC17" - }, - { - "name": "AddTo", - "unicode": "ECC8" - }, - { - "name": "SortUp", - "unicode": "EE68" - }, - { - "name": "SortDown", - "unicode": "EE69" - }, - { - "name": "Info", - "unicode": "E946" - }, - { - "name": "ChromeMinimize", - "unicode": "E921" - }, - { - "name": "ChromeRestore", - "unicode": "E923" - }, - { - "name": "Label", - "unicode": "E932" - }, - { - "name": "Copy", - "unicode": "E8C8" - }, - { - "name": "Rename", - "unicode": "E8AC" - }, - { - "name": "Download", - "unicode": "E896" - }, - { - "name": "Help", - "unicode": "E897" - }, - { - "name": "ZoomIn", - "unicode": "E8A3" - }, - { - "name": "Tag", - "unicode": "E8EC" - }, - { - "name": "CircleRing", - "unicode": "EA3A" - }, - { - "name": "SquareShape", - "unicode": "F1A6" - }, - { - "name": "RectangleShape", - "unicode": "F1A9" - }, - { - "name": "DocumentManagement", - "unicode": "EFFC" - }, - { - "name": "Relationship", - "unicode": "F003" - }, - { - "name": "TextDocument", - "unicode": "F029" - }, - { - "name": "StatusCircleCheckmark", - "unicode": "F13E" - }, - { - "name": "PlugConnected", - "unicode": "F302" - }, - { - "name": "Plug", - "unicode": "F300" - }, - { - "name": "AlertSolid", - "unicode": "F331" - }, - { - "name": "BranchMerge", - "unicode": "F295" - }, - { - "name": "View", - "unicode": "E890" - }, - { - "name": "ReceiptProcessing", - "unicode": "E496" + "name": "Add", + "unicode": "E710" }, { "name": "AddField", "unicode": "E4C7" }, { - "name": "TagGroup", - "unicode": "E3F6" + "name": "AddTo", + "unicode": "ECC8" }, { - "name": "Insights", - "unicode": "E3AF" + "name": "AlertSolid", + "unicode": "F331" }, { - "name": "MachineLearning", - "unicode": "E3B8" + "name": "AzureAPIManagement", + "unicode": "F37F" }, { - "name": "Merge", - "unicode": "E7D5" + "name": "BookAnswers", + "unicode": "F8A4" }, { - "name": "MapLayers", - "unicode": "E81E" - }, - { - "name": "Home", - "unicode": "E80F" - }, - { - "name": "ZoomOut", - "unicode": "E71F" - }, - { - "name": "Search", - "unicode": "E721" - }, - { - "name": "Refresh", - "unicode": "E72C" - }, - { - "name": "Share", - "unicode": "E72D" - }, - { - "name": "Link", - "unicode": "E71B" - }, - { - "name": "ChevronDown", - "unicode": "E70D" - }, - { - "name": "ChevronUp", - "unicode": "E70E" - }, - { - "name": "Edit", - "unicode": "E70F" - }, - { - "name": "Add", - "unicode": "E710" + "name": "BranchMerge", + "unicode": "F295" }, { "name": "Cancel", "unicode": "E711" }, - { - "name": "More", - "unicode": "E712" - }, - { - "name": "Settings", - "unicode": "E713" - }, - { - "name": "Filter", - "unicode": "E71C" - }, - { - "name": "ChevronLeft", - "unicode": "E76B" - }, - { - "name": "ChevronRight", - "unicode": "E76C" - }, - { - "name": "System", - "unicode": "E770" - }, { "name": "CheckboxComposite", "unicode": "E73A" @@ -227,36 +47,228 @@ "unicode": "E73E" }, { - "name": "Down", - "unicode": "E74B" + "name": "ChevronDown", + "unicode": "E70D" }, { - "name": "Delete", - "unicode": "E74D" + "name": "ChevronLeft", + "unicode": "E76B" + }, + { + "name": "ChevronRight", + "unicode": "E76C" + }, + { + "name": "ChevronUp", + "unicode": "E70E" + }, + { + "name": "ChromeMinimize", + "unicode": "E921" + }, + { + "name": "ChromeRestore", + "unicode": "E923" + }, + { + "name": "CircleRing", + "unicode": "EA3A" }, { "name": "Cloud", "unicode": "E753" }, { - "name": "Up", - "unicode": "E74A" + "name": "Copy", + "unicode": "E8C8" }, { - "name": "KeyPhraseExtraction", - "unicode": "E395" + "name": "Delete", + "unicode": "E74D" + }, + { + "name": "Documentation", + "unicode": "EC17" + }, + { + "name": "DocumentManagement", + "unicode": "EFFC" + }, + { + "name": "Down", + "unicode": "E74B" + }, + { + "name": "Download", + "unicode": "E896" + }, + { + "name": "Edit", + "unicode": "E70F" + }, + { + "name": "Filter", + "unicode": "E71C" + }, + { + "name": "Help", + "unicode": "E897" }, { "name": "Hide3", "unicode": "F6AC" }, + { + "name": "Home", + "unicode": "E80F" + }, + { + "name": "Info", + "unicode": "E946" + }, + { + "name": "Insights", + "unicode": "E3AF" + }, + { + "name": "KeyPhraseExtraction", + "unicode": "E395" + }, + { + "name": "Label", + "unicode": "E932" + }, + { + "name": "Link", + "unicode": "E71B" + }, + { + "name": "MachineLearning", + "unicode": "E3B8" + }, + { + "name": "MapLayers", + "unicode": "E81E" + }, + { + "name": "Merge", + "unicode": "E7D5" + }, + { + "name": "More", + "unicode": "E712" + }, + { + "name": "OpenFolderHorizontal", + "unicode": "ED25" + }, + { + "name": "Plug", + "unicode": "F300" + }, + { + "name": "PlugConnected", + "unicode": "F302" + }, + { + "name": "ReceiptProcessing", + "unicode": "E496" + }, + { + "name": "RectangleShape", + "unicode": "F1A9" + }, + { + "name": "Refresh", + "unicode": "E72C" + }, + { + "name": "Relationship", + "unicode": "F003" + }, + { + "name": "Rename", + "unicode": "E8AC" + }, + { + "name": "Rotate90Clockwise", + "unicode": "F80D" + }, + { + "name": "Rotate90CounterClockwise", + "unicode": "F80E" + }, + { + "name": "Search", + "unicode": "E721" + }, + { + "name": "Settings", + "unicode": "E713" + }, + { + "name": "Share", + "unicode": "E72D" + }, + { + "name": "SortDown", + "unicode": "EE69" + }, + { + "name": "SortUp", + "unicode": "EE68" + }, + { + "name": "SquareShape", + "unicode": "F1A6" + }, + { + "name": "StatusCircleCheckmark", + "unicode": "F13E" + }, + { + "name": "System", + "unicode": "E770" + }, + { + "name": "Table", + "unicode": "ED86" + }, + { + "name": "Tag", + "unicode": "E8EC" + }, + { + "name": "TagGroup", + "unicode": "E3F6" + }, + { + "name": "TextDocument", + "unicode": "F029" + }, + { + "name": "TextField", + "unicode": "EDC3" + }, + { + "name": "Up", + "unicode": "E74A" + }, + { + "name": "View", + "unicode": "E890" + }, { "name": "WarningSolid", "unicode": "F736" }, { - "name": "BookAnswers", - "unicode": "F8A4" + "name": "ZoomIn", + "unicode": "E8A3" + }, + { + "name": "ZoomOut", + "unicode": "E71F" } ] } \ No newline at end of file diff --git a/src/electron/providers/storage/localFileSystem.ts b/src/electron/providers/storage/localFileSystem.ts index 394a3227..2a683b6a 100644 --- a/src/electron/providers/storage/localFileSystem.ts +++ b/src/electron/providers/storage/localFileSystem.ts @@ -123,6 +123,10 @@ export default class LocalFileSystem implements IStorageProvider { return this.listItems(path.normalize(folderPath), (stats) => !stats.isDirectory()); } + public isFileExists(filePath: string): Promise { + return Promise.resolve(fs.existsSync(path.normalize(filePath))); + } + public listContainers(folderPath: string): Promise { return this.listItems(path.normalize(folderPath), (stats) => stats.isDirectory()); } diff --git a/src/models/applicationState.ts b/src/models/applicationState.ts index 37b158b7..04e1f701 100644 --- a/src/models/applicationState.ts +++ b/src/models/applicationState.ts @@ -93,6 +93,7 @@ export interface IProject { lastVisitedAssetId?: string, apiUriBase: string, apiKey?: string | ISecureString, + apiVersion?: string; folderPath: string, trainRecord: ITrainRecordProps, recentModelRecords: IRecentModel[], @@ -166,6 +167,7 @@ export interface IAsset { id: string, type: AssetType, state: AssetState, + labelingState?: AssetLabelingState, name: string, path: string, size: ISize, @@ -174,7 +176,9 @@ export interface IAsset { predicted?: boolean, ocr?: any, isRunningOCR?: boolean, + isRunningAutoLabeling?: boolean, cachedImage?: string, + mimeType?: string, } /** @@ -219,6 +223,8 @@ export interface IRegion { value?: string, pageNumber: number, isTableRegion?: boolean, + changed?: boolean, + } export interface ITableRegion extends IRegion { @@ -232,6 +238,7 @@ export interface ITableRegion extends IRegion { */ export interface ILabelData { document: string, + labelingState?: AssetLabelingState; labels: ILabel[], tableLabels?: ITableLabel[], } @@ -245,6 +252,7 @@ export interface ILabel { key?: IFormRegion[], value: IFormRegion[], labelType?: string, + confidence?: number, } export interface ITableLabel { @@ -356,6 +364,12 @@ export enum ErrorCode { ProjectUploadError = "ProjectUploadError", } +export enum APIVersionPatches { + patch1 = "v2.1-preview.1", + patch2 = "v2.1-preview.2", + patch3 = "v2.1-preview.3", +} + /** * @enum LOCAL - Local storage type * @enum CLOUD - Cloud storage type @@ -381,6 +395,14 @@ export enum AssetType { TIFF = 6, } +export enum AssetMimeType { + PDF = "application/pdf", + TIFF = "image/tiff", + JPG = "image/jpg", + PNG = "image/png", + BMP = "image/bmp", +} + /** * @name - Asset State * @description - Defines the state of the asset with regard to the tagging process @@ -393,6 +415,20 @@ export enum AssetState { Visited = 1, Tagged = 2, } +/** + * @name - Asset Labeling State + * @description - Defines the labeling state for the asset + * @member ManualLabeling - Specifies as an asset that has manual labeling the tags + * @member Training - Specifies as an asset tagged data has been used for training model + * @member AutoLabeling - Specifies as an asset that has run auto-labeling + * @member AutoLabeledAndAdjusted -specifies as an asset that has run auto-labeling and tags manual adjusted + */ +export enum AssetLabelingState { + ManuallyLabeled = 1, + Trained = 2, + AutoLabeled = 3, + AutoLabeledAndAdjusted = 4, +} /** * @name - Region Type @@ -430,7 +466,7 @@ export enum FieldType { } export enum LabelType { - DrawnRegion = "drawnRegion" + DrawnRegion = "region" } export enum FieldFormat { @@ -451,7 +487,7 @@ export enum FeatureCategory { Text = "text", Checkbox = "checkbox", Label = "label", - DrawnRegion = "drawnRegion" + DrawnRegion = "region" } export enum ImageMapParent { diff --git a/src/providers/storage/azureBlobStorage.ts b/src/providers/storage/azureBlobStorage.ts index 3cafcec7..d4a3960a 100644 --- a/src/providers/storage/azureBlobStorage.ts +++ b/src/providers/storage/azureBlobStorage.ts @@ -3,7 +3,7 @@ import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { constants } from "../../common/constants"; import { strings } from "../../common/strings"; -import { AppError, AssetState, AssetType, ErrorCode, IAsset, StorageType } from "../../models/applicationState"; +import { AppError, AssetState, AssetType, ErrorCode, IAsset, StorageType, ILabelData, AssetLabelingState } from "../../models/applicationState"; import { throwUnhandledRejectionForEdge } from "../../react/components/common/errorHandler/errorHandler"; import { AssetService } from "../../services/assetService"; import { IStorageProvider } from "./storageProviderFactory"; @@ -150,6 +150,14 @@ export class AzureBlobStorage implements IStorageProvider { } } + /** + * check file is exists + * @param filePath + */ + public async isFileExists(filePath: string) :Promise { + const client = this.containerClient.getBlobClient(filePath); + return await client.exists(); + } /** * Lists the containers with in the Azure Blob Storage account * @param path - NOT USED IN CURRENT IMPLEMENTATION. Lists containers in storage account. @@ -206,12 +214,17 @@ export class AzureBlobStorage implements IStorageProvider { if (files.find((str) => str === labelFileName)) { asset.state = AssetState.Tagged; + const labelFileName = decodeURIComponent(`${asset.name}${constants.labelFileExtension}`); + const json = await this.readText(labelFileName, true); + 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 { asset.state = AssetState.NotVisited; } - result.push(asset); } } diff --git a/src/providers/storage/localFileSystemProxy.ts b/src/providers/storage/localFileSystemProxy.ts index a3523200..1378c84c 100644 --- a/src/providers/storage/localFileSystemProxy.ts +++ b/src/providers/storage/localFileSystemProxy.ts @@ -110,6 +110,15 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider { return IpcRendererProxy.send(`${PROXY_NAME}:listFiles`, [folderPath]); } + /** + * check file is exists + * @param fileName Name of target file + */ + public isFileExists(fileName: string): Promise { + const filePath = [this.options.folderPath, fileName].join("/"); + return IpcRendererProxy.send(`${PROXY_NAME}:isFileExists`, [filePath]); + } + /** * List directories inside another directory * @param folderName - Directory from which to list directories diff --git a/src/providers/storage/storageProviderFactory.test.ts b/src/providers/storage/storageProviderFactory.test.ts index 70b3f6de..b3c0b497 100644 --- a/src/providers/storage/storageProviderFactory.test.ts +++ b/src/providers/storage/storageProviderFactory.test.ts @@ -48,6 +48,9 @@ class TestStorageProvider implements IStorageProvider { public listFiles(folderPath?: string): Promise { throw new Error("Method not implemented."); } + isFileExists(filepath: string): Promise { + throw new Error("Method not implemented."); + } public listContainers(folderPath?: string): Promise { throw new Error("Method not implemented."); } diff --git a/src/providers/storage/storageProviderFactory.ts b/src/providers/storage/storageProviderFactory.ts index bd9f61b1..93003b1c 100644 --- a/src/providers/storage/storageProviderFactory.ts +++ b/src/providers/storage/storageProviderFactory.ts @@ -35,6 +35,7 @@ export interface IStorageProvider extends IAssetProvider { isValidProjectConnection(filepath?): Promise; listFiles(folderPath?: string, ext?: string): Promise; + isFileExists(filepath: string): Promise; listContainers(folderPath?: string): Promise; createContainer(folderPath: string): Promise; diff --git a/src/react/components/common/apiVersionPicker/apiVersionPicker.tsx b/src/react/components/common/apiVersionPicker/apiVersionPicker.tsx new file mode 100644 index 00000000..2f048eda --- /dev/null +++ b/src/react/components/common/apiVersionPicker/apiVersionPicker.tsx @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import React, { SyntheticEvent } from "react"; +import { APIVersionPatches } from "../../../../models/applicationState"; + +/** + * api version Picker Properties + * @member id - The id to bind to the input element + * @member value - The value to bind to the input element + * @member onChange - The event handler to call when the input value changes + */ +export interface IAPIVersionPickerProps { + id?: string; + value: string; + onChange: (value: string) => void; +} + +/** + * api version Picker + */ +export class APIVersionPicker extends React.Component { + constructor(props) { + super(props); + + this.onChange = this.onChange.bind(this); + } + + public render() { + return ( + + ); + } + + private onChange(e: SyntheticEvent) { + const inputElement = e.target as HTMLSelectElement; + this.props.onChange(inputElement.value ? inputElement.value : "2.1-preview.3"); + } +} diff --git a/src/react/components/common/assetPreview/assetPreview.tsx b/src/react/components/common/assetPreview/assetPreview.tsx index 484f9278..de209685 100644 --- a/src/react/components/common/assetPreview/assetPreview.tsx +++ b/src/react/components/common/assetPreview/assetPreview.tsx @@ -110,7 +110,15 @@ export class AssetPreview extends React.Component
- + +
+ + } + {this.props.asset.isRunningAutoLabeling && +
+
+ +
} diff --git a/src/react/components/common/condensedList/condensedList.scss b/src/react/components/common/condensedList/condensedList.scss index 797dbed4..b07d28c8 100644 --- a/src/react/components/common/condensedList/condensedList.scss +++ b/src/react/components/common/condensedList/condensedList.scss @@ -42,6 +42,9 @@ ul.condensed-list-items { &.active, &:hover { background-color: $lighter-2; } + &.current{ + background-color: $lighter-3; + } } } } diff --git a/src/react/components/common/condensedList/condensedList.tsx b/src/react/components/common/condensedList/condensedList.tsx index c02d6834..55c79abf 100644 --- a/src/react/components/common/condensedList/condensedList.tsx +++ b/src/react/components/common/condensedList/condensedList.tsx @@ -27,13 +27,18 @@ interface ICondensedListProps { onDelete?: (item) => void; } +interface ICondensedListState { + currentId: string; +} + /** * @name - Condensed List * @description - Clickable, deletable and linkable list of items */ -export default class CondensedList extends React.Component { +export default class CondensedList extends React.Component { constructor(props, context) { super(props, context); + this.state = { currentId: null }; this.onItemClick = this.onItemClick.bind(this); this.onItemDelete = this.onItemDelete.bind(this); @@ -66,6 +71,7 @@ export default class CondensedList extends React.Component
    {items.map((item) => this.onItemClick(e, item)} onDelete={(e) => this.onItemDelete(e, item)} />)}
@@ -79,6 +85,7 @@ export default class CondensedList extends React.Component if (this.props.onClick) { this.props.onClick(item); } + this.setState({ currentId: item.id }); } private onItemDelete = (e: SyntheticEvent, item) => { @@ -95,11 +102,11 @@ export default class CondensedList extends React.Component * Generic list item with an onClick function and a name * @param param0 - {item: {name: ""}, onClick: (item) => void;} */ -export function ListItem({ item, onClick }) { +export function ListItem({ item, onClick, currentId }) { return (
  • {/* eslint-disable-next-line */} - + {item.name}
  • diff --git a/src/react/components/common/imageMap/imageMap.tsx b/src/react/components/common/imageMap/imageMap.tsx index f9bca46b..88e95a3c 100644 --- a/src/react/components/common/imageMap/imageMap.tsx +++ b/src/react/components/common/imageMap/imageMap.tsx @@ -43,6 +43,7 @@ interface IImageMapProps { enableFeatureSelection?: boolean; handleFeatureSelect?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void; + handleFeatureDoubleClick?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void; groupSelectMode?: boolean; handleIsPointerOnImage?: (isPointerOnImage: boolean) => void; isPointerOnImage?: boolean; @@ -58,7 +59,7 @@ interface IImageMapProps { hoveringFeature?: string; onMapReady: () => void; handleTableToolTipChange?: (display: string, width: number, height: number, top: number, - left: number, rows: number, columns: number, featureID: string) => void; + left: number, rows: number, columns: number, featureID: string) => void; addDrawnRegionFeatureProps?: (feature) => void; updateFeatureAfterModify?: (features) => any; @@ -84,7 +85,7 @@ export class ImageMap extends React.Component { 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[]; @@ -198,7 +199,7 @@ export class ImageMap extends React.Component { onMouseEnter={this.handlePointerEnterImageMap} className="map-wrapper" > -
    this.mapElement = el}/> +
    this.mapElement = el} />
    ); } @@ -361,7 +362,7 @@ export class ImageMap extends React.Component { */ public addInteraction = (interaction: Interaction) => { if (undefined === this.map.getInteractions().array_.find((existingInteraction) => { - return interaction.constructor.name === existingInteraction.constructor.name + return interaction.constructor === existingInteraction.constructor; })) { this.map.addInteraction(interaction); } @@ -477,7 +478,7 @@ export class ImageMap extends React.Component { 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); @@ -516,7 +517,7 @@ export class ImageMap extends React.Component { */ public removeInteraction = (interaction: Interaction) => { const existingInteraction = this.map.getInteractions().array_.find((existingInteraction) => { - return interaction.constructor.name === existingInteraction.constructor.name + return interaction.constructor === existingInteraction.constructor; }); if (existingInteraction !== undefined) { @@ -577,6 +578,7 @@ export class ImageMap extends React.Component { 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(); @@ -647,12 +649,12 @@ export class ImageMap extends React.Component { return; } - const eventPixel = this.map.getEventPixel(event.originalEvent); + const eventPixel = this.map.getEventPixel(event.originalEvent); const filter = this.getLayerFilterAtPixel(eventPixel); const isPixelOnFeature = !!filter; - if (isPixelOnFeature) { + if (isPixelOnFeature && !this.props.isSnapped) { this.setDragPanInteraction(false); } @@ -666,6 +668,20 @@ export class ImageMap extends React.Component { ); } } + private handleDoubleClick = (event: MapBrowserEvent) => { + const eventPixel = this.map.getEventPixel(event.originalEvent); + + const filter = this.getLayerFilterAtPixel(eventPixel); + if (filter && this.props.handleFeatureDoubleClick) { + this.map.forEachFeatureAtPixel( + eventPixel, + (feature) => { + this.props.handleFeatureDoubleClick(feature, true, filter.category); + }, + filter.layerfilter, + ); + } + } private getLayerFilterAtPixel = (eventPixel: any) => { const isPointerOnLabelledFeature = this.map.hasFeatureAtPixel( @@ -691,7 +707,7 @@ export class ImageMap extends React.Component { this.textVectorLayerFilter); if (isPointerOnTextFeature) { return { - layerfilter : this.textVectorLayerFilter, + layerfilter: this.textVectorLayerFilter, category: FeatureCategory.Text, }; } @@ -719,9 +735,9 @@ export class ImageMap extends React.Component { private handlePointerMoveOnTableIcon = (event: MapBrowserEvent) => { if (this.props.handleTableToolTipChange) { const eventPixel = this.map.getEventPixel(event.originalEvent); - const isPointerOnTableIconFeature = this.map.hasFeatureAtPixel(eventPixel,this.tableIconBorderVectorLayerFilter); + const isPointerOnTableIconFeature = this.map.hasFeatureAtPixel(eventPixel, this.tableIconBorderVectorLayerFilter); if (isPointerOnTableIconFeature) { - const features = this.map.getFeaturesAtPixel( eventPixel, this.tableIconBorderVectorLayerFilter); + const features = this.map.getFeaturesAtPixel(eventPixel, this.tableIconBorderVectorLayerFilter); if (features.length > 0) { const feature = features[0]; if (feature && this.props.hoveringFeature !== feature.get("id")) { @@ -789,6 +805,9 @@ export class ImageMap extends React.Component { } this.setDragPanInteraction(true); + this.removeInteraction(this.modify); + this.initializeModify(); + this.addInteraction(this.modify) } private setDragPanInteraction = (dragPanEnabled: boolean) => { @@ -855,6 +874,7 @@ export class ImageMap extends React.Component { this.initializeModify(); this.initializeSnap(); this.initializeDraw(); + this.addInteraction(this.dragBox); this.addInteraction(this.modify); this.addInteraction(this.snap); } @@ -891,7 +911,7 @@ export class ImageMap extends React.Component { source: this.drawRegionVectorLayer.getSource(), style: this.props.drawRegionStyler, geometryFunction: (coordinates, optGeometry) => { - const extent = boundingExtent(/** @type {LineCoordType} */ (coordinates)); + const extent = boundingExtent(/** @type {LineCoordType} */(coordinates)); const boxCoordinates = [[ [extent[0], extent[3]], [extent[2], extent[3]], @@ -1078,8 +1098,8 @@ export class ImageMap extends React.Component { this.initializeDrawnRegionLabelLayer(); this.initializeDrawnRegionLayer(); return [this.imageLayer, this.textVectorLayer, this.tableBorderVectorLayer, this.tableIconBorderVectorLayer, - this.tableIconVectorLayer, this.checkboxVectorLayer, this.drawRegionVectorLayer, this.labelVectorLayer, - this.drawnLabelVectorLayer]; + this.tableIconVectorLayer, this.checkboxVectorLayer, this.drawRegionVectorLayer, this.labelVectorLayer, + this.drawnLabelVectorLayer]; } private initializePredictLayers = (projection: Projection) => { @@ -1181,7 +1201,7 @@ export class ImageMap extends React.Component { private initializeMap = (projection, layers) => { this.map = new Map({ - controls: [] , + controls: [], interactions: defaultInteractions({ shiftDragZoom: false, doubleClickZoom: false, diff --git a/src/react/components/common/tagInput/tagInput.scss b/src/react/components/common/tagInput/tagInput.scss index 8f0b16eb..c044776a 100644 --- a/src/react/components/common/tagInput/tagInput.scss +++ b/src/react/components/common/tagInput/tagInput.scss @@ -37,6 +37,18 @@ &-container { overflow-x: visible; overflow-y: auto; + padding: 0 0 0 100px; + margin: 0 0 0 -100px; + &::before{ + content: " "; + display: inline-block; + position: absolute; + width: 80px; + height: 100%; + left: -80px; + background: linear-gradient(to right, #00000000 0%,#000000 100%); + } + }; } @@ -73,6 +85,7 @@ } &-item-block { + position: relative; display: flex; flex-direction: row; margin: 2px 0; @@ -80,11 +93,23 @@ &-2 { width: 100%; } + .tag-item-confidence{ + position: absolute; + line-height: 2em; + left: -70PX; + z-index: 900; + text-align: right; + width:50px; + text-shadow: 1px 1px 1px #333; + } } &-item { display: flex; flex-direction: row; + .tag-content { + transition: 1s; + } &-selected { .tag-content { @@ -100,7 +125,12 @@ background: $darker-10 !important; } } - + &-highlight { + .tag-content { + background-color: $lighter-5 !important; + box-shadow:4px 4px 5px $lighter-5; + } + } &-label { min-height: 1em; display: flex; @@ -172,7 +202,7 @@ } &-item-label { - color: #A0A0A0; + color: #a0a0a0; } &-item-label:hover { @@ -224,7 +254,7 @@ width: 0.1px; border: 0.5px solid $lighter-2; height: 18px; - margin: 0 0.25em + margin: 0 0.25em; } &-iconbutton { @@ -234,7 +264,8 @@ padding: 0 0.25em; background-color: transparent; - &.active, &:hover { + &.active, + &:hover { background-color: transparent; color: #fff; } @@ -276,5 +307,5 @@ div.circle-picker-container { } .loading-tag { - height: 100%; + height: 100%; } diff --git a/src/react/components/common/tagInput/tagInput.tsx b/src/react/components/common/tagInput/tagInput.tsx index 0f1d61ea..627e8a5f 100644 --- a/src/react/components/common/tagInput/tagInput.tsx +++ b/src/react/components/common/tagInput/tagInput.tsx @@ -81,7 +81,6 @@ export interface ITagInputProps { onLabelLeave: (label: ILabel) => void; /** Function to handle tag change */ onTagChanged?: (oldTag: ITag, newTag: ITag) => void; - setTagInputMode?: (tagInputMode: TagInputMode, selectedTableTagToLabel?: ITableTag) => void; tagInputMode: TagInputMode; selectedTableTagToLabel: ITableTag; @@ -91,6 +90,7 @@ export interface ITagInputProps { handleTableCellClick: (iTableCellIndex, jTableCellIndex) => void; selectedTableTagBody: ITableRegion[][][]; splitPaneWidth: number; + onTagDoubleClick?: (label: ILabel) => void; } export interface ITagInputState { @@ -149,7 +149,7 @@ export class TagInput extends React.Component { public render() { const dark: ICustomizations = { settings: { - theme: getDarkTheme(), + theme: getDarkTheme(), }, scopedSettings: {}, }; @@ -207,6 +207,7 @@ export class TagInput extends React.Component { searchTags: !this.state.searchTags, searchQuery: "", })} + searchingTags={this.state.searchQuery.length > 0} onRenameTag={this.onRenameTag} onLockTag={this.onLockTag} onDelete={this.onDeleteTag} @@ -226,7 +227,8 @@ export class TagInput extends React.Component { onKeyDown={this.onSearchKeyDown} onChange={(e) => this.setState({ searchQuery: e.target.value })} placeholder="Search tags" - autoFocus={true} + autoFocus={true} + onFocus={() => this.setState({ selectedTag: null, tagOperation: TagOperationMode.Rename })} />
    @@ -245,6 +247,7 @@ export class TagInput extends React.Component { } {this.getColorPickerPortal()} + { this.state.addTags && @@ -394,7 +397,7 @@ export class TagInput extends React.Component { const { selectedTag } = this.state; const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker; return ( - this.headerRef.current}> + this.headerRef.current}>
    { showColorPicker && @@ -429,6 +432,7 @@ export class TagInput extends React.Component { onLabelLeave={this.props.onLabelLeave} onTagChanged={this.props.onTagChanged} handleLabelTable={this.props.handleLabelTable} + onTagDoubleClick={this.props.onTagDoubleClick} />); } @@ -519,11 +523,11 @@ export class TagInput extends React.Component { deselect = false; } else if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) { if (isTagLabelTypeDrawnRegion) { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCategory: category})); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: category})); } else if (tagCategory === FeatureCategory.Checkbox) { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCategory: FeatureCategory.Checkbox})); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Checkbox})); } else { - toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCategory: FeatureCategory.Text})); + toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Text})); } return; } else if (tagCategory === category || category === FeatureCategory.DrawnRegion || @@ -535,7 +539,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({ @@ -544,14 +548,23 @@ export class TagInput extends React.Component { }); } } - + focusTag(tag: string) { + const tagItemRef = this.tagItemRefs.get(tag)?.getTagNameRef(); + if (tagItemRef) { + tagItemRef.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); + tagItemRef.current.classList.add("tag-item-highlight"); + setTimeout(() => { + tagItemRef.current.classList.remove("tag-item-highlight"); + }, 2000); + } + } public labelAssigned = (labels: ILabel[], name): boolean => { - const label = labels.find((label) => label.label === name ? true : false); - if (!label) { - return false; - } else { - return true; - } + const label = labels.find((label) => label.label === name ? true : false); + if (!label) { + return false; + } else { + return true; + } } public labelAssignedDrawnRegion = (labels: ILabel[], name): boolean => { @@ -606,11 +619,11 @@ export class TagInput extends React.Component { private creatTagInput = (value: any) => { const newTag: ITag = { - name: value, - color: getNextColor(this.state.tags), - type: FieldType.String, - format: FieldFormat.NotSpecified, - documentCount: 0, + name: value, + color: getNextColor(this.state.tags), + type: FieldType.String, + format: FieldFormat.NotSpecified, + documentCount: 0, }; if (newTag.name.length && ![...this.state.tags, newTag].containsDuplicates((t) => t.name)) { this.addTag(newTag); @@ -664,7 +677,7 @@ export class TagInput extends React.Component { } private onHideContextualMenu = () => { - this.setState({tagOperation: TagOperationMode.None}); + this.setState({ tagOperation: TagOperationMode.None }); } private getContextualMenuItems = (): IContextualMenuItem[] => { @@ -728,6 +741,7 @@ export class TagInput extends React.Component { }, text: strings.tags.toolbar.moveUp, onClick: this.onMenuItemClick, + disabled: this.state.searchQuery.length > 0, }, { key: TagMenuItem.MoveDown, @@ -736,6 +750,7 @@ export class TagInput extends React.Component { }, text: strings.tags.toolbar.moveDown, onClick: this.onMenuItemClick, + disabled: this.state.searchQuery.length > 0, }, { key: TagMenuItem.Delete, diff --git a/src/react/components/common/tagInput/tagInputItem.tsx b/src/react/components/common/tagInput/tagInputItem.tsx index fafb6d90..f3b59b5b 100644 --- a/src/react/components/common/tagInput/tagInputItem.tsx +++ b/src/react/components/common/tagInput/tagInputItem.tsx @@ -7,6 +7,7 @@ import { ITag, ILabel, FieldType, FieldFormat, TagInputMode } from "../../../../ import { strings } from "../../../../common/strings"; import TagInputItemLabel from "./tagInputItemLabel"; import { tagIndexKeys } from "./tagIndexKeys"; +import _ from "lodash"; export interface ITagClickProps { ctrlKey?: boolean; @@ -43,6 +44,7 @@ export interface ITagInputItemProps { onTagChanged?: (oldTag: ITag, newTag: ITag) => void; handleLabelTable: (tagInputMode: TagInputMode, selectedTableTagToLabel) => void; addRowToDynamicTable: () => void; + onTagDoubleClick?: (label:ILabel) => void; } export interface ITagInputItemState { @@ -81,9 +83,14 @@ export default class TagInputItem extends React.Component + {confidence && +
    + {confidence} +
    + }
    {this.getTagContent()}
    @@ -141,6 +149,14 @@ export default class TagInputItem extends React.Component { + e.stopPropagation(); + const { labels } = this.props; + if (labels.length > 0) { + this.props.onTagDoubleClick(labels[0]); + } + } + private getItemClassName = () => { const classNames = ["tag-item"]; if (this.props.isSelected) { @@ -165,20 +181,20 @@ export default class TagInputItem extends React.Component { this.state.isRenaming - ? - this.onInputKeyDown(e)} - onBlur={this.onInputBlur} - autoFocus={true} - /> - : - - {this.props.tag.name} - + ? + this.onInputKeyDown(e)} + onBlur={this.onInputBlur} + autoFocus={true} + /> + : + + {this.props.tag.name} + }
    @@ -192,7 +208,7 @@ export default class TagInputItem extends React.Component
    @@ -276,7 +292,7 @@ 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); } diff --git a/src/react/components/common/tagInput/tagInputToolbar.tsx b/src/react/components/common/tagInput/tagInputToolbar.tsx index cf789545..172f1af9 100644 --- a/src/react/components/common/tagInput/tagInputToolbar.tsx +++ b/src/react/components/common/tagInput/tagInputToolbar.tsx @@ -9,7 +9,8 @@ import { ITableRegion, ITableTag, ITag, TagInputMode } from "../../../../models/ enum Categories { General, Separator, - Modifier, + RenameModifier, + MoveModifier, } /** Properties for tag input toolbar */ @@ -30,6 +31,7 @@ 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; + searchingTags: boolean; } interface ITagInputToolbarItemProps { @@ -76,38 +78,44 @@ export default class TagInputToolbar extends React.Component { - const modifierDisabled = !this.props.selectedTag; - const modifierClassNames = ["tag-input-toolbar-iconbutton"]; - if (modifierDisabled) { - modifierClassNames.push("tag-input-toolbar-iconbutton-disabled"); + const moveModifierDisabled = !this.props.selectedTag || this.props.searchingTags; + const renameModifierDisabled = !this.props.selectedTag; + const moveModifierClassNames = ["tag-input-toolbar-iconbutton"]; + const renameModifierClassNames = ["tag-input-toolbar-iconbutton"]; + if (moveModifierDisabled) { + moveModifierClassNames.push("tag-input-toolbar-iconbutton-disabled"); + } + if (renameModifierDisabled) { + renameModifierClassNames.push("tag-input-toolbar-iconbutton-disabled"); } - const modifierClassName = modifierClassNames.join(" "); + const moveModifierClassName = moveModifierClassNames.join(" "); + const renameModifierClassName = renameModifierClassNames.join(" "); return( this.getToolbarItems().map((itemConfig, index) => { @@ -124,14 +132,25 @@ export default class TagInputToolbar extends React.Component); - } else if (itemConfig.category === Categories.Modifier) { + } else if (itemConfig.category === Categories.RenameModifier) { return ( this.onToolbarItemClick(e, itemConfig)} /> + ); + } else if (itemConfig.category === Categories.MoveModifier) { + return ( + this.onToolbarItemClick(e, itemConfig)} /> ); diff --git a/src/react/components/pages/editorPage/canvas.scss b/src/react/components/pages/editorPage/canvas.scss index 317826c9..adc0e25e 100644 --- a/src/react/components/pages/editorPage/canvas.scss +++ b/src/react/components/pages/editorPage/canvas.scss @@ -22,6 +22,7 @@ background-color: $darker-1; border: solid 1px $lighter-2; color: rgb(0, 161, 241); + z-index: 10; &:hover, &.active { background-color: $darker-2; @@ -39,14 +40,14 @@ .prev { position: absolute; top: 50%; - left: 0; + left: 50px; margin-left: 10px; } .next { position: absolute; top: 50%; - right: 0; + right: 50px; margin-right: 10px; } @@ -77,6 +78,7 @@ background-color: rgba(0, 0, 0, 0.8); text-align: center; display: flex; + z-index: 11; } .canvas-ocr-loading-spinner { diff --git a/src/react/components/pages/editorPage/canvas.tsx b/src/react/components/pages/editorPage/canvas.tsx index a1f99570..d1d1cb2d 100644 --- a/src/react/components/pages/editorPage/canvas.tsx +++ b/src/react/components/pages/editorPage/canvas.tsx @@ -9,7 +9,7 @@ import { EditorMode, IAssetMetadata, IProject, IRegion, RegionType, AssetType, ILabelData, ILabel, - ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, ImageMapParent, LabelType, ITableRegion, ITableTag, ITableLabel, ITableCellLabel + ITag, IAsset, IFormRegion, FeatureCategory, FieldType, FieldFormat, ImageMapParent, LabelType, ITableRegion, ITableTag, ITableLabel, ITableCellLabel, AssetLabelingState, APIVersionPatches } from "../../../../models/applicationState"; import CanvasHelpers from "./canvasHelpers"; import { AssetPreview } from "../../common/assetPreview/assetPreview"; @@ -37,7 +37,8 @@ import { TooltipHost, ITooltipHostStyles } from "@fluentui/react"; import { IAppSettings } from '../../../../models/applicationState'; import { AutoLabelingStatus, PredictService } from "../../../../services/predictService"; import { AssetService } from "../../../../services/assetService"; -import { strings } from "../../../../common/strings"; +import { interpolate, strings } from "../../../../common/strings"; +import { toast } from "react-toastify"; pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version); @@ -55,11 +56,13 @@ export interface ICanvasProps extends React.Props { closeTableView?: (state: string) => void; onAssetMetadataChanged?: (assetMetadata: IAssetMetadata) => void; onSelectedRegionsChanged?: (regions: IRegion[]) => void; + onRegionDoubleClick?: (region: IRegion) => void; onCanvasRendered?: (canvas: HTMLCanvasElement) => void; onRunningOCRStatusChanged?: (isRunning: boolean) => void; onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void; runOcrForAllDocs?: (runForAllDocs: boolean) => void; + runAutoLabelingOnNextBatch?: () => Promise; onAssetDeleted?: () => void; handleLabelTable?: () => void; } @@ -181,7 +184,9 @@ export default class Canvas extends React.Component public componentDidUpdate = async (prevProps: Readonly, prevState: Readonly) => { // Handles asset changing if (this.props.selectedAsset.asset.name !== prevProps.selectedAsset.asset.name || - this.props.selectedAsset.asset.isRunningOCR !== prevProps.selectedAsset.asset.isRunningOCR) { + this.props.selectedAsset.asset.isRunningOCR !== prevProps.selectedAsset.asset.isRunningOCR || + this.props.selectedAsset.asset.labelingState !== prevProps.selectedAsset.asset.labelingState + ) { this.selectedRegionIds = []; this.imageMap.removeAllFeatures(); this.imageMap.resetAllLayerVisibility(); @@ -253,10 +258,12 @@ export default class Canvas extends React.Component handleAssetDeleted={this.props.onAssetDeleted} handleRunOcrForAllDocuments={this.runOcrForAllDocuments} handleRunAutoLabelingOnCurrentDocument={this.runAutoLabelingOnCurrentDocument} - connectionType={this.props.project.sourceConnection.providerType} + handleRunAutoLabelingForRestDocuments={this.runAutoLabelingForRestDocuments} handleToggleDrawRegionMode={this.handleToggleDrawRegionMode} + connectionType={this.props.project.sourceConnection.providerType} drawRegionMode={this.state.drawRegionMode} project={this.props.project} + selectedAsset={this.props.selectedAsset} parentPage={strings.editorPage.title} /> imageHeight={this.state.imageHeight} enableFeatureSelection={!this.state.drawRegionMode && !this.state.groupSelectMode} handleFeatureSelect={this.handleFeatureSelect} + handleFeatureDoubleClick={this.handleFeatureDoubleClick} featureStyler={this.featureStyler} groupSelectMode={this.state.groupSelectMode} handleIsPointerOnImage={this.handleIsPointerOnImage} @@ -370,16 +378,19 @@ export default class Canvas extends React.Component const assetPath = asset.path; const predictService = new PredictService(this.props.project); const result = await predictService.getPrediction(assetPath); - const assetService = new AssetService(this.props.project); - await assetService.uploadAssetPredictResult(asset, result); - const assetMetadata = await assetService.getAssetMetadata(asset); + const assetMetadata = assetService.getAssetPredictMetadata(asset, result); await this.props.onAssetMetadataChanged(assetMetadata); } finally { this.setAutoLabelingStatus(AutoLabelingStatus.done); } } + private runAutoLabelingForRestDocuments = async () => { + this.setState({ autoLableingStatus: AutoLabelingStatus.running }); + await this.props.runAutoLabelingOnNextBatch(); + this.setState({ autoLableingStatus: AutoLabelingStatus.done }); + } public updateSize() { this.imageMap.updateSize(); @@ -557,7 +568,7 @@ export default class Canvas extends React.Component const filteredRegions = this.state.currentAsset.regions.filter((assetRegion) => { return regions.findIndex((r) => r.id === assetRegion.id) === -1; }); - this.updateAssetRegions(filteredRegions); + this.updateAssetRegions(filteredRegions, regions.length > 0); } private deleteRegionsFromImageMap = (regions: IRegion[]) => { @@ -606,7 +617,7 @@ export default class Canvas extends React.Component * @param regions * @param selectedRegions */ - private updateAssetRegions = (regions: IRegion[]) => { + private updateAssetRegions = (regions: IRegion[], manualOption: boolean = false) => { const labelData = this.convertRegionsToLabelData(regions, this.state.currentAsset.asset.name); console.log("Canvas -> privateupdateAssetRegions -> labelData", labelData) const currentAsset: IAssetMetadata = { @@ -621,6 +632,41 @@ export default class Canvas extends React.Component (region) => region.tags[0] !== undefined && region.pageNumber === this.state.currentPage)); } + if (manualOption) { + if (currentAsset.labelData) { + const labelingState = _.get(this.state, "currentAsset.labelData.labelingState", null); + if (labelingState) { + switch (labelingState) { + case AssetLabelingState.AutoLabeled: + case AssetLabelingState.AutoLabeledAndAdjusted: + currentAsset.labelData.labelingState = AssetLabelingState.AutoLabeledAndAdjusted; + break; + case AssetLabelingState.ManuallyLabeled: + case AssetLabelingState.Trained: + currentAsset.labelData.labelingState = AssetLabelingState.ManuallyLabeled; + break; + default: + currentAsset.labelData.labelingState = AssetLabelingState.ManuallyLabeled; + break; + } + } + else { + currentAsset.labelData.labelingState = AssetLabelingState.ManuallyLabeled; + } + } + } + else { + if (this.state.currentAsset.labelData && currentAsset.labelData) { + currentAsset.labelData.labelingState = this.state.currentAsset.labelData.labelingState; + } + } + + if (currentAsset.labelData) { + currentAsset.asset.labelingState = currentAsset.labelData.labelingState; + } else if (currentAsset.asset.labelingState) { + delete currentAsset.asset.labelingState; + } + this.setState({ currentAsset, }, () => { @@ -640,7 +686,7 @@ export default class Canvas extends React.Component const deletedRegionIndex = currentRegions.findIndex((region) => region.id === id); currentRegions.splice(deletedRegionIndex, 1); - this.updateAssetRegions(currentRegions); + this.updateAssetRegions(currentRegions, true); } /** @@ -655,6 +701,12 @@ export default class Canvas extends React.Component this.props.onSelectedRegionsChanged(selectedRegions); } } + private onRegionDoubleClick = (id: string) => { + if (this.props.onRegionDoubleClick) { + const region = this.state.currentAsset.regions.find(region=>region.id === id); + this.props.onRegionDoubleClick(region); + } + } /** * Updates regions in both Canvas Tools and the asset data store @@ -667,14 +719,14 @@ export default class Canvas extends React.Component for (const update of updates) { const region = regions.find((r) => r.id === update.id); if (region) { - // skip + region.changed = true; } else { updatedRegions.push(update); } } console.log("Canvas -> privateupdateRegions -> updatedRegions", updatedRegions) updatedRegions.sort(this.compareRegionOrder); - this.updateAssetRegions(updatedRegions); + this.updateAssetRegions(updatedRegions, true); } private createBoundingBoxVectorFeature = (text, boundingBox, imageExtent, ocrExtent, page) => { @@ -1021,6 +1073,12 @@ export default class Canvas extends React.Component } this.redrawAllFeatures(); } + private handleFeatureDoubleClick = (feature: Feature, isToggle: boolean = true, category: FeatureCategory) => { + const regionId = feature.get("id"); + if (this.isRegionSelected(regionId)) { + this.onRegionDoubleClick(regionId); + } + } private handleMultiSelection = (regionId: any, category: FeatureCategory) => { const selectedRegions = this.getSelectedRegions(); @@ -1174,7 +1232,7 @@ export default class Canvas extends React.Component return; } try { - const ocr = await this.ocrService.getRecognizedText(asset.path, asset.name, this.setOCRStatus, force); + const ocr = await this.ocrService.getRecognizedText(asset.path, asset.name, asset.mimeType, this.setOCRStatus, force); if (asset.id === this.state.currentAsset.asset.id) { // since get OCR is async, we only set currentAsset's OCR this.setState({ @@ -1340,6 +1398,13 @@ export default class Canvas extends React.Component } private convertRegionsToLabelData = (regions: IRegion[], assetName: string) => { + const labels = (this.props.selectedAsset + && this.props.selectedAsset.labelData + && this.props.selectedAsset.labelData.labels + && this.props.selectedAsset.labelData.labels.map(label => ({ + ...label, value: [] + }))) || []; + const labelData: ILabelData = { document: decodeURIComponent(assetName).split("/").pop(), labels: [] as ILabel[], @@ -1347,66 +1412,76 @@ export default class Canvas extends React.Component }; regions.forEach((region) => { - const labelType = this.getLabelType(region.category); - const boundingBox = region.id.split(",").map(parseFloat); - const formRegion = { - page: region.pageNumber, - text: region.value, - boundingBoxes: [boundingBox], - } as IFormRegion; - region.tags.forEach((tag) => { - if (region.isTableRegion) { - const tableRegion = region as ITableRegion; - const tableLabel: ITableLabel = labelData.tableLabels.find((tableLabel) => { return tableLabel.tableKey === tag }); - if (tableLabel) { - const tableLabelCell = tableLabel.labels.find((tableLabelCell) => { return tableLabelCell.columnKey === tableRegion.columnKey &&tableLabelCell.rowKey === tableRegion.rowKey }); - if (tableLabelCell) { - tableLabelCell.value.push(formRegion) - } else { - tableLabel.labels.push({ - rowKey: tableRegion.rowKey, - columnKey: tableRegion.columnKey, - value: [formRegion] - }); - } + const labelType = this.getLabelType(region.category); + const boundingBox = region.id.split(",").map(parseFloat); + const formRegion = { + page: region.pageNumber, + text: region.value, + boundingBoxes: [boundingBox], + } as IFormRegion; + region.tags.forEach((tag) => { + if (region.isTableRegion) { + const tableRegion = region as ITableRegion; + const tableLabel: ITableLabel = labelData.tableLabels.find((tableLabel) => tableLabel.tableKey === tag); + if (tableLabel) { + const tableLabelCell = tableLabel.labels.find((tableLabelCell) => tableLabelCell.columnKey === tableRegion.columnKey && tableLabelCell.rowKey === tableRegion.rowKey); + if (tableLabelCell) { + tableLabelCell.value.push(formRegion) } else { - const tableCellLabel: ITableCellLabel = { + tableLabel.labels.push({ rowKey: tableRegion.rowKey, columnKey: tableRegion.columnKey, value: [formRegion] - } - labelData.tableLabels.push({ - tableKey: tag, - labels: [tableCellLabel] - }) + }); } } else { - const label = labelData.labels.find((label) => { return label.label === tag }); - if (label) { - label.value.push(formRegion); - } else { - let newLabel; - if (labelType) { - newLabel = { - label: tag, - key: null, - labelType, - value: [formRegion], - } as ILabel; - } else { - newLabel = { - label: tag, - key: null, - value: [formRegion], - } as ILabel; - } - labelData.labels.push(newLabel); + const tableCellLabel: ITableCellLabel = { + rowKey: tableRegion.rowKey, + columnKey: tableRegion.columnKey, + value: [formRegion] } + labelData.tableLabels.push({ + tableKey: tag, + labels: [tableCellLabel] + }) } - }); + } else { + const label = labelData.labels.find((label) => label.label === tag); + if (label) { + if (label.confidence && region.changed) { + delete label.confidence; + } + label.value.push(formRegion); + } else { + let newLabel; + if (labelType) { + newLabel = { + label: tag, + key: null, + labelType, + value: [formRegion], + } as ILabel; + } else { + newLabel = { + label: tag, + key: null, + value: [formRegion], + } as ILabel; + } + labelData.labels.push(newLabel); + } + } + }); }); - return labelData; + const newLabels = labelData.labels.filter(label => label.value.length > 0); + + return newLabels.length > 0 || labelData.tableLabels.length > 0 ? + { + document: decodeURIComponent(assetName).split("/").pop(), + labels: newLabels, + tableLabels: labelData.tableLabels + } as ILabelData : null; } private getLabelType = (regionCategory: string) => { @@ -1650,10 +1725,20 @@ export default class Canvas extends React.Component } else if (newLabels.length > 0) { const newFieldNames = newLabels.map((label) => label.label); const prevFieldNames = prevLabels.map((label) => label.label); - return !_.isEqual(newFieldNames.sort(), prevFieldNames.sort()); + 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(","); + if (newValue !== prevValue) { + return true; + } + } + return false; + } + else { + return true; + } } - - return false; } private getBoundingBoxTextFromRegion = (formRegion: IFormRegion, boundingBoxIndex: number) => { @@ -1918,7 +2003,6 @@ export default class Canvas extends React.Component }); } const tag: ITag = this.props.project.tags.find((tag) => tag.name === tagName); - let regionCategory: string; if (labelType) { regionCategory = labelType; @@ -2057,11 +2141,17 @@ export default class Canvas extends React.Component } const prevTypes = {}; - prevTags.forEach((tag) => prevTypes[tag.name] = tag.type); - + const prevColors = {}; + prevTags.forEach((tag) => { + prevTypes[tag.name] = tag.type; + prevColors[tag.name] = tag.color; + }); const types = {}; - tags.forEach((tag) => types[tag.name] = tag.type); - + const colors = {}; + tags.forEach((tag) => { + types[tag.name] = tag.type; + colors[tag.name] = tag.color; + }); for (const name of names) { const prevType = prevTypes[name]; const type = types[name]; @@ -2070,6 +2160,12 @@ export default class Canvas extends React.Component // some tag change between checkbox and text return true; } + const prevColor = prevColors[name]; + const color = colors[name]; + if (prevColor !== color) { + // some tag color changed + return true; + } } return false; @@ -2125,6 +2221,9 @@ export default class Canvas extends React.Component } private handleToggleDrawRegionMode = () => { + if (!this.state.drawRegionMode && this.props.project.apiVersion !== APIVersionPatches.patch3) { + toast.warn(interpolate(strings.editorPage.canvas.canvasCommandBar.warings.drawRegionUnsupportedAPIVersion, { apiVersion: (this.props.project.apiVersion || constants.appVersion ) }), {autoClose: 7000}); + } this.setState({ drawRegionMode: !this.state.drawRegionMode }); @@ -2240,4 +2339,11 @@ export default class Canvas extends React.Component }); this.imageMap.modifyStartFeatureCoordinates = {}; } + + async focusOnLabel(label: ILabel) { + const { page } = label.value[ 0 ]; + if (this.state.currentPage !== page) { + await this.goToPage(page); + } + } } diff --git a/src/react/components/pages/editorPage/canvasCommandBar.tsx b/src/react/components/pages/editorPage/canvasCommandBar.tsx index 2d4e9d70..e9557d83 100644 --- a/src/react/components/pages/editorPage/canvasCommandBar.tsx +++ b/src/react/components/pages/editorPage/canvasCommandBar.tsx @@ -2,24 +2,29 @@ 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 { strings } from '../../../../common/strings'; +import { interpolate, strings } from '../../../../common/strings'; import { ContextualMenuItemType } from "@fluentui/react"; -import { IProject } from "../../../../models/applicationState"; +import { IProject, IAssetMetadata, AssetLabelingState } from "../../../../models/applicationState"; +import _ from "lodash"; import "./canvasCommandBar.scss"; +import { constants } from "../../../../common/constants"; interface ICanvasCommandBarProps { handleZoomIn: () => void; handleZoomOut: () => void; - handleRunAutoLabelingOnCurrentDocument?: () => void; - project: IProject; - handleRotateImage: (degrees: number) => void; handleRunOcr?: () => void; handleRunOcrForAllDocuments?: () => void; + handleRunAutoLabelingOnCurrentDocument?: () => void; + handleRunAutoLabelingForRestDocuments?: () => void; handleLayerChange?: (layer: string) => void; handleToggleDrawRegionMode?: () => void; + handleAssetDeleted?: () => void; + project: IProject; + selectedAsset?: IAssetMetadata; + handleRotateImage: (degrees: number) => void; + drawRegionMode?: boolean; connectionType?: string; - handleAssetDeleted?: () => void; layers?: any; parentPage: string; } @@ -31,6 +36,14 @@ export const CanvasCommandBar: React.FunctionComponent = }, scopedSettings: {}, }; + const disableAutoLabeling = !props.project.predictModelId; + let disableAutoLabelingCurrentAsset = disableAutoLabeling; + if (!disableAutoLabeling) { + const labelingState = _.get(props.selectedAsset, "labelData.labelingState"); + if (labelingState === AssetLabelingState.ManuallyLabeled || labelingState === AssetLabelingState.Trained) { + disableAutoLabelingCurrentAsset = true; + } + } let commandBarItems: ICommandBarItemProps[] = []; if (props.parentPage === strings.editorPage.title) { @@ -64,16 +77,16 @@ export const CanvasCommandBar: React.FunctionComponent = isChecked: props.layers["checkboxes"], onClick: () => props.handleLayerChange("checkboxes"), }, - // { - // key: "DrawnRegions", - // text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.drawnRegions, - // canCheck: true, - // iconProps: { iconName: "AddField" }, - // 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: "AddField" }, + 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, @@ -85,16 +98,16 @@ export const CanvasCommandBar: React.FunctionComponent = ], }, }, - // { - // key: "drawRegion", - // text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion, - // iconProps: { iconName: "AddField" }, - // toggle: true, - // checked: props.drawRegionMode, - // className: !props.layers["drawnRegions"] ? "disabled" : "", - // onClick: () => props.handleToggleDrawRegionMode(), - // disabled: !props.layers["drawnRegions"], - // } + { + key: "drawRegion", + text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion, + iconProps: { iconName: "AddField" }, + toggle: true, + checked: props.drawRegionMode, + className: !props.layers["drawnRegions"] ? "disabled" : "", + onClick: () => props.handleToggleDrawRegionMode(), + disabled: !props.layers["drawnRegions"], + } ]; } @@ -155,23 +168,34 @@ export const CanvasCommandBar: React.FunctionComponent = key: "runOcrForCurrentDocument", text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnCurrentDocument, iconProps: { iconName: "TextDocument" }, - onClick: () => props.handleRunOcr(), + onClick: () => { if (props.handleRunOcr) props.handleRunOcr(); }, }, { key: "runOcrForAllDocuments", text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnAllDocuments, iconProps: { iconName: "Documentation" }, - onClick: () => props.handleRunOcrForAllDocuments(), + onClick: () => { if (props.handleRunOcrForAllDocuments) props.handleRunOcrForAllDocuments(); }, }, { key: "runAutoLabelingCurrentDocument", text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingCurrentDocument, iconProps: { iconName: "Tag" }, - disabled: !props.project.predictModelId, + disabled: disableAutoLabelingCurrentAsset, title: props.project.predictModelId ? "" : strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject, onClick: () => { - props.handleRunAutoLabelingOnCurrentDocument(); + if (props.handleRunAutoLabelingOnCurrentDocument) props.handleRunAutoLabelingOnCurrentDocument(); + }, + }, + { + key: "runAutoLabelingForRestDocuments", + text: interpolate(strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingOnNotLabelingDocuments, { batchSize: constants.autoLabelBatchSize }), + iconProps: { iconName: "Tag" }, + disabled: disableAutoLabeling, + title: props.project.predictModelId ? "" : + strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject, + onClick: () => { + if (props.handleRunAutoLabelingForRestDocuments) props.handleRunAutoLabelingForRestDocuments(); }, }, { @@ -182,7 +206,7 @@ export const CanvasCommandBar: React.FunctionComponent = key: "deleteAsset", text: strings.editorPage.asset.delete.title, iconProps: { iconName: "Delete" }, - onClick: () => props.handleAssetDeleted(), + onClick: () => { if (props.handleAssetDeleted) props.handleAssetDeleted(); }, } ], }, diff --git a/src/react/components/pages/editorPage/editorPage.scss b/src/react/components/pages/editorPage/editorPage.scss index cf401221..c448d48d 100644 --- a/src/react/components/pages/editorPage/editorPage.scss +++ b/src/react/components/pages/editorPage/editorPage.scss @@ -232,6 +232,18 @@ canvas { .badge-tagged { background-color: rgba(green, 0.9); border: 1px solid $lighter-2; + &-ManuallyLabeled{ + background-color: rgba(rgb(128, 41, 0), 0.9); + } + &-Trained { + background-color: rgba(green, 0.9); + } + &-AutoLabeled { + background-color: rgba(rgb(136, 0, 91), 0.9); + } + &-AutoLabeledAndAdjusted{ + background-color: rgba(rgb(0, 92, 128), 0.9); + } } .badge-visited { diff --git a/src/react/components/pages/editorPage/editorPage.tsx b/src/react/components/pages/editorPage/editorPage.tsx index 65714eb3..5f864b1d 100644 --- a/src/react/components/pages/editorPage/editorPage.tsx +++ b/src/react/components/pages/editorPage/editorPage.tsx @@ -13,7 +13,7 @@ import { strings, interpolate } from "../../../../common/strings"; import { AssetState, AssetType, EditorMode, FieldType, IApplicationState, IAppSettings, IAsset, IAssetMetadata, - ILabel, IProject, IRegion, ISize, ITag, FeatureCategory, TagInputMode,FieldFormat, ITableTag, ITableRegion + ILabel, IProject, IRegion, ISize, ITag, FeatureCategory, TagInputMode, FieldFormat, ITableTag, ITableRegion, AssetLabelingState } from "../../../../models/applicationState"; import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions"; import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions"; @@ -89,6 +89,7 @@ export interface IEditorPageState { hoveredLabel: ILabel; /** Whether the task for loading all OCRs is running */ isRunningOCRs?: boolean; + isRunningAutoLabelings?: boolean; /** Whether OCR is running in the main canvas */ isCanvasRunningOCR?: boolean; isCanvasRunningAutoLabeling?: boolean; @@ -301,6 +302,7 @@ export default class EditorPage extends React.Component @@ -349,7 +352,8 @@ export default class EditorPage extends React.Component + onTagDoubleClick={this.onLabelDoubleClicked} + /> => { - console.log("EditorPage -> assetMetadata", assetMetadata) + console.log("EditorPage -> assetMetadata", assetMetadata) // Comment out below code as we allow regions without tags, it would make labeler's work easier. - + assetMetadata = JSON.parse(JSON.stringify(assetMetadata)); // alex const initialState = assetMetadata.asset.state; const asset = { ...assetMetadata.asset }; - console.log("EditorPage -> asset", asset) - if (this.isTaggableAssetType(assetMetadata.asset)) { + // console.log("EditorPage -> asset", asset) + if (this.isTaggableAssetType(asset)) { const hasLabels = _.get(assetMetadata, "labelData.labels.length", 0) > 0; - const hasTableLabels = _.get(assetMetadata, "labelData.tableLabels.length", 0) > 0 - assetMetadata.asset.state = hasLabels || hasTableLabels ? + const hasTableLabels = _.get(assetMetadata, "labelData.tableLabels.length", 0) > 0; + asset.state = hasLabels || hasTableLabels ? AssetState.Tagged : AssetState.Visited; - } else if (assetMetadata.asset.state === AssetState.NotVisited) { - assetMetadata.asset.state = AssetState.Visited; + } else if (asset.state === AssetState.NotVisited) { + asset.state = AssetState.Visited; } // Only update asset metadata if state changes or is different - if (initialState !== assetMetadata.asset.state || this.state.selectedAsset !== assetMetadata) { - if (this.state.selectedAsset?.labelData?.labels && assetMetadata?.labelData?.labels && - assetMetadata.labelData.labels.toString() !== this.state.selectedAsset.labelData.labels.toString()) { + if (initialState !== asset.state || this.state.selectedAsset !== assetMetadata) { + if (this.state.selectedAsset?.labelData?.labels && assetMetadata?.labelData?.labels && assetMetadata.labelData.labels.toString() !== this.state.selectedAsset.labelData.labels.toString()) { await this.updatedAssetMetadata(assetMetadata); } + assetMetadata.asset = asset; await this.props.actions.saveAssetMetadata(this.props.project, assetMetadata); - if (this.props.project.lastVisitedAssetId === assetMetadata.asset.id) { + if (this.props.project.lastVisitedAssetId === asset.id) { this.setState({ selectedAsset: assetMetadata }); } } @@ -742,6 +746,7 @@ export default class EditorPage extends React.Component a.id === asset.id); if (assetIndex > -1) { assets[assetIndex] = { @@ -783,6 +788,12 @@ export default class EditorPage extends React.Component { + if (region.tags?.length > 0) { + this.tagInputRef.current.focusTag(region.tags[0]); + } + } + private onTagsChanged = async (tags) => { const project = { ...this.props.project, @@ -816,6 +827,9 @@ export default class EditorPage extends React.Component asset.id === assetId); if (asset && (asset.state === AssetState.NotVisited || runForAll)) { try { - this.updateAssetState(asset.id, true); - await ocrService.getRecognizedText(asset.path, asset.name, undefined, runForAll); - this.updateAssetState(asset.id, false, AssetState.Visited); + this.updateAssetState({ id: asset.id, isRunningOCR: true }); + await ocrService.getRecognizedText(asset.path, asset.name, asset.mimeType, undefined, runForAll); + this.updateAssetState({ id: asset.id, isRunningOCR: false, assetState: AssetState.Visited }); } catch (err) { - this.updateAssetState(asset.id, false); + this.updateAssetState({ id: asset.id, isRunningOCR: false }); this.setState({ isError: true, errorTitle: err.title, @@ -920,14 +934,75 @@ export default class EditorPage extends React.Component { + if (this.isBusy()) { + return; + } + const { project } = this.props; + const predictService = new PredictService(project); + const assetService = new AssetService(project); - private updateAssetState = (id: string, isRunningOCR: boolean, assetState?: AssetState) => { + if (this.state.assets) { + this.setState({ isRunningAutoLabelings: true }); + const unlabeledAssetsBatch = []; + for (let i = 0; i < this.state.assets.length && unlabeledAssetsBatch.length < constants.autoLabelBatchSize; i++) { + const asset = this.state.assets[i]; + if (asset.state === AssetState.NotVisited || asset.state === AssetState.Visited) { + unlabeledAssetsBatch.push(asset); + } + } + try { + await throttle(constants.maxConcurrentServiceRequests, + unlabeledAssetsBatch, + async (asset) => { + try { + this.updateAssetState({ id: asset.id, isRunningAutoLabeling: true }); + const predictResult = await predictService.getPrediction(asset.path); + const assetMetadata = await assetService.getAssetPredictMetadata(asset, predictResult); + await assetService.uploadPredictResultAsOrcResult(asset, predictResult); + this.onAssetMetadataChanged(assetMetadata); + this.updateAssetState({ + id: asset.id, isRunningAutoLabeling: false, + assetState: AssetState.Tagged, + labelingState: AssetLabelingState.AutoLabeled, + }); + this.props.actions.updatedAssetMetadata(this.props.project, assetMetadata); + } catch (err) { + this.updateAssetState({ id: asset.id, isRunningOCR: false, isRunningAutoLabeling: false }); + this.setState({ + isError: true, + errorTitle: err.title, + errorMessage: err.message + }) + } + } + ); + + } finally { + this.setState({ isRunningAutoLabelings: false }); + } + } + } + + private updateAssetState = (newState: { + id: string, + isRunningOCR?: boolean, + isRunningAutoLabeling?: boolean, + assetState?: AssetState, + labelingState?: AssetLabelingState + }) => { this.setState((state) => ({ assets: state.assets.map((asset) => { - if (asset.id === id) { - const updatedAsset = { ...asset, isRunningOCR }; - if (assetState !== undefined && asset.state === AssetState.NotVisited) { - updatedAsset.state = assetState; + if (asset.id === newState.id) { + const updatedAsset = { ...asset, isRunningOCR: newState.isRunningOCR || false }; + if (newState.assetState !== undefined && asset.state === AssetState.NotVisited) { + updatedAsset.state = newState.assetState; + } + if (newState.labelingState) { + updatedAsset.labelingState = newState.labelingState; + } + if (newState.isRunningAutoLabeling !== undefined) { + updatedAsset.isRunningAutoLabeling = newState.isRunningAutoLabeling; } return updatedAsset; } else { @@ -935,8 +1010,8 @@ export default class EditorPage extends React.Component { - if (this.state.selectedAsset && id === this.state.selectedAsset.asset.id) { - const asset = this.state.assets.find((asset) => asset.id === id); + const asset = this.state.assets.find((asset) => asset.id === newState.id); + if (this.state.selectedAsset && newState.id === this.state.selectedAsset.asset.id) { if (asset) { this.setState({ selectedAsset: { ...this.state.selectedAsset, asset: { ...asset } }, @@ -953,24 +1028,56 @@ export default class EditorPage extends React.Component { - const projectAsset = _.get(this.props, "project.assets[asset.id]", null); + const projectAsset = _.get(this.props, `project.assets[${asset.id}]`, null); if (projectAsset) { - if (asset.state !== projectAsset.state) { + if (asset.state !== projectAsset.state || asset.labelingState !== projectAsset.labelingState) { needUpdate = true; asset.state = projectAsset.state; + asset.labelingState = projectAsset.labelingState; } } }); if (needUpdate) { this.setState({ assets: updatedAssets }); + if (this.state.selectedAsset) { + const asset = this.state.selectedAsset.asset; + const currentAsset = _.get(this.props, `project.assets[${this.state.selectedAsset.asset.id}]`, null); + if (asset.state !== currentAsset.state || asset.labelingState !== currentAsset.labelingState) { + this.updateSelectAsset(asset); + } + } } } + private updateSelectAsset = async (asset: IAsset) => { + const assetMetadata = await this.props.actions.loadAssetMetadata(this.props.project, asset); + + try { + if (!assetMetadata.asset.size) { + const assetProps = await HtmlFileReader.readAssetAttributes(asset); + assetMetadata.asset.size = { width: assetProps.width, height: assetProps.height }; + } + } catch (err) { + console.warn("Error computing asset size"); + } + this.setState({ + tableToView: null, + tableToViewId: null, + selectedAsset: assetMetadata, + }, async () => { + await this.onAssetMetadataChanged(assetMetadata); + await this.props.actions.saveProject(this.props.project, false, false); + }); + } private onLabelEnter = (label: ILabel) => { this.setState({ hoveredLabel: label }); } + private onLabelDoubleClicked = (label:ILabel) =>{ + this.canvas.current.focusOnLabel(label); + } + private onLabelLeave = (label: ILabel) => { this.setState({ hoveredLabel: null }); } diff --git a/src/react/components/pages/editorPage/editorSideBar.tsx b/src/react/components/pages/editorPage/editorSideBar.tsx index 4717851c..961ee05a 100644 --- a/src/react/components/pages/editorPage/editorSideBar.tsx +++ b/src/react/components/pages/editorPage/editorSideBar.tsx @@ -4,9 +4,10 @@ import React from "react"; import { AutoSizer, List } from "react-virtualized"; import { FontIcon } from "@fluentui/react"; -import { IAsset, AssetState, ISize } from "../../../../models/applicationState"; -import {AssetPreview, ContentSource} from "../../common/assetPreview/assetPreview"; +import { IAsset, AssetState, ISize, AssetLabelingState } from "../../../../models/applicationState"; +import { AssetPreview, ContentSource } from "../../common/assetPreview/assetPreview"; import { strings } from "../../../../common/strings"; +import _ from "lodash"; /** * Properties for Editor Side Bar @@ -135,11 +136,14 @@ export default class EditorSideBar extends React.Component { + const getBadgeTaggedClass = (state: AssetLabelingState): string => { + return state ? `badge-tagged-${AssetLabelingState[state]}` : ""; + }; switch (asset.state) { case AssetState.Tagged: return ( - + ); diff --git a/src/react/components/pages/homepage/homePage.tsx b/src/react/components/pages/homepage/homePage.tsx index 243c823c..b9e9768b 100644 --- a/src/react/components/pages/homepage/homePage.tsx +++ b/src/react/components/pages/homepage/homePage.tsx @@ -217,7 +217,16 @@ export default class HomePage extends React.Component { - await this.props.actions.deleteProject(project); + try { + await this.props.actions.deleteProject(project); + } catch (error) { + if(error instanceof AppError && error.errorCode === ErrorCode.SecurityTokenNotFound){ + toast.error(error.message, {autoClose:false}); + } + else{ + throw error; + } + } } private onProjectFileUpload = async (e, project) => { diff --git a/src/react/components/pages/modelCompose/modelCompose.tsx b/src/react/components/pages/modelCompose/modelCompose.tsx index a7e0c266..133f8d59 100644 --- a/src/react/components/pages/modelCompose/modelCompose.tsx +++ b/src/react/components/pages/modelCompose/modelCompose.tsx @@ -369,13 +369,13 @@ export default class ModelComposePage extends React.Component => { const recentModelsList: IModel[] = []; const recentModelRequest = await allSettled(this.props.project.recentModelRecords.map(async (model) => { - return this.getModelByURl(constants.apiModelsPath + "/" + model.modelInfo.modelId); + return this.getModelByURl(interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }) + "/" + model.modelInfo.modelId); })) recentModelRequest.forEach((recentModelRequest) => { if (recentModelRequest.status === "fulfilled") { @@ -528,7 +528,7 @@ export default class ModelComposePage extends React.Component -
    - docType: - {docType} -
    modelId: {modelId}
    docTypeConfidence: - {docTypeConfidence} + {(docTypeConfidence * 100).toFixed(2) + "%"}
    ) diff --git a/src/react/components/pages/predict/predictPage.tsx b/src/react/components/pages/predict/predictPage.tsx index 83dff4f2..74908d53 100644 --- a/src/react/components/pages/predict/predictPage.tsx +++ b/src/react/components/pages/predict/predictPage.tsx @@ -649,6 +649,7 @@ export default class PredictPage extends React.Component ": return modelID; case "": - return constants.apiVersion; + return (this.props.project?.apiVersion || constants.apiVersion); } }); const fileURL = window.URL.createObjectURL( @@ -811,7 +812,7 @@ export default class PredictPage extends React.Component { if (this.state.file) { - // this.props.project.assets const fileName = `${this.props.project.folderPath}/${decodeURIComponent(this.state.file.name)}`; const asset = Object.values(this.props.project.assets).find(asset => asset.name === fileName); if (asset) { @@ -1167,7 +1167,7 @@ export default class PredictPage extends React.Component
    - {item.confidence} + {(item.confidence * 100).toFixed(2)+"%" }
    ); diff --git a/src/react/components/pages/projectSettings/editProjectForm.json b/src/react/components/pages/projectSettings/editProjectForm.json index 9b468c50..a17eba84 100644 --- a/src/react/components/pages/projectSettings/editProjectForm.json +++ b/src/react/components/pages/projectSettings/editProjectForm.json @@ -26,6 +26,11 @@ "description": "API key", "type": "string" }, + "apiVersion" : { + "title": "API version", + "description": "API version", + "type": "string" + }, "description": { "title": "${strings.common.description}", "type": "string" diff --git a/src/react/components/pages/projectSettings/newProjectForm.json b/src/react/components/pages/projectSettings/newProjectForm.json index 4c22059d..e45565a3 100644 --- a/src/react/components/pages/projectSettings/newProjectForm.json +++ b/src/react/components/pages/projectSettings/newProjectForm.json @@ -31,6 +31,11 @@ "description": "API key", "type": "string" }, + "apiVersion" : { + "title": "API version", + "description": "API version", + "type": "string" + }, "description": { "title": "${strings.common.description}", "type": "string" diff --git a/src/react/components/pages/projectSettings/projectForm.tsx b/src/react/components/pages/projectSettings/projectForm.tsx index efed31c8..ae1a3add 100644 --- a/src/react/components/pages/projectSettings/projectForm.tsx +++ b/src/react/components/pages/projectSettings/projectForm.tsx @@ -16,6 +16,7 @@ import { ProjectSettingAction } from "./projectSettingAction"; import { ProtectedInput } from "../../common/protectedInput/protectedInput"; import { PrimaryButton } from "@fluentui/react"; import { getPrimaryGreenTheme, getPrimaryGreyTheme } from "../../../../common/themes"; +import { APIVersionPicker, IAPIVersionPickerProps } from "../../common/apiVersionPicker/apiVersionPicker"; // tslint:disable-next-line:no-var-requires const newFormSchema = addLocValues(require("./newProjectForm.json")); @@ -62,6 +63,7 @@ export interface IProjectFormState { export default class ProjectForm extends React.Component { private widgets = { protectedInput: (ProtectedInput as any) as Widget, + apiVersion: (APIVersionPicker as any) as Widget }; constructor(props, context) { @@ -155,6 +157,11 @@ export default class ProjectForm extends React.Component(APIVersionPicker, (props) => ({ + id: props.idSchema.$id, + value: props.formData, + onChange: props.onChange, + })), targetConnection: CustomField(ConnectionPickerWithRouter, (props) => { const targetConnections = this.props.connections .filter((connection) => StorageProviderFactory.isRegistered(connection.providerType)); @@ -209,6 +216,7 @@ export default class ProjectForm extends React.Component } + {this.state.isCommiting && +
    +
    + + +
    +
    + } ); } @@ -171,34 +186,40 @@ export default class ProjectSettingsPage extends React.Component { if (this.isPartialProject(project)) { setStorageItem(constants.projectFormTempKey, JSON.stringify(project)); + this.setState({ project }); } } private onFormSubmit = async (project: IProject) => { const isNew = !(!!project.id); + try { + this.setState({ isCommiting: true }); + const projectService = new ProjectService(); + if (!(await projectService.isValidProjectConnection(project))) { + return; + } - const projectService = new ProjectService(); - if (!(await projectService.isValidProjectConnection(project))) { - return; + if (await this.isValidProjectName(project, isNew)) { + toast.error(interpolate(strings.projectSettings.messages.projectExisted, { project })); + return; + } + + 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 })); + + if (isNew) { + this.props.history.push(`/projects/${this.props.project.id}/edit`); + } else { + this.props.history.goBack(); + } + } finally { + this.setState({ isCommiting: false }); } - if (await this.isValidProjectName(project, isNew)) { - toast.error(interpolate(strings.projectSettings.messages.projectExisted, { project })); - return; - } - - 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 })); - - if (isNew) { - this.props.history.push(`/projects/${this.props.project.id}/edit`); - } else { - this.props.history.goBack(); - } } private onFormCancel = () => { @@ -210,7 +231,7 @@ export default class ProjectSettingsPage extends React.Component { - return project && !(!!project.id) && + return project && ( !!project.name || !!project.description diff --git a/src/react/components/pages/train/trainPage.tsx b/src/react/components/pages/train/trainPage.tsx index 68c3e611..e155c6ab 100644 --- a/src/react/components/pages/train/trainPage.tsx +++ b/src/react/components/pages/train/trainPage.tsx @@ -5,12 +5,12 @@ 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 { FontIcon, PrimaryButton, Spinner, SpinnerSize, TextField } from "@fluentui/react"; 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 { - IApplicationState, IConnection, IProject, IAppSettings, FieldType, IRecentModel, + IApplicationState, IConnection, IProject, IAppSettings, FieldType, IRecentModel, AssetLabelingState, } from "../../../../models/applicationState"; import TrainChart from "./trainChart"; import TrainPanel from "./trainPanel"; @@ -26,6 +26,8 @@ import PreventLeaving from "../../common/preventLeaving/preventLeaving"; 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 UseLocalStorage from '../../../../services/useLocalStorage'; import { isElectron } from "../../../../common/hostProcess"; @@ -80,6 +82,7 @@ function mapDispatchToProps(dispatch) { @connect(mapStateToProps, mapDispatchToProps) export default class TrainPage extends React.Component { private appInsights: any = null; + private notAdjustedLabelsConfirm: React.RefObject = React.createRef(); constructor(props) { super(props); @@ -122,7 +125,9 @@ export default class TrainPage extends React.Component @@ -178,9 +183,9 @@ export default class TrainPage extends React.Component {!this.state.isTraining ? ( -
    +
    ) : ( -
    - -
    - ) +
    + +
    + ) }
    @@ -212,22 +217,22 @@ export default class TrainPage extends React.Component - - -
    - {strings.train.downloadJson}
    -
    + /> + + +
    + {strings.train.downloadJson}
    +
    }
    @@ -244,6 +249,13 @@ export default class TrainPage extends React.Component + ); } @@ -268,7 +280,7 @@ export default class TrainPage extends React.Component { if (this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL) { - this.setState({inputtedLabelFolderURL: ""}); + this.setState({ inputtedLabelFolderURL: "" }); } } @@ -284,18 +296,43 @@ export default class TrainPage extends React.Component { + const assets = Object.values(this.props.project.assets) + .filter(asset => asset.labelingState === AssetLabelingState.AutoLabeled); + if (assets.length > 0) { + this.notAdjustedLabelsConfirm.current.open(); + } else { + this.handleModelTrain(); + } + } + + private handleModelTrainConfirm = () => { + this.handleModelTrain(); + } + + private handleModelTrain = () => { this.setState({ isTraining: true, trainMessage: strings.train.training, }); - this.trainProcess().then((trainResult) => { + this.trainProcess().then(async (trainResult) => { this.setState((prevState, props) => ({ isTraining: false, trainMessage: this.getTrainMessage(trainResult), currTrainRecord: this.getProjectTrainRecord(), modelName: "", })); + const assets = Object.values(this.props.project.assets); + const assetService = new AssetService(this.props.project); + for (const asset of assets) { + const newAsset = JSON.parse(JSON.stringify(asset)); + newAsset.labelingState = AssetLabelingState.Trained; + const metadata = await assetService.getAssetMetadata(newAsset); + if (metadata.labelData && metadata.labelData.labelingState !== AssetLabelingState.Trained) { + metadata.labelData.labelingState = AssetLabelingState.Trained; + await assetService.save({ ...metadata }); + } + } // reset localStorage successful train process localStorage.setItem("trainPage_inputs", "{}"); }).catch((err) => { @@ -305,7 +342,7 @@ export default class TrainPage extends React.Component { const baseURL = url.resolve( this.props.project.apiUriBase, - constants.apiModelsPath, + interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }), ); const provider = this.props.project.sourceConnection.providerOptions as any; let trainSourceURL; @@ -367,7 +404,7 @@ export default class TrainPage extends React.Component { const recentModelRecords: IRecentModel[] = this.props.project.recentModelRecords ? - [...this.props.project.recentModelRecords] : []; - recentModelRecords.unshift({...newTrainRecord, isComposed: false} as IRecentModel); + [...this.props.project.recentModelRecords] : []; + recentModelRecords.unshift({ ...newTrainRecord, isComposed: false } as IRecentModel); if (recentModelRecords.length > constants.recentModelRecordsCount) { recentModelRecords.pop(); } @@ -487,7 +524,7 @@ export default class TrainPage extends React.Component { - const currModelUrl = this.props.project.apiUriBase + constants.apiModelsPath + "/" + this.state.currTrainRecord.modelInfo.modelId; + const currModelUrl = this.props.project.apiUriBase + interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }) + "/" + this.state.currTrainRecord.modelInfo.modelId; const modelUrl = this.state.modelUrl.length ? this.state.modelUrl : currModelUrl; const modelJSON = await this.getModelsJson(this.props.project, modelUrl); @@ -495,7 +532,7 @@ export default class TrainPage extends React.Component
    Average accuracy:

    - {this.props.averageAccuracy} + {(this.props.averageAccuracy * 100).toFixed(2)+"%"}

    diff --git a/src/react/components/pages/train/trainTable.tsx b/src/react/components/pages/train/trainTable.tsx index ade73eb7..24d8dc8f 100644 --- a/src/react/components/pages/train/trainTable.tsx +++ b/src/react/components/pages/train/trainTable.tsx @@ -39,7 +39,7 @@ export default class TrainTable Object.entries(this.props.accuracies).map((entry) => {entry[0]} - {entry[1]} + {(entry[1] * 100).toFixed(2) + "%"} ) } diff --git a/src/react/components/shell/statusBar.test.tsx b/src/react/components/shell/statusBar.test.tsx index 1b76a76d..2fe36eaf 100644 --- a/src/react/components/shell/statusBar.test.tsx +++ b/src/react/components/shell/statusBar.test.tsx @@ -11,7 +11,7 @@ describe("StatusBar component", () => { function createComponent() { return mount( - +
    Child Component
    , ); diff --git a/src/react/components/shell/statusBar.tsx b/src/react/components/shell/statusBar.tsx index 9fb95ba7..6e8cc572 100644 --- a/src/react/components/shell/statusBar.tsx +++ b/src/react/components/shell/statusBar.tsx @@ -5,18 +5,31 @@ import React from "react"; import { FontIcon } from "@fluentui/react"; import { constants } from "../../../common/constants"; import "./statusBar.scss"; +import { IProject } from "../../../models/applicationState"; -export class StatusBar extends React.Component { +export interface IStatusBarProps { + project: IProject; +} + +export class StatusBar extends React.Component { public render() { return (
    {this.props.children}
    diff --git a/src/redux/actions/projectActions.ts b/src/redux/actions/projectActions.ts index f98c81d1..0d143e55 100644 --- a/src/redux/actions/projectActions.ts +++ b/src/redux/actions/projectActions.ts @@ -152,11 +152,11 @@ export function deleteProject(project: IProject) .find((securityToken) => securityToken.name === project.securityToken); if (!projectToken) { - throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found"); + dispatch(deleteProjectAction(project)); + throw new AppError(ErrorCode.SecurityTokenNotFound, interpolate(strings.errors.projectDeleteErrorSecurityTokenNotFound.message, {project})); } const decryptedProject = await projectService.load(project, projectToken); - await projectService.delete(decryptedProject); dispatch(deleteProjectAction(decryptedProject)); }; @@ -181,7 +181,7 @@ export function addAssetToProject(project: IProject, fileName: string, buffer: B const assetName = project.folderPath ? `${project.folderPath}/${fileName}` : fileName; const asset = assets.find(a => a.name === assetName); - await assetService.uploadAssetPredictResult(asset, analyzeResult); + await assetService.syncAssetPredictResult(asset, analyzeResult); dispatch(addAssetToProjectAction(asset)); return asset; }; diff --git a/src/redux/reducers/currentProjectReducer.ts b/src/redux/reducers/currentProjectReducer.ts index 56ed1962..3bfce022 100644 --- a/src/redux/reducers/currentProjectReducer.ts +++ b/src/redux/reducers/currentProjectReducer.ts @@ -38,9 +38,9 @@ export const reducer = (state: IProject = null, action: AnyAction): IProject => }; case ActionTypes.DELETE_PROJECT_ASSET_SUCCESS: case ActionTypes.LOAD_PROJECT_ASSETS_SUCCESS: - const assets = {}; + let assets = {}; action.payload.forEach((asset) => { - assets[asset.id] = asset; + assets = { ...assets, [asset.id]: { ...asset } }; }); return { diff --git a/src/redux/reducers/recentProjectsReducer.ts b/src/redux/reducers/recentProjectsReducer.ts index 97372c4b..3e76d61d 100644 --- a/src/redux/reducers/recentProjectsReducer.ts +++ b/src/redux/reducers/recentProjectsReducer.ts @@ -22,6 +22,7 @@ export const reducer = (state: IProject[] = [], action: AnyAction): IProject[] = let newState: IProject[] = null; switch (action.type) { + case ActionTypes.LOAD_PROJECT_SUCCESS: case ActionTypes.SAVE_PROJECT_SUCCESS: return [ { ...action.payload }, @@ -38,6 +39,9 @@ export const reducer = (state: IProject[] = [], action: AnyAction): IProject[] = return updatedProject; }); return newState; + case ActionTypes.UPDATE_TAG_LABEL_COUNTS_SUCCESS: + return [{ ...action.payload }, + ...state.filter(project => project.id !== action.payload.id)]; default: return state; } diff --git a/src/registerIcons.ts b/src/registerIcons.ts index a142ab8b..3ef3a625 100644 --- a/src/registerIcons.ts +++ b/src/registerIcons.ts @@ -80,6 +80,6 @@ export function registerIcons() { RectangleShape: "\uF1A9", Rotate90CounterClockwise: "\uF80E", Rotate90Clockwise: "\uF80D", - }, + AzureAPIManagement: "\uF37F", }, }); } diff --git a/src/services/assetService.ts b/src/services/assetService.ts index a8547ce9..4b31a716 100644 --- a/src/services/assetService.ts +++ b/src/services/assetService.ts @@ -5,7 +5,7 @@ import _ from "lodash"; import Guard from "../common/guard"; import { IAsset, AssetType, IProject, IAssetMetadata, AssetState, - ILabelData, ILabel, + ILabelData, ILabel, AssetLabelingState } from "../models/applicationState"; import { AssetProviderFactory, IAssetProvider } from "../providers/storage/assetProviderFactory"; import { StorageProviderFactory, IStorageProvider } from "../providers/storage/storageProviderFactory"; @@ -16,6 +16,9 @@ import { strings, interpolate } from "../common/strings"; import { sha256Hash } from "../common/crypto"; import { toast } from "react-toastify"; import allSettled from "promise.allsettled" +import mime from 'mime'; +import FileType from 'file-type'; +import BrowserFileType from 'file-type/browser'; const supportedImageFormats = { jpg: null, jpeg: null, null: null, png: null, bmp: null, tif: null, tiff: null, pdf: null, @@ -65,9 +68,10 @@ export class AssetService { private getOcrFromAnalyzeResult(analyzeResult: any) { return _.get(analyzeResult, "analyzeResult.readResults", []); } - async uploadAssetPredictResult(asset: IAsset, readResults: any): Promise { + getAssetPredictMetadata(asset: IAsset, predictResults: any) { + asset = JSON.parse(JSON.stringify(asset)); const getBoundingBox = (pageIndex, arr: number[]) => { - const ocrForCurrentPage: any = this.getOcrFromAnalyzeResult(readResults)[pageIndex - 1]; + const ocrForCurrentPage: any = this.getOcrFromAnalyzeResult(predictResults)[pageIndex - 1]; const ocrExtent = [0, 0, ocrForCurrentPage.width, ocrForCurrentPage.height]; const ocrWidth = ocrExtent[2] - ocrExtent[0]; const ocrHeight = ocrExtent[3] - ocrExtent[1]; @@ -83,7 +87,7 @@ export class AssetService { const getLabelValues = (field: any) => { return field.elements.map((path: string) => { const pathArr = path.split('/').slice(1); - const word = pathArr.reduce((obj: any, key: string) => obj[key], { ...readResults.analyzeResult }); + const word = pathArr.reduce((obj: any, key: string) => obj[key], { ...predictResults.analyzeResult }); return { page: field.page, text: word.text || word.state, @@ -92,59 +96,76 @@ export class AssetService { }; }); }; - const labels = []; - readResults.analyzeResult.documentResults - .map(result => Object.keys(result.fields) - .filter(key => result.fields[key]) - .map(key => ( - { - label: key, - key: null, - value: getLabelValues(result.fields[key]) - }))).forEach(items => { - labels.push(...items); - }); + const labels = + predictResults.analyzeResult.documentResults + .map(result => Object.keys(result.fields) + .filter(key => result.fields[key]) + .map(key => ( + { + label: key, + key: null, + confidence: result.fields[key].confidence, + value: getLabelValues(result.fields[key]) + }))).flat(2); if (labels.length > 0) { const fileName = decodeURIComponent(asset.name).split('/').pop(); const labelData: ILabelData = { document: fileName, + labelingState: AssetLabelingState.AutoLabeled, labels }; - const metadata = { - ...await this.getAssetMetadata(asset), - labelData + const metadata: IAssetMetadata = { + asset: { ...asset, labelingState: AssetLabelingState.AutoLabeled }, + regions: [], + version: appInfo.version, + labelData, }; metadata.asset.state = AssetState.Tagged; - - const ocrData = JSON.parse(JSON.stringify(readResults)); - delete ocrData.analyzeResult.documentResults; - if (ocrData.analyzeResult.errors) { - delete ocrData.analyzeResult.errors; - } - const ocrFileName = `${asset.name}${constants.ocrFileExtension}`; - await Promise.all([ - this.save(metadata), - this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)) - ]); + return metadata; + } + else { + return null; + } + } + async uploadPredictResultAsOrcResult(asset: IAsset, predictResults: any): Promise { + const ocrData = JSON.parse(JSON.stringify(predictResults)); + delete ocrData.analyzeResult.documentResults; + if (ocrData.analyzeResult.errors) { + delete ocrData.analyzeResult.errors; + } + const ocrFileName = `${asset.name}${constants.ocrFileExtension}`; + await this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)); + } + + async syncAssetPredictResult(asset: IAsset, predictResults: any): Promise { + const assetMeatadata = this.getAssetPredictMetadata(asset, predictResults); + const ocrData = JSON.parse(JSON.stringify(predictResults)); + delete ocrData.analyzeResult.documentResults; + if (ocrData.analyzeResult.errors) { + delete ocrData.analyzeResult.errors; + } + const ocrFileName = `${asset.name}${constants.ocrFileExtension}`; + if (assetMeatadata) { + + + await Promise.all([ + this.save(assetMeatadata), + this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)) + ]); + return assetMeatadata; } else { - const ocrData = { ...readResults }; - delete ocrData.analyzeResult.documentResults; - if (ocrData.analyzeResult.errors) { - delete ocrData.analyzeResult.errors; - } const labelFileName = decodeURIComponent(`${asset.name}${constants.labelFileExtension}`); - const ocrFileName = decodeURIComponent(`${asset.name}${constants.ocrFileExtension}`); try { await Promise.all([ this.storageProvider.deleteFile(labelFileName, true, true), this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)) ]); + } catch (err) { + // The label file may not exist - that's OK. } - catch{ - return; - } + return null; } } /** @@ -175,26 +196,42 @@ export class AssetService { // eslint-disable-next-line const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/); let assetFormat = extensionParts[0].toLowerCase(); - + let assetMimeType = mime.getType(assetFormat); if (supportedImageFormats.hasOwnProperty(assetFormat)) { - let types; + let checkFileType; let corruptFileName; if (nodejsMode) { - const FileType = require('file-type'); - const fileType = await FileType.fromFile(normalizedPath); - types = [fileType.ext]; + try { + checkFileType = await FileType.fromFile(normalizedPath); + } catch { + // do nothing + } corruptFileName = fileName.split(/[\\\/]/).pop().replace(/%20/g, " "); } else { - types = await this.getMimeType(filePath); + try { + const getFetchSteam = (): Promise => this.pollForFetchAPI(() => fetch(filePath), 1000, 200); + const response = await getFetchSteam(); + checkFileType = await BrowserFileType.fromStream(response.body); + } catch { + // do nothing + } corruptFileName = fileName.split("%2F").pop().replace(/%20/g, " "); } - if (!types) { + let fileType; + let mimeType; + if (checkFileType) { + fileType = checkFileType.ext; + mimeType = checkFileType.mime; + } + + if (!fileType) { console.error(interpolate(strings.editorPage.assetWarning.incorrectFileExtension.failedToFetch, { fileName: corruptFileName.toLocaleUpperCase() })); } - // If file was renamed/spoofed - fix file extension to true MIME type and show message - else if (!types.includes(assetFormat)) { - assetFormat = types[0]; + // If file was renamed/spoofed - fix file extension to true MIME if it's type is in supported file types and show message + else if (fileType !== assetFormat) { + assetFormat = fileType; + assetMimeType = mimeType; console.error(`${strings.editorPage.assetWarning.incorrectFileExtension.attention} ${corruptFileName.toLocaleUpperCase()} ${strings.editorPage.assetWarning.incorrectFileExtension.text} ${corruptFileName.toLocaleUpperCase()}`); } } @@ -209,6 +246,7 @@ export class AssetService { name: fileName, path: filePath, size: null, + mimeType: assetMimeType, }; } @@ -233,36 +271,6 @@ export class AssetService { } } - // If extension of a file was spoofed, we fetch only first 4 or needed amount of bytes of the file and read MIME type - public static async getMimeType(uri: string): Promise { - const getFirst4bytes = (): Promise => this.pollForFetchAPI(() => fetch(uri, { headers: { range: `bytes=0-${mimeBytesNeeded}` } }), 1000, 200); - let first4bytes: Response; - try { - first4bytes = await getFirst4bytes() - } catch { - return new Promise((resolve) => { - resolve(null); - }); - } - const arrayBuffer: ArrayBuffer = await first4bytes.arrayBuffer(); - const blob: Blob = new Blob([new Uint8Array(arrayBuffer).buffer]); - const isMime = (bytes: Uint8Array, mime: IMime): boolean => { - return mime.pattern.every((p, i) => !p || bytes[i] === p); - }; - const fileReader: FileReader = new FileReader(); - - return new Promise((resolve, reject) => { - fileReader.onloadend = (e) => { - if (!e || !fileReader.result) { - return []; - } - const bytes: Uint8Array = new Uint8Array(fileReader.result as ArrayBuffer); - const type: string[] = imageMimes.filter((mime) => isMime(bytes, mime))?.[0]?.types; - resolve(type || []); - }; - fileReader.readAsArrayBuffer(blob); - }); - } private assetProviderInstance: IAssetProvider; private storageProviderInstance: IStorageProvider; @@ -369,7 +377,7 @@ export class AssetService { // The file may not exist - that's OK. } } - return metadata; + return JSON.parse(JSON.stringify(metadata)); } /** @@ -383,6 +391,11 @@ export class AssetService { try { const json = await this.storageProvider.readText(labelFileName, true); const labelData = JSON.parse(json) as ILabelData; + + if (labelData) { + labelData.labelingState = labelData.labelingState || AssetLabelingState.ManuallyLabeled; + asset.labelingState = labelData.labelingState; + } // if (!labelData.document || !labelData.labels && !labelData.tableLabels) { // const reason = interpolate(strings.errors.missingRequiredFieldInLabelFile.message, { labelFileName }); // toast.error(reason, { autoClose: false }); @@ -427,7 +440,7 @@ export class AssetService { // } // toast.dismiss(); return { - asset: { ...asset }, + asset: { ...asset, labelingState: labelData.labelingState }, regions: [], version: appInfo.version, labelData, diff --git a/src/services/ocrService.ts b/src/services/ocrService.ts index 4243b262..affebef2 100644 --- a/src/services/ocrService.ts +++ b/src/services/ocrService.ts @@ -33,6 +33,7 @@ export class OCRService { public async getRecognizedText( filePath: string, fileName: string, + mimeType: string, onStatusChanged?: (ocrStatus: OcrStatus) => void, rewrite?: boolean ): Promise { @@ -47,11 +48,11 @@ export class OCRService { notifyStatusChanged(OcrStatus.loadingFromAzureBlob); ocrJson = await this.readOcrFile(ocrFileName); if (!this.isValidOcrFormat(ocrJson) || rewrite) { - ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName); + ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName, mimeType); } } catch (e) { notifyStatusChanged(OcrStatus.runningOCR); - ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName); + ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName, mimeType); } finally { notifyStatusChanged(OcrStatus.done); } @@ -81,7 +82,7 @@ export class OCRService { } } - private fetchOcrUriResult = async (filePath: string, fileName: string, ocrFileName: string) => { + private fetchOcrUriResult = async (filePath: string, fileName: string, ocrFileName: string, mimeType: string) => { try { let body; let headers; @@ -93,15 +94,13 @@ export class OCRService { ] ); body = bodyAndType[0]; - const fileType = bodyAndType[1].mime; - headers = { "Content-Type": fileType, "cache-control": "no-cache" }; - } - else { + headers = { "Content-Type": mimeType, "cache-control": "no-cache" }; + } else { body = { url: filePath }; headers = { "Content-Type": "application/json" }; } const response = await ServiceHelper.postWithAutoRetry( - this.project.apiUriBase + `/formrecognizer/${constants.apiVersion}/layout/analyze`, + this.project.apiUriBase + `/formrecognizer/${ (this.project.apiVersion || constants.apiVersion) }/layout/analyze`, body, { headers }, this.project.apiKey as string, diff --git a/src/services/predictService.ts b/src/services/predictService.ts index 13b400da..3bb4f1f6 100644 --- a/src/services/predictService.ts +++ b/src/services/predictService.ts @@ -24,7 +24,7 @@ export class PredictService { } const endpointURL = url.resolve( this.project.apiUriBase, - `${constants.apiModelsPath}/${modelID}/analyze?includeTextDetails=true`, + `${interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) })}/${modelID}/analyze?includeTextDetails=true`, ); const headers = { "Content-Type": "application/json", "cache-control": "no-cache" }; @@ -60,11 +60,10 @@ export class PredictService { if (response.data.status.toLowerCase() === constants.statusCodeSucceeded) { resolve(response.data); // prediction response from API - console.log("raw data", JSON.parse(response.request.response)); } else if (response.data.status.toLowerCase() === constants.statusCodeFailed) { reject(_.get( response, - "data.analyzeResult.errors[0].errorMessage", + "data.analyzeResult.errors[0]", "Generic error during prediction")); } else if (Number(new Date()) < endTime) { // If the request isn't succeeded and the timeout hasn't elapsed, go again diff --git a/src/services/projectService.ts b/src/services/projectService.ts index aee339d4..099eb1a7 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -152,14 +152,7 @@ export default class ProjectService implements IProjectService { public async isProjectNameAlreadyUsed(project: IProject): Promise { const storageProvider = StorageProviderFactory.createFromConnection(project.sourceConnection); - const fileList = await storageProvider.listFiles("", constants.projectFileExtension/*ext*/); - for (const fileName of fileList) { - if (fileName === `${project.name}${constants.projectFileExtension}`) { - return true; - } - } - - return false; + return await storageProvider.isFileExists(`${project.name}${constants.projectFileExtension}`); } public async isValidProjectConnection(project: IProject): Promise { diff --git a/yarn.lock b/yarn.lock index 586b0c07..88979309 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8438,7 +8438,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.4, mime@^2.4.5: +mime@^2.4.4, mime@^2.4.5, mime@^2.4.6: version "2.4.6" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== @@ -8708,6 +8708,11 @@ node-forge@0.9.0: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579" integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== +node-forge@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3" + integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA== + node-gyp@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"