* 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 # relative to index.html
# without it, you'll see error like this # without it, you'll see error like this
# Failed to load resource: net::ERR_FILE_NOT_FOUND /favicon.ico:1 # Failed to load resource: net::ERR_FILE_NOT_FOUND /favicon.ico:1
PUBLIC_URL= PUBLIC_URL=
BROWSER=none

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

@ -1,8 +1,44 @@
# FoTT Changelog # 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 ## 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 ## 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) ### 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: 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)) * 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 ### 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 ### Run as web application

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

@ -51,7 +51,7 @@
"yarn": "^1.22.4" "yarn": "^1.22.4"
}, },
"scripts": { "scripts": {
"start": "nf start -p 3000", "start": "env-cmd -f .env.electron nf start -p 3000",
"compile": "tsc", "compile": "tsc",
"build": "react-scripts build", "build": "react-scripts build",
"react-start": "react-scripts start", "react-start": "react-scripts start",
@ -64,7 +64,7 @@
"electron:start:dev": "yarn electron-start", "electron:start:dev": "yarn electron-start",
"electron:start:prod": "yarn webpack:prod && yarn electron-start", "electron:start:prod": "yarn webpack:prod && yarn electron-start",
"electron-start": "node src/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*'", "tslint": "./node_modules/.bin/tslint 'src/**/*.ts*'",
"tslintfix": "./node_modules/.bin/tslint 'src/**/*.ts*' --fix" "tslintfix": "./node_modules/.bin/tslint 'src/**/*.ts*' --fix"
}, },
@ -108,7 +108,9 @@
"foreman": "^3.0.1", "foreman": "^3.0.1",
"jquery": "^3.5.0", "jquery": "^3.5.0",
"kind-of": "^6.0.3", "kind-of": "^6.0.3",
"mime": "^2.4.6",
"minimist": "^1.2.2", "minimist": "^1.2.2",
"node-forge": "^0.10.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"pdfjs-dist": "^2.4.456", "pdfjs-dist": "^2.4.456",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",

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

@ -37,15 +37,15 @@ repo = git.Repo("../")
commits = list(repo.iter_commits("master")) commits = list(repo.iter_commits("master"))
for commit in commits: for commit in commits:
commitHex = commit.hexsha[:7] commitHex = commit.hexsha[:7]
if commitHex == lastChanglogCommit:
print("found last change log commit")
break
commitDate = commit.committed_datetime.strftime("%m-%d-%Y") commitDate = commit.committed_datetime.strftime("%m-%d-%Y")
if currentCommitDate != commitDate: if currentCommitDate != commitDate:
if currentCommitDate is not None: if currentCommitDate is not None:
insterIntoChanglogContents("\n") insterIntoChanglogContents("\n")
currentCommitDate = commitDate currentCommitDate = commitDate
insterIntoChanglogContents("### " + appVersion + "-" + commitHex + " (" + commitDate + ")\n") insterIntoChanglogContents("### " + appVersion + "-" + commitHex + " (" + commitDate + ")\n")
if commitHex == lastChanglogCommit:
print("found last change log commit")
break
commitMessage = commit.message.partition('\n')[0] commitMessage = commit.message.partition('\n')[0]
commitMessageRegex = re.compile("(.*)\(\#(\d+)\)\s*$") commitMessageRegex = re.compile("(.*)\(\#(\d+)\)\s*$")
match = commitMessageRegex.search(commitMessage) match = commitMessageRegex.search(commitMessage)

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

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

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

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

@ -3,7 +3,8 @@
import { appInfo } from "./appInfo" import { appInfo } from "./appInfo"
const appVersionArr = appInfo.version.split("."); const appVersionRaw = appInfo.version
const appVersionArr = appVersionRaw.split(".");
appVersionArr[1] = appVersionArr[1] + "-preview"; appVersionArr[1] = appVersionArr[1] + "-preview";
const appVersion = appVersionArr.join("."); const appVersion = appVersionArr.join(".");
@ -14,6 +15,7 @@ const apiVersion = "v2.1-preview.1";
*/ */
export const constants = { export const constants = {
version: "pubpreview_1.0", version: "pubpreview_1.0",
appVersionRaw,
appVersion, appVersion,
apiVersion, apiVersion,
projectFormTempKey: "projectForm", projectFormTempKey: "projectForm",
@ -35,6 +37,7 @@ export const constants = {
convertedThumbnailQuality: 0.2, convertedThumbnailQuality: 0.2,
recentModelRecordsCount: 5, recentModelRecordsCount: 5,
apiModelsPath: `/formrecognizer/${apiVersion}/custom/models`, apiModelsPath: `/formrecognizer/${apiVersion}/custom/models`,
autoLabelBatchSize: 10,
pdfjsWorkerSrc(version: string) { pdfjsWorkerSrc(version: string) {
return `https://fotts.azureedge.net/npm/pdfjs-dist/${version}/pdf.worker.js`; 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.", backEndNotAvailable: "Checkbox feature will work in future version of Form Recognizer service, please stay tuned.",
addName: "Add a model name...", addName: "Add a model name...",
downloadJson: "Download JSON file", 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: { errors: {
electron: { electron: {
cantAccessFiles: "Cannot access files in '${folderUri}' for training. Please check if specified folder URI is correct." 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...", composing: "Model is composing, please wait...",
column: { column: {
icon: { icon: {
name:"Composed Icon", name: "Composed Icon",
}, },
id: { id: {
headerName: "Model Id", headerName: "Model Id",
@ -209,7 +213,7 @@ export const english: IAppStrings = {
defaultURLInput: "Paste or type URL...", defaultURLInput: "Paste or type URL...",
editAndUploadToTrainingSet: "Edit & upload to training set", 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.", 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...", uploadInPrgoress: "Upload in progress...",
confirmDuplicatedAssetName: { confirmDuplicatedAssetName: {
title: "Asset name exists", title: "Asset name exists",
@ -264,7 +268,7 @@ export const english: IAppStrings = {
unknownTagName: "Unknown", 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.", 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", 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: { regionTableTags: {
configureTag: { configureTag: {
@ -476,10 +480,14 @@ export const english: IAppStrings = {
subIMenuItems: { subIMenuItems: {
runOcrOnCurrentDocument: "Run OCR on current document", runOcrOnCurrentDocument: "Run OCR on current document",
runOcrOnAllDocuments: "Run OCR on all documents", 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.", 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: { keys: {
lessThan: "<", lessThan: "<",
greaterThan: ">", greaterThan: ">",
}, },
description: { description: {
prevPage: "Go to previous page", prevPage: "Go to previous page",
nextPage: "Go to next page", nextPage: "Go to next page",
@ -520,7 +528,7 @@ export const english: IAppStrings = {
minus: "-", minus: "-",
plus: "=", plus: "=",
slash: "/", slash: "/",
}, },
description: { description: {
in: "Zoom in", in: "Zoom in",
out: "Zoom out", out: "Zoom out",
@ -531,11 +539,11 @@ export const english: IAppStrings = {
keys: { keys: {
delete: "Delete", delete: "Delete",
backSpace: "Backspace", backSpace: "Backspace",
}, },
description: { description: {
delete: "Remove selection and delete labels of selected words", delete: "Remove selection and delete labels of selected words",
backSpace: "Remove selection and delete labels of selected words", backSpace: "Remove selection and delete labels of selected words",
}, },
}, },
drawnRegions: { drawnRegions: {
keys: { keys: {
@ -551,7 +559,7 @@ export const english: IAppStrings = {
tips: { tips: {
quickLabeling: { quickLabeling: {
name: "Lable with hot keys", 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: { renameTag: {
name: "Rename tag", name: "Rename tag",
@ -604,6 +612,10 @@ export const english: IAppStrings = {
message: `An error occured while deleting the project. message: `An error occured while deleting the project.
Validate the project file and security token exist and try again`, 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: { projectNotFound: {
title: "Error loading project", title: "Error loading project",
message: "We couldn't find the project file ${file} at the target blob container ${container}.\ 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", title: "Model not found",
message: "Model \"${modelID}\" not found. Please use another model.", message: "Model \"${modelID}\" not found. Please use another model.",
}, },
connectionNotExistError: {
title: "Connection doesn't exist",
message: "Connection doesn't exist."
},
getOcrError: { getOcrError: {
title: "Cannot load OCR file", title: "Cannot load OCR file",
message: "Failed to load from OCR file. Please check your connection or network settings.", 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.", 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 ...", addName: "Agregar nombre de modelo ...",
downloadJson: "Descargar archivo JSON", 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: { errors: {
electron: { electron: {
cantAccessFiles: "No se puede acceder a los archivos en '${folderUri}' para entrenamiento. Compruebe si el URI de la carpeta especificada es correcto." 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: { canvasCommandBar: {
items: { items: {
layers:{ layers: {
text: "Capas", text: "Capas",
subMenuItems: { subMenuItems: {
text: "Texto", text: "Texto",
@ -477,10 +481,14 @@ export const spanish: IAppStrings = {
subIMenuItems: { subIMenuItems: {
runOcrOnCurrentDocument: "Ejecutar OCR en el documento actual", runOcrOnCurrentDocument: "Ejecutar OCR en el documento actual",
runOcrOnAllDocuments: "Ejecute OCR en todos los documentos", 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.", 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: { keys: {
lessThan: "<", lessThan: "<",
greaterThan: ">", greaterThan: ">",
}, },
description: { description: {
prevPage: "Ir a la página anterior en documentos de varias páginas", 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", nextPage: "Ir a la página siguiente en documentos de varias páginas",
@ -521,7 +529,7 @@ export const spanish: IAppStrings = {
minus: "-", minus: "-",
plus: "=", plus: "=",
slash: "/", slash: "/",
}, },
description: { description: {
in: "Acercarse", in: "Acercarse",
out: "Disminuir el zoom", out: "Disminuir el zoom",
@ -532,11 +540,11 @@ export const spanish: IAppStrings = {
keys: { keys: {
delete: "Delete", delete: "Delete",
backSpace: "Backspace", backSpace: "Backspace",
}, },
description: { description: {
delete: "Eliminar selección del mapa del documento o clave de selección de una etiqueta", 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", backSpace: "Eliminar selección del mapa del documento o clave de selección de una etiqueta",
}, },
}, },
drawnRegions: { drawnRegions: {
keys: { keys: {
@ -552,7 +560,7 @@ export const spanish: IAppStrings = {
tips: { tips: {
quickLabeling: { quickLabeling: {
name: "Etiquetado rápido", 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: { renameTag: {
name: "Rename Tag", name: "Rename Tag",
@ -604,6 +612,10 @@ export const spanish: IAppStrings = {
message: `Se ha producido un error al eliminar el proyecto. 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`, 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: { projectNotFound: {
title: "", title: "",
message: "", message: "",
@ -701,6 +713,10 @@ export const spanish: IAppStrings = {
title: "Modelo no encontrado", title: "Modelo no encontrado",
message: "Modelo \"${modelID}\" no encontrado. Por favor use otro modelo.", message: "Modelo \"${modelID}\" no encontrado. Por favor use otro modelo.",
}, },
connectionNotExistError: {
title: "La conexión no existe",
message: "La conexión no existe."
},
getOcrError: { getOcrError: {
title: "No se puede cargar el archivo OCR", 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." 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(), createContainer: jest.fn(),
deleteContainer: jest.fn(), deleteContainer: jest.fn(),
getAssets: jest.fn(), getAssets: jest.fn(),
isFileExists: jest.fn(),
}; };
} }

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

@ -129,6 +129,10 @@ export interface IAppStrings {
backEndNotAvailable: string, backEndNotAvailable: string,
addName: string, addName: string,
downloadJson: string; downloadJson: string;
trainConfirm: {
title: string;
message: string;
},
errors: { errors: {
electron: { electron: {
cantAccessFiles: string; cantAccessFiles: string;
@ -472,9 +476,13 @@ export interface IAppStrings {
runOcrOnCurrentDocument: string, runOcrOnCurrentDocument: string,
runOcrOnAllDocuments: string, runOcrOnAllDocuments: string,
runAutoLabelingCurrentDocument: string, runAutoLabelingCurrentDocument: string,
runAutoLabelingOnNotLabelingDocuments: string,
noPredictModelOnProject: string, noPredictModelOnProject: string,
} }
} }
},
warings: {
drawRegionUnsupportedAPIVersion: string,
} }
}, },
}, },
@ -576,6 +584,7 @@ export interface IAppStrings {
projectInvalidSecurityToken: IErrorMetadata, projectInvalidSecurityToken: IErrorMetadata,
projectUploadError: IErrorMetadata, projectUploadError: IErrorMetadata,
projectDeleteError: IErrorMetadata, projectDeleteError: IErrorMetadata,
projectDeleteErrorSecurityTokenNotFound: IErrorMetadata,
projectNotFound: IErrorMetadata, projectNotFound: IErrorMetadata,
genericRenderError: IErrorMetadata, genericRenderError: IErrorMetadata,
securityTokenNotFound: IErrorMetadata, securityTokenNotFound: IErrorMetadata,
@ -600,6 +609,7 @@ export interface IAppStrings {
modelCountLimitExceeded: IErrorMetadata, modelCountLimitExceeded: IErrorMetadata,
requestSendError: IErrorMetadata, requestSendError: IErrorMetadata,
modelNotFound: IErrorMetadata, modelNotFound: IErrorMetadata,
connectionNotExistError: IErrorMetadata,
getOcrError: IErrorMetadata, getOcrError: IErrorMetadata,
}; };
shareProject: { shareProject: {

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

@ -7,217 +7,37 @@
"hashFontFileName": true, "hashFontFileName": true,
"glyphs": [ "glyphs": [
{ {
"name": "Table", "name": "Add",
"unicode": "ED86" "unicode": "E710"
},
{
"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": "AddField", "name": "AddField",
"unicode": "E4C7" "unicode": "E4C7"
}, },
{ {
"name": "TagGroup", "name": "AddTo",
"unicode": "E3F6" "unicode": "ECC8"
}, },
{ {
"name": "Insights", "name": "AlertSolid",
"unicode": "E3AF" "unicode": "F331"
}, },
{ {
"name": "MachineLearning", "name": "AzureAPIManagement",
"unicode": "E3B8" "unicode": "F37F"
}, },
{ {
"name": "Merge", "name": "BookAnswers",
"unicode": "E7D5" "unicode": "F8A4"
}, },
{ {
"name": "MapLayers", "name": "BranchMerge",
"unicode": "E81E" "unicode": "F295"
},
{
"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": "Cancel", "name": "Cancel",
"unicode": "E711" "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", "name": "CheckboxComposite",
"unicode": "E73A" "unicode": "E73A"
@ -227,36 +47,228 @@
"unicode": "E73E" "unicode": "E73E"
}, },
{ {
"name": "Down", "name": "ChevronDown",
"unicode": "E74B" "unicode": "E70D"
}, },
{ {
"name": "Delete", "name": "ChevronLeft",
"unicode": "E74D" "unicode": "E76B"
},
{
"name": "ChevronRight",
"unicode": "E76C"
},
{
"name": "ChevronUp",
"unicode": "E70E"
},
{
"name": "ChromeMinimize",
"unicode": "E921"
},
{
"name": "ChromeRestore",
"unicode": "E923"
},
{
"name": "CircleRing",
"unicode": "EA3A"
}, },
{ {
"name": "Cloud", "name": "Cloud",
"unicode": "E753" "unicode": "E753"
}, },
{ {
"name": "Up", "name": "Copy",
"unicode": "E74A" "unicode": "E8C8"
}, },
{ {
"name": "KeyPhraseExtraction", "name": "Delete",
"unicode": "E395" "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", "name": "Hide3",
"unicode": "F6AC" "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", "name": "WarningSolid",
"unicode": "F736" "unicode": "F736"
}, },
{ {
"name": "BookAnswers", "name": "ZoomIn",
"unicode": "F8A4" "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()); 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[]> { public listContainers(folderPath: string): Promise<string[]> {
return this.listItems(path.normalize(folderPath), (stats) => stats.isDirectory()); return this.listItems(path.normalize(folderPath), (stats) => stats.isDirectory());
} }

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

@ -93,6 +93,7 @@ export interface IProject {
lastVisitedAssetId?: string, lastVisitedAssetId?: string,
apiUriBase: string, apiUriBase: string,
apiKey?: string | ISecureString, apiKey?: string | ISecureString,
apiVersion?: string;
folderPath: string, folderPath: string,
trainRecord: ITrainRecordProps, trainRecord: ITrainRecordProps,
recentModelRecords: IRecentModel[], recentModelRecords: IRecentModel[],
@ -166,6 +167,7 @@ export interface IAsset {
id: string, id: string,
type: AssetType, type: AssetType,
state: AssetState, state: AssetState,
labelingState?: AssetLabelingState,
name: string, name: string,
path: string, path: string,
size: ISize, size: ISize,
@ -174,7 +176,9 @@ export interface IAsset {
predicted?: boolean, predicted?: boolean,
ocr?: any, ocr?: any,
isRunningOCR?: boolean, isRunningOCR?: boolean,
isRunningAutoLabeling?: boolean,
cachedImage?: string, cachedImage?: string,
mimeType?: string,
} }
/** /**
@ -219,6 +223,8 @@ export interface IRegion {
value?: string, value?: string,
pageNumber: number, pageNumber: number,
isTableRegion?: boolean, isTableRegion?: boolean,
changed?: boolean,
} }
export interface ITableRegion extends IRegion { export interface ITableRegion extends IRegion {
@ -232,6 +238,7 @@ export interface ITableRegion extends IRegion {
*/ */
export interface ILabelData { export interface ILabelData {
document: string, document: string,
labelingState?: AssetLabelingState;
labels: ILabel[], labels: ILabel[],
tableLabels?: ITableLabel[], tableLabels?: ITableLabel[],
} }
@ -245,6 +252,7 @@ export interface ILabel {
key?: IFormRegion[], key?: IFormRegion[],
value: IFormRegion[], value: IFormRegion[],
labelType?: string, labelType?: string,
confidence?: number,
} }
export interface ITableLabel { export interface ITableLabel {
@ -356,6 +364,12 @@ export enum ErrorCode {
ProjectUploadError = "ProjectUploadError", 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 LOCAL - Local storage type
* @enum CLOUD - Cloud storage type * @enum CLOUD - Cloud storage type
@ -381,6 +395,14 @@ export enum AssetType {
TIFF = 6, TIFF = 6,
} }
export enum AssetMimeType {
PDF = "application/pdf",
TIFF = "image/tiff",
JPG = "image/jpg",
PNG = "image/png",
BMP = "image/bmp",
}
/** /**
* @name - Asset State * @name - Asset State
* @description - Defines the state of the asset with regard to the tagging process * @description - Defines the state of the asset with regard to the tagging process
@ -393,6 +415,20 @@ export enum AssetState {
Visited = 1, Visited = 1,
Tagged = 2, 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 * @name - Region Type
@ -430,7 +466,7 @@ export enum FieldType {
} }
export enum LabelType { export enum LabelType {
DrawnRegion = "drawnRegion" DrawnRegion = "region"
} }
export enum FieldFormat { export enum FieldFormat {
@ -451,7 +487,7 @@ export enum FeatureCategory {
Text = "text", Text = "text",
Checkbox = "checkbox", Checkbox = "checkbox",
Label = "label", Label = "label",
DrawnRegion = "drawnRegion" DrawnRegion = "region"
} }
export enum ImageMapParent { export enum ImageMapParent {

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

@ -3,7 +3,7 @@
import { BlobServiceClient, ContainerClient } from "@azure/storage-blob"; import { BlobServiceClient, ContainerClient } from "@azure/storage-blob";
import { constants } from "../../common/constants"; import { constants } from "../../common/constants";
import { strings } from "../../common/strings"; 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 { throwUnhandledRejectionForEdge } from "../../react/components/common/errorHandler/errorHandler";
import { AssetService } from "../../services/assetService"; import { AssetService } from "../../services/assetService";
import { IStorageProvider } from "./storageProviderFactory"; 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 * Lists the containers with in the Azure Blob Storage account
* @param path - NOT USED IN CURRENT IMPLEMENTATION. Lists containers in 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)) { if (files.find((str) => str === labelFileName)) {
asset.state = AssetState.Tagged; 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)) { } else if (files.find((str) => str === ocrFileName)) {
asset.state = AssetState.Visited; asset.state = AssetState.Visited;
} else { } else {
asset.state = AssetState.NotVisited; asset.state = AssetState.NotVisited;
} }
result.push(asset); result.push(asset);
} }
} }

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

@ -110,6 +110,15 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
return IpcRendererProxy.send(`${PROXY_NAME}:listFiles`, [folderPath]); 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 * List directories inside another directory
* @param folderName - Directory from which to list directories * @param folderName - Directory from which to list directories

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

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

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

@ -35,6 +35,7 @@ export interface IStorageProvider extends IAssetProvider {
isValidProjectConnection(filepath?): Promise<boolean>; isValidProjectConnection(filepath?): Promise<boolean>;
listFiles(folderPath?: string, ext?: string): Promise<string[]>; listFiles(folderPath?: string, ext?: string): Promise<string[]>;
isFileExists(filepath: string): Promise<boolean>;
listContainers(folderPath?: string): Promise<string[]>; listContainers(folderPath?: string): Promise<string[]>;
createContainer(folderPath: string): Promise<void>; 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">
<div className="asset-loading-ocr-spinner"> <div className="asset-loading-ocr-spinner">
<Label className="p-0" ></Label> <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>
</div> </div>
} }

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

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

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

@ -27,13 +27,18 @@ interface ICondensedListProps {
onDelete?: (item) => void; onDelete?: (item) => void;
} }
interface ICondensedListState {
currentId: string;
}
/** /**
* @name - Condensed List * @name - Condensed List
* @description - Clickable, deletable and linkable list of items * @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) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { currentId: null };
this.onItemClick = this.onItemClick.bind(this); this.onItemClick = this.onItemClick.bind(this);
this.onItemDelete = this.onItemDelete.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"> <ul className="condensed-list-items">
{items.map((item) => <Component key={item.id} {items.map((item) => <Component key={item.id}
item={item} item={item}
currentId={this.state.currentId}
onClick={(e) => this.onItemClick(e, item)} onClick={(e) => this.onItemClick(e, item)}
onDelete={(e) => this.onItemDelete(e, item)} />)} onDelete={(e) => this.onItemDelete(e, item)} />)}
</ul> </ul>
@ -79,6 +85,7 @@ export default class CondensedList extends React.Component<ICondensedListProps>
if (this.props.onClick) { if (this.props.onClick) {
this.props.onClick(item); this.props.onClick(item);
} }
this.setState({ currentId: item.id });
} }
private onItemDelete = (e: SyntheticEvent, item) => { 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 * Generic list item with an onClick function and a name
* @param param0 - {item: {name: ""}, onClick: (item) => void;} * @param param0 - {item: {name: ""}, onClick: (item) => void;}
*/ */
export function ListItem({ item, onClick }) { export function ListItem({ item, onClick, currentId }) {
return ( return (
<li> <li>
{/* eslint-disable-next-line */} {/* 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> <span className="px-2">{item.name}</span>
</a> </a>
</li> </li>

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

@ -43,6 +43,7 @@ interface IImageMapProps {
enableFeatureSelection?: boolean; enableFeatureSelection?: boolean;
handleFeatureSelect?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void; handleFeatureSelect?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void;
handleFeatureDoubleClick?: (feature: any, isTaggle: boolean, category: FeatureCategory) => void;
groupSelectMode?: boolean; groupSelectMode?: boolean;
handleIsPointerOnImage?: (isPointerOnImage: boolean) => void; handleIsPointerOnImage?: (isPointerOnImage: boolean) => void;
isPointerOnImage?: boolean; isPointerOnImage?: boolean;
@ -58,7 +59,7 @@ interface IImageMapProps {
hoveringFeature?: string; hoveringFeature?: string;
onMapReady: () => void; onMapReady: () => void;
handleTableToolTipChange?: (display: string, width: number, height: number, top: number, 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; addDrawnRegionFeatureProps?: (feature) => void;
updateFeatureAfterModify?: (features) => any; updateFeatureAfterModify?: (features) => any;
@ -84,7 +85,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
private modify: Modify; private modify: Modify;
private snap: Snap; private snap: Snap;
private drawnFeatures: Collection = new Collection([], {unique: true}); private drawnFeatures: Collection = new Collection([], { unique: true });
public modifyStartFeatureCoordinates: any = {}; public modifyStartFeatureCoordinates: any = {};
private imageExtent: number[]; private imageExtent: number[];
@ -198,7 +199,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
onMouseEnter={this.handlePointerEnterImageMap} onMouseEnter={this.handlePointerEnterImageMap}
className="map-wrapper" 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> </div>
); );
} }
@ -361,7 +362,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
*/ */
public addInteraction = (interaction: Interaction) => { public addInteraction = (interaction: Interaction) => {
if (undefined === this.map.getInteractions().array_.find((existingInteraction) => { if (undefined === this.map.getInteractions().array_.find((existingInteraction) => {
return interaction.constructor.name === existingInteraction.constructor.name return interaction.constructor === existingInteraction.constructor;
})) { })) {
this.map.addInteraction(interaction); this.map.addInteraction(interaction);
} }
@ -477,7 +478,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
this.drawRegionVectorLayer?.getSource().clear(); this.drawRegionVectorLayer?.getSource().clear();
this.drawnLabelVectorLayer?.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.drawRegionVectorLayer.getSource().on("addfeature", (evt) => {
this.pushToDrawnFeatures(evt.feature, this.drawnFeatures); this.pushToDrawnFeatures(evt.feature, this.drawnFeatures);
@ -516,7 +517,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
*/ */
public removeInteraction = (interaction: Interaction) => { public removeInteraction = (interaction: Interaction) => {
const existingInteraction = this.map.getInteractions().array_.find((existingInteraction) => { const existingInteraction = this.map.getInteractions().array_.find((existingInteraction) => {
return interaction.constructor.name === existingInteraction.constructor.name return interaction.constructor === existingInteraction.constructor;
}); });
if (existingInteraction !== undefined) { 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.handlePointerMove);
this.map.on("pointermove", this.handlePointerMoveOnTableIcon); this.map.on("pointermove", this.handlePointerMoveOnTableIcon);
this.map.on("pointerup", this.handlePointerUp); this.map.on("pointerup", this.handlePointerUp);
this.map.on("dblclick", this.handleDoubleClick);
this.initializeDefaultSelectionMode(); this.initializeDefaultSelectionMode();
this.initializeDragPan(); this.initializeDragPan();
@ -647,12 +649,12 @@ export class ImageMap extends React.Component<IImageMapProps> {
return; return;
} }
const eventPixel = this.map.getEventPixel(event.originalEvent); const eventPixel = this.map.getEventPixel(event.originalEvent);
const filter = this.getLayerFilterAtPixel(eventPixel); const filter = this.getLayerFilterAtPixel(eventPixel);
const isPixelOnFeature = !!filter; const isPixelOnFeature = !!filter;
if (isPixelOnFeature) { if (isPixelOnFeature && !this.props.isSnapped) {
this.setDragPanInteraction(false); 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) => { private getLayerFilterAtPixel = (eventPixel: any) => {
const isPointerOnLabelledFeature = this.map.hasFeatureAtPixel( const isPointerOnLabelledFeature = this.map.hasFeatureAtPixel(
@ -691,7 +707,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
this.textVectorLayerFilter); this.textVectorLayerFilter);
if (isPointerOnTextFeature) { if (isPointerOnTextFeature) {
return { return {
layerfilter : this.textVectorLayerFilter, layerfilter: this.textVectorLayerFilter,
category: FeatureCategory.Text, category: FeatureCategory.Text,
}; };
} }
@ -719,9 +735,9 @@ export class ImageMap extends React.Component<IImageMapProps> {
private handlePointerMoveOnTableIcon = (event: MapBrowserEvent) => { private handlePointerMoveOnTableIcon = (event: MapBrowserEvent) => {
if (this.props.handleTableToolTipChange) { if (this.props.handleTableToolTipChange) {
const eventPixel = this.map.getEventPixel(event.originalEvent); 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) { if (isPointerOnTableIconFeature) {
const features = this.map.getFeaturesAtPixel( eventPixel, this.tableIconBorderVectorLayerFilter); const features = this.map.getFeaturesAtPixel(eventPixel, this.tableIconBorderVectorLayerFilter);
if (features.length > 0) { if (features.length > 0) {
const feature = features[0]; const feature = features[0];
if (feature && this.props.hoveringFeature !== feature.get("id")) { if (feature && this.props.hoveringFeature !== feature.get("id")) {
@ -789,6 +805,9 @@ export class ImageMap extends React.Component<IImageMapProps> {
} }
this.setDragPanInteraction(true); this.setDragPanInteraction(true);
this.removeInteraction(this.modify);
this.initializeModify();
this.addInteraction(this.modify)
} }
private setDragPanInteraction = (dragPanEnabled: boolean) => { private setDragPanInteraction = (dragPanEnabled: boolean) => {
@ -855,6 +874,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
this.initializeModify(); this.initializeModify();
this.initializeSnap(); this.initializeSnap();
this.initializeDraw(); this.initializeDraw();
this.addInteraction(this.dragBox);
this.addInteraction(this.modify); this.addInteraction(this.modify);
this.addInteraction(this.snap); this.addInteraction(this.snap);
} }
@ -891,7 +911,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
source: this.drawRegionVectorLayer.getSource(), source: this.drawRegionVectorLayer.getSource(),
style: this.props.drawRegionStyler, style: this.props.drawRegionStyler,
geometryFunction: (coordinates, optGeometry) => { geometryFunction: (coordinates, optGeometry) => {
const extent = boundingExtent(/** @type {LineCoordType} */ (coordinates)); const extent = boundingExtent(/** @type {LineCoordType} */(coordinates));
const boxCoordinates = [[ const boxCoordinates = [[
[extent[0], extent[3]], [extent[0], extent[3]],
[extent[2], extent[3]], [extent[2], extent[3]],
@ -1078,8 +1098,8 @@ export class ImageMap extends React.Component<IImageMapProps> {
this.initializeDrawnRegionLabelLayer(); this.initializeDrawnRegionLabelLayer();
this.initializeDrawnRegionLayer(); this.initializeDrawnRegionLayer();
return [this.imageLayer, this.textVectorLayer, this.tableBorderVectorLayer, this.tableIconBorderVectorLayer, return [this.imageLayer, this.textVectorLayer, this.tableBorderVectorLayer, this.tableIconBorderVectorLayer,
this.tableIconVectorLayer, this.checkboxVectorLayer, this.drawRegionVectorLayer, this.labelVectorLayer, this.tableIconVectorLayer, this.checkboxVectorLayer, this.drawRegionVectorLayer, this.labelVectorLayer,
this.drawnLabelVectorLayer]; this.drawnLabelVectorLayer];
} }
private initializePredictLayers = (projection: Projection) => { private initializePredictLayers = (projection: Projection) => {
@ -1181,7 +1201,7 @@ export class ImageMap extends React.Component<IImageMapProps> {
private initializeMap = (projection, layers) => { private initializeMap = (projection, layers) => {
this.map = new Map({ this.map = new Map({
controls: [] , controls: [],
interactions: defaultInteractions({ interactions: defaultInteractions({
shiftDragZoom: false, shiftDragZoom: false,
doubleClickZoom: false, doubleClickZoom: false,

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

@ -37,6 +37,18 @@
&-container { &-container {
overflow-x: visible; overflow-x: visible;
overflow-y: auto; 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 { &-item-block {
position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin: 2px 0; margin: 2px 0;
@ -80,11 +93,23 @@
&-2 { &-2 {
width: 100%; 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 { &-item {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
.tag-content {
transition: 1s;
}
&-selected { &-selected {
.tag-content { .tag-content {
@ -100,7 +125,12 @@
background: $darker-10 !important; background: $darker-10 !important;
} }
} }
&-highlight {
.tag-content {
background-color: $lighter-5 !important;
box-shadow:4px 4px 5px $lighter-5;
}
}
&-label { &-label {
min-height: 1em; min-height: 1em;
display: flex; display: flex;
@ -172,7 +202,7 @@
} }
&-item-label { &-item-label {
color: #A0A0A0; color: #a0a0a0;
} }
&-item-label:hover { &-item-label:hover {
@ -224,7 +254,7 @@
width: 0.1px; width: 0.1px;
border: 0.5px solid $lighter-2; border: 0.5px solid $lighter-2;
height: 18px; height: 18px;
margin: 0 0.25em margin: 0 0.25em;
} }
&-iconbutton { &-iconbutton {
@ -234,7 +264,8 @@
padding: 0 0.25em; padding: 0 0.25em;
background-color: transparent; background-color: transparent;
&.active, &:hover { &.active,
&:hover {
background-color: transparent; background-color: transparent;
color: #fff; color: #fff;
} }
@ -276,5 +307,5 @@ div.circle-picker-container {
} }
.loading-tag { .loading-tag {
height: 100%; height: 100%;
} }

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

@ -81,7 +81,6 @@ export interface ITagInputProps {
onLabelLeave: (label: ILabel) => void; onLabelLeave: (label: ILabel) => void;
/** Function to handle tag change */ /** Function to handle tag change */
onTagChanged?: (oldTag: ITag, newTag: ITag) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
setTagInputMode?: (tagInputMode: TagInputMode, selectedTableTagToLabel?: ITableTag) => void; setTagInputMode?: (tagInputMode: TagInputMode, selectedTableTagToLabel?: ITableTag) => void;
tagInputMode: TagInputMode; tagInputMode: TagInputMode;
selectedTableTagToLabel: ITableTag; selectedTableTagToLabel: ITableTag;
@ -91,6 +90,7 @@ export interface ITagInputProps {
handleTableCellClick: (iTableCellIndex, jTableCellIndex) => void; handleTableCellClick: (iTableCellIndex, jTableCellIndex) => void;
selectedTableTagBody: ITableRegion[][][]; selectedTableTagBody: ITableRegion[][][];
splitPaneWidth: number; splitPaneWidth: number;
onTagDoubleClick?: (label: ILabel) => void;
} }
export interface ITagInputState { export interface ITagInputState {
@ -149,7 +149,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
public render() { public render() {
const dark: ICustomizations = { const dark: ICustomizations = {
settings: { settings: {
theme: getDarkTheme(), theme: getDarkTheme(),
}, },
scopedSettings: {}, scopedSettings: {},
}; };
@ -207,6 +207,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
searchTags: !this.state.searchTags, searchTags: !this.state.searchTags,
searchQuery: "", searchQuery: "",
})} })}
searchingTags={this.state.searchQuery.length > 0}
onRenameTag={this.onRenameTag} onRenameTag={this.onRenameTag}
onLockTag={this.onLockTag} onLockTag={this.onLockTag}
onDelete={this.onDeleteTag} onDelete={this.onDeleteTag}
@ -226,7 +227,8 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onKeyDown={this.onSearchKeyDown} onKeyDown={this.onSearchKeyDown}
onChange={(e) => this.setState({ searchQuery: e.target.value })} onChange={(e) => this.setState({ searchQuery: e.target.value })}
placeholder="Search tags" placeholder="Search tags"
autoFocus={true} autoFocus={true}
onFocus={() => this.setState({ selectedTag: null, tagOperation: TagOperationMode.Rename })}
/> />
<FontIcon iconName="Search" /> <FontIcon iconName="Search" />
</div> </div>
@ -245,6 +247,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
</Customizer> </Customizer>
{this.getColorPickerPortal()} {this.getColorPickerPortal()}
</div> </div>
{ {
this.state.addTags && this.state.addTags &&
@ -394,7 +397,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
const { selectedTag } = this.state; const { selectedTag } = this.state;
const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker; const showColorPicker = this.state.tagOperation === TagOperationMode.ColorPicker;
return ( 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"> <div className="tag-input-colorpicker-container">
{ {
showColorPicker && showColorPicker &&
@ -429,6 +432,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onLabelLeave={this.props.onLabelLeave} onLabelLeave={this.props.onLabelLeave}
onTagChanged={this.props.onTagChanged} onTagChanged={this.props.onTagChanged}
handleLabelTable={this.props.handleLabelTable} handleLabelTable={this.props.handleLabelTable}
onTagDoubleClick={this.props.onTagDoubleClick}
/>); />);
} }
@ -519,11 +523,11 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
deselect = false; deselect = false;
} else if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) { } else if (labelAssigned && ((category === FeatureCategory.DrawnRegion) !== isTagLabelTypeDrawnRegion)) {
if (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) { } 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 { } else {
toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, {otherCategory: FeatureCategory.Text})); toast.warn(interpolate(strings.tags.warnings.notCompatibleWithDrawnRegionTag, { otherCatagory: FeatureCategory.Text}));
} }
return; return;
} else if (tagCategory === category || category === FeatureCategory.DrawnRegion || } else if (tagCategory === category || category === FeatureCategory.DrawnRegion ||
@ -535,7 +539,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
onTagClick(tag); onTagClick(tag);
deselect = false; deselect = false;
} else { } else {
toast.warn(strings.tags.warnings.notCompatibleTagType, {autoClose: 7000}); toast.warn(strings.tags.warnings.notCompatibleTagType, { autoClose: 7000 });
} }
} }
this.setState({ 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 => { public labelAssigned = (labels: ILabel[], name): boolean => {
const label = labels.find((label) => label.label === name ? true : false); const label = labels.find((label) => label.label === name ? true : false);
if (!label) { if (!label) {
return false; return false;
} else { } else {
return true; return true;
} }
} }
public labelAssignedDrawnRegion = (labels: ILabel[], name): boolean => { public labelAssignedDrawnRegion = (labels: ILabel[], name): boolean => {
@ -606,11 +619,11 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
private creatTagInput = (value: any) => { private creatTagInput = (value: any) => {
const newTag: ITag = { const newTag: ITag = {
name: value, name: value,
color: getNextColor(this.state.tags), color: getNextColor(this.state.tags),
type: FieldType.String, type: FieldType.String,
format: FieldFormat.NotSpecified, format: FieldFormat.NotSpecified,
documentCount: 0, documentCount: 0,
}; };
if (newTag.name.length && ![...this.state.tags, newTag].containsDuplicates((t) => t.name)) { if (newTag.name.length && ![...this.state.tags, newTag].containsDuplicates((t) => t.name)) {
this.addTag(newTag); this.addTag(newTag);
@ -664,7 +677,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
} }
private onHideContextualMenu = () => { private onHideContextualMenu = () => {
this.setState({tagOperation: TagOperationMode.None}); this.setState({ tagOperation: TagOperationMode.None });
} }
private getContextualMenuItems = (): IContextualMenuItem[] => { private getContextualMenuItems = (): IContextualMenuItem[] => {
@ -728,6 +741,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}, },
text: strings.tags.toolbar.moveUp, text: strings.tags.toolbar.moveUp,
onClick: this.onMenuItemClick, onClick: this.onMenuItemClick,
disabled: this.state.searchQuery.length > 0,
}, },
{ {
key: TagMenuItem.MoveDown, key: TagMenuItem.MoveDown,
@ -736,6 +750,7 @@ export class TagInput extends React.Component<ITagInputProps, ITagInputState> {
}, },
text: strings.tags.toolbar.moveDown, text: strings.tags.toolbar.moveDown,
onClick: this.onMenuItemClick, onClick: this.onMenuItemClick,
disabled: this.state.searchQuery.length > 0,
}, },
{ {
key: TagMenuItem.Delete, key: TagMenuItem.Delete,

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

@ -7,6 +7,7 @@ import { ITag, ILabel, FieldType, FieldFormat, TagInputMode } from "../../../../
import { strings } from "../../../../common/strings"; import { strings } from "../../../../common/strings";
import TagInputItemLabel from "./tagInputItemLabel"; import TagInputItemLabel from "./tagInputItemLabel";
import { tagIndexKeys } from "./tagIndexKeys"; import { tagIndexKeys } from "./tagIndexKeys";
import _ from "lodash";
export interface ITagClickProps { export interface ITagClickProps {
ctrlKey?: boolean; ctrlKey?: boolean;
@ -43,6 +44,7 @@ export interface ITagInputItemProps {
onTagChanged?: (oldTag: ITag, newTag: ITag) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
handleLabelTable: (tagInputMode: TagInputMode, selectedTableTagToLabel) => void; handleLabelTable: (tagInputMode: TagInputMode, selectedTableTagToLabel) => void;
addRowToDynamicTable: () => void; addRowToDynamicTable: () => void;
onTagDoubleClick?: (label:ILabel) => void;
} }
export interface ITagInputItemState { export interface ITagInputItemState {
@ -81,9 +83,14 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
const style: any = { const style: any = {
background: this.props.tag.color, background: this.props.tag.color,
}; };
const confidence = _.get(this.props, "labels[0].confidence", null);
return ( return (
<div className={"tag-item-block"}> <div className={"tag-item-block"}>
{confidence &&
<div className="tag-item-confidence">
{confidence}
</div>
}
<div <div
className={"tag-color"} className={"tag-color"}
style={style} style={style}
@ -98,6 +105,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
style={style}> style={style}>
<div <div
className={"tag-content pr-2"} className={"tag-content pr-2"}
onDoubleClick={this.onNameDoubleClick}
onClick={this.onNameClick}> onClick={this.onNameClick}>
{this.getTagContent()} {this.getTagContent()}
</div> </div>
@ -141,6 +149,14 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
this.props.onClick(this.props.tag, { ctrlKey, altKey }); 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 = () => { private getItemClassName = () => {
const classNames = ["tag-item"]; const classNames = ["tag-item"];
if (this.props.isSelected) { if (this.props.isSelected) {
@ -165,20 +181,20 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
<div className="tag-name-body"> <div className="tag-name-body">
{ {
this.state.isRenaming this.state.isRenaming
? ?
<input <input
ref={this.onInputRef} ref={this.onInputRef}
className={`tag-name-editor ${this.getContentClassName()}`} className={`tag-name-editor ${this.getContentClassName()}`}
type="text" type="text"
defaultValue={this.props.tag.name} defaultValue={this.props.tag.name}
onKeyDown={(e) => this.onInputKeyDown(e)} onKeyDown={(e) => this.onInputKeyDown(e)}
onBlur={this.onInputBlur} onBlur={this.onInputBlur}
autoFocus={true} autoFocus={true}
/> />
: :
<span title={this.props.tag.name} className={this.getContentClassName()}> <span title={this.props.tag.name} className={this.getContentClassName()}>
{this.props.tag.name} {this.props.tag.name}
</span> </span>
} }
</div> </div>
<div className={"tag-icons-container"}> <div className={"tag-icons-container"}>
@ -192,7 +208,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
title={strings.tags.toolbar.contextualMenu} title={strings.tags.toolbar.contextualMenu}
ariaLabel={strings.tags.toolbar.contextualMenu} ariaLabel={strings.tags.toolbar.contextualMenu}
className="tag-input-toolbar-iconbutton ml-2" className="tag-input-toolbar-iconbutton ml-2"
iconProps={{iconName: "ChevronDown"}} iconProps={{ iconName: "ChevronDown" }}
onClick={this.onDropdownClick} /> onClick={this.onDropdownClick} />
</div> </div>
</div> </div>
@ -276,7 +292,7 @@ export default class TagInputItem extends React.Component<ITagInputItemProps, IT
} }
private isTypeOrFormatSpecified = () => { private isTypeOrFormatSpecified = () => {
const {tag} = this.props; const { tag } = this.props;
return (tag.type && tag.type !== FieldType.String) || return (tag.type && tag.type !== FieldType.String) ||
(tag.format && tag.format !== FieldFormat.NotSpecified); (tag.format && tag.format !== FieldFormat.NotSpecified);
} }

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

@ -9,7 +9,8 @@ import { ITableRegion, ITableTag, ITag, TagInputMode } from "../../../../models/
enum Categories { enum Categories {
General, General,
Separator, Separator,
Modifier, RenameModifier,
MoveModifier,
} }
/** Properties for tag input toolbar */ /** Properties for tag input toolbar */
@ -30,6 +31,7 @@ export interface ITagInputToolbarProps {
onDelete: (tag: ITag) => void; onDelete: (tag: ITag) => void;
/** Function to call when one of the re-order buttons is clicked */ /** Function to call when one of the re-order buttons is clicked */
onReorder: (tag: ITag, displacement: number) => void; onReorder: (tag: ITag, displacement: number) => void;
searchingTags: boolean;
} }
interface ITagInputToolbarItemProps { interface ITagInputToolbarItemProps {
@ -76,38 +78,44 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
{ {
displayName: strings.tags.toolbar.rename, displayName: strings.tags.toolbar.rename,
icon: "Rename", icon: "Rename",
category: Categories.Modifier, category: Categories.RenameModifier,
handler: this.handleRename, handler: this.handleRename,
}, },
{ {
displayName: strings.tags.toolbar.moveUp, displayName: strings.tags.toolbar.moveUp,
icon: "Up", icon: "Up",
category: Categories.Modifier, category: Categories.MoveModifier,
handler: this.handleMoveUp, handler: this.handleMoveUp,
}, },
{ {
displayName: strings.tags.toolbar.moveDown, displayName: strings.tags.toolbar.moveDown,
icon: "Down", icon: "Down",
category: Categories.Modifier, category: Categories.MoveModifier,
handler: this.handleMoveDown, handler: this.handleMoveDown,
}, },
{ {
displayName: strings.tags.toolbar.delete, displayName: strings.tags.toolbar.delete,
icon: "Delete", icon: "Delete",
category: Categories.Modifier, category: Categories.MoveModifier,
handler: this.handleDelete, handler: this.handleDelete,
}, },
]; ];
} }
private renderItems = () => { private renderItems = () => {
const modifierDisabled = !this.props.selectedTag; const moveModifierDisabled = !this.props.selectedTag || this.props.searchingTags;
const modifierClassNames = ["tag-input-toolbar-iconbutton"]; const renameModifierDisabled = !this.props.selectedTag;
if (modifierDisabled) { const moveModifierClassNames = ["tag-input-toolbar-iconbutton"];
modifierClassNames.push("tag-input-toolbar-iconbutton-disabled"); 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( return(
this.getToolbarItems().map((itemConfig, index) => { this.getToolbarItems().map((itemConfig, index) => {
@ -124,14 +132,25 @@ export default class TagInputToolbar extends React.Component<ITagInputToolbarPro
); );
} else if (itemConfig.category === Categories.Separator) { } else if (itemConfig.category === Categories.Separator) {
return (<div className="tag-input-toolbar-separator" key={itemConfig.displayName}></div>); return (<div className="tag-input-toolbar-separator" key={itemConfig.displayName}></div>);
} else if (itemConfig.category === Categories.Modifier) { } else if (itemConfig.category === Categories.RenameModifier) {
return ( return (
<IconButton <IconButton
key={itemConfig.displayName} key={itemConfig.displayName}
disabled={modifierDisabled} disabled={renameModifierDisabled}
title={itemConfig.displayName} title={itemConfig.displayName}
ariaLabel={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}} iconProps={{iconName: itemConfig.icon}}
onClick={(e) => this.onToolbarItemClick(e, itemConfig)} /> onClick={(e) => this.onToolbarItemClick(e, itemConfig)} />
); );

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

@ -22,6 +22,7 @@
background-color: $darker-1; background-color: $darker-1;
border: solid 1px $lighter-2; border: solid 1px $lighter-2;
color: rgb(0, 161, 241); color: rgb(0, 161, 241);
z-index: 10;
&:hover, &.active { &:hover, &.active {
background-color: $darker-2; background-color: $darker-2;
@ -39,14 +40,14 @@
.prev { .prev {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 0; left: 50px;
margin-left: 10px; margin-left: 10px;
} }
.next { .next {
position: absolute; position: absolute;
top: 50%; top: 50%;
right: 0; right: 50px;
margin-right: 10px; margin-right: 10px;
} }
@ -77,6 +78,7 @@
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
text-align: center; text-align: center;
display: flex; display: flex;
z-index: 11;
} }
.canvas-ocr-loading-spinner { .canvas-ocr-loading-spinner {

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

@ -9,7 +9,7 @@ import {
EditorMode, IAssetMetadata, EditorMode, IAssetMetadata,
IProject, IRegion, RegionType, IProject, IRegion, RegionType,
AssetType, ILabelData, ILabel, 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"; } from "../../../../models/applicationState";
import CanvasHelpers from "./canvasHelpers"; import CanvasHelpers from "./canvasHelpers";
import { AssetPreview } from "../../common/assetPreview/assetPreview"; import { AssetPreview } from "../../common/assetPreview/assetPreview";
@ -37,7 +37,8 @@ import { TooltipHost, ITooltipHostStyles } from "@fluentui/react";
import { IAppSettings } from '../../../../models/applicationState'; import { IAppSettings } from '../../../../models/applicationState';
import { AutoLabelingStatus, PredictService } from "../../../../services/predictService"; import { AutoLabelingStatus, PredictService } from "../../../../services/predictService";
import { AssetService } from "../../../../services/assetService"; 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); pdfjsLib.GlobalWorkerOptions.workerSrc = constants.pdfjsWorkerSrc(pdfjsLib.version);
@ -55,11 +56,13 @@ export interface ICanvasProps extends React.Props<Canvas> {
closeTableView?: (state: string) => void; closeTableView?: (state: string) => void;
onAssetMetadataChanged?: (assetMetadata: IAssetMetadata) => void; onAssetMetadataChanged?: (assetMetadata: IAssetMetadata) => void;
onSelectedRegionsChanged?: (regions: IRegion[]) => void; onSelectedRegionsChanged?: (regions: IRegion[]) => void;
onRegionDoubleClick?: (region: IRegion) => void;
onCanvasRendered?: (canvas: HTMLCanvasElement) => void; onCanvasRendered?: (canvas: HTMLCanvasElement) => void;
onRunningOCRStatusChanged?: (isRunning: boolean) => void; onRunningOCRStatusChanged?: (isRunning: boolean) => void;
onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void; onRunningAutoLabelingStatusChanged?: (isRunning: boolean) => void;
onTagChanged?: (oldTag: ITag, newTag: ITag) => void; onTagChanged?: (oldTag: ITag, newTag: ITag) => void;
runOcrForAllDocs?: (runForAllDocs: boolean) => void; runOcrForAllDocs?: (runForAllDocs: boolean) => void;
runAutoLabelingOnNextBatch?: () => Promise<void>;
onAssetDeleted?: () => void; onAssetDeleted?: () => void;
handleLabelTable?: () => 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>) => { public componentDidUpdate = async (prevProps: Readonly<ICanvasProps>, prevState: Readonly<ICanvasState>) => {
// Handles asset changing // Handles asset changing
if (this.props.selectedAsset.asset.name !== prevProps.selectedAsset.asset.name || 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.selectedRegionIds = [];
this.imageMap.removeAllFeatures(); this.imageMap.removeAllFeatures();
this.imageMap.resetAllLayerVisibility(); this.imageMap.resetAllLayerVisibility();
@ -253,10 +258,12 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
handleAssetDeleted={this.props.onAssetDeleted} handleAssetDeleted={this.props.onAssetDeleted}
handleRunOcrForAllDocuments={this.runOcrForAllDocuments} handleRunOcrForAllDocuments={this.runOcrForAllDocuments}
handleRunAutoLabelingOnCurrentDocument={this.runAutoLabelingOnCurrentDocument} handleRunAutoLabelingOnCurrentDocument={this.runAutoLabelingOnCurrentDocument}
connectionType={this.props.project.sourceConnection.providerType} handleRunAutoLabelingForRestDocuments={this.runAutoLabelingForRestDocuments}
handleToggleDrawRegionMode={this.handleToggleDrawRegionMode} handleToggleDrawRegionMode={this.handleToggleDrawRegionMode}
connectionType={this.props.project.sourceConnection.providerType}
drawRegionMode={this.state.drawRegionMode} drawRegionMode={this.state.drawRegionMode}
project={this.props.project} project={this.props.project}
selectedAsset={this.props.selectedAsset}
parentPage={strings.editorPage.title} parentPage={strings.editorPage.title}
/> />
<ImageMap <ImageMap
@ -267,6 +274,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
imageHeight={this.state.imageHeight} imageHeight={this.state.imageHeight}
enableFeatureSelection={!this.state.drawRegionMode && !this.state.groupSelectMode} enableFeatureSelection={!this.state.drawRegionMode && !this.state.groupSelectMode}
handleFeatureSelect={this.handleFeatureSelect} handleFeatureSelect={this.handleFeatureSelect}
handleFeatureDoubleClick={this.handleFeatureDoubleClick}
featureStyler={this.featureStyler} featureStyler={this.featureStyler}
groupSelectMode={this.state.groupSelectMode} groupSelectMode={this.state.groupSelectMode}
handleIsPointerOnImage={this.handleIsPointerOnImage} handleIsPointerOnImage={this.handleIsPointerOnImage}
@ -370,16 +378,19 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
const assetPath = asset.path; const assetPath = asset.path;
const predictService = new PredictService(this.props.project); const predictService = new PredictService(this.props.project);
const result = await predictService.getPrediction(assetPath); const result = await predictService.getPrediction(assetPath);
const assetService = new AssetService(this.props.project); const assetService = new AssetService(this.props.project);
await assetService.uploadAssetPredictResult(asset, result); const assetMetadata = assetService.getAssetPredictMetadata(asset, result);
const assetMetadata = await assetService.getAssetMetadata(asset);
await this.props.onAssetMetadataChanged(assetMetadata); await this.props.onAssetMetadataChanged(assetMetadata);
} }
finally { finally {
this.setAutoLabelingStatus(AutoLabelingStatus.done); this.setAutoLabelingStatus(AutoLabelingStatus.done);
} }
} }
private runAutoLabelingForRestDocuments = async () => {
this.setState({ autoLableingStatus: AutoLabelingStatus.running });
await this.props.runAutoLabelingOnNextBatch();
this.setState({ autoLableingStatus: AutoLabelingStatus.done });
}
public updateSize() { public updateSize() {
this.imageMap.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) => { const filteredRegions = this.state.currentAsset.regions.filter((assetRegion) => {
return regions.findIndex((r) => r.id === assetRegion.id) === -1; return regions.findIndex((r) => r.id === assetRegion.id) === -1;
}); });
this.updateAssetRegions(filteredRegions); this.updateAssetRegions(filteredRegions, regions.length > 0);
} }
private deleteRegionsFromImageMap = (regions: IRegion[]) => { private deleteRegionsFromImageMap = (regions: IRegion[]) => {
@ -606,7 +617,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
* @param regions * @param regions
* @param selectedRegions * @param selectedRegions
*/ */
private updateAssetRegions = (regions: IRegion[]) => { private updateAssetRegions = (regions: IRegion[], manualOption: boolean = false) => {
const labelData = this.convertRegionsToLabelData(regions, this.state.currentAsset.asset.name); const labelData = this.convertRegionsToLabelData(regions, this.state.currentAsset.asset.name);
console.log("Canvas -> privateupdateAssetRegions -> labelData", labelData) console.log("Canvas -> privateupdateAssetRegions -> labelData", labelData)
const currentAsset: IAssetMetadata = { const currentAsset: IAssetMetadata = {
@ -621,6 +632,41 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
(region) => region.tags[0] !== undefined && (region) => region.tags[0] !== undefined &&
region.pageNumber === this.state.currentPage)); 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({ this.setState({
currentAsset, currentAsset,
}, () => { }, () => {
@ -640,7 +686,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
const deletedRegionIndex = currentRegions.findIndex((region) => region.id === id); const deletedRegionIndex = currentRegions.findIndex((region) => region.id === id);
currentRegions.splice(deletedRegionIndex, 1); 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); 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 * 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) { for (const update of updates) {
const region = regions.find((r) => r.id === update.id); const region = regions.find((r) => r.id === update.id);
if (region) { if (region) {
// skip region.changed = true;
} else { } else {
updatedRegions.push(update); updatedRegions.push(update);
} }
} }
console.log("Canvas -> privateupdateRegions -> updatedRegions", updatedRegions) console.log("Canvas -> privateupdateRegions -> updatedRegions", updatedRegions)
updatedRegions.sort(this.compareRegionOrder); updatedRegions.sort(this.compareRegionOrder);
this.updateAssetRegions(updatedRegions); this.updateAssetRegions(updatedRegions, true);
} }
private createBoundingBoxVectorFeature = (text, boundingBox, imageExtent, ocrExtent, page) => { private createBoundingBoxVectorFeature = (text, boundingBox, imageExtent, ocrExtent, page) => {
@ -1021,6 +1073,12 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
} }
this.redrawAllFeatures(); 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) => { private handleMultiSelection = (regionId: any, category: FeatureCategory) => {
const selectedRegions = this.getSelectedRegions(); const selectedRegions = this.getSelectedRegions();
@ -1174,7 +1232,7 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
return; return;
} }
try { 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) { if (asset.id === this.state.currentAsset.asset.id) {
// since get OCR is async, we only set currentAsset's OCR // since get OCR is async, we only set currentAsset's OCR
this.setState({ this.setState({
@ -1340,6 +1398,13 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
} }
private convertRegionsToLabelData = (regions: IRegion[], assetName: string) => { 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 = { const labelData: ILabelData = {
document: decodeURIComponent(assetName).split("/").pop(), document: decodeURIComponent(assetName).split("/").pop(),
labels: [] as ILabel[], labels: [] as ILabel[],
@ -1347,66 +1412,76 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}; };
regions.forEach((region) => { regions.forEach((region) => {
const labelType = this.getLabelType(region.category); const labelType = this.getLabelType(region.category);
const boundingBox = region.id.split(",").map(parseFloat); const boundingBox = region.id.split(",").map(parseFloat);
const formRegion = { const formRegion = {
page: region.pageNumber, page: region.pageNumber,
text: region.value, text: region.value,
boundingBoxes: [boundingBox], boundingBoxes: [boundingBox],
} as IFormRegion; } as IFormRegion;
region.tags.forEach((tag) => { region.tags.forEach((tag) => {
if (region.isTableRegion) { if (region.isTableRegion) {
const tableRegion = region as ITableRegion; const tableRegion = region as ITableRegion;
const tableLabel: ITableLabel = labelData.tableLabels.find((tableLabel) => { return tableLabel.tableKey === tag }); const tableLabel: ITableLabel = labelData.tableLabels.find((tableLabel) => tableLabel.tableKey === tag);
if (tableLabel) { if (tableLabel) {
const tableLabelCell = tableLabel.labels.find((tableLabelCell) => { return tableLabelCell.columnKey === tableRegion.columnKey &&tableLabelCell.rowKey === tableRegion.rowKey }); const tableLabelCell = tableLabel.labels.find((tableLabelCell) => tableLabelCell.columnKey === tableRegion.columnKey && tableLabelCell.rowKey === tableRegion.rowKey);
if (tableLabelCell) { if (tableLabelCell) {
tableLabelCell.value.push(formRegion) tableLabelCell.value.push(formRegion)
} else {
tableLabel.labels.push({
rowKey: tableRegion.rowKey,
columnKey: tableRegion.columnKey,
value: [formRegion]
});
}
} else { } else {
const tableCellLabel: ITableCellLabel = { tableLabel.labels.push({
rowKey: tableRegion.rowKey, rowKey: tableRegion.rowKey,
columnKey: tableRegion.columnKey, columnKey: tableRegion.columnKey,
value: [formRegion] value: [formRegion]
} });
labelData.tableLabels.push({
tableKey: tag,
labels: [tableCellLabel]
})
} }
} else { } else {
const label = labelData.labels.find((label) => { return label.label === tag }); const tableCellLabel: ITableCellLabel = {
if (label) { rowKey: tableRegion.rowKey,
label.value.push(formRegion); columnKey: tableRegion.columnKey,
} else { value: [formRegion]
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);
} }
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) => { private getLabelType = (regionCategory: string) => {
@ -1650,10 +1725,20 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
} else if (newLabels.length > 0) { } else if (newLabels.length > 0) {
const newFieldNames = newLabels.map((label) => label.label); const newFieldNames = newLabels.map((label) => label.label);
const prevFieldNames = prevLabels.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) => { 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); const tag: ITag = this.props.project.tags.find((tag) => tag.name === tagName);
let regionCategory: string; let regionCategory: string;
if (labelType) { if (labelType) {
regionCategory = labelType; regionCategory = labelType;
@ -2057,11 +2141,17 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
} }
const prevTypes = {}; 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 = {}; 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) { for (const name of names) {
const prevType = prevTypes[name]; const prevType = prevTypes[name];
const type = types[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 // some tag change between checkbox and text
return true; return true;
} }
const prevColor = prevColors[name];
const color = colors[name];
if (prevColor !== color) {
// some tag color changed
return true;
}
} }
return false; return false;
@ -2125,6 +2221,9 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
} }
private handleToggleDrawRegionMode = () => { 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({ this.setState({
drawRegionMode: !this.state.drawRegionMode drawRegionMode: !this.state.drawRegionMode
}); });
@ -2240,4 +2339,11 @@ export default class Canvas extends React.Component<ICanvasProps, ICanvasState>
}); });
this.imageMap.modifyStartFeatureCoordinates = {}; 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 { CommandBar, ICommandBarItemProps } from "@fluentui/react/lib/CommandBar";
import { ICustomizations, Customizer } from "@fluentui/react/lib/Utilities"; import { ICustomizations, Customizer } from "@fluentui/react/lib/Utilities";
import { getDarkGreyTheme } from "../../../../common/themes"; import { getDarkGreyTheme } from "../../../../common/themes";
import { strings } from '../../../../common/strings'; import { interpolate, strings } from '../../../../common/strings';
import { ContextualMenuItemType } from "@fluentui/react"; import { ContextualMenuItemType } from "@fluentui/react";
import { IProject } from "../../../../models/applicationState"; import { IProject, IAssetMetadata, AssetLabelingState } from "../../../../models/applicationState";
import _ from "lodash";
import "./canvasCommandBar.scss"; import "./canvasCommandBar.scss";
import { constants } from "../../../../common/constants";
interface ICanvasCommandBarProps { interface ICanvasCommandBarProps {
handleZoomIn: () => void; handleZoomIn: () => void;
handleZoomOut: () => void; handleZoomOut: () => void;
handleRunAutoLabelingOnCurrentDocument?: () => void;
project: IProject;
handleRotateImage: (degrees: number) => void;
handleRunOcr?: () => void; handleRunOcr?: () => void;
handleRunOcrForAllDocuments?: () => void; handleRunOcrForAllDocuments?: () => void;
handleRunAutoLabelingOnCurrentDocument?: () => void;
handleRunAutoLabelingForRestDocuments?: () => void;
handleLayerChange?: (layer: string) => void; handleLayerChange?: (layer: string) => void;
handleToggleDrawRegionMode?: () => void; handleToggleDrawRegionMode?: () => void;
handleAssetDeleted?: () => void;
project: IProject;
selectedAsset?: IAssetMetadata;
handleRotateImage: (degrees: number) => void;
drawRegionMode?: boolean; drawRegionMode?: boolean;
connectionType?: string; connectionType?: string;
handleAssetDeleted?: () => void;
layers?: any; layers?: any;
parentPage: string; parentPage: string;
} }
@ -31,6 +36,14 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
}, },
scopedSettings: {}, 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[] = []; let commandBarItems: ICommandBarItemProps[] = [];
if (props.parentPage === strings.editorPage.title) { if (props.parentPage === strings.editorPage.title) {
@ -64,16 +77,16 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
isChecked: props.layers["checkboxes"], isChecked: props.layers["checkboxes"],
onClick: () => props.handleLayerChange("checkboxes"), onClick: () => props.handleLayerChange("checkboxes"),
}, },
// { {
// key: "DrawnRegions", key: "DrawnRegions",
// text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.drawnRegions, text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.drawnRegions,
// canCheck: true, canCheck: true,
// iconProps: { iconName: "AddField" }, iconProps: { iconName: "AddField" },
// isChecked: props.layers["drawnRegions"], isChecked: props.layers["drawnRegions"],
// className: props.drawRegionMode ? "disabled" : "", className: props.drawRegionMode ? "disabled" : "",
// onClick: () => props.handleLayerChange("drawnRegions"), onClick: () => props.handleLayerChange("drawnRegions"),
// disabled: props.drawRegionMode disabled: props.drawRegionMode
// }, },
{ {
key: "Label", key: "Label",
text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.labels, text: strings.editorPage.canvas.canvasCommandBar.items.layers.subMenuItems.labels,
@ -85,16 +98,16 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
], ],
}, },
}, },
// { {
// key: "drawRegion", key: "drawRegion",
// text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion, text: strings.editorPage.canvas.canvasCommandBar.items.drawRegion,
// iconProps: { iconName: "AddField" }, iconProps: { iconName: "AddField" },
// toggle: true, toggle: true,
// checked: props.drawRegionMode, checked: props.drawRegionMode,
// className: !props.layers["drawnRegions"] ? "disabled" : "", className: !props.layers["drawnRegions"] ? "disabled" : "",
// onClick: () => props.handleToggleDrawRegionMode(), onClick: () => props.handleToggleDrawRegionMode(),
// disabled: !props.layers["drawnRegions"], disabled: !props.layers["drawnRegions"],
// } }
]; ];
} }
@ -155,23 +168,34 @@ export const CanvasCommandBar: React.FunctionComponent<ICanvasCommandBarProps> =
key: "runOcrForCurrentDocument", key: "runOcrForCurrentDocument",
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnCurrentDocument, text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnCurrentDocument,
iconProps: { iconName: "TextDocument" }, iconProps: { iconName: "TextDocument" },
onClick: () => props.handleRunOcr(), onClick: () => { if (props.handleRunOcr) props.handleRunOcr(); },
}, },
{ {
key: "runOcrForAllDocuments", key: "runOcrForAllDocuments",
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnAllDocuments, text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runOcrOnAllDocuments,
iconProps: { iconName: "Documentation" }, iconProps: { iconName: "Documentation" },
onClick: () => props.handleRunOcrForAllDocuments(), onClick: () => { if (props.handleRunOcrForAllDocuments) props.handleRunOcrForAllDocuments(); },
}, },
{ {
key: "runAutoLabelingCurrentDocument", key: "runAutoLabelingCurrentDocument",
text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingCurrentDocument, text: strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.runAutoLabelingCurrentDocument,
iconProps: { iconName: "Tag" }, iconProps: { iconName: "Tag" },
disabled: !props.project.predictModelId, disabled: disableAutoLabelingCurrentAsset,
title: props.project.predictModelId ? "" : title: props.project.predictModelId ? "" :
strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject, strings.editorPage.canvas.canvasCommandBar.farItems.additionalActions.subIMenuItems.noPredictModelOnProject,
onClick: () => { 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", key: "deleteAsset",
text: strings.editorPage.asset.delete.title, text: strings.editorPage.asset.delete.title,
iconProps: { iconName: "Delete" }, iconProps: { iconName: "Delete" },
onClick: () => props.handleAssetDeleted(), onClick: () => { if (props.handleAssetDeleted) props.handleAssetDeleted(); },
} }
], ],
}, },

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

@ -232,6 +232,18 @@ canvas {
.badge-tagged { .badge-tagged {
background-color: rgba(green, 0.9); background-color: rgba(green, 0.9);
border: 1px solid $lighter-2; 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 { .badge-visited {

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

@ -13,7 +13,7 @@ import { strings, interpolate } from "../../../../common/strings";
import { import {
AssetState, AssetType, EditorMode, FieldType, AssetState, AssetType, EditorMode, FieldType,
IApplicationState, IAppSettings, IAsset, IAssetMetadata, 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"; } from "../../../../models/applicationState";
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions"; import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions"; import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
@ -89,6 +89,7 @@ export interface IEditorPageState {
hoveredLabel: ILabel; hoveredLabel: ILabel;
/** Whether the task for loading all OCRs is running */ /** Whether the task for loading all OCRs is running */
isRunningOCRs?: boolean; isRunningOCRs?: boolean;
isRunningAutoLabelings?: boolean;
/** Whether OCR is running in the main canvas */ /** Whether OCR is running in the main canvas */
isCanvasRunningOCR?: boolean; isCanvasRunningOCR?: boolean;
isCanvasRunningAutoLabeling?: boolean; isCanvasRunningAutoLabeling?: boolean;
@ -301,6 +302,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
onAssetMetadataChanged={this.onAssetMetadataChanged} onAssetMetadataChanged={this.onAssetMetadataChanged}
onCanvasRendered={this.onCanvasRendered} onCanvasRendered={this.onCanvasRendered}
onSelectedRegionsChanged={this.onSelectedRegionsChanged} onSelectedRegionsChanged={this.onSelectedRegionsChanged}
onRegionDoubleClick={this.onRegionDoubleClick}
onRunningOCRStatusChanged={this.onCanvasRunningOCRStatusChanged} onRunningOCRStatusChanged={this.onCanvasRunningOCRStatusChanged}
onRunningAutoLabelingStatusChanged={this.onCanvasRunningAutoLabelingStatusChanged} onRunningAutoLabelingStatusChanged={this.onCanvasRunningAutoLabelingStatusChanged}
onTagChanged={this.onTagChanged} onTagChanged={this.onTagChanged}
@ -312,6 +314,7 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
setTableToView={this.setTableToView} setTableToView={this.setTableToView}
closeTableView={this.closeTableView} closeTableView={this.closeTableView}
runOcrForAllDocs={this.loadOcrForNotVisited} runOcrForAllDocs={this.loadOcrForNotVisited}
runAutoLabelingOnNextBatch={this.runAutoLabelingOnNextBatch}
appSettings={this.props.appSettings} appSettings={this.props.appSettings}
handleLabelTable={this.handleLabelTable} handleLabelTable={this.handleLabelTable}
> >
@ -349,7 +352,8 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
splitPaneWidth={this.state.rightSplitPaneWidth} splitPaneWidth={this.state.rightSplitPaneWidth}
reconfigureTableConfirm={this.reconfigureTableConfirm} reconfigureTableConfirm={this.reconfigureTableConfirm}
addRowToDynamicTable={this.addRowToDynamicTable} addRowToDynamicTable={this.addRowToDynamicTable}
/> onTagDoubleClick={this.onLabelDoubleClicked}
/>
<Confirm <Confirm
title={strings.editorPage.tags.rename.title} title={strings.editorPage.tags.rename.title}
ref={this.renameTagConfirm} 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 * This can either be a parent or child asset
*/ */
private onAssetMetadataChanged = async (assetMetadata: IAssetMetadata): Promise<void> => { 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. // 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 initialState = assetMetadata.asset.state;
const asset = { ...assetMetadata.asset }; const asset = { ...assetMetadata.asset };
console.log("EditorPage -> asset", asset) // console.log("EditorPage -> asset", asset)
if (this.isTaggableAssetType(assetMetadata.asset)) { if (this.isTaggableAssetType(asset)) {
const hasLabels = _.get(assetMetadata, "labelData.labels.length", 0) > 0; const hasLabels = _.get(assetMetadata, "labelData.labels.length", 0) > 0;
const hasTableLabels = _.get(assetMetadata, "labelData.tableLabels.length", 0) > 0 const hasTableLabels = _.get(assetMetadata, "labelData.tableLabels.length", 0) > 0;
assetMetadata.asset.state = hasLabels || hasTableLabels ? asset.state = hasLabels || hasTableLabels ?
AssetState.Tagged : AssetState.Tagged :
AssetState.Visited; AssetState.Visited;
} else if (assetMetadata.asset.state === AssetState.NotVisited) { } else if (asset.state === AssetState.NotVisited) {
assetMetadata.asset.state = AssetState.Visited; asset.state = AssetState.Visited;
} }
// Only update asset metadata if state changes or is different // Only update asset metadata if state changes or is different
if (initialState !== assetMetadata.asset.state || this.state.selectedAsset !== assetMetadata) { if (initialState !== asset.state || this.state.selectedAsset !== assetMetadata) {
if (this.state.selectedAsset?.labelData?.labels && assetMetadata?.labelData?.labels && if (this.state.selectedAsset?.labelData?.labels && assetMetadata?.labelData?.labels && assetMetadata.labelData.labels.toString() !== this.state.selectedAsset.labelData.labels.toString()) {
assetMetadata.labelData.labels.toString() !== this.state.selectedAsset.labelData.labels.toString()) {
await this.updatedAssetMetadata(assetMetadata); await this.updatedAssetMetadata(assetMetadata);
} }
assetMetadata.asset = asset;
await this.props.actions.saveAssetMetadata(this.props.project, assetMetadata); 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 }); 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 // This forces the root assets that are displayed in the sidebar to
// accurately show their correct state (not-visited, visited or tagged) // accurately show their correct state (not-visited, visited or tagged)
const assets = [...this.state.assets]; const assets = [...this.state.assets];
// const asset = { ...assetMetadata.asset };
const assetIndex = assets.findIndex((a) => a.id === asset.id); const assetIndex = assets.findIndex((a) => a.id === asset.id);
if (assetIndex > -1) { if (assetIndex > -1) {
assets[assetIndex] = { assets[assetIndex] = {
@ -783,6 +788,12 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
this.setState({ selectedRegions }); this.setState({ selectedRegions });
} }
private onRegionDoubleClick = (region: IRegion) => {
if (region.tags?.length > 0) {
this.tagInputRef.current.focusTag(region.tags[0]);
}
}
private onTagsChanged = async (tags) => { private onTagsChanged = async (tags) => {
const project = { const project = {
...this.props.project, ...this.props.project,
@ -816,6 +827,9 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
if (this.state.isCanvasRunningAutoLabeling) { if (this.state.isCanvasRunningAutoLabeling) {
return; return;
} }
if (this.state.isRunningAutoLabelings) {
return;
}
const assetMetadata = await this.props.actions.loadAssetMetadata(this.props.project, asset); 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); const asset = this.state.assets.find((asset) => asset.id === assetId);
if (asset && (asset.state === AssetState.NotVisited || runForAll)) { if (asset && (asset.state === AssetState.NotVisited || runForAll)) {
try { try {
this.updateAssetState(asset.id, true); this.updateAssetState({ id: asset.id, isRunningOCR: true });
await ocrService.getRecognizedText(asset.path, asset.name, undefined, runForAll); await ocrService.getRecognizedText(asset.path, asset.name, asset.mimeType, undefined, runForAll);
this.updateAssetState(asset.id, false, AssetState.Visited); this.updateAssetState({ id: asset.id, isRunningOCR: false, assetState: AssetState.Visited });
} catch (err) { } catch (err) {
this.updateAssetState(asset.id, false); this.updateAssetState({ id: asset.id, isRunningOCR: false });
this.setState({ this.setState({
isError: true, isError: true,
errorTitle: err.title, 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) => ({ this.setState((state) => ({
assets: state.assets.map((asset) => { assets: state.assets.map((asset) => {
if (asset.id === id) { if (asset.id === newState.id) {
const updatedAsset = { ...asset, isRunningOCR }; const updatedAsset = { ...asset, isRunningOCR: newState.isRunningOCR || false };
if (assetState !== undefined && asset.state === AssetState.NotVisited) { if (newState.assetState !== undefined && asset.state === AssetState.NotVisited) {
updatedAsset.state = assetState; updatedAsset.state = newState.assetState;
}
if (newState.labelingState) {
updatedAsset.labelingState = newState.labelingState;
}
if (newState.isRunningAutoLabeling !== undefined) {
updatedAsset.isRunningAutoLabeling = newState.isRunningAutoLabeling;
} }
return updatedAsset; return updatedAsset;
} else { } 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 === newState.id);
const asset = this.state.assets.find((asset) => asset.id === id); if (this.state.selectedAsset && newState.id === this.state.selectedAsset.asset.id) {
if (asset) { if (asset) {
this.setState({ this.setState({
selectedAsset: { ...this.state.selectedAsset, asset: { ...asset } }, selectedAsset: { ...this.state.selectedAsset, asset: { ...asset } },
@ -953,24 +1028,56 @@ export default class EditorPage extends React.Component<IEditorPageProps, IEdito
const updatedAssets = [...this.state.assets]; const updatedAssets = [...this.state.assets];
let needUpdate = false; let needUpdate = false;
updatedAssets.forEach((asset) => { 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 (projectAsset) {
if (asset.state !== projectAsset.state) { if (asset.state !== projectAsset.state || asset.labelingState !== projectAsset.labelingState) {
needUpdate = true; needUpdate = true;
asset.state = projectAsset.state; asset.state = projectAsset.state;
asset.labelingState = projectAsset.labelingState;
} }
} }
}); });
if (needUpdate) { if (needUpdate) {
this.setState({ assets: updatedAssets }); 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) => { private onLabelEnter = (label: ILabel) => {
this.setState({ hoveredLabel: label }); this.setState({ hoveredLabel: label });
} }
private onLabelDoubleClicked = (label:ILabel) =>{
this.canvas.current.focusOnLabel(label);
}
private onLabelLeave = (label: ILabel) => { private onLabelLeave = (label: ILabel) => {
this.setState({ hoveredLabel: null }); this.setState({ hoveredLabel: null });
} }

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

@ -4,9 +4,10 @@
import React from "react"; import React from "react";
import { AutoSizer, List } from "react-virtualized"; import { AutoSizer, List } from "react-virtualized";
import { FontIcon } from "@fluentui/react"; import { FontIcon } from "@fluentui/react";
import { IAsset, AssetState, ISize } from "../../../../models/applicationState"; import { IAsset, AssetState, ISize, AssetLabelingState } from "../../../../models/applicationState";
import {AssetPreview, ContentSource} from "../../common/assetPreview/assetPreview"; import { AssetPreview, ContentSource } from "../../common/assetPreview/assetPreview";
import { strings } from "../../../../common/strings"; import { strings } from "../../../../common/strings";
import _ from "lodash";
/** /**
* Properties for Editor Side Bar * Properties for Editor Side Bar
@ -135,11 +136,14 @@ export default class EditorSideBar extends React.Component<IEditorSideBarProps,
} }
private renderBadges = (asset: IAsset): JSX.Element => { private renderBadges = (asset: IAsset): JSX.Element => {
const getBadgeTaggedClass = (state: AssetLabelingState): string => {
return state ? `badge-tagged-${AssetLabelingState[state]}` : "";
};
switch (asset.state) { switch (asset.state) {
case AssetState.Tagged: case AssetState.Tagged:
return ( return (
<span title={strings.editorPage.tagged} <span title={_.startCase(AssetLabelingState[asset.labelingState])}
className="badge badge-tagged"> className={["badge", "badge-tagged", getBadgeTaggedClass(asset.labelingState)].join(" ")}>
<FontIcon iconName="Tag" /> <FontIcon iconName="Tag" />
</span> </span>
); );

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

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

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

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

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

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

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

@ -649,6 +649,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
handleRotateImage={this.handleRotateCanvas} handleRotateImage={this.handleRotateCanvas}
project={this.props.project} project={this.props.project}
parentPage={"predict"} parentPage={"predict"}
layers={{}}
/> />
<ImageMap <ImageMap
parentPage={ImageMapParent.Predict} parentPage={ImageMapParent.Predict}
@ -767,7 +768,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
case "<model_id>": case "<model_id>":
return modelID; return modelID;
case "<API_version>": case "<API_version>":
return constants.apiVersion; return (this.props.project?.apiVersion || constants.apiVersion);
} }
}); });
const fileURL = window.URL.createObjectURL( const fileURL = window.URL.createObjectURL(
@ -811,7 +812,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
} }
const endpointURL = url.resolve( const endpointURL = url.resolve(
this.props.project.apiUriBase, 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 headers;
let body; let body;
@ -1037,7 +1038,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
} else if (response.data.status.toLowerCase() === constants.statusCodeFailed) { } else if (response.data.status.toLowerCase() === constants.statusCodeFailed) {
reject(_.get( reject(_.get(
response, response,
"data.analyzeResult.errors[0].errorMessage", "data.analyzeResult.errors[0]",
"Generic error during prediction")); "Generic error during prediction"));
} else if (Number(new Date()) < endTime) { } else if (Number(new Date()) < endTime) {
// If the request isn't succeeded and the timeout hasn't elapsed, go again // 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 () => { private onAddAssetToProjectClick = async () => {
if (this.state.file) { if (this.state.file) {
// this.props.project.assets
const fileName = `${this.props.project.folderPath}/${decodeURIComponent(this.state.file.name)}`; const fileName = `${this.props.project.folderPath}/${decodeURIComponent(this.state.file.name)}`;
const asset = Object.values(this.props.project.assets).find(asset => asset.name === fileName); const asset = Object.values(this.props.project.assets).find(asset => asset.name === fileName);
if (asset) { if (asset) {
@ -1167,7 +1167,7 @@ export default class PredictPage extends React.Component<IPredictPageProps, IPre
const modelID = this.props.project.predictModelId; const modelID = this.props.project.predictModelId;
const endpointURL = url.resolve( const endpointURL = url.resolve(
this.props.project.apiUriBase, this.props.project.apiUriBase,
`${constants.apiModelsPath}/${modelID}`, `${interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) })}/${modelID}`,
); );
let response; let response;
try { try {

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

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

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

@ -26,6 +26,11 @@
"description": "API key", "description": "API key",
"type": "string" "type": "string"
}, },
"apiVersion" : {
"title": "API version",
"description": "API version",
"type": "string"
},
"description": { "description": {
"title": "${strings.common.description}", "title": "${strings.common.description}",
"type": "string" "type": "string"

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

@ -31,6 +31,11 @@
"description": "API key", "description": "API key",
"type": "string" "type": "string"
}, },
"apiVersion" : {
"title": "API version",
"description": "API version",
"type": "string"
},
"description": { "description": {
"title": "${strings.common.description}", "title": "${strings.common.description}",
"type": "string" "type": "string"

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

@ -16,6 +16,7 @@ import { ProjectSettingAction } from "./projectSettingAction";
import { ProtectedInput } from "../../common/protectedInput/protectedInput"; import { ProtectedInput } from "../../common/protectedInput/protectedInput";
import { PrimaryButton } from "@fluentui/react"; import { PrimaryButton } from "@fluentui/react";
import { getPrimaryGreenTheme, getPrimaryGreyTheme } from "../../../../common/themes"; import { getPrimaryGreenTheme, getPrimaryGreyTheme } from "../../../../common/themes";
import { APIVersionPicker, IAPIVersionPickerProps } from "../../common/apiVersionPicker/apiVersionPicker";
// tslint:disable-next-line:no-var-requires // tslint:disable-next-line:no-var-requires
const newFormSchema = addLocValues(require("./newProjectForm.json")); const newFormSchema = addLocValues(require("./newProjectForm.json"));
@ -62,6 +63,7 @@ export interface IProjectFormState {
export default class ProjectForm extends React.Component<IProjectFormProps, IProjectFormState> { export default class ProjectForm extends React.Component<IProjectFormProps, IProjectFormState> {
private widgets = { private widgets = {
protectedInput: (ProtectedInput as any) as Widget, protectedInput: (ProtectedInput as any) as Widget,
apiVersion: (APIVersionPicker as any) as Widget
}; };
constructor(props, context) { constructor(props, context) {
@ -155,6 +157,11 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
onChange: props.onChange, onChange: props.onChange,
}; };
}), }),
apiVersion: CustomField<IAPIVersionPickerProps>(APIVersionPicker, (props) => ({
id: props.idSchema.$id,
value: props.formData,
onChange: props.onChange,
})),
targetConnection: CustomField<IConnectionProviderPickerProps>(ConnectionPickerWithRouter, (props) => { targetConnection: CustomField<IConnectionProviderPickerProps>(ConnectionPickerWithRouter, (props) => {
const targetConnections = this.props.connections const targetConnections = this.props.connections
.filter((connection) => StorageProviderFactory.isRegistered(connection.providerType)); .filter((connection) => StorageProviderFactory.isRegistered(connection.providerType));
@ -209,6 +216,7 @@ export default class ProjectForm extends React.Component<IProjectFormProps, IPro
sourceConnection: args.formData.sourceConnection, sourceConnection: args.formData.sourceConnection,
folderPath: this.normalizeFolderPath(args.formData.folderPath), folderPath: this.normalizeFolderPath(args.formData.folderPath),
apiUriBase: args.formData.apiUriBase.trim(), apiUriBase: args.formData.apiUriBase.trim(),
apiVersion: args.formData.apiVersion,
}; };
this.props.onSubmit(project); this.props.onSubmit(project);
} }

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

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

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

@ -41,8 +41,8 @@
font-size: 90%; font-size: 90%;
@media (max-width: 1920px) { @media (max-width: 1920px) {
display: flex; display: flex;
align-items: center; align-items: center;
} }
.hint-content { .hint-content {
@ -63,7 +63,7 @@
} }
.rv-hint { .rv-hint {
white-space: nowrap; white-space: nowrap;
} }
} }
@ -80,3 +80,18 @@
display: flex; display: flex;
align-items: center; 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 { connect } from "react-redux";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { FontIcon } from "@fluentui/react"; import { FontIcon, Label, Spinner, SpinnerSize } from "@fluentui/react";
import ProjectForm from "./projectForm"; import ProjectForm from "./projectForm";
import { constants } from "../../../../common/constants"; import { constants } from "../../../../common/constants";
import { strings, interpolate } from "../../../../common/strings"; import { strings, interpolate } from "../../../../common/strings";
@ -43,6 +43,7 @@ export interface IProjectSettingsPageState {
project: IProject; project: IProject;
action: ProjectSettingAction; action: ProjectSettingAction;
isError: boolean; isError: boolean;
isCommiting: boolean;
} }
function mapStateToProps(state: IApplicationState) { function mapStateToProps(state: IApplicationState) {
@ -72,6 +73,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
project: this.props.project, project: this.props.project,
action: null, action: null,
isError: false, isError: false,
isCommiting: false,
}; };
public async componentDidMount() { 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 // Hide ProjectMetrics for private-preview
public render() { public render() {
return ( return (
@ -132,6 +139,14 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
{this.state.isError && {this.state.isError &&
<Redirect to="/" /> <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> </div>
); );
} }
@ -171,34 +186,40 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
private onFormChange = (project: IProject) => { private onFormChange = (project: IProject) => {
if (this.isPartialProject(project)) { if (this.isPartialProject(project)) {
setStorageItem(constants.projectFormTempKey, JSON.stringify(project)); setStorageItem(constants.projectFormTempKey, JSON.stringify(project));
this.setState({ project });
} }
} }
private onFormSubmit = async (project: IProject) => { private onFormSubmit = async (project: IProject) => {
const isNew = !(!!project.id); 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 this.isValidProjectName(project, isNew)) {
if (!(await projectService.isValidProjectConnection(project))) { toast.error(interpolate(strings.projectSettings.messages.projectExisted, { project }));
return; 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 = () => { private onFormCancel = () => {
@ -210,7 +231,7 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
* Checks whether a project is partially populated * Checks whether a project is partially populated
*/ */
private isPartialProject = (project: IProject): boolean => { private isPartialProject = (project: IProject): boolean => {
return project && !(!!project.id) && return project &&
( (
!!project.name !!project.name
|| !!project.description || !!project.description

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

@ -5,12 +5,12 @@ import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { RouteComponentProps } from "react-router-dom"; import { RouteComponentProps } from "react-router-dom";
import { bindActionCreators } from "redux"; 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 IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions"; import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions"; import IAppTitleActions, * as appTitleActions from "../../../../redux/actions/appTitleActions";
import { import {
IApplicationState, IConnection, IProject, IAppSettings, FieldType, IRecentModel, IApplicationState, IConnection, IProject, IAppSettings, FieldType, IRecentModel, AssetLabelingState,
} from "../../../../models/applicationState"; } from "../../../../models/applicationState";
import TrainChart from "./trainChart"; import TrainChart from "./trainChart";
import TrainPanel from "./trainPanel"; import TrainPanel from "./trainPanel";
@ -26,6 +26,8 @@ import PreventLeaving from "../../common/preventLeaving/preventLeaving";
import ServiceHelper from "../../../../services/serviceHelper"; import ServiceHelper from "../../../../services/serviceHelper";
import { getPrimaryGreenTheme, getGreenWithWhiteBackgroundTheme } from "../../../../common/themes"; import { getPrimaryGreenTheme, getGreenWithWhiteBackgroundTheme } from "../../../../common/themes";
import { getAppInsights } from '../../../../services/telemetryService'; import { getAppInsights } from '../../../../services/telemetryService';
import { AssetService } from "../../../../services/assetService";
import Confirm from "../../common/confirm/confirm";
import UseLocalStorage from '../../../../services/useLocalStorage'; import UseLocalStorage from '../../../../services/useLocalStorage';
import { isElectron } from "../../../../common/hostProcess"; import { isElectron } from "../../../../common/hostProcess";
@ -80,6 +82,7 @@ function mapDispatchToProps(dispatch) {
@connect(mapStateToProps, mapDispatchToProps) @connect(mapStateToProps, mapDispatchToProps)
export default class TrainPage extends React.Component<ITrainPageProps, ITrainPageState> { export default class TrainPage extends React.Component<ITrainPageProps, ITrainPageState> {
private appInsights: any = null; private appInsights: any = null;
private notAdjustedLabelsConfirm: React.RefObject<Confirm> = React.createRef();
constructor(props) { constructor(props) {
super(props); super(props);
@ -122,7 +125,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
public render() { public render() {
const currTrainRecord = this.state.currTrainRecord; const currTrainRecord = this.state.currTrainRecord;
const localFileSystemProvider: boolean = this.props.project && this.props.project.sourceConnection && 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 ( return (
<div className="train-page skipToMainContent" id="pageTrain"> <div className="train-page skipToMainContent" id="pageTrain">
@ -178,9 +183,9 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
> >
</TextField> </TextField>
{!this.state.isTraining ? ( {!this.state.isTraining ? (
<div className="container-items-end"> <div className="container-items-end">
<PrimaryButton <PrimaryButton
style={{"margin": "15px 0px"}} style={{ "margin": "15px 0px" }}
id="train_trainButton" id="train_trainButton"
theme={getPrimaryGreenTheme()} theme={getPrimaryGreenTheme()}
autoFocus={true} autoFocus={true}
@ -193,16 +198,16 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
</PrimaryButton> </PrimaryButton>
</div> </div>
) : ( ) : (
<div className="loading-container"> <div className="loading-container">
<Spinner <Spinner
label="Training in progress..." label="Training in progress..."
ariaLive="assertive" ariaLive="assertive"
labelPosition="right" labelPosition="right"
size={SpinnerSize.large} size={SpinnerSize.large}
className={"training-spinner"} className={"training-spinner"}
/> />
</div> </div>
) )
} }
</div> </div>
<div className={!this.state.isTraining ? "" : "greyOut"}> <div className={!this.state.isTraining ? "" : "greyOut"}>
@ -212,22 +217,22 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
currTrainRecord={currTrainRecord} currTrainRecord={currTrainRecord}
viewType={this.state.viewType} viewType={this.state.viewType}
updateViewTypeCallback={this.handleViewTypeClick} updateViewTypeCallback={this.handleViewTypeClick}
/> />
<PrimaryButton <PrimaryButton
ariaDescription={strings.train.downloadJson} ariaDescription={strings.train.downloadJson}
style={{ "margin": "2rem auto" }} style={{ "margin": "2rem auto" }}
id="train-download-json_button" id="train-download-json_button"
theme={getPrimaryGreenTheme()} theme={getPrimaryGreenTheme()}
autoFocus={true} autoFocus={true}
className="flex-center" className="flex-center"
onClick={this.handleDownloadJSONClick} onClick={this.handleDownloadJSONClick}
disabled={this.state.isTraining}> disabled={trainDisabled}>
<FontIcon <FontIcon
iconName="Download" iconName="Download"
style={{ fontWeight: 600 }}/> style={{ fontWeight: 600 }} />
<h6 className="d-inline text-shadow-none ml-2 mb-0"> <h6 className="d-inline text-shadow-none ml-2 mb-0">
{strings.train.downloadJson}</h6> {strings.train.downloadJson}</h6>
</PrimaryButton> </PrimaryButton>
</> </>
} }
</div> </div>
@ -244,6 +249,13 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
when={this.state.isTraining} when={this.state.isTraining}
message={"A training operation is currently in progress, are you sure you want to leave?"} 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> </div>
); );
} }
@ -268,7 +280,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
private removeDefaultInputtedLabelFolderURL = () => { private removeDefaultInputtedLabelFolderURL = () => {
if (this.state.inputtedLabelFolderURL === strings.train.defaultLabelFolderURL) { 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 = () => { 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({ this.setState({
isTraining: true, isTraining: true,
trainMessage: strings.train.training, trainMessage: strings.train.training,
}); });
this.trainProcess().then((trainResult) => { this.trainProcess().then(async (trainResult) => {
this.setState((prevState, props) => ({ this.setState((prevState, props) => ({
isTraining: false, isTraining: false,
trainMessage: this.getTrainMessage(trainResult), trainMessage: this.getTrainMessage(trainResult),
currTrainRecord: this.getProjectTrainRecord(), currTrainRecord: this.getProjectTrainRecord(),
modelName: "", 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 // reset localStorage successful train process
localStorage.setItem("trainPage_inputs", "{}"); localStorage.setItem("trainPage_inputs", "{}");
}).catch((err) => { }).catch((err) => {
@ -305,7 +342,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
}); });
}); });
if (this.appInsights) { 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> { private async train(): Promise<any> {
const baseURL = url.resolve( const baseURL = url.resolve(
this.props.project.apiUriBase, this.props.project.apiUriBase,
constants.apiModelsPath, interpolate(constants.apiModelsPath, {apiVersion : (constants.apiVersion || constants.appVersion) }),
); );
const provider = this.props.project.sourceConnection.providerOptions as any; const provider = this.props.project.sourceConnection.providerOptions as any;
let trainSourceURL; let trainSourceURL;
@ -367,7 +404,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
{}, {},
this.props.project.apiKey as string, this.props.project.apiKey as string,
); );
this.setState({modelUrl: result.headers.location}); this.setState({ modelUrl: result.headers.location });
return result; return result;
} catch (err) { } catch (err) {
ServiceHelper.handleServiceError(err); ServiceHelper.handleServiceError(err);
@ -389,8 +426,8 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
private buildUpdatedProject = (newTrainRecord: ITrainRecordProps): IProject => { private buildUpdatedProject = (newTrainRecord: ITrainRecordProps): IProject => {
const recentModelRecords: IRecentModel[] = this.props.project.recentModelRecords ? const recentModelRecords: IRecentModel[] = this.props.project.recentModelRecords ?
[...this.props.project.recentModelRecords] : []; [...this.props.project.recentModelRecords] : [];
recentModelRecords.unshift({...newTrainRecord, isComposed: false} as IRecentModel); recentModelRecords.unshift({ ...newTrainRecord, isComposed: false } as IRecentModel);
if (recentModelRecords.length > constants.recentModelRecordsCount) { if (recentModelRecords.length > constants.recentModelRecordsCount) {
recentModelRecords.pop(); recentModelRecords.pop();
} }
@ -487,7 +524,7 @@ export default class TrainPage extends React.Component<ITrainPageProps, ITrainPa
} }
private async triggerJsonDownload(): Promise<any> { 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 modelUrl = this.state.modelUrl.length ? this.state.modelUrl : currModelUrl;
const modelJSON = await this.getModelsJson(this.props.project, modelUrl); 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])); new Blob([modelJSON]));
const fileLink = document.createElement("a"); const fileLink = document.createElement("a");
const fileBaseName = "model"; 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.href = fileURL;
fileLink.setAttribute("download", downloadFileName); fileLink.setAttribute("download", downloadFileName);

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

@ -41,7 +41,7 @@ export default class TrainRecord extends React.Component<ITrainRecordProps, ITra
</p> </p>
<h6>Average accuracy:</h6> <h6>Average accuracy:</h6>
<p> <p>
{this.props.averageAccuracy} {(this.props.averageAccuracy * 100).toFixed(2)+"%"}
</p> </p>
<div className="accuracy-info"> <div className="accuracy-info">
<a href="https://aka.ms/form-recognizer/docs/train" target="_blank" rel="noopener noreferrer"> <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) => Object.entries(this.props.accuracies).map((entry) =>
<tr key={entry[0]}> <tr key={entry[0]}>
<td>{entry[0]}</td> <td>{entry[0]}</td>
<td className="text-right">{entry[1]}</td> <td className="text-right">{(entry[1] * 100).toFixed(2) + "%"}</td>
</tr>) </tr>)
} }
</tbody> </tbody>

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

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

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

@ -5,18 +5,31 @@ import React from "react";
import { FontIcon } from "@fluentui/react"; import { FontIcon } from "@fluentui/react";
import { constants } from "../../../common/constants"; import { constants } from "../../../common/constants";
import "./statusBar.scss"; 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() { public render() {
return ( return (
<div className="status-bar"> <div className="status-bar">
<div className="status-bar-main">{this.props.children}</div> <div className="status-bar-main">{this.props.children}</div>
<div className="status-bar-version"> <div className="status-bar-version">
<ul> <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> <li>
<a href="https://github.com/microsoft/OCR-Form-Tools/blob/master/CHANGELOG.md" target="blank" rel="noopener noreferrer"> <a href="https://github.com/microsoft/OCR-Form-Tools/blob/master/CHANGELOG.md" target="blank" rel="noopener noreferrer">
<FontIcon iconName="BranchMerge" /> <FontIcon iconName="BranchMerge" />
<span>{constants.appVersion}-b92b73b</span> <span>{constants.appVersionRaw}-1f33130</span>
</a> </a>
</li> </li>
</ul> </ul>

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

@ -152,11 +152,11 @@ export function deleteProject(project: IProject)
.find((securityToken) => securityToken.name === project.securityToken); .find((securityToken) => securityToken.name === project.securityToken);
if (!projectToken) { 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); const decryptedProject = await projectService.load(project, projectToken);
await projectService.delete(decryptedProject); await projectService.delete(decryptedProject);
dispatch(deleteProjectAction(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 assetName = project.folderPath ? `${project.folderPath}/${fileName}` : fileName;
const asset = assets.find(a => a.name === assetName); const asset = assets.find(a => a.name === assetName);
await assetService.uploadAssetPredictResult(asset, analyzeResult); await assetService.syncAssetPredictResult(asset, analyzeResult);
dispatch(addAssetToProjectAction(asset)); dispatch(addAssetToProjectAction(asset));
return asset; return asset;
}; };

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

@ -38,9 +38,9 @@ export const reducer = (state: IProject = null, action: AnyAction): IProject =>
}; };
case ActionTypes.DELETE_PROJECT_ASSET_SUCCESS: case ActionTypes.DELETE_PROJECT_ASSET_SUCCESS:
case ActionTypes.LOAD_PROJECT_ASSETS_SUCCESS: case ActionTypes.LOAD_PROJECT_ASSETS_SUCCESS:
const assets = {}; let assets = {};
action.payload.forEach((asset) => { action.payload.forEach((asset) => {
assets[asset.id] = asset; assets = { ...assets, [asset.id]: { ...asset } };
}); });
return { return {

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

@ -22,6 +22,7 @@ export const reducer = (state: IProject[] = [], action: AnyAction): IProject[] =
let newState: IProject[] = null; let newState: IProject[] = null;
switch (action.type) { switch (action.type) {
case ActionTypes.LOAD_PROJECT_SUCCESS:
case ActionTypes.SAVE_PROJECT_SUCCESS: case ActionTypes.SAVE_PROJECT_SUCCESS:
return [ return [
{ ...action.payload }, { ...action.payload },
@ -38,6 +39,9 @@ export const reducer = (state: IProject[] = [], action: AnyAction): IProject[] =
return updatedProject; return updatedProject;
}); });
return newState; return newState;
case ActionTypes.UPDATE_TAG_LABEL_COUNTS_SUCCESS:
return [{ ...action.payload },
...state.filter(project => project.id !== action.payload.id)];
default: default:
return state; return state;
} }

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

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

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

@ -5,7 +5,7 @@ import _ from "lodash";
import Guard from "../common/guard"; import Guard from "../common/guard";
import { import {
IAsset, AssetType, IProject, IAssetMetadata, AssetState, IAsset, AssetType, IProject, IAssetMetadata, AssetState,
ILabelData, ILabel, ILabelData, ILabel, AssetLabelingState
} from "../models/applicationState"; } from "../models/applicationState";
import { AssetProviderFactory, IAssetProvider } from "../providers/storage/assetProviderFactory"; import { AssetProviderFactory, IAssetProvider } from "../providers/storage/assetProviderFactory";
import { StorageProviderFactory, IStorageProvider } from "../providers/storage/storageProviderFactory"; import { StorageProviderFactory, IStorageProvider } from "../providers/storage/storageProviderFactory";
@ -16,6 +16,9 @@ import { strings, interpolate } from "../common/strings";
import { sha256Hash } from "../common/crypto"; import { sha256Hash } from "../common/crypto";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import allSettled from "promise.allsettled" import allSettled from "promise.allsettled"
import mime from 'mime';
import FileType from 'file-type';
import BrowserFileType from 'file-type/browser';
const supportedImageFormats = { const supportedImageFormats = {
jpg: null, jpeg: null, null: null, png: null, bmp: null, tif: null, tiff: null, pdf: null, 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) { private getOcrFromAnalyzeResult(analyzeResult: any) {
return _.get(analyzeResult, "analyzeResult.readResults", []); 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 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 ocrExtent = [0, 0, ocrForCurrentPage.width, ocrForCurrentPage.height];
const ocrWidth = ocrExtent[2] - ocrExtent[0]; const ocrWidth = ocrExtent[2] - ocrExtent[0];
const ocrHeight = ocrExtent[3] - ocrExtent[1]; const ocrHeight = ocrExtent[3] - ocrExtent[1];
@ -83,7 +87,7 @@ export class AssetService {
const getLabelValues = (field: any) => { const getLabelValues = (field: any) => {
return field.elements.map((path: string) => { return field.elements.map((path: string) => {
const pathArr = path.split('/').slice(1); 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 { return {
page: field.page, page: field.page,
text: word.text || word.state, text: word.text || word.state,
@ -92,59 +96,76 @@ export class AssetService {
}; };
}); });
}; };
const labels = []; const labels =
readResults.analyzeResult.documentResults predictResults.analyzeResult.documentResults
.map(result => Object.keys(result.fields) .map(result => Object.keys(result.fields)
.filter(key => result.fields[key]) .filter(key => result.fields[key])
.map<ILabel>(key => ( .map<ILabel>(key => (
{ {
label: key, label: key,
key: null, key: null,
value: getLabelValues(result.fields[key]) confidence: result.fields[key].confidence,
}))).forEach(items => { value: getLabelValues(result.fields[key])
labels.push(...items); }))).flat(2);
});
if (labels.length > 0) { if (labels.length > 0) {
const fileName = decodeURIComponent(asset.name).split('/').pop(); const fileName = decodeURIComponent(asset.name).split('/').pop();
const labelData: ILabelData = { const labelData: ILabelData = {
document: fileName, document: fileName,
labelingState: AssetLabelingState.AutoLabeled,
labels labels
}; };
const metadata = { const metadata: IAssetMetadata = {
...await this.getAssetMetadata(asset), asset: { ...asset, labelingState: AssetLabelingState.AutoLabeled },
labelData regions: [],
version: appInfo.version,
labelData,
}; };
metadata.asset.state = AssetState.Tagged; metadata.asset.state = AssetState.Tagged;
return metadata;
const ocrData = JSON.parse(JSON.stringify(readResults)); }
delete ocrData.analyzeResult.documentResults; else {
if (ocrData.analyzeResult.errors) { return null;
delete ocrData.analyzeResult.errors; }
} }
const ocrFileName = `${asset.name}${constants.ocrFileExtension}`; async uploadPredictResultAsOrcResult(asset: IAsset, predictResults: any): Promise<void> {
await Promise.all([ const ocrData = JSON.parse(JSON.stringify(predictResults));
this.save(metadata), delete ocrData.analyzeResult.documentResults;
this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)) 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 { else {
const ocrData = { ...readResults };
delete ocrData.analyzeResult.documentResults;
if (ocrData.analyzeResult.errors) {
delete ocrData.analyzeResult.errors;
}
const labelFileName = decodeURIComponent(`${asset.name}${constants.labelFileExtension}`); const labelFileName = decodeURIComponent(`${asset.name}${constants.labelFileExtension}`);
const ocrFileName = decodeURIComponent(`${asset.name}${constants.ocrFileExtension}`);
try { try {
await Promise.all([ await Promise.all([
this.storageProvider.deleteFile(labelFileName, true, true), this.storageProvider.deleteFile(labelFileName, true, true),
this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2)) this.storageProvider.writeText(ocrFileName, JSON.stringify(ocrData, null, 2))
]); ]);
} catch (err) {
// The label file may not exist - that's OK.
} }
catch{ return null;
return;
}
} }
} }
/** /**
@ -175,26 +196,42 @@ export class AssetService {
// eslint-disable-next-line // eslint-disable-next-line
const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/); const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/);
let assetFormat = extensionParts[0].toLowerCase(); let assetFormat = extensionParts[0].toLowerCase();
let assetMimeType = mime.getType(assetFormat);
if (supportedImageFormats.hasOwnProperty(assetFormat)) { if (supportedImageFormats.hasOwnProperty(assetFormat)) {
let types; let checkFileType;
let corruptFileName; let corruptFileName;
if (nodejsMode) { if (nodejsMode) {
const FileType = require('file-type'); try {
const fileType = await FileType.fromFile(normalizedPath); checkFileType = await FileType.fromFile(normalizedPath);
types = [fileType.ext]; } catch {
// do nothing
}
corruptFileName = fileName.split(/[\\\/]/).pop().replace(/%20/g, " "); corruptFileName = fileName.split(/[\\\/]/).pop().replace(/%20/g, " ");
} else { } 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, " "); 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() })); 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 // 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 (!types.includes(assetFormat)) { else if (fileType !== assetFormat) {
assetFormat = types[0]; assetFormat = fileType;
assetMimeType = mimeType;
console.error(`${strings.editorPage.assetWarning.incorrectFileExtension.attention} ${corruptFileName.toLocaleUpperCase()} ${strings.editorPage.assetWarning.incorrectFileExtension.text} ${corruptFileName.toLocaleUpperCase()}`); 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, name: fileName,
path: filePath, path: filePath,
size: null, 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 assetProviderInstance: IAssetProvider;
private storageProviderInstance: IStorageProvider; private storageProviderInstance: IStorageProvider;
@ -369,7 +377,7 @@ export class AssetService {
// The file may not exist - that's OK. // The file may not exist - that's OK.
} }
} }
return metadata; return JSON.parse(JSON.stringify(metadata));
} }
/** /**
@ -383,6 +391,11 @@ export class AssetService {
try { try {
const json = await this.storageProvider.readText(labelFileName, true); const json = await this.storageProvider.readText(labelFileName, true);
const labelData = JSON.parse(json) as ILabelData; 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) { // if (!labelData.document || !labelData.labels && !labelData.tableLabels) {
// const reason = interpolate(strings.errors.missingRequiredFieldInLabelFile.message, { labelFileName }); // const reason = interpolate(strings.errors.missingRequiredFieldInLabelFile.message, { labelFileName });
// toast.error(reason, { autoClose: false }); // toast.error(reason, { autoClose: false });
@ -427,7 +440,7 @@ export class AssetService {
// } // }
// toast.dismiss(); // toast.dismiss();
return { return {
asset: { ...asset }, asset: { ...asset, labelingState: labelData.labelingState },
regions: [], regions: [],
version: appInfo.version, version: appInfo.version,
labelData, labelData,

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

@ -33,6 +33,7 @@ export class OCRService {
public async getRecognizedText( public async getRecognizedText(
filePath: string, filePath: string,
fileName: string, fileName: string,
mimeType: string,
onStatusChanged?: (ocrStatus: OcrStatus) => void, onStatusChanged?: (ocrStatus: OcrStatus) => void,
rewrite?: boolean rewrite?: boolean
): Promise<any> { ): Promise<any> {
@ -47,11 +48,11 @@ export class OCRService {
notifyStatusChanged(OcrStatus.loadingFromAzureBlob); notifyStatusChanged(OcrStatus.loadingFromAzureBlob);
ocrJson = await this.readOcrFile(ocrFileName); ocrJson = await this.readOcrFile(ocrFileName);
if (!this.isValidOcrFormat(ocrJson) || rewrite) { if (!this.isValidOcrFormat(ocrJson) || rewrite) {
ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName); ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName, mimeType);
} }
} catch (e) { } catch (e) {
notifyStatusChanged(OcrStatus.runningOCR); notifyStatusChanged(OcrStatus.runningOCR);
ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName); ocrJson = await this.fetchOcrUriResult(filePath, fileName, ocrFileName, mimeType);
} finally { } finally {
notifyStatusChanged(OcrStatus.done); 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 { try {
let body; let body;
let headers; let headers;
@ -93,15 +94,13 @@ export class OCRService {
] ]
); );
body = bodyAndType[0]; body = bodyAndType[0];
const fileType = bodyAndType[1].mime; headers = { "Content-Type": mimeType, "cache-control": "no-cache" };
headers = { "Content-Type": fileType, "cache-control": "no-cache" }; } else {
}
else {
body = { url: filePath }; body = { url: filePath };
headers = { "Content-Type": "application/json" }; headers = { "Content-Type": "application/json" };
} }
const response = await ServiceHelper.postWithAutoRetry( 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, body,
{ headers }, { headers },
this.project.apiKey as string, this.project.apiKey as string,

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

@ -24,7 +24,7 @@ export class PredictService {
} }
const endpointURL = url.resolve( const endpointURL = url.resolve(
this.project.apiUriBase, 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" }; const headers = { "Content-Type": "application/json", "cache-control": "no-cache" };
@ -60,11 +60,10 @@ export class PredictService {
if (response.data.status.toLowerCase() === constants.statusCodeSucceeded) { if (response.data.status.toLowerCase() === constants.statusCodeSucceeded) {
resolve(response.data); resolve(response.data);
// prediction response from API // prediction response from API
console.log("raw data", JSON.parse(response.request.response));
} else if (response.data.status.toLowerCase() === constants.statusCodeFailed) { } else if (response.data.status.toLowerCase() === constants.statusCodeFailed) {
reject(_.get( reject(_.get(
response, response,
"data.analyzeResult.errors[0].errorMessage", "data.analyzeResult.errors[0]",
"Generic error during prediction")); "Generic error during prediction"));
} else if (Number(new Date()) < endTime) { } else if (Number(new Date()) < endTime) {
// If the request isn't succeeded and the timeout hasn't elapsed, go again // 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> { public async isProjectNameAlreadyUsed(project: IProject): Promise<boolean> {
const storageProvider = StorageProviderFactory.createFromConnection(project.sourceConnection); const storageProvider = StorageProviderFactory.createFromConnection(project.sourceConnection);
const fileList = await storageProvider.listFiles("", constants.projectFileExtension/*ext*/); return await storageProvider.isFileExists(`${project.name}${constants.projectFileExtension}`);
for (const fileName of fileList) {
if (fileName === `${project.name}${constants.projectFileExtension}`) {
return true;
}
}
return false;
} }
public async isValidProjectConnection(project: IProject): Promise<boolean> { 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" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 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" version "2.4.6"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1"
integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== 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" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
integrity sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ== 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: node-gyp@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"