* merge master into RTL branch

* fix an API icon
This commit is contained in:
alex-krasn 2020-10-16 12:18:36 -07:00 коммит произвёл GitHub
Родитель b6557e864c
Коммит 34627adaa0
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
59 изменённых файлов: 1465 добавлений и 750 удалений

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

@ -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=
PUBLIC_URL=
BROWSER=none

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

@ -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))

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

@ -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

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

@ -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",

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

@ -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)

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

@ -95,7 +95,7 @@ export default class App extends React.Component<IAppProps> {
<Sidebar project={this.props.currentProject} />
<MainContentRouter />
</div>
<StatusBar>
<StatusBar project={this.props.currentProject} >
<StatusBarMetrics project={this.props.currentProject} />
</StatusBar>
<ToastContainer className="frtt-toast-container" role="alert" />

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -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`;

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

@ -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.",

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

@ -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."

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

@ -319,6 +319,7 @@ export default class MockFactory {
createContainer: jest.fn(),
deleteContainer: jest.fn(),
getAssets: jest.fn(),
isFileExists: jest.fn(),
};
}

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

@ -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: {

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

@ -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"
}
]
}

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

@ -123,6 +123,10 @@ export default class LocalFileSystem implements IStorageProvider {
return this.listItems(path.normalize(folderPath), (stats) => !stats.isDirectory());
}
public isFileExists(filePath: string): Promise<boolean> {
return Promise.resolve(fs.existsSync(path.normalize(filePath)));
}
public listContainers(folderPath: string): Promise<string[]> {
return this.listItems(path.normalize(folderPath), (stats) => stats.isDirectory());
}

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

@ -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 {

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

@ -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<boolean> {
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);
}
}

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

@ -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<boolean> {
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

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

@ -48,6 +48,9 @@ class TestStorageProvider implements IStorageProvider {
public listFiles(folderPath?: string): Promise<string[]> {
throw new Error("Method not implemented.");
}
isFileExists(filepath: string): Promise<boolean> {
throw new Error("Method not implemented.");
}
public listContainers(folderPath?: string): Promise<string[]> {
throw new Error("Method not implemented.");
}

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

@ -35,6 +35,7 @@ export interface IStorageProvider extends IAssetProvider {
isValidProjectConnection(filepath?): Promise<boolean>;
listFiles(folderPath?: string, ext?: string): Promise<string[]>;
isFileExists(filepath: string): Promise<boolean>;
listContainers(folderPath?: string): Promise<string[]>;
createContainer(folderPath: string): Promise<void>;

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

@ -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<IAPIVersionPickerProps> {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
}
public render() {
return (
<select id={this.props.id}
className="form-control"
value={this.props.value}
onChange={this.onChange}>
<option value={APIVersionPatches.patch1}>{APIVersionPatches.patch1}</option>
<option value={APIVersionPatches.patch2}>{APIVersionPatches.patch2}</option>
<option value={APIVersionPatches.patch3}>{APIVersionPatches.patch3 + " (testing)"}</option>
</select>
);
}
private onChange(e: SyntheticEvent) {
const inputElement = e.target as HTMLSelectElement;
this.props.onChange(inputElement.value ? inputElement.value : "2.1-preview.3");
}
}

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

@ -110,7 +110,15 @@ export class AssetPreview extends React.Component<IAssetPreviewProps, IAssetPrev
<div className="asset-loading">
<div className="asset-loading-ocr-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.small} label="Running OCR..." ariaLive="off" labelPosition="right"/>
<Spinner size={SpinnerSize.small} label="Running OCR..." ariaLive="off" labelPosition="right" />
</div>
</div>
}
{this.props.asset.isRunningAutoLabeling &&
<div className="asset-loading">
<div className="asset-loading-ocr-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.small} label="Auto Labeling..." ariaLive="off" labelPosition="right" />
</div>
</div>
}

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

@ -42,6 +42,9 @@ ul.condensed-list-items {
&.active, &:hover {
background-color: $lighter-2;
}
&.current{
background-color: $lighter-3;
}
}
}
}

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

@ -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<ICondensedListProps> {
export default class CondensedList extends React.Component<ICondensedListProps, ICondensedListState> {
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<ICondensedListProps>
<ul className="condensed-list-items">
{items.map((item) => <Component key={item.id}
item={item}
currentId={this.state.currentId}
onClick={(e) => this.onItemClick(e, item)}
onDelete={(e) => this.onItemDelete(e, item)} />)}
</ul>
@ -79,6 +85,7 @@ export default class CondensedList extends React.Component<ICondensedListProps>
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<ICondensedListProps>
* 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 (
<li>
{/* eslint-disable-next-line */}
<a className="condensed-list-item" onClick={onClick}>
<a className={["condensed-list-item", currentId === item.id? "current":""].join(" ")} onClick={onClick}>
<span className="px-2">{item.name}</span>
</a>
</li>

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

@ -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<IImageMapProps> {
private modify: Modify;
private snap: Snap;
private drawnFeatures: Collection = new Collection([], {unique: true});
private drawnFeatures: Collection = new Collection([], { unique: true });
public modifyStartFeatureCoordinates: any = {};
private imageExtent: number[];
@ -198,7 +199,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
onMouseEnter={this.handlePointerEnterImageMap}
className="map-wrapper"
>
<div style={{cursor: this.getCursor()}} id="map" className="map" ref={(el) => this.mapElement = el}/>
<div style={{ cursor: this.getCursor() }} id="map" className="map" ref={(el) => this.mapElement = el} />
</div>
);
}
@ -361,7 +362,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
*/
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<IImageMapProps> {
this.drawRegionVectorLayer?.getSource().clear();
this.drawnLabelVectorLayer?.getSource().clear();
this.drawnFeatures = new Collection([], {unique: true});
this.drawnFeatures = new Collection([], { unique: true });
this.drawRegionVectorLayer.getSource().on("addfeature", (evt) => {
this.pushToDrawnFeatures(evt.feature, this.drawnFeatures);
@ -516,7 +517,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
*/
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<IImageMapProps> {
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<IImageMapProps> {
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<IImageMapProps> {
);
}
}
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<IImageMapProps> {
this.textVectorLayerFilter);
if (isPointerOnTextFeature) {
return {
layerfilter : this.textVectorLayerFilter,
layerfilter: this.textVectorLayerFilter,
category: FeatureCategory.Text,
};
}
@ -719,9 +735,9 @@ export class ImageMap extends React.Component<IImageMapProps> {
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<IImageMapProps> {
}
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<IImageMapProps> {
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<IImageMapProps> {
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<IImageMapProps> {
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<IImageMapProps> {
private initializeMap = (projection, layers) => {
this.map = new Map({
controls: [] ,
controls: [],
interactions: defaultInteractions({
shiftDragZoom: false,
doubleClickZoom: false,

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

@ -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%;
}

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

@ -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<ITagInputProps, ITagInputState> {
public render() {
const dark: ICustomizations = {
settings: {
theme: getDarkTheme(),
theme: getDarkTheme(),
},
scopedSettings: {},
};
@ -207,6 +207,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
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<ITagInputProps, ITagInputState> {
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 })}
/>
<FontIcon iconName="Search" />
</div>
@ -245,6 +247,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}
</Customizer>
{this.getColorPickerPortal()}
</div>
{
this.state.addTags &&
@ -394,7 +397,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
const { selectedTag } = this.state;
const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker;
return (
<AlignPortal align={{points: [ "tr", "tl" ]}} target={() => this.headerRef.current}>
<AlignPortal align={{ points: ["tr", "tl"] }} target={() => this.headerRef.current}>
<div className="tag-input-colorpicker-container">
{
showColorPicker &&
@ -429,6 +432,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
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<ITagInputProps, ITagInputState> {
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<ITagInputProps, ITagInputState> {
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<ITagInputProps, ITagInputState> {
});
}
}
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<ITagInputProps, ITagInputState> {
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<ITagInputProps, ITagInputState> {
}
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<ITagInputProps, ITagInputState> {
},
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<ITagInputProps, ITagInputState> {
},
text: strings.tags.toolbar.moveDown,
onClick: this.onMenuItemClick,
disabled: this.state.searchQuery.length > 0,
},
{
key: TagMenuItem.Delete,

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

@ -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<ITagInputItemProps, IT
const style: any = {
background: this.props.tag.color,
};
const confidence = _.get(this.props, "labels[0].confidence", null);
return (
<div className={"tag-item-block"}>
{confidence &&
<div className="tag-item-confidence">
{confidence}
</div>
}
<div
className={"tag-color"}
style={style}
@ -98,6 +105,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
style={style}>
<div
className={"tag-content pr-2"}
onDoubleClick={this.onNameDoubleClick}
onClick={this.onNameClick}>
{this.getTagContent()}
</div>
@ -141,6 +149,14 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
this.props.onClick(this.props.tag, { ctrlKey, altKey });
}
private onNameDoubleClick = (e:MouseEvent) => {
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<ITagInputItemProps, IT
<div className="tag-name-body">
{
this.state.isRenaming
?
<input
ref={this.onInputRef}
className={`tag-name-editor ${this.getContentClassName()}`}
type="text"
defaultValue={this.props.tag.name}
onKeyDown={(e) => this.onInputKeyDown(e)}
onBlur={this.onInputBlur}
autoFocus={true}
/>
:
<span title={this.props.tag.name} className={this.getContentClassName()}>
{this.props.tag.name}
</span>
?
<input
ref={this.onInputRef}
className={`tag-name-editor ${this.getContentClassName()}`}
type="text"
defaultValue={this.props.tag.name}
onKeyDown={(e) => this.onInputKeyDown(e)}
onBlur={this.onInputBlur}
autoFocus={true}
/>
:
<span title={this.props.tag.name} className={this.getContentClassName()}>
{this.props.tag.name}
</span>
}
</div>
<div className={"tag-icons-container"}>
@ -192,7 +208,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
title={strings.tags.toolbar.contextualMenu}
ariaLabel={strings.tags.toolbar.contextualMenu}
className="tag-input-toolbar-iconbutton ml-2"
iconProps={{iconName: "ChevronDown"}}
iconProps={{ iconName: "ChevronDown" }}
onClick={this.onDropdownClick} />
</div>
</div>
@ -276,7 +292,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
}
private isTypeOrFormatSpecified = () => {
const {tag} = this.props;
const { tag } = this.props;
return (tag.type && tag.type !== FieldType.String) ||
(tag.format && tag.format !== FieldFormat.NotSpecified);
}

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

@ -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<ITagInputToolbarPro
{
displayName: strings.tags.toolbar.rename,
icon: "Rename",
category: Categories.Modifier,
category: Categories.RenameModifier,
handler: this.handleRename,
},
{
displayName: strings.tags.toolbar.moveUp,
icon: "Up",
category: Categories.Modifier,
category: Categories.MoveModifier,
handler: this.handleMoveUp,
},
{
displayName: strings.tags.toolbar.moveDown,
icon: "Down",
category: Categories.Modifier,
category: Categories.MoveModifier,
handler: this.handleMoveDown,
},
{
displayName: strings.tags.toolbar.delete,
icon: "Delete",
category: Categories.Modifier,
category: Categories.MoveModifier,
handler: this.handleDelete,
},
];
}
private renderItems = () => {
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<ITagInputToolbarPro
);
} else if (itemConfig.category === Categories.Separator) {
return (<div className="tag-input-toolbar-separator" key={itemConfig.displayName}></div>);
} else if (itemConfig.category === Categories.Modifier) {
} else if (itemConfig.category === Categories.RenameModifier) {
return (
<IconButton
key={itemConfig.displayName}
disabled={modifierDisabled}
disabled={renameModifierDisabled}
title={itemConfig.displayName}
ariaLabel={itemConfig.displayName}
className={modifierClassName}
className={renameModifierClassName}
iconProps={{iconName: itemConfig.icon}}
onClick={(e) => this.onToolbarItemClick(e, itemConfig)} />
);
} else if (itemConfig.category === Categories.MoveModifier) {
return (
<IconButton
key={itemConfig.displayName}
disabled={moveModifierDisabled}
title={itemConfig.displayName}
ariaLabel={itemConfig.displayName}
className={moveModifierClassName}
iconProps={{iconName: itemConfig.icon}}
onClick={(e) => this.onToolbarItemClick(e, itemConfig)} />
);

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

@ -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 {

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

@ -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<Canvas> {
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<void>;
onAssetDeleted?: () => void;
handleLabelTable?: () => void;
}
@ -181,7 +184,9 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
public componentDidUpdate = async (prevProps: Readonly<ICanvasProps>, prevState: Readonly<ICanvasState>) => {
// 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<ICanvasProps, ICanvasState>
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}
/>
<ImageMap
@ -267,6 +274,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
* @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<ICanvasProps, ICanvasState>
(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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
}
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<ICanvasProps, ICanvasState>
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<ICanvasProps, ICanvasState>
}
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<ICanvasProps, ICanvasState>
};
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<ICanvasProps, ICanvasState>
} 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<ICanvasProps, ICanvasState>
});
}
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<ICanvasProps, ICanvasState>
}
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<ICanvasProps, ICanvasState>
// 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<ICanvasProps, ICanvasState>
}
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<ICanvasProps, ICanvasState>
});
this.imageMap.modifyStartFeatureCoordinates = {};
}
async focusOnLabel(label: ILabel) {
const { page } = label.value[ 0 ];
if (this.state.currentPage !== page) {
await this.goToPage(page);
}
}
}

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

@ -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<ICanvasCommandBarProps> =
},
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<ICanvasCommandBarProps> =
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<ICanvasCommandBarProps> =
],
},
},
// {
// 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<ICanvasCommandBarProps> =
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<ICanvasCommandBarProps> =
key: "deleteAsset",
text: strings.editorPage.asset.delete.title,
iconProps: { iconName: "Delete" },
onClick: () => props.handleAssetDeleted(),
onClick: () => { if (props.handleAssetDeleted) props.handleAssetDeleted(); },
}
],
},

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

@ -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 {

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

@ -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<IEditorPageProps, IEdito
onAssetMetadataChanged={this.onAssetMetadataChanged}
onCanvasRendered={this.onCanvasRendered}
onSelectedRegionsChanged={this.onSelectedRegionsChanged}
onRegionDoubleClick={this.onRegionDoubleClick}
onRunningOCRStatusChanged={this.onCanvasRunningOCRStatusChanged}
onRunningAutoLabelingStatusChanged={this.onCanvasRunningAutoLabelingStatusChanged}
onTagChanged={this.onTagChanged}
@ -312,6 +314,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
setTableToView={this.setTableToView}
closeTableView={this.closeTableView}
runOcrForAllDocs={this.loadOcrForNotVisited}
runAutoLabelingOnNextBatch={this.runAutoLabelingOnNextBatch}
appSettings={this.props.appSettings}
handleLabelTable={this.handleLabelTable}
>
@ -349,7 +352,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
splitPaneWidth={this.state.rightSplitPaneWidth}
reconfigureTableConfirm={this.reconfigureTableConfirm}
addRowToDynamicTable={this.addRowToDynamicTable}
/>
onTagDoubleClick={this.onLabelDoubleClicked}
/>
<Confirm
title={strings.editorPage.tags.rename.title}
ref={this.renameTagConfirm}
@ -708,32 +712,32 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
* This can either be a parent or child asset
*/
private onAssetMetadataChanged = async (assetMetadata: IAssetMetadata): Promise<void> => {
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<IEditorPageProps, IEdito
// This forces the root assets that are displayed in the sidebar to
// accurately show their correct state (not-visited, visited or tagged)
const assets = [...this.state.assets];
// const asset = { ...assetMetadata.asset };
const assetIndex = assets.findIndex((a) => a.id === asset.id);
if (assetIndex > -1) {
assets[assetIndex] = {
@ -783,6 +788,12 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
this.setState({ selectedRegions });
}
private onRegionDoubleClick = (region: IRegion) => {
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<IEditorPageProps, IEdito
if (this.state.isCanvasRunningAutoLabeling) {
return;
}
if (this.state.isRunningAutoLabelings) {
return;
}
const assetMetadata = await this.props.actions.loadAssetMetadata(this.props.project, asset);
@ -901,11 +915,11 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
const asset = this.state.assets.find((asset) => 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<IEditorPageProps, IEdito
}
}
}
private runAutoLabelingOnNextBatch = async () => {
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<IEditorPageProps, IEdito
}
}),
}), () => {
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<IEditorPageProps, IEdito
const updatedAssets = [...this.state.assets];
let needUpdate = false;
updatedAssets.forEach((asset) => {
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 });
}

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

@ -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<IEditorSideBarProps,
}
private renderBadges = (asset: IAsset): JSX.Element => {
const getBadgeTaggedClass = (state: AssetLabelingState): string => {
return state ? `badge-tagged-${AssetLabelingState[state]}` : "";
};
switch (asset.state) {
case AssetState.Tagged:
return (
<span title={strings.editorPage.tagged}
className="badge badge-tagged">
<span title={_.startCase(AssetLabelingState[asset.labelingState])}
className={["badge", "badge-tagged", getBadgeTaggedClass(asset.labelingState)].join(" ")}>
<FontIcon iconName="Tag" />
</span>
);

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

@ -217,7 +217,16 @@ export default class HomePage extends React.Component<IHomePageProps, IHomePageS
}
private deleteProject = async (project: IProject) => {
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) => {

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

@ -369,13 +369,13 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
if (model.attributes.isComposed) {
const inclModels = model.composedTrainResults ?
model.composedTrainResults
: (await this.getModelByURl(constants.apiModelsPath + "/" + model.modelId)).composedTrainResults;
: (await this.getModelByURl(interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }) + "/" + model.modelId)).composedTrainResults;
for (const i of Object.keys(inclModels)) {
let _model: IModel;
let modelInfo: IComposedModelInfo;
try {
_model = await this.getModelByURl(constants.apiModelsPath + "/" + inclModels[i].modelId);
_model = await this.getModelByURl(interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }) + "/" + inclModels[i].modelId);
modelInfo = {
id: _model.modelId,
name: _model.modelName,
@ -458,7 +458,7 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
private getRecentModels = async ():Promise<IModel[]> => {
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<IModelComposePageP
private async getResponse(nextLink?: string) {
const baseURL = nextLink === undefined ? url.resolve(
this.props.project.apiUriBase,
constants.apiModelsPath,
interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }),
) : url.resolve(
this.props.project.apiUriBase,
nextLink,
@ -734,7 +734,7 @@ export default class ModelComposePage extends React.Component<IModelComposePageP
modelName: name,
};
const link = constants.apiModelsPath + "/compose";
const link = interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }) + "/compose";
const composeRes = await this.post(link, payload);
const composedModel = await this.waitUntilModelIsReady(composeRes["headers"]["location"]);

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

@ -5,20 +5,16 @@ import React from 'react'
import './predictModelInfo.scss';
export default function PredictModelInfo({ modelInfo }) {
const { docType, modelId, docTypeConfidence } = modelInfo;
const { modelId, docTypeConfidence } = modelInfo;
return (
<div className="model-info-container">
<div className="model-info-item">
<span className="title" >docType:</span>
<span className="value" >{docType}</span>
</div>
<div className="model-info-item">
<span className="title" >modelId:</span>
<span className="value" >{modelId}</span>
</div>
<div className="model-info-item">
<span className="title" >docTypeConfidence:</span>
<span className="value" >{docTypeConfidence}</span>
<span className="value" >{(docTypeConfidence * 100).toFixed(2) + "%"}</span>
</div>
</div>
)

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

@ -649,6 +649,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
handleRotateImage={this.handleRotateCanvas}
project={this.props.project}
parentPage={"predict"}
layers={{}}
/>
<ImageMap
parentPage={ImageMapParent.Predict}
@ -767,7 +768,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
case "<model_id>":
return modelID;
case "<API_version>":
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<IPredictPageProps, IPre
}
const endpointURL = url.resolve(
this.props.project.apiUriBase,
`${constants.apiModelsPath}/${modelID}/analyze?includeTextDetails=true`,
`${interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) })}/${modelID}/analyze?includeTextDetails=true`,
);
let headers;
let body;
@ -1037,7 +1038,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
} 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
@ -1075,7 +1076,6 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
}
private onAddAssetToProjectClick = async () => {
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<IPredictPageProps, IPre
const modelID = this.props.project.predictModelId;
const endpointURL = url.resolve(
this.props.project.apiUriBase,
`${constants.apiModelsPath}/${modelID}`,
`${interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) })}/${modelID}`,
);
let response;
try {

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

@ -127,7 +127,7 @@ export default class PredictResult extends React.Component<IPredictResultProps,
}
</div>
<div className={"predictiontag-confidence"}>
<span>{item.confidence}</span>
<span>{(item.confidence * 100).toFixed(2)+"%" }</span>
</div>
</div>
);

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

@ -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"

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

@ -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"

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

@ -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<IProjectFormProps, IProjectFormState> {
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<IProjectFormProps, IPro
onChange: props.onChange,
};
}),
apiVersion: CustomField<IAPIVersionPickerProps>(APIVersionPicker, (props) => ({
id: props.idSchema.$id,
value: props.formData,
onChange: props.onChange,
})),
targetConnection: CustomField<IConnectionProviderPickerProps>(ConnectionPickerWithRouter, (props) => {
const targetConnections = this.props.connections
.filter((connection) => StorageProviderFactory.isRegistered(connection.providerType));
@ -209,6 +216,7 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
sourceConnection: args.formData.sourceConnection,
folderPath: this.normalizeFolderPath(args.formData.folderPath),
apiUriBase: args.formData.apiUriBase.trim(),
apiVersion: args.formData.apiVersion,
};
this.props.onSubmit(project);
}

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

@ -16,5 +16,8 @@
},
"apiKey": {
"ui:widget": "protectedInput"
},
"apiVersion": {
"ui:widget": "apiVersion"
}
}

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

@ -41,8 +41,8 @@
font-size: 90%;
@media (max-width: 1920px) {
display: flex;
align-items: center;
display: flex;
align-items: center;
}
.hint-content {
@ -63,7 +63,7 @@
}
.rv-hint {
white-space: nowrap;
white-space: nowrap;
}
}
@ -80,3 +80,18 @@
display: flex;
align-items: center;
}
.project-saving {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.8);
text-align: center;
display: flex;
.project-saving-spinner {
margin: auto;
font-size: 24px;
}
}

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

@ -6,7 +6,7 @@ import { Redirect } from "react-router";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { RouteComponentProps } from "react-router-dom";
import { FontIcon } from "@fluentui/react";
import { FontIcon, Label, Spinner, SpinnerSize } from "@fluentui/react";
import ProjectForm from "./projectForm";
import { constants } from "../../../../common/constants";
import { strings, interpolate } from "../../../../common/strings";
@ -43,6 +43,7 @@ export interface IProjectSettingsPageState {
project: IProject;
action: ProjectSettingAction;
isError: boolean;
isCommiting: boolean;
}
function mapStateToProps(state: IApplicationState) {
@ -72,6 +73,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
project: this.props.project,
action: null,
isError: false,
isCommiting: false,
};
public async componentDidMount() {
@ -107,6 +109,11 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
}
}
componentWillUnmount() {
if (this.state.project?.id) {
removeStorageItem(constants.projectFormTempKey);
}
}
// Hide ProjectMetrics for private-preview
public render() {
return (
@ -132,6 +139,14 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
{this.state.isError &&
<Redirect to="/" />
}
{this.state.isCommiting &&
<div className="project-saving">
<div className="project-saving-spinner">
<Label className="p-0" ></Label>
<Spinner size={SpinnerSize.large} label="Saving Project..." ariaLive="assertive" labelPosition="right" />
</div>
</div>
}
</div>
);
}
@ -171,34 +186,40 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
private onFormChange = (project: IProject) => {
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<IProjectSetting
* Checks whether a project is partially populated
*/
private isPartialProject = (project: IProject): boolean => {
return project && !(!!project.id) &&
return project &&
(
!!project.name
|| !!project.description

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

@ -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<ITrainPageProps, ITrainPageState> {
private appInsights: any = null;
private notAdjustedLabelsConfirm: React.RefObject<Confirm> = React.createRef();
constructor(props) {
super(props);
@ -122,7 +125,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
public render() {
const currTrainRecord = this.state.currTrainRecord;
const localFileSystemProvider: boolean = this.props.project && this.props.project.sourceConnection &&
this.props.project.sourceConnection.providerType === "localFileSystemProxy";
this.props.project.sourceConnection.providerType === "localFileSystemProxy";
const trainDisabled: boolean = localFileSystemProvider && (this.state.inputtedLabelFolderURL.length === 0 ||
this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL);
return (
<div className="train-page skipToMainContent" id="pageTrain">
@ -178,9 +183,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
>
</TextField>
{!this.state.isTraining ? (
<div className="container-items-end">
<div className="container-items-end">
<PrimaryButton
style={{"margin": "15px 0px"}}
style={{ "margin": "15px 0px" }}
id="train_trainButton"
theme={getPrimaryGreenTheme()}
autoFocus={true}
@ -193,16 +198,16 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
</PrimaryButton>
</div>
) : (
<div className="loading-container">
<Spinner
label="Training in progress..."
ariaLive="assertive"
labelPosition="right"
size={SpinnerSize.large}
className={"training-spinner"}
/>
</div>
)
<div className="loading-container">
<Spinner
label="Training in progress..."
ariaLive="assertive"
labelPosition="right"
size={SpinnerSize.large}
className={"training-spinner"}
/>
</div>
)
}
</div>
<div className={!this.state.isTraining ? "" : "greyOut"}>
@ -212,22 +217,22 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
currTrainRecord={currTrainRecord}
viewType={this.state.viewType}
updateViewTypeCallback={this.handleViewTypeClick}
/>
<PrimaryButton
ariaDescription={strings.train.downloadJson}
style={{ "margin": "2rem auto" }}
id="train-download-json_button"
theme={getPrimaryGreenTheme()}
autoFocus={true}
className="flex-center"
onClick={this.handleDownloadJSONClick}
disabled={this.state.isTraining}>
<FontIcon
iconName="Download"
style={{ fontWeight: 600 }}/>
<h6 className="d-inline text-shadow-none ml-2 mb-0">
{strings.train.downloadJson}</h6>
</PrimaryButton>
/>
<PrimaryButton
ariaDescription={strings.train.downloadJson}
style={{ "margin": "2rem auto" }}
id="train-download-json_button"
theme={getPrimaryGreenTheme()}
autoFocus={true}
className="flex-center"
onClick={this.handleDownloadJSONClick}
disabled={trainDisabled}>
<FontIcon
iconName="Download"
style={{ fontWeight: 600 }} />
<h6 className="d-inline text-shadow-none ml-2 mb-0">
{strings.train.downloadJson}</h6>
</PrimaryButton>
</>
}
</div>
@ -244,6 +249,13 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
when={this.state.isTraining}
message={"A training operation is currently in progress, are you sure you want to leave?"}
/>
<Confirm
ref={this.notAdjustedLabelsConfirm}
title={strings.train.trainConfirm.title}
message={strings.train.trainConfirm.message}
onConfirm={this.handleModelTrainConfirm}
confirmButtonTheme={getPrimaryGreenTheme()}
/>
</div>
);
}
@ -268,7 +280,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
private removeDefaultInputtedLabelFolderURL = () => {
if (this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL) {
this.setState({inputtedLabelFolderURL: ""});
this.setState({ inputtedLabelFolderURL: "" });
}
}
@ -284,18 +296,43 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
}
private handleTrainClick = () => {
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<ITrainPageProps, ITrainPa
});
});
if (this.appInsights) {
this.appInsights.trackEvent({name: "TRAIN_MODEL_EVENT"});
this.appInsights.trackEvent({ name: "TRAIN_MODEL_EVENT" });
}
}
@ -338,7 +375,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
private async train(): Promise<any> {
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<ITrainPageProps, ITrainPa
{},
this.props.project.apiKey as string,
);
this.setState({modelUrl: result.headers.location});
this.setState({ modelUrl: result.headers.location });
return result;
} catch (err) {
ServiceHelper.handleServiceError(err);
@ -389,8 +426,8 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
private buildUpdatedProject = (newTrainRecord: ITrainRecordProps): IProject => {
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<ITrainPageProps, ITrainPa
}
private async triggerJsonDownload(): Promise<any> {
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<ITrainPageProps, ITrainPa
new Blob([modelJSON]));
const fileLink = document.createElement("a");
const fileBaseName = "model";
const downloadFileName =`${fileBaseName}-${this.state.currTrainRecord.modelInfo.modelId}.json`;
const downloadFileName = `${fileBaseName}-${this.state.currTrainRecord.modelInfo.modelId}.json`;
fileLink.href = fileURL;
fileLink.setAttribute("download", downloadFileName);

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

@ -41,7 +41,7 @@ export default class TrainRecord extends React.Component<ITrainRecordProps, ITra
</p>
<h6>Average accuracy:</h6>
<p>
{this.props.averageAccuracy}
{(this.props.averageAccuracy * 100).toFixed(2)+"%"}
</p>
<div className="accuracy-info">
<a href="https://aka.ms/form-recognizer/docs/train" target="_blank" rel="noopener noreferrer">

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

@ -39,7 +39,7 @@ export default class TrainTable
Object.entries(this.props.accuracies).map((entry) =>
<tr key={entry[0]}>
<td>{entry[0]}</td>
<td className="text-right">{entry[1]}</td>
<td className="text-right">{(entry[1] * 100).toFixed(2) + "%"}</td>
</tr>)
}
</tbody>

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

@ -11,7 +11,7 @@ describe("StatusBar component", () => {
function createComponent() {
return mount(
<StatusBar>
<StatusBar project={undefined}>
<div className="child-component">Child Component</div>
</StatusBar>,
);

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

@ -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<IStatusBarProps> {
public render() {
return (
<div className="status-bar">
<div className="status-bar-main">{this.props.children}</div>
<div className="status-bar-version">
<ul>
{this.props.project &&
<li>
<a href="https://github.com/microsoft/OCR-Form-Tools/blob/master/CHANGELOG.md" target="blank" rel="noopener noreferrer">
<FontIcon iconName="AzureAPIManagement" />
<span>{ this.props.project.apiVersion || constants.apiVersion }</span>
</a>
</li>
}
<li>
<a href="https://github.com/microsoft/OCR-Form-Tools/blob/master/CHANGELOG.md" target="blank" rel="noopener noreferrer">
<FontIcon iconName="BranchMerge" />
<span>{constants.appVersion}-b92b73b</span>
<span>{constants.appVersionRaw}-1f33130</span>
</a>
</li>
</ul>

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

@ -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;
};

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

@ -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 {

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

@ -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;
}

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

@ -80,6 +80,6 @@ export function registerIcons() {
RectangleShape: "\uF1A9",
Rotate90CounterClockwise: "\uF80E",
Rotate90Clockwise: "\uF80D",
},
AzureAPIManagement: "\uF37F", },
});
}

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

@ -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<void> {
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<ILabel>(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<ILabel>(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<void> {
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<IAssetMetadata> {
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<Response> => 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<string[]> {
const getFirst4bytes = (): Promise<Response> => this.pollForFetchAPI(() => fetch(uri, { headers: { range: `bytes=0-${mimeBytesNeeded}` } }), 1000, 200);
let first4bytes: Response;
try {
first4bytes = await getFirst4bytes()
} catch {
return new Promise<string[]>((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<string[]>((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,

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

@ -33,6 +33,7 @@ export class OCRService {
public async getRecognizedText(
filePath: string,
fileName: string,
mimeType: string,
onStatusChanged?: (ocrStatus: OcrStatus) => void,
rewrite?: boolean
): Promise<any> {
@ -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,

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

@ -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

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

@ -152,14 +152,7 @@ export default class ProjectService implements IProjectService {
public async isProjectNameAlreadyUsed(project: IProject): Promise<boolean> {
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<boolean> {

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

@ -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"