From 40d4fae3a13f5c2646a69c97fb0bb119787c68db Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Tue, 12 Jan 2021 17:54:59 -0500 Subject: [PATCH 1/7] CrossDoc: create a crossdoc project and upload/delete files to the crossdoc project --- .eslintignore | 2 +- .gitignore | 4 + simple-backend/nlpviewer_backend/admin.py | 6 + .../nlpviewer_backend/handlers/crossdoc.py | 39 ++++ .../nlpviewer_backend/handlers/project.py | 33 ++- .../migrations/0006_project_config.py | 18 ++ .../migrations/0007_auto_20210111_1741.py | 23 ++ .../migrations/0008_crossdoc.py | 23 ++ simple-backend/nlpviewer_backend/models.py | 20 ++ simple-backend/nlpviewer_backend/urls.py | 6 +- src/app/lib/api.ts | 32 ++- src/app/pages/Project.tsx | 125 +++++++++-- src/app/pages/Projects.tsx | 203 +++++++++++++----- 13 files changed, 453 insertions(+), 81 deletions(-) create mode 100644 simple-backend/nlpviewer_backend/admin.py create mode 100644 simple-backend/nlpviewer_backend/handlers/crossdoc.py create mode 100644 simple-backend/nlpviewer_backend/migrations/0006_project_config.py create mode 100644 simple-backend/nlpviewer_backend/migrations/0007_auto_20210111_1741.py create mode 100644 simple-backend/nlpviewer_backend/migrations/0008_crossdoc.py diff --git a/.eslintignore b/.eslintignore index 14b5271..ecab6d5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,4 @@ raw_data/ src/__tests__/ src/declaration.d.ts src/serviceWorker.js -src/setupProxy.js +src/setupProxy.js \ No newline at end of file diff --git a/.gitignore b/.gitignore index 99d34d4..0f72e68 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ simple-backend/db-example-old.sqlite3 simple-backend/db-old.sqlite3 .python-version* /.idea/ + +.eslintcache +stave.iml +package-lock.json \ No newline at end of file diff --git a/simple-backend/nlpviewer_backend/admin.py b/simple-backend/nlpviewer_backend/admin.py new file mode 100644 index 0000000..58ba001 --- /dev/null +++ b/simple-backend/nlpviewer_backend/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from nlpviewer_backend.models import User, Document, CrossDoc, Project +admin.site.register(User) +admin.site.register(Document) +admin.site.register(CrossDoc) +admin.site.register(Project) \ No newline at end of file diff --git a/simple-backend/nlpviewer_backend/handlers/crossdoc.py b/simple-backend/nlpviewer_backend/handlers/crossdoc.py new file mode 100644 index 0000000..ebb3029 --- /dev/null +++ b/simple-backend/nlpviewer_backend/handlers/crossdoc.py @@ -0,0 +1,39 @@ +from django.contrib import admin +from django.conf import settings +from django.urls import include, path +from django.http import HttpResponse, HttpResponseBadRequest, JsonResponse +from django.forms import model_to_dict +import uuid +import json +from ..models import Document, User, CrossDoc, Project +import os +import re +from copy import deepcopy +from datetime import datetime + + + +def listAll(request): + crossDocs = CrossDoc.objects.all().values() + return JsonResponse(list(crossDocs), safe=False) + +def create(request): + received_json_data = json.loads(request.body) + + crossdoc = CrossDoc( + name=received_json_data.get('name'), + textPack=received_json_data.get('textPack'), + project = Project.objects.get( + pk=received_json_data.get('project_id') + ) + ) + crossdoc.save() + + return JsonResponse({"id": crossdoc.id}, safe=False) + +def delete(request, crossdoc_id): + crossdoc = CrossDoc.objects.get(pk=crossdoc_id) + crossdoc.delete() + + return HttpResponse('ok') + diff --git a/simple-backend/nlpviewer_backend/handlers/project.py b/simple-backend/nlpviewer_backend/handlers/project.py index 54ff5dd..26abc78 100644 --- a/simple-backend/nlpviewer_backend/handlers/project.py +++ b/simple-backend/nlpviewer_backend/handlers/project.py @@ -18,11 +18,25 @@ def listAll(request): def create(request): received_json_data = json.loads(request.body) - project = Project( - name=received_json_data.get('name'), - ontology=received_json_data.get('ontology'), - config=received_json_data.get('config') - ) + project_type = received_json_data.get('type', 'indoc') + + if project_type == 'indoc': + + project = Project( + project_type = project_type, + name=received_json_data.get('name'), + ontology=received_json_data.get('ontology'), + config=received_json_data.get('config') + ) + elif project_type == 'crossdoc': + project = Project( + project_type = project_type, + name=received_json_data.get('name'), + ontology=received_json_data.get('ontology'), + multi_ontology = received_json_data.get('multiOntology'), + config=received_json_data.get('config') + ) + project.save() @@ -56,6 +70,15 @@ def query_docs(request, project_id): return JsonResponse(list(docs), safe=False) +@require_login +def query_crossdocs(request, project_id): + project = Project.objects.get(pk=project_id) + docs = project.documents.all().values() + crossdocs = project.crossdocs.all().values() + response = {"docs": list(docs), "crossdocs": list(crossdocs)} + + return JsonResponse(response, safe=False) + @require_login def delete(request, project_id): project = Project.objects.get(pk=project_id) diff --git a/simple-backend/nlpviewer_backend/migrations/0006_project_config.py b/simple-backend/nlpviewer_backend/migrations/0006_project_config.py new file mode 100644 index 0000000..5ef30c9 --- /dev/null +++ b/simple-backend/nlpviewer_backend/migrations/0006_project_config.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2021-01-02 21:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nlpviewer_backend', '0005_remove_document_ontology'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='config', + field=models.TextField(default=''), + ), + ] diff --git a/simple-backend/nlpviewer_backend/migrations/0007_auto_20210111_1741.py b/simple-backend/nlpviewer_backend/migrations/0007_auto_20210111_1741.py new file mode 100644 index 0000000..c1c1c24 --- /dev/null +++ b/simple-backend/nlpviewer_backend/migrations/0007_auto_20210111_1741.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-01-11 22:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nlpviewer_backend', '0006_project_config'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='multi_ontology', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='project', + name='project_type', + field=models.CharField(default='single_pack', max_length=100), + ), + ] diff --git a/simple-backend/nlpviewer_backend/migrations/0008_crossdoc.py b/simple-backend/nlpviewer_backend/migrations/0008_crossdoc.py new file mode 100644 index 0000000..0c7f1ea --- /dev/null +++ b/simple-backend/nlpviewer_backend/migrations/0008_crossdoc.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-01-12 01:40 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('nlpviewer_backend', '0007_auto_20210111_1741'), + ] + + operations = [ + migrations.CreateModel( + name='CrossDoc', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200)), + ('textPack', models.TextField()), + ('project', models.ForeignKey(blank=True, default='', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='crossdocs', to='nlpviewer_backend.Project')), + ], + ), + ] diff --git a/simple-backend/nlpviewer_backend/models.py b/simple-backend/nlpviewer_backend/models.py index 9e0faa2..15859c5 100644 --- a/simple-backend/nlpviewer_backend/models.py +++ b/simple-backend/nlpviewer_backend/models.py @@ -5,9 +5,14 @@ class Project(models.Model): # realtionship: Project.document name = models.CharField(max_length=200) + project_type = models.CharField(max_length=100, default='single_pack') + ontology = models.TextField(default='') + multi_ontology = models.TextField(default='') config = models.TextField(default='') + + class Document(models.Model): # content: textPack: text body + annotation @@ -25,6 +30,21 @@ class Document(models.Model): textPack = models.TextField() +class CrossDoc(models.Model): + + name = models.CharField(max_length=200) + + # relationship: project + project = models.ForeignKey( + Project, + on_delete=models.CASCADE, + default='', + related_name='crossdocs', + null=True, + blank=True + ) + textPack = models.TextField() + class User(models.Model): name = models.CharField(max_length=200) password = models.CharField(max_length=200) diff --git a/simple-backend/nlpviewer_backend/urls.py b/simple-backend/nlpviewer_backend/urls.py index eb8f384..ed2eb8c 100644 --- a/simple-backend/nlpviewer_backend/urls.py +++ b/simple-backend/nlpviewer_backend/urls.py @@ -16,7 +16,7 @@ from django.contrib import admin from django.urls import include, path -from nlpviewer_backend.handlers import session, user, document, project, nlp +from nlpviewer_backend.handlers import session, user, document, project, nlp, crossdoc urlpatterns = [ path('login', session.login), @@ -46,6 +46,9 @@ path('documents//links//edit', document.edit_link), path('documents//links//delete', document.delete_link), + + path('crossdocs/new', crossdoc.create), + path('crossdocs//delete', crossdoc.delete), path('next_doc/', document.get_next_document_id), path('prev_doc/', document.get_prev_document_id), @@ -54,6 +57,7 @@ path('projects/new', project.create), path('projects/', project.query), path('projects//docs', project.query_docs), + path('projects//crossdocs', project.query_crossdocs), path('projects//delete', project.delete), path('documents//text/edit', document.edit_text), diff --git a/src/app/lib/api.ts b/src/app/lib/api.ts index 931b44c..d53c9ce 100644 --- a/src/app/lib/api.ts +++ b/src/app/lib/api.ts @@ -24,6 +24,10 @@ export function fetchProjects(): Promise { return fetch('/api/projects').then(r => r.json()); } +export function fetchProject(id: string) { + return fetch(`/api/projects/${id}`).then(r => r.json()); +} + export function fetchDocument(id: string): Promise { return fetch(`/api/documents/${id}`).then(r => r.json()); } @@ -69,10 +73,29 @@ export function createDocument( }).then(r => r.json()); } -export function createProject(name: string, ontology: string, config: string) { +export function createCrossDoc( + name: string, + textPack: string, + project_id: string +) { + return postData('/api/crossdocs/new', { + name: name, + textPack: textPack, + project_id: project_id, + }).then(r => r.json()); +} +export function createProject( + type: string, + name: string, + ontology: string, + config: string, + multiOntology = '{}' +) { return postData('/api/projects/new', { + type: type, name: name, ontology: ontology, + multiOntology: multiOntology, config: config, }).then(r => r.json()); } @@ -81,6 +104,10 @@ export function deleteDocument(id: string) { return postData(`/api/documents/${id}/delete`); } +export function deleteCrossDoc(id: string) { + return postData(`/api/crossdocs/${id}/delete`); +} + export function deleteProject(id: string) { return postData(`/api/projects/${id}/delete`); } @@ -88,6 +115,9 @@ export function deleteProject(id: string) { export function fetchDocumentsProject(id: string) { return postData(`/api/projects/${id}/docs`).then(r => r.json()); } +export function fetchDocumentsAndMultiPacksProject(id: string) { + return postData(`/api/projects/${id}/crossdocs`).then(r => r.json()); +} // export function fetchOntologyByDocument(id: string):Promise{ // return postData(`/api/doc_ontology_by_id/${id}`).then(r => r.json()); diff --git a/src/app/pages/Project.tsx b/src/app/pages/Project.tsx index d785706..b27f42f 100644 --- a/src/app/pages/Project.tsx +++ b/src/app/pages/Project.tsx @@ -1,8 +1,12 @@ import React, {useState, useEffect} from 'react'; import { + fetchProject, createDocument, deleteDocument, fetchDocumentsProject, + fetchDocumentsAndMultiPacksProject, + createCrossDoc, + deleteCrossDoc, } from '../lib/api'; import {Link, useHistory} from 'react-router-dom'; import DropUpload from '../components/dropUpload'; @@ -10,42 +14,83 @@ import {FileWithPath} from 'react-dropzone'; function Docs() { // eslint-disable-next-line @typescript-eslint/no-explicit-any + const [projectInfo, setProjectInfo] = useState(); const [docs, setDocs] = useState([]); + const [crossdocs, setCrossDocs] = useState(); const history = useHistory(); useEffect(() => { - updateDocs().catch(() => { + getProjectInfo().catch(() => { history.push('/login'); }); }, [history]); - function updateDocs() { - const project_id = window.location.pathname.split('/').pop()!; + useEffect(() => { + updateDocs(); + }, [projectInfo]); - return fetchDocumentsProject(project_id).then(docs => { - setDocs(docs); + function getProjectInfo() { + const project_id = window.location.pathname.split('/').pop()!; + return fetchProject(project_id).then(info => { + setProjectInfo(info); }); } - - function handleAdd(filesToUpload: FileWithPath[]) { - const project_id = window.location.pathname.split('/').pop()!; - - filesToUpload.forEach(f => { - const reader = new FileReader(); - reader.readAsText(f); - reader.onload = function () { - createDocument(f.name, reader.result as string, project_id).then(() => { - updateDocs(); + function updateDocs() { + if (projectInfo) { + const project_id = window.location.pathname.split('/').pop()!; + if (projectInfo.project_type === 'indoc') { + return fetchDocumentsProject(project_id).then(docs => { + setDocs(docs); }); - }; - }); + } else if (projectInfo.project_type === 'crossdoc') { + return fetchDocumentsAndMultiPacksProject(project_id).then(result => { + console.log(result); + setDocs(result.docs); + setCrossDocs(result.crossdocs); + }); + } + } + } + function handleAdd(filesToUpload: FileWithPath[], pack_type = 'single_pack') { + const project_id = window.location.pathname.split('/').pop()!; + if (pack_type === 'single_pack') { + filesToUpload.forEach(f => { + const reader = new FileReader(); + reader.readAsText(f); + reader.onload = function () { + createDocument(f.name, reader.result as string, project_id).then( + () => { + updateDocs(); + } + ); + }; + }); + } else if (pack_type === 'multi_pack') { + filesToUpload.forEach(f => { + const reader = new FileReader(); + reader.readAsText(f); + reader.onload = function () { + createCrossDoc(f.name, reader.result as string, project_id).then( + () => { + updateDocs(); + } + ); + }; + }); + } } - function handleDelete(id: string) { - deleteDocument(id).then(() => { - updateDocs(); - }); + function handleDelete(id: string, pack_type = 'single_pack') { + if (pack_type === 'single_pack') { + deleteDocument(id).then(() => { + updateDocs(); + }); + } else if (pack_type === 'multi_pack') { + deleteCrossDoc(id).then(() => { + updateDocs(); + }); + } } return ( @@ -57,7 +102,9 @@ function Docs() {
  • {d.name}{' '} - +
)) @@ -68,7 +115,7 @@ function Docs() {

new pack

handleAdd(file, 'single_pack')} fileActionButtonText={'ADD'} mimeType="application/json" // Do not support zip now. @@ -76,6 +123,38 @@ function Docs() { allowMultiple={true} /> + + {projectInfo && projectInfo.project_type === 'crossdoc' ? ( +
+

All multi docs:

+ {crossdocs + ? crossdocs.map(d => ( +
    +
  • + {d.name}{' '} + +
  • +
+ )) + : 'Empty'} +
+ ) : null} + {projectInfo && projectInfo.project_type === 'crossdoc' ? ( +
+

new multi pack

+ handleAdd(file, 'multi_pack')} + fileActionButtonText={'ADD'} + mimeType="application/json" + // Do not support zip now. + // mimeType='application/json, application/x-rar-compressed, application/octet-stream, application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip' + allowMultiple={true} + /> +
+ ) : null} ); } diff --git a/src/app/pages/Projects.tsx b/src/app/pages/Projects.tsx index f90e7d1..8d1d1cc 100644 --- a/src/app/pages/Projects.tsx +++ b/src/app/pages/Projects.tsx @@ -18,6 +18,8 @@ import DialogContent from '@material-ui/core/DialogContent'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; import PostAddSharpIcon from '@material-ui/icons/PostAddSharp'; +import Select from '@material-ui/core/Select'; +import MenuItem from '@material-ui/core/MenuItem'; import { ILegendAttributeConfig, ILegendConfig, @@ -26,6 +28,7 @@ import { } from '../../nlpviewer'; import {isEntryAnnotation, camelCaseDeep} from '../../nlpviewer/lib/utils'; import JsonEditor from '../components/jsonEditor'; +import {InputLabel} from '@material-ui/core'; const useStyles = makeStyles({ root: { @@ -41,6 +44,7 @@ const useStyles = makeStyles({ marginBottom: 15, }, }); +const PROJECT_TYPES = ['indoc', 'crossdoc']; function Projects() { const classes = useStyles(); @@ -48,10 +52,13 @@ function Projects() { const [projects, setProjects] = useState([]); const [name, setName] = useState(''); const [ontology, setOntology] = useState('{}'); + const [multiOntology, setMultiOntology] = useState('{}'); const [config, setConfig] = useState('{}'); const history = useHistory(); const [open, setOpen] = React.useState(false); + const [projectType, setProjectType] = useState('single_pack'); + const handleClickOpen = () => { setOpen(true); }; @@ -62,6 +69,7 @@ function Projects() { const clearDialog = () => { setOntology('{}'); + setMultiOntology('{}'); setConfig('{}'); setName(''); }; @@ -79,10 +87,22 @@ function Projects() { } function handleAdd() { - if (name && ontology && config) { - createProject(name, ontology, config).then(() => { + if (projectType === 'indoc' && name && ontology !== '{}' && config) { + createProject(projectType, name, ontology, config).then(() => { updateProjects(); }); + } else if ( + projectType === 'crossdoc' && + name && + ontology !== '{}' && + multiOntology !== '{}' && + config + ) { + createProject(projectType, name, ontology, config, multiOntology).then( + () => { + updateProjects(); + } + ); } else { alert('Please fill in project name and upload ontology file.'); } @@ -94,20 +114,32 @@ function Projects() { }); } - function userAddFiles(acceptedFiles: FileWithPath[]) { + function handleProjectTypeChange(event: any) { + setProjectType(event.target.value); + } + + function userAddFiles( + acceptedFiles: FileWithPath[], + file_type = 'single_pack' + ) { if (acceptedFiles.length > 0) { const reader = new FileReader(); reader.readAsText(acceptedFiles[0]); reader.onload = function () { - setOntology(reader.result as string); - const defaultConfig = createDefaultConfig(reader.result as string); - setConfig(JSON.stringify(defaultConfig)); + if (file_type === 'single_pack') { + setOntology(reader.result as string); + const defaultConfig = createDefaultConfig(reader.result as string); + setConfig(JSON.stringify(defaultConfig)); + } else if (file_type === 'multi_pack') { + setMultiOntology(reader.result as string); + } }; } } function createDefaultConfig(ontology: string): IProjectConfigs { const ontologyJson = JSON.parse(ontology); + console.log(ontologyJson); const ontologyObject: IOntology = camelCaseDeep(ontologyJson); const config: IProjectConfigs = { legendConfigs: {}, @@ -201,52 +233,123 @@ function Projects() { - + -
- setName(e.target.value)} - autoFocus - fullWidth - margin="normal" - /> -
- setOntology(text)} - /> - setConfig(text)} - /> -
- -
-
- +
+ + ) : ( +
+
+ setName(e.target.value)} + autoFocus + fullWidth + margin="normal" + /> +
+
+ + userAddFiles(file, 'single_pack') + } + mimeType="application/json" + allowMultiple={false} + /> +
+
+ + userAddFiles(file, 'multi_pack') + } + mimeType="application/json" + allowMultiple={false} + /> +
+
+ +
+
+ )}
From 4705d0176bcf4903c5471c0e9f942ba603399bf7 Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Wed, 13 Jan 2021 22:57:22 -0500 Subject: [PATCH 2/7] change type specification to pass type checks --- src/app/pages/Project.tsx | 10 ++++++++-- src/app/pages/Projects.tsx | 15 ++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/app/pages/Project.tsx b/src/app/pages/Project.tsx index b27f42f..265b91c 100644 --- a/src/app/pages/Project.tsx +++ b/src/app/pages/Project.tsx @@ -15,7 +15,9 @@ import {FileWithPath} from 'react-dropzone'; function Docs() { // eslint-disable-next-line @typescript-eslint/no-explicit-any const [projectInfo, setProjectInfo] = useState(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [docs, setDocs] = useState([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const [crossdocs, setCrossDocs] = useState(); const history = useHistory(); @@ -115,7 +117,9 @@ function Docs() {

new pack

handleAdd(file, 'single_pack')} + fileActionButtonFunc={(file: FileWithPath[]) => + handleAdd(file, 'single_pack') + } fileActionButtonText={'ADD'} mimeType="application/json" // Do not support zip now. @@ -146,7 +150,9 @@ function Docs() {

new multi pack

handleAdd(file, 'multi_pack')} + fileActionButtonFunc={(file: FileWithPath[]) => + handleAdd(file, 'multi_pack') + } fileActionButtonText={'ADD'} mimeType="application/json" // Do not support zip now. diff --git a/src/app/pages/Projects.tsx b/src/app/pages/Projects.tsx index 63fd3a5..7161f64 100644 --- a/src/app/pages/Projects.tsx +++ b/src/app/pages/Projects.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react'; +import React, {useState, useEffect, ChangeEvent} from 'react'; import {fetchProjects, createProject, deleteProject} from '../lib/api'; import {Link, useHistory} from 'react-router-dom'; import {FileWithPath} from 'react-dropzone'; @@ -57,7 +57,7 @@ function Projects() { const history = useHistory(); const [open, setOpen] = React.useState(false); - const [projectType, setProjectType] = useState('single_pack'); + const [projectType, setProjectType] = useState('indoc'); const handleClickOpen = () => { setOpen(true); @@ -114,9 +114,10 @@ function Projects() { }); } - function handleProjectTypeChange(event: any) { - console.log(event.target.value); - setProjectType(event.target.value); + function handleProjectTypeChange(event: ChangeEvent) { + if (event.target) { + setProjectType((event.target as HTMLTextAreaElement).value); + } } function userAddFiles( @@ -317,7 +318,7 @@ function Projects() {
+ fileDropFunc={(file: FileWithPath[]) => userAddFiles(file, 'single_pack') } mimeType="application/json" @@ -327,7 +328,7 @@ function Projects() {
+ fileDropFunc={(file: FileWithPath[]) => userAddFiles(file, 'multi_pack') } mimeType="application/json" From 0e0d770fbe180b01270a222aa0f379d78816c1b3 Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Fri, 15 Jan 2021 22:11:54 -0500 Subject: [PATCH 3/7] CrossDoc: all functions of crossdoc annotation implemented including view/add_link/delete_link/ --- .eslintignore | 4 +- package.json | 4 + .../nlpviewer_backend/handlers/crossdoc.py | 168 +++++++++++ .../nlpviewer_backend/handlers/document.py | 4 +- simple-backend/nlpviewer_backend/lib/utils.py | 8 + .../migrations/0012_auto_20210115_1610.py | 23 ++ .../migrations/0013_auto_20210115_2047.py | 18 ++ simple-backend/nlpviewer_backend/models.py | 3 +- simple-backend/nlpviewer_backend/urls.py | 9 +- src/app/App.tsx | 5 + src/app/lib/api.ts | 33 +++ src/app/pages/CrossDoc.tsx | 104 +++++++ src/app/pages/Project.tsx | 91 +++--- src/app/pages/Projects.tsx | 10 +- src/crossviewer/components/Event.tsx | 58 ++++ src/crossviewer/components/TextAreaA.tsx | 104 +++++++ src/crossviewer/components/TextAreaB.tsx | 116 ++++++++ src/crossviewer/index.tsx | 260 ++++++++++++++++++ src/crossviewer/lib/definitions.ts | 4 + src/crossviewer/lib/interfaces.ts | 61 ++++ src/crossviewer/lib/utils.ts | 140 ++++++++++ .../styles/CrossDocStyle.module.css | 109 ++++++++ src/crossviewer/styles/TextViewer.module.css | 196 +++++++++++++ tsconfig.json | 3 +- 24 files changed, 1477 insertions(+), 58 deletions(-) create mode 100644 simple-backend/nlpviewer_backend/migrations/0012_auto_20210115_1610.py create mode 100644 simple-backend/nlpviewer_backend/migrations/0013_auto_20210115_2047.py create mode 100644 src/app/pages/CrossDoc.tsx create mode 100644 src/crossviewer/components/Event.tsx create mode 100644 src/crossviewer/components/TextAreaA.tsx create mode 100644 src/crossviewer/components/TextAreaB.tsx create mode 100644 src/crossviewer/index.tsx create mode 100644 src/crossviewer/lib/definitions.ts create mode 100644 src/crossviewer/lib/interfaces.ts create mode 100644 src/crossviewer/lib/utils.ts create mode 100644 src/crossviewer/styles/CrossDocStyle.module.css create mode 100644 src/crossviewer/styles/TextViewer.module.css diff --git a/.eslintignore b/.eslintignore index ecab6d5..2b4b786 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,6 @@ raw_data/ src/__tests__/ src/declaration.d.ts src/serviceWorker.js -src/setupProxy.js \ No newline at end of file +src/setupProxy.js +src/crossviewer +src/app/pages/CrossDoc.tsx \ No newline at end of file diff --git a/package.json b/package.json index c7801b0..982363a 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "react-is": "^17.0.1", "react-router-dom": "^5.2.0", "react-select": "^3.0.8", + "react-modal": "^3.11.2", + "react-alert": "^7.0.2", + "react-alert-template-basic": "^1.0.0", + "react-progressbar": "^15.4.1", "styled-components": "^5.1.1" }, "devDependencies": { diff --git a/simple-backend/nlpviewer_backend/handlers/crossdoc.py b/simple-backend/nlpviewer_backend/handlers/crossdoc.py index ebb3029..eb078f6 100644 --- a/simple-backend/nlpviewer_backend/handlers/crossdoc.py +++ b/simple-backend/nlpviewer_backend/handlers/crossdoc.py @@ -10,7 +10,92 @@ import re from copy import deepcopy from datetime import datetime +from ..lib.utils import format_forte_id +default_type = "edu.cmu.CrossEventRelation" + +def read_creation_record(textPack): + """ + Read teh creation record of the forte json file + Get a mapping from username to a set of tids + """ + mapping = {} # from username/forteid to their creation records + for username in textPack["py/state"]["creation_records"]: + tids = set(textPack["py/state"]["creation_records"][username]["py/set"]) + mapping[username] = tids + return mapping + +def delete_link(textPack, parent_event_id, child_event_id, forteID): + """ + Delete both link and its creation record + This function does not return, it did operations on the original textPack + """ + mapping = read_creation_record(textPack) + tid_to_delete = None + index_to_delete = None + + # delete by iterating all, and record down the wanted ones, skip the deleted one + for index, item in enumerate(textPack["py/state"]["links"]): + if item["py/state"]["_parent"]["py/tuple"][1] == parent_event_id and \ + item["py/state"]["_child"]["py/tuple"][1] == child_event_id and \ + forteID in mapping and \ + item["py/state"]["_tid"] in mapping[forteID]: + tid_to_delete = item["py/state"]["_tid"] + index_to_delete = index + + if tid_to_delete is not None: + del textPack["py/state"]["links"][index_to_delete] + textPack["py/state"]["creation_records"][forteID]["py/set"].remove(tid_to_delete) + + + +def format_cross_doc_helper(uploaded_link, next_tid): + """ + format the cross doc link uploaded from the frontend + + """ + link = deepcopy(uploaded_link) + del link["py/state"]['coref_question_answers'] + + link["py/object"] = default_type + link["py/state"]['_tid'] = next_tid + link["py/state"]["_embedding"] = [] + + # coref + link["py/state"]["coref_questions"] = { + "py/object": "forte.data.ontology.core.FList", + "py/state": { + "_FList__data": [] + } + } + link["py/state"]["coref_answers"] = [] + for item in uploaded_link["py/state"]["coref_question_answers"]: + link["py/state"]["coref_questions"]["py/state"]["_FList__data"].append( + { + "py/object": "forte.data.ontology.core.Pointer", + "py/state": { + "_tid": item["question_id"] + } + }) + link["py/state"]["coref_answers"].append(item["option_id"]) + + return link + +def find_and_advance_next_tid(textPackJson): + """ + find the global maximum tid and return tid+1 + """ + textPackJson['py/state']['serialization']["next_id"] += 1 + return textPackJson['py/state']['serialization']["next_id"] - 1 + +def extract_doc_id_from_crossdoc(cross_doc): + text_pack = json.loads(cross_doc.textPack) + doc_external_ids = text_pack["py/state"]["_pack_ref"] + doc_external_id_0 = doc_external_ids[0] + doc_external_id_1 = doc_external_ids[1] + doc_0 = cross_doc.project.documents.get(packID=doc_external_id_0) + doc_1 = cross_doc.project.documents.get(packID=doc_external_id_1) + return doc_0, doc_1 def listAll(request): @@ -37,3 +122,86 @@ def delete(request, crossdoc_id): return HttpResponse('ok') + +def query(request, crossdoc_id): + cross_doc = CrossDoc.objects.get(pk=crossdoc_id) + doc_0, doc_1 = extract_doc_id_from_crossdoc(cross_doc) + parent = { + 'id': doc_0.pk, + 'textPack': doc_0.textPack, + 'ontology': doc_0.project.ontology + } + child = { + 'id': doc_1.pk, + 'textPack': doc_1.textPack, + 'ontology': doc_1.project.ontology + } + forteID = format_forte_id(request.user.pk) + to_return = {"crossDocPack":model_to_dict(cross_doc),"_parent": parent, "_child":child, "forteID":forteID} + return JsonResponse(to_return, safe=False) + + +def new_cross_doc_link(request, crossdoc_id): + + crossDoc = CrossDoc.objects.get(pk=crossdoc_id) + docJson = model_to_dict(crossDoc) + textPackJson = json.loads(docJson['textPack']) + forteID = format_forte_id(request.user.pk) + + received_json_data = json.loads(request.body) + data = received_json_data.get('data') + link = data["link"] + + link_id = find_and_advance_next_tid(textPackJson) + link = format_cross_doc_helper(link, link_id) + + # delete possible duplicate link before and the creation records + parent_event_id = link["py/state"]["_parent"]["py/tuple"][1] + child_event_id = link["py/state"]["_child"]["py/tuple"][1] + delete_link(textPackJson, parent_event_id, child_event_id, forteID) + + # append new link to the textpack + textPackJson['py/state']['links'].append(link) + + # append the creation records + if forteID not in textPackJson["py/state"]["creation_records"]: + textPackJson["py/state"]["creation_records"][forteID] = {"py/set":[]} + textPackJson["py/state"]["creation_records"][forteID]["py/set"].append(link_id) + + # commit to the database + crossDoc.textPack = json.dumps(textPackJson) + crossDoc.save() + return JsonResponse({"crossDocPack": model_to_dict(crossDoc)}, safe=False) + + +def delete_cross_doc_link(request, crossdoc_id, link_id): + """ + request handler, delete by tid + """ + + crossDoc = CrossDoc.objects.get(pk=crossdoc_id) + docJson = model_to_dict(crossDoc) + textPackJson = json.loads(docJson['textPack']) + forteID = format_forte_id(request.user.pk) + + deleteIndex = -1 + success = False + for index, item in enumerate(textPackJson['py/state']['links']): + if item["py/state"]['_tid'] == link_id: + deleteIndex = index + success = True + + if deleteIndex == -1: + success = False + else: + del textPackJson['py/state']['links'][deleteIndex] + textPackJson["py/state"]["creation_records"][forteID]["py/set"].remove(link_id) + crossDoc.textPack = json.dumps(textPackJson) + crossDoc.save() + + return JsonResponse({"crossDocPack": model_to_dict(crossDoc), "update_success": success}, safe=False) + + + + + diff --git a/simple-backend/nlpviewer_backend/handlers/document.py b/simple-backend/nlpviewer_backend/handlers/document.py index 7233a01..1942868 100644 --- a/simple-backend/nlpviewer_backend/handlers/document.py +++ b/simple-backend/nlpviewer_backend/handlers/document.py @@ -56,9 +56,11 @@ def create(request): project_id = received_json_data.get('project_id') project = Project.objects.get(id=project_id) check_perm_project(project, request.user, 'nlpviewer_backend.new_project') - + pack_json = json.loads(received_json_data.get('textPack')) + pack_id = int(pack_json["py/state"]["meta"]["py/state"]["_pack_id"]) doc = Document( name=received_json_data.get('name'), + packID=pack_id, textPack=received_json_data.get('textPack'), project = Project.objects.get( pk=project_id diff --git a/simple-backend/nlpviewer_backend/lib/utils.py b/simple-backend/nlpviewer_backend/lib/utils.py index 2a79727..6a02fa1 100644 --- a/simple-backend/nlpviewer_backend/lib/utils.py +++ b/simple-backend/nlpviewer_backend/lib/utils.py @@ -55,3 +55,11 @@ def fetch_project_check_perm(id, user, perm): return project + +def format_forte_id(id): + """ + + convert the user id (pk) to stave id. Used for crossdoc annotation creation record. + """ + return "stave." + str(id) + diff --git a/simple-backend/nlpviewer_backend/migrations/0012_auto_20210115_1610.py b/simple-backend/nlpviewer_backend/migrations/0012_auto_20210115_1610.py new file mode 100644 index 0000000..cbf6f25 --- /dev/null +++ b/simple-backend/nlpviewer_backend/migrations/0012_auto_20210115_1610.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.4 on 2021-01-15 21:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nlpviewer_backend', '0011_auto_20210113_2148'), + ] + + operations = [ + migrations.AddField( + model_name='crossdoc', + name='packID', + field=models.IntegerField(null=True, unique=True), + ), + migrations.AddField( + model_name='document', + name='packID', + field=models.IntegerField(null=True, unique=True), + ), + ] diff --git a/simple-backend/nlpviewer_backend/migrations/0013_auto_20210115_2047.py b/simple-backend/nlpviewer_backend/migrations/0013_auto_20210115_2047.py new file mode 100644 index 0000000..4d868a8 --- /dev/null +++ b/simple-backend/nlpviewer_backend/migrations/0013_auto_20210115_2047.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.4 on 2021-01-16 01:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('nlpviewer_backend', '0012_auto_20210115_1610'), + ] + + operations = [ + migrations.AlterField( + model_name='document', + name='packID', + field=models.IntegerField(null=True), + ), + ] diff --git a/simple-backend/nlpviewer_backend/models.py b/simple-backend/nlpviewer_backend/models.py index 911b1d6..f8d1729 100644 --- a/simple-backend/nlpviewer_backend/models.py +++ b/simple-backend/nlpviewer_backend/models.py @@ -39,6 +39,7 @@ class Document(models.Model): # content: textPack: text body + annotation name = models.CharField(max_length=200) + packID = models.IntegerField(null=True) # relationship: project project = models.ForeignKey( @@ -53,8 +54,8 @@ class Document(models.Model): textPack = models.TextField() class CrossDoc(models.Model): - name = models.CharField(max_length=200) + packID = models.IntegerField(unique = True, null=True) # relationship: project project = models.ForeignKey( diff --git a/simple-backend/nlpviewer_backend/urls.py b/simple-backend/nlpviewer_backend/urls.py index c8d9bae..6975ba9 100644 --- a/simple-backend/nlpviewer_backend/urls.py +++ b/simple-backend/nlpviewer_backend/urls.py @@ -46,13 +46,16 @@ path('documents//links//edit', document.edit_link), path('documents//links//delete', document.delete_link), - - path('crossdocs/new', crossdoc.create), - path('crossdocs//delete', crossdoc.delete), path('next_doc/', document.get_next_document_id), path('prev_doc/', document.get_prev_document_id), + path('crossdocs/new', crossdoc.create), + path('crossdocs//delete', crossdoc.delete), + path('crossdocs/', crossdoc.query), + path('crossdocs//links/new', crossdoc.new_cross_doc_link), + path('crossdocs//links//delete', crossdoc.delete_cross_doc_link), + path('projects/all', project.listAll), path('projects', project.list_user_projects), path('projects/new', project.create), diff --git a/src/app/App.tsx b/src/app/App.tsx index 176b176..fd1bf0e 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -5,6 +5,7 @@ import {BrowserRouter as Router, Switch, Route, Link} from 'react-router-dom'; import Login from './pages/Login'; import SignUp from './pages/SignUp'; import Viewer from './pages/Viewer'; +import CrossDoc from './pages/CrossDoc'; import Projects from './pages/Projects'; import Project from './pages/Project'; import Users from './pages/Users'; @@ -49,6 +50,10 @@ function App() { + + + + diff --git a/src/app/lib/api.ts b/src/app/lib/api.ts index d53c9ce..056b7cf 100644 --- a/src/app/lib/api.ts +++ b/src/app/lib/api.ts @@ -16,6 +16,20 @@ export interface APIDocConfig { config: string; } +interface APICrossDocPack { + id: string; + textPack: string; +} +interface APICrossDoc { + crossDocPack: APICrossDocPack; + _parent: APIDocument; + _child: APIDocument; + nextCrossDocId: string; + forteID: string; + nextID: string; + secret_code: string; +} + export function fetchDocuments(): Promise { return fetch('/api/documents').then(r => r.json()); } @@ -197,6 +211,25 @@ export function deleteLink(documentId: string, linkId: string) { return postData(`/api/documents/${documentId}/links/${linkId}/delete`, {}); } +export function fetchCrossDoc(id: string): Promise { + return fetch(`/api/crossdocs/${id}`).then(r => r.json()); +} +export function addCrossLink(crossDocID: string, data: any) { + return postData(`/api/crossdocs/${crossDocID}/links/new`, { + data, + }).then(r => r.json()); +} + +export function deleteCrossLink(crossDocID: string, linkID: string) { + return postData( + `/api/crossdocs/${crossDocID}/links/${linkID}/delete` + ).then(r => r.json()); +} + +export function nextCrossDoc() { + return postData(`/api/crossdocs/next-crossdoc`, {}).then(r => r.json()); +} + export function loadNlpModel(modelName: string) { return postData(`/api/nlp/load/${modelName}`, {}); } diff --git a/src/app/pages/CrossDoc.tsx b/src/app/pages/CrossDoc.tsx new file mode 100644 index 0000000..0b7b9fd --- /dev/null +++ b/src/app/pages/CrossDoc.tsx @@ -0,0 +1,104 @@ +import React, {useEffect, useState} from 'react'; +import CrossViewer from '../../crossviewer'; +import {transformPack, ISinglePack} from '../../nlpviewer'; +import {IMultiPack, IMultiPackQuestion} from '../../crossviewer'; +import { + transformMultiPack, + transformBackMultiPack, + transformMultiPackQuestion, +} from '../../crossviewer'; +import {useParams, useHistory} from 'react-router-dom'; +import { + fetchCrossDoc, + addCrossLink, + deleteCrossLink, + nextCrossDoc} from '../lib/api'; +// @ts-ignore +import { transitions, positions, Provider as AlertProvider } from 'react-alert' +// @ts-ignore +import AlertTemplate from 'react-alert-template-basic' + +// optional configuration +const options = { + // you can also just use 'bottom center' + position: positions.TOP_CENTER, + timeout: 3000, + offset: '30px', + // you can also just use 'scale' + transition: transitions.SCALE +}; + + +function CrossDoc() { + const {id} = useParams(); + const [packA, setPackA] = useState(null); + const [packB, setPackB] = useState(null); + const [multiPack, setMultiPack] = useState(null); + const [ + multiPackQuestion, + setMultiPackQuestion, + ] = useState(null); + const [forteID, setForteID] = useState(''); + const history = useHistory(); + useEffect(() => { + if (id) { + fetchCrossDoc(id).then(data => { + const [singlePackFromAPI, ontologyFromAPI] = transformPack( + data._parent.textPack, + data._parent.ontology + ); + setPackA(singlePackFromAPI); + const [singlePackFromAPI1, ontologyFromAPI1] = transformPack( + data._child.textPack, + data._child.ontology + ); + setPackB(singlePackFromAPI1); + setForteID(data.forteID); + const MultiPack = transformMultiPack( + data.crossDocPack.textPack, + data.forteID + ); + const MultiPackQuestion = transformMultiPackQuestion( + data.crossDocPack.textPack + ); + setMultiPack(MultiPack); + setMultiPackQuestion(MultiPackQuestion); + }); + } + }, [id]); + + if (!packA || !packB || !multiPack || !multiPackQuestion) { + return
Loading...
; + } + + return ( + + { + if (!id) return; + if (event.type === 'link-add') { + const { type, newLink } = event; + const linkAPIData = transformBackMultiPack(newLink); + const finalAPIData = {link:linkAPIData}; + addCrossLink(id, finalAPIData).then((return_object ) => { + setMultiPack(transformMultiPack(return_object.crossDocPack.textPack, forteID)); + }); + } else if (event.type ==="link-delete") { + const { type, linkID } = event; + deleteCrossLink(id, linkID).then((return_object ) => { + setMultiPack(transformMultiPack(return_object.crossDocPack.textPack, forteID)); + }); + + } + }} + /> + + ); +} + +export default CrossDoc; diff --git a/src/app/pages/Project.tsx b/src/app/pages/Project.tsx index 265b91c..bf150ad 100644 --- a/src/app/pages/Project.tsx +++ b/src/app/pages/Project.tsx @@ -22,6 +22,8 @@ function Docs() { const history = useHistory(); + const project_id = window.location.pathname.split('/').pop()!; + useEffect(() => { getProjectInfo().catch(() => { history.push('/login'); @@ -40,7 +42,6 @@ function Docs() { } function updateDocs() { if (projectInfo) { - const project_id = window.location.pathname.split('/').pop()!; if (projectInfo.project_type === 'indoc') { return fetchDocumentsProject(project_id).then(docs => { setDocs(docs); @@ -55,7 +56,6 @@ function Docs() { } } function handleAdd(filesToUpload: FileWithPath[], pack_type = 'single_pack') { - const project_id = window.location.pathname.split('/').pop()!; if (pack_type === 'single_pack') { filesToUpload.forEach(f => { const reader = new FileReader(); @@ -83,16 +83,15 @@ function Docs() { } } - function handleDelete(id: string, pack_type = 'single_pack') { - if (pack_type === 'single_pack') { - deleteDocument(id).then(() => { - updateDocs(); - }); - } else if (pack_type === 'multi_pack') { - deleteCrossDoc(id).then(() => { - updateDocs(); - }); - } + function handleSinglePackDelete(id: string) { + deleteDocument(id).then(() => { + updateDocs(); + }); + } + function handleMultiPackDelete(id: string) { + deleteCrossDoc(id).then(() => { + updateDocs(); + }); } return ( @@ -104,7 +103,7 @@ function Docs() {
  • {d.name}{' '} -
  • @@ -128,39 +127,39 @@ function Docs() { />
- {projectInfo && projectInfo.project_type === 'crossdoc' ? ( -
-

All multi docs:

- {crossdocs - ? crossdocs.map(d => ( -
    -
  • - {d.name}{' '} - -
  • -
- )) - : 'Empty'} -
- ) : null} - {projectInfo && projectInfo.project_type === 'crossdoc' ? ( -
-

new multi pack

- - handleAdd(file, 'multi_pack') - } - fileActionButtonText={'ADD'} - mimeType="application/json" - // Do not support zip now. - // mimeType='application/json, application/x-rar-compressed, application/octet-stream, application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip' - allowMultiple={true} - /> -
- ) : null} + {projectInfo && projectInfo.project_type === 'crossdoc' + ? [ +
+

All multi docs:

+ {crossdocs + ? crossdocs.map(d => ( +
    +
  • + {d.name}{' '} + +
  • +
+ )) + : 'Empty'} +
, +
+

new multi pack

+ + handleAdd(file, 'multi_pack') + } + fileActionButtonText={'ADD'} + mimeType="application/json" + // Do not support zip now. + // mimeType='application/json, application/x-rar-compressed, application/octet-stream, application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip' + allowMultiple={true} + /> +
, + ] + : null}
); } diff --git a/src/app/pages/Projects.tsx b/src/app/pages/Projects.tsx index 7161f64..8bccf5b 100644 --- a/src/app/pages/Projects.tsx +++ b/src/app/pages/Projects.tsx @@ -114,10 +114,8 @@ function Projects() { }); } - function handleProjectTypeChange(event: ChangeEvent) { - if (event.target) { - setProjectType((event.target as HTMLTextAreaElement).value); - } + function handleProjectTypeChange(newType: string) { + setProjectType(newType); } function userAddFiles( @@ -248,7 +246,9 @@ function Projects() { labelId="label" id="select" value={projectType} - onChange={handleProjectTypeChange} + onChange={e => + handleProjectTypeChange(e.target.value as string) + } > {PROJECT_TYPES.map(d => ( {d} diff --git a/src/crossviewer/components/Event.tsx b/src/crossviewer/components/Event.tsx new file mode 100644 index 0000000..b640a11 --- /dev/null +++ b/src/crossviewer/components/Event.tsx @@ -0,0 +1,58 @@ +import React, {useRef, useEffect, useState} from 'react'; +// @ts-ignore +import style from '../styles/CrossDocStyle.module.css'; + +export interface EventProp { + eventIndex: number; + eventText: String; + AnowOnEventIndex: number; + initSelected: number; + eventClickCallBack: any; +} + + +function Event({eventIndex, eventText, AnowOnEventIndex, initSelected,eventClickCallBack}: EventProp) { + const [selected, setSelected] = useState(initSelected); + const [hovered, setHovered] = useState(false); + useEffect(()=> { + setSelected(initSelected); + }, [AnowOnEventIndex, initSelected, eventClickCallBack]); + + function mouseOn() { + setHovered(true); + } + function mouseOff() { + setHovered(false); + } + function onClick(e: any) { + eventClickCallBack(eventIndex, !selected); + return false; + } + const myRef = useRef(null) + + let eventStyle = ""; + if (selected === 0 && !hovered) { + eventStyle = style.event_not_selected; + } else if (selected === 0 && hovered) { + eventStyle = style.event_not_selected_hovered; + } else if (selected === 1) { + // @ts-ignore + myRef.current.scrollIntoView(); + eventStyle = style.event_now_on; + } else if (selected === 2 && !hovered) { + eventStyle = style.event_selected; + } else if (selected === 2 && hovered) { + eventStyle = style.event_selected_hovered; + } + + return ( + onClick(e)}> + {eventText} + + ); +} + + + +export default Event; diff --git a/src/crossviewer/components/TextAreaA.tsx b/src/crossviewer/components/TextAreaA.tsx new file mode 100644 index 0000000..bc21e0f --- /dev/null +++ b/src/crossviewer/components/TextAreaA.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +// @ts-ignore +import style from '../styles/CrossDocStyle.module.css'; +import {IAnnotation, ISinglePack} from "../../nlpviewer"; + + +export interface TextAreaAProp { + text: string; + annotations : IAnnotation[]; + NER: IAnnotation[]; + AnowOnEventIndex:number; +} + + +function TextAreaA({ text, annotations, NER, AnowOnEventIndex}: TextAreaAProp) { + if (AnowOnEventIndex >= annotations.length) { + return ( +
+
+ {text} +
+
+ ) + } + + const highlightedText = highlighHelper(text, annotations, NER, AnowOnEventIndex); + return ( +
+
+ {highlightedText} +
+
+ ); +} + +function highlighHelper(text:String, annotations: IAnnotation[], NER: IAnnotation[], AnowOnEventIndex:number) { + let to_return : any[] = []; + // Outer loop to create parent + + let i:number; + let fragment = modifyNER(text, 0, annotations[0].span.begin, NER); + to_return = [...to_return, ...fragment]; + + // to_return.push(text.substring(0,annotations[0].span.begin)); + + for (i = 0; i < annotations.length-1; i ++) { + const nowStyle = AnowOnEventIndex === i ? style.a_now_event : style.a_other_event; + to_return.push(({text.substring(annotations[i].span.begin, annotations[i].span.end)})); + fragment = modifyNER(text, annotations[i].span.end, annotations[i+1].span.begin, NER); + to_return = to_return.concat(fragment); + // to_return.push(text.substring(annotations[i].span.end, annotations[i+1].span.begin)); + + } + const nowStyle = AnowOnEventIndex === i ? style.a_now_event : style.a_other_event; + to_return.push(({text.substring(annotations[i].span.begin, annotations[i].span.end)})); + + fragment = modifyNER(text, annotations[i].span.end, text.length, NER); + to_return = to_return.concat(fragment); + // to_return.push(text.substring(annotations[i].span.end)); + return to_return; +} + +function modifyNER(text:String, start:number, end:number, NER: IAnnotation[]) { + let result = []; + let ner_start = -1; + let ner_end = -1; + for (let i = 0; i < NER.length; i++) { + if (NER[i].span.end > start){ + ner_start = i; + break; + } + } + for (let i = NER.length-1; i >=0; i--) { + if (NER[i].span.begin < end){ + ner_end = i; + break; + } + } + if (ner_start === -1 || ner_end === -1) { + return [text.substring(start,end)]; + } + + let prev_end = start; + if (NER[ner_start].span.begin < start) { + result.push({text.substring(start, Math.min(NER[ner_start].span.end, end))}); + prev_end = NER[ner_start].span.end; + ner_start ++; + } + + + for (let i = ner_start; i <= ner_end; i++) { + result.push(text.substring(prev_end, NER[i].span.begin)); + result.push({text.substring(NER[i].span.begin, Math.min(NER[i].span.end, end))}); + prev_end = Math.min(NER[i].span.end, end); + } + if (prev_end < end){ + result.push(text.substring(prev_end, end)); + } + return result; +} + +export default TextAreaA; diff --git a/src/crossviewer/components/TextAreaB.tsx b/src/crossviewer/components/TextAreaB.tsx new file mode 100644 index 0000000..5c21b6e --- /dev/null +++ b/src/crossviewer/components/TextAreaB.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +// @ts-ignore +import style from '../styles/CrossDocStyle.module.css'; +import {ISinglePack, IAnnotation} from "../../nlpviewer"; +import Event from "./Event"; + +export interface TextAreaBProp { + text: string; + annotations : IAnnotation[]; + NER: IAnnotation[]; + AnowOnEventIndex:number; + BnowOnEventIndex: number; + BSelectedIndex:number[] + eventClickCallBack: any; +} + +function TextAreaB({ text, annotations, NER , AnowOnEventIndex, BnowOnEventIndex, BSelectedIndex, eventClickCallBack}: TextAreaBProp) { + const highlightedText = highlighHelper(text, annotations, NER); + + + function highlighHelper(text:String, annotations: IAnnotation[], NER : IAnnotation[]) { + let to_return : any[]= []; + let i:number; + let fragment = modifyNER(text, 0, annotations[0].span.begin, NER); + to_return = to_return.concat(fragment); + // to_return.push(text.substring(0,annotations[0].span.begin)); + for (i = 0; i < annotations.length-1; i ++) { + let initSelected = 0; + if (i === BnowOnEventIndex) { + initSelected = 1; + } else if (BSelectedIndex.includes(i)) { + initSelected = 2; + } + to_return.push(()); + fragment = modifyNER(text, annotations[i].span.end, annotations[i+1].span.begin, NER); + to_return = to_return.concat(fragment); + // to_return.push(text.substring(annotations[i].span.end, annotations[i+1].span.begin)); + } + + let initSelected = 0; + if (i === BnowOnEventIndex) { + initSelected = 1; + } else if (BSelectedIndex.includes(i)) { + initSelected = 2; + } + + to_return.push(()); + fragment = modifyNER(text, annotations[i].span.end, text.length, NER); + to_return = to_return.concat(fragment); + // to_return.push(text.substring(annotations[i].span.end)); + return to_return; + } + + return ( +
+
+ {highlightedText} +
+
+ ); +} + +function modifyNER(text:String, start:number, end:number, NER: IAnnotation[]) { + let result = []; + let ner_start = -1; + let ner_end = -1; + for (let i = 0; i < NER.length; i++) { + if (NER[i].span.end > start){ + ner_start = i; + break; + } + } + for (let i = NER.length-1; i >=0; i--) { + if (NER[i].span.begin < end){ + ner_end = i; + break; + } + } + if (ner_start === -1 || ner_end === -1) { + return [text.substring(start,end)]; + } + + let prev_end = start; + if (NER[ner_start].span.begin < start) { + result.push({text.substring(start, Math.min(NER[ner_start].span.end, end))}); + prev_end = NER[ner_start].span.end; + ner_start ++; + } + + + for (let i = ner_start; i <= ner_end; i++) { + result.push(text.substring(prev_end, NER[i].span.begin)); + result.push({text.substring(NER[i].span.begin, Math.min(NER[i].span.end, end))}); + prev_end = Math.min(NER[i].span.end, end); + } + if (prev_end < end){ + result.push(text.substring(prev_end, end)); + } + return result; +} + + + +export default TextAreaB; diff --git a/src/crossviewer/index.tsx b/src/crossviewer/index.tsx new file mode 100644 index 0000000..a4f93c5 --- /dev/null +++ b/src/crossviewer/index.tsx @@ -0,0 +1,260 @@ +import React, {useEffect, useState, } from 'react'; +// @ts-ignore +import ReactModal from 'react-modal'; +import style from "./styles/TextViewer.module.css"; +import TextAreaA from "./components/TextAreaA"; +import TextAreaB from "./components/TextAreaB"; + +// @ts-ignore +import Progress from 'react-progressbar'; +import {IAnnotation, ISinglePack,} from '../nlpviewer/lib/interfaces'; +import { + ICrossDocLink, + IMultiPack, IMultiPackQuestion, +} from "./lib/interfaces"; +import {cross_doc_event_legend, ner_legend} from "./lib/definitions"; +// @ts-ignore +import { useAlert } from 'react-alert' +import {useHistory} from "react-router"; + +export * from './lib/interfaces'; +export * from './lib/utils'; + +export type OnEventType = (event: any) => void; + +export interface CrossDocProp { + textPackA: ISinglePack; + textPackB: ISinglePack; + multiPack: IMultiPack; + multiPackQuestion: IMultiPackQuestion; + onEvent: OnEventType; + +} +export default function CrossViewer(props: CrossDocProp) { + + const history = useHistory(); + + const {textPackA, textPackB, multiPack, multiPackQuestion, onEvent} = props; + + + let annotationsA = textPackA.annotations; + let annotationsB = textPackB.annotations; + annotationsA.sort(function(a, b){return a.span.begin - b.span.begin}); + annotationsB.sort(function(a, b){return a.span.begin - b.span.begin}); + + const all_events_A : IAnnotation[] = annotationsA.filter((entry:IAnnotation)=>entry.legendId === cross_doc_event_legend); + const all_events_B : IAnnotation[] = annotationsB.filter((entry:IAnnotation)=>entry.legendId === cross_doc_event_legend); + const all_NER_A : IAnnotation[] = annotationsA.filter((entry:IAnnotation)=>entry.legendId === ner_legend); + const all_NER_B : IAnnotation[] = annotationsB.filter((entry:IAnnotation)=>entry.legendId === ner_legend); + // textPackA.annotations = all_events_A; + // textPackB.annotations = all_events_B; + + + const [AnowOnEventIndex, setANowOnEventIndex] = useState(0); + const [BnowOnEventIndex, setBNowOnEventIndex] = useState(-1); + const nowAOnEvent = all_events_A[AnowOnEventIndex]; + + const [nowQuestionIndex, setNowQuestionIndex] = useState(-1); + const now_question = nowQuestionIndex >=0 ? multiPackQuestion.coref_questions[nowQuestionIndex] : undefined; + const [currentAnswers, setCurrentAnswers] = useState([]); + + + let dynamic_instruction = ""; + if (BnowOnEventIndex===-1){ + dynamic_instruction = "Click events on the right if they are coreferential to the left event. Or click next event if there is no more." + } else if (nowQuestionIndex !== -1) { + dynamic_instruction = "Answer why you think these two events are coreferential." + } + + const BSelectedIndex = multiPack.crossDocLink.filter(item => item._parent_token === +nowAOnEvent.id && item.coref==="coref") + .map(item => item._child_token) + .map(event_id => all_events_B.findIndex(event => +event.id===event_id)); + + + + const BackEnable: boolean = AnowOnEventIndex > 0 && BnowOnEventIndex === -1; + const nextEventEnable:boolean = AnowOnEventIndex < all_events_A.length - 1 && BnowOnEventIndex === -1; + const progress_percent = Math.floor(AnowOnEventIndex / all_events_A.length * 100); + + const alert = useAlert(); + + function constructNewLink(whetherCoref:boolean, new_answers:number[]) : ICrossDocLink { + const newLink :ICrossDocLink= { + id: undefined, + _parent_token: +nowAOnEvent.id, + _child_token: +all_events_B[BnowOnEventIndex].id, + coref: whetherCoref? "coref" : "not-coref", + coref_answers: new_answers.map((option_id, index) => ({ + question_id: multiPackQuestion.coref_questions[index].question_id, + option_id: option_id + })), + }; + return newLink; + } + + function clickNextEvent() { + // no effect when we are asking questions + if (now_question || AnowOnEventIndex === all_events_A.length-1) { + return + } + resetBAndQuestions(); + setANowOnEventIndex(AnowOnEventIndex + 1); + + } + + function clickBack() { + // no effect when we are asking questions + if (now_question) { + return + } + setANowOnEventIndex(AnowOnEventIndex-1); + resetBAndQuestions(); + } + function clickOption(option_id:number) { + if (option_id === -1) { + resetBAndQuestions(); + return; + } + let new_answers = [...currentAnswers]; + new_answers.push(option_id); + if (nowQuestionIndex < multiPackQuestion.coref_questions.length-1){ + setNowQuestionIndex(nowQuestionIndex+1); + setCurrentAnswers(new_answers); + } else { + const newLink = constructNewLink(true, new_answers); + onEvent({ + type:"link-add", + newLink: newLink, + }); + resetBAndQuestions(); + } + } + + // this function is triggered when any event is clicked + function eventClickCallBack(eventIndex:number, selected:boolean){ + if (BnowOnEventIndex>=0) { + return + } + if (selected) { + // if there is no questions, directly send this link to server + if (multiPackQuestion.coref_questions.length === 0) { + const newLink = constructNewLink(true, []); + onEvent({ + type:"link-add", + newLink: newLink, + }); + return + } + + //else start to ask questions + setBNowOnEventIndex(eventIndex); + setNowQuestionIndex(0); + + } else { + if (!window.confirm("Are you sure you wish to delete this pair?")) return; + // @ts-ignore + const linkID = multiPack.crossDocLink.find(item => item._parent_token === +nowAOnEvent.id && item._child_token === +all_events_B[eventIndex].id).id; + onEvent({ + type: "link-delete", + linkID: linkID, + }); + } + return + } + + function resetBAndQuestions() { + setBNowOnEventIndex(-1); + setNowQuestionIndex(-1); + setCurrentAnswers([]); + } + + + return ( +
+
+ {/*discription here*/} +
+
+
{dynamic_instruction}
+
+
+ + + {/*next event and document*/} +
+
+ + + + {/*
*/} + {/* Click next event only if you have finished this event*/} + {/*
*/} +
+
+ {now_question ? +
+
+ {now_question.question_text} +
+
+ {now_question.options.map(option => { + return ( + + ) + })} +
+ +
+ : null} +
+
+ + +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+
+ ); +} + + diff --git a/src/crossviewer/lib/definitions.ts b/src/crossviewer/lib/definitions.ts new file mode 100644 index 0000000..c831a96 --- /dev/null +++ b/src/crossviewer/lib/definitions.ts @@ -0,0 +1,4 @@ +export const ner_legend = "ft.onto.base_ontology.EntityMention"; +export const cross_doc_event_legend = "edu.cmu.EventMention"; +export const coref_question_entry_name = "edu.cmu.CorefQuestion"; +export const suggest_question_entry_name = "edu.cmu.SuggestionQuestion"; \ No newline at end of file diff --git a/src/crossviewer/lib/interfaces.ts b/src/crossviewer/lib/interfaces.ts new file mode 100644 index 0000000..3f518f6 --- /dev/null +++ b/src/crossviewer/lib/interfaces.ts @@ -0,0 +1,61 @@ +export interface ICrossDocLink { + // link id is always a number + id: number|undefined; + _parent_token: number; + _child_token: number; + coref: string; + coref_answers: ICrossDocLinkAnswer[]; + // suggested_answers: ICrossDocLinkAnswer[]; +} +export interface ICrossDocLinkAnswer { + question_id: number; + option_id: number; +} + +export interface ICreationRecordPerson { + forteID: string; + records: number[]; +} + +export interface IMultiPack { + _parent_doc: number; + _child_doc: number; + crossDocLink : ICrossDocLink[]; + // suggestedLink: ICrossDocLink[]; + creation_records: ICreationRecordPerson[]; +} + + +export interface IMultiPackQuestion { + coref_questions: IQuestion[]; + suggest_questions: IQuestion[]; +} + +export interface IQuestion { + question_id: number; + question_text:string; + options: IOption[]; +} +export interface IOption{ + option_id: number; + option_text: string; +} +export interface IRange { + start: number; + end: number; + color?: string; +} + +export interface IAllRangesForOneType { + evidenceTypeID: number; + evidenceTypeName: string; + parent_ranges: IRange[]; + child_ranges: IRange[]; +} + +export interface I { + evidenceTypeID: number; + evidenceTypeName: string; + parent_ranges: IRange[]; + child_ranges: IRange[]; +} \ No newline at end of file diff --git a/src/crossviewer/lib/utils.ts b/src/crossviewer/lib/utils.ts new file mode 100644 index 0000000..c3637c6 --- /dev/null +++ b/src/crossviewer/lib/utils.ts @@ -0,0 +1,140 @@ +import {ICreationRecordPerson, ICrossDocLink, IMultiPack, IMultiPackQuestion, IQuestion} from "./interfaces"; +import {coref_question_entry_name, suggest_question_entry_name} from "./definitions"; + + +export function transformMultiPackQuestion(rawOntology: string): IMultiPackQuestion { + const data = JSON.parse(rawOntology); + // @ts-ignore + const coref_questions = data['py/state']["generics"].filter(item => item["py/object"] === coref_question_entry_name).map(raw_question => { + const question : IQuestion = { + question_id: raw_question["py/state"]["_tid"], + question_text: raw_question["py/state"]["question_body"], + // @ts-ignore + options : raw_question["py/state"]["options"].map((raw_option, index) => + ({ + option_id: index, + option_text: raw_option + }) + ) + }; + return question + }); + + //@ts-ignore + const suggest_questions = data['py/state']["generics"].filter(item => item["py/object"] === suggest_question_entry_name).map(raw_question => { + const question : IQuestion = { + question_id: raw_question["py/state"]["_tid"], + question_text: raw_question["py/state"]["question_body"], + // @ts-ignore + options : raw_question["py/state"]["options"].map((raw_option, index) => + ({ + option_id: index, + option_text: raw_option + }) + ) + }; + return question + }); + + return {coref_questions : coref_questions, + suggest_questions: suggest_questions,} +} + +export function transformMultiPack (rawPack: string, forteID: string): IMultiPack { + const data = JSON.parse(rawPack); + const packData = data['py/state']; + const [doc0, doc1] = packData['_pack_ref']; + + var annotated_tids : number[] = []; + if (forteID in packData['creation_records']) { + annotated_tids = packData['creation_records'][forteID]["py/set"]; + } + + const linkData = packData['links']; + const crossLinks :ICrossDocLink[]= linkData.flatMap((a: any)=> { + const link = { + id: a["py/state"]["_tid"], + _parent_token: a["py/state"]["_parent"]["py/tuple"][1], + _child_token: a["py/state"]["_child"]["py/tuple"][1], + coref: a["py/state"]["rel_type"], + }; + + if (a["py/state"]["rel_type"] !== "suggested" && annotated_tids.includes(a["py/state"]["_tid"])) { + return [link]; + } else { + return []; + } + }); + + + return { + _parent_doc: doc0, + _child_doc: doc1, + crossDocLink : crossLinks, + creation_records: [], + }; +} + + + +export function transformBackMultiPack(link: ICrossDocLink): any { + if (!link.hasOwnProperty('id') || link.id === undefined) { + return { + 'py/state': { + _child: { "py/tuple":[1, link._child_token]}, + _parent: { "py/tuple":[0, link._parent_token]}, + rel_type: link.coref, + coref_question_answers: link.coref_answers, + }, + } + } else { + return { + 'py/state': { + _child: {"py/tuple": [1, link._child_token]}, + _parent: {"py/tuple": [0, link._parent_token]}, + rel_type: link.coref, + _tid: link.id, + coref_question_answers: link.coref_answers, + } + } + } +} + + + + +export function transformMultiPackAnnoViewer (rawPack: string): IMultiPack { + const data = JSON.parse(rawPack); + const packData = data['py/state']; + const [doc0, doc1] = packData['_pack_ref']; + + + const linkData = packData['links']; + const crossLinks :ICrossDocLink[]= linkData.flatMap((a: any)=> { + const link = { + id: a["py/state"]["_tid"], + _parent_token: a["py/state"]["_parent"]["py/tuple"][1], + _child_token: a["py/state"]["_child"]["py/tuple"][1], + coref: a["py/state"]["rel_type"], + }; + + if (a["py/state"]["rel_type"] !== "suggested") { + return [link]; + } else { + return []; + } + }); + const creation_records_data = packData["creation_records"]; + const creation_records: ICreationRecordPerson[] = Object.keys(creation_records_data).map((forteID : any) => ( + { + forteID: forteID, + records: creation_records_data[forteID]["py/set"] + })); + const suggestedLinks :ICrossDocLink[] = []; + return { + _parent_doc: doc0, + _child_doc: doc1, + crossDocLink : crossLinks, + creation_records: creation_records, + }; +} \ No newline at end of file diff --git a/src/crossviewer/styles/CrossDocStyle.module.css b/src/crossviewer/styles/CrossDocStyle.module.css new file mode 100644 index 0000000..36e2df7 --- /dev/null +++ b/src/crossviewer/styles/CrossDocStyle.module.css @@ -0,0 +1,109 @@ +.instruction { + font-size: 15px; +} + +.text_area_container { + position: relative; + opacity: 0; +} + +.text_area_container_visible { + opacity: 1; + transition: opacity 0.2s; +} + +.text_node_container { + white-space: pre-wrap; + position: relative; + line-height: 25px; + font-size: 1.1rem; +} + +.annotation_line_toggle { + position: absolute; + left: -22px; + width: 18px; + font-size: 10px; + padding: 0px; +} + +.link_edit_container { + position: absolute; + top: 0; + left: 0; + z-index: 10; + transform: translateZ(0); +} + +.ann_edit_rect { + position: absolute; + top: 0; + left: 0; +} + +.annotation_text_selection_cursor { + position: absolute; + top: 0; + left: 0; + z-index: 7; +} + +.cursor { + border-left: 3px solid transparent; + border-right: 3px solid transparent; + border-top: 8px solid black; + transform: translate(-50%, 0); + display: block; + margin-top: -8px; + animation: floating 0.36s ease-in 0s infinite alternate; +} + +@keyframes floating { + from { + transform: translate(-50%, -5px); + } + to { + transform: translate(-50%, 0); + } +} + +.annotations_container { + position: absolute; + top: 0; + left: 0; + z-index: 5; +} + +.links_container { + position: absolute; + top: 0; + left: 0; + z-index: 1; +} + +.event_not_selected { + border-bottom: 3px solid #ff8b90; +} +.event_not_selected_hovered { + background-color: #ff8b90; + cursor: pointer; +} +.event_now_on{ + border: 3px solid#0071ff; + background-color: #a7dcff; +} +.event_selected { + background-color: #fe346e; +} +.event_selected_hovered { + background-color: #fe346e; + cursor: pointer; +} + +.a_now_event { + border: 3px solid #0071ff; +} +.a_other_event { + border-bottom: 3px solid #0071ff; +} + diff --git a/src/crossviewer/styles/TextViewer.module.css b/src/crossviewer/styles/TextViewer.module.css new file mode 100644 index 0000000..da574c8 --- /dev/null +++ b/src/crossviewer/styles/TextViewer.module.css @@ -0,0 +1,196 @@ +.text_viewer { + font-size: 13px; + min-width: 800px; +} + +.layout_container { + display: flex; + height: calc(100% - 35px); +} + +.center_area_container { + border: 1px solid #ccc; + border-top: none; + height: calc(100vh - 200px); + flex: 1; + position: relative; + transition: background 0.2s; + display: flex; + flex-direction: column; +} + +.tool_bar_container { + border-bottom: 1px solid #ccc; + padding: 8px; + background: #f8f8fa; + display: flex; + justify-content: space-between; + align-items: center; +} + +.spread_flex_container{ + display: flex; + width: 100%; + justify-content: space-between; +} +.text_area_container { + padding: 16px; + height: 100%; + overflow: scroll; +} + +.button_action_description { + margin-top: 4px; + font-size: 12px; + color: #999; +} + +.button_next_event{ + display:inline-block; + padding:0.3em 1.2em; + margin:0 0.3em 0.3em 0; + border-radius:2em; + border-style: none; + box-sizing: border-box; + text-decoration:none; + font-family:'Roboto',sans-serif; + font-weight:bold; + color:#FFFFFF; + background-color:#4eb5f1; + text-align:center; + transition: all 0.2s; +} +.button_next_event:hover{ + background-color:#4095c6; + cursor: pointer; +} +.button_next_event:disabled, +.button_next_event[disabled]{ + background-color:grey; +} + +.button_view_instruction{ + display:inline-block; + padding:0.3em 1.2em; + margin:0 0.3em 0.3em 0; + border-radius:2em; + border-style: none; + box-sizing: border-box; + text-decoration:none; + font-family:'Roboto',sans-serif; + font-weight:bold; + color:#FFFFFF; + background-color: #ff800d; + text-align:center; + transition: all 0.2s; +} +.button_view_instruction:hover{ + background-color: #da6d0d; + cursor: pointer; +} + + + +.bottom_box { + display: flex; + width: 100%; + justify-content: center; + padding: 10px; + height: 150px; + position: sticky; + bottom: 0; + +} +.question_container{ + font-size: 15px; +} +.option_container{ + display: flex; + justify-content: space-between; + cursor: pointer; +} +.button_option{ + display:inline-block; + padding:0.3em 1.2em; + margin:0.8em 0.8em 0.8em 0; + border-radius:2em; + border-style: none; + box-sizing: border-box; + text-decoration:none; + font-family:'Roboto',sans-serif; + font-weight:bold; + color:#FFFFFF; + background-color: #4eb5f1; + text-align:center; + transition: all 0.2s; +} +.button_option:hover{ + background-color: #4095c6; + cursor: pointer; +} + +.button_option_alert{ + display:inline-block; + padding:0.3em 1.2em; + margin:0.8em 0.8em 0.8em 0; + border-radius:2em; + border-style: none; + box-sizing: border-box; + text-decoration:none; + font-family:'Roboto',sans-serif; + font-weight:bold; + color:#FFFFFF; + background-color: #fe310b; + text-align:center; + transition: all 0.2s; +} +.button_option_alert:hover{ + background-color: #d92009; + cursor: pointer; +} + +.modal { + position: absolute; + top: 50%; + left: 50%; + display: block; + padding: 2em; + height: 80%; + width: 70%; + max-width: 70%; + background-color: #fff; + border-radius: 1em; + transform: translate(-50%, -50%); + outline: transparent; + overflow-y: auto; +} + +.modal_overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba(0,0,0,.4); + z-index: 999; +} + +.answer_box{ + position: absolute; + left: 50%; + transform: translate(-50%, 0); +} + +.second_tool_bar_container { + height: 120px; + border-bottom: 1px solid #ccc; + padding: 8px; + background: #f8f8fa; + display: flex; + justify-content: space-between; + align-items: center; +} + +.custom{ + font-size: 50px; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index f4b0500..76a6034 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ }, "include": [ "src/**/*.ts", - "test/**/*.ts" + "test/**/*.ts", + "src/**/*.tsx" ] } From e952c549f632614b1d083b5811cbde5cf265e043 Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Fri, 15 Jan 2021 23:24:23 -0500 Subject: [PATCH 4/7] CrossDoc: resolved code issues discussed in PR --- src/app/lib/api.ts | 2 +- src/app/pages/Project.tsx | 175 +++++++++++++++++-------------------- src/app/pages/Projects.tsx | 11 ++- 3 files changed, 87 insertions(+), 101 deletions(-) diff --git a/src/app/lib/api.ts b/src/app/lib/api.ts index d53c9ce..29e3649 100644 --- a/src/app/lib/api.ts +++ b/src/app/lib/api.ts @@ -24,7 +24,7 @@ export function fetchProjects(): Promise { return fetch('/api/projects').then(r => r.json()); } -export function fetchProject(id: string) { +export function fetchProject(id: string): Promise { return fetch(`/api/projects/${id}`).then(r => r.json()); } diff --git a/src/app/pages/Project.tsx b/src/app/pages/Project.tsx index 265b91c..1198279 100644 --- a/src/app/pages/Project.tsx +++ b/src/app/pages/Project.tsx @@ -22,77 +22,68 @@ function Docs() { const history = useHistory(); - useEffect(() => { - getProjectInfo().catch(() => { - history.push('/login'); - }); - }, [history]); + const project_id = window.location.pathname.split('/').pop()!; useEffect(() => { - updateDocs(); - }, [projectInfo]); + getProjectInfo(); + // .catch(() => { + // history.push('/login'); + // }); + }, [history]); function getProjectInfo() { const project_id = window.location.pathname.split('/').pop()!; return fetchProject(project_id).then(info => { setProjectInfo(info); + updateDocs(info); }); } - function updateDocs() { - if (projectInfo) { - const project_id = window.location.pathname.split('/').pop()!; - if (projectInfo.project_type === 'indoc') { - return fetchDocumentsProject(project_id).then(docs => { - setDocs(docs); - }); - } else if (projectInfo.project_type === 'crossdoc') { - return fetchDocumentsAndMultiPacksProject(project_id).then(result => { - console.log(result); - setDocs(result.docs); - setCrossDocs(result.crossdocs); - }); - } - } - } - function handleAdd(filesToUpload: FileWithPath[], pack_type = 'single_pack') { - const project_id = window.location.pathname.split('/').pop()!; - if (pack_type === 'single_pack') { - filesToUpload.forEach(f => { - const reader = new FileReader(); - reader.readAsText(f); - reader.onload = function () { - createDocument(f.name, reader.result as string, project_id).then( - () => { - updateDocs(); - } - ); - }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function updateDocs(info: any) { + if (info.project_type === 'indoc') { + return fetchDocumentsProject(project_id).then(docs => { + setDocs(docs); }); - } else if (pack_type === 'multi_pack') { - filesToUpload.forEach(f => { - const reader = new FileReader(); - reader.readAsText(f); - reader.onload = function () { - createCrossDoc(f.name, reader.result as string, project_id).then( - () => { - updateDocs(); - } - ); - }; + } else if (info.project_type === 'crossdoc') { + return fetchDocumentsAndMultiPacksProject(project_id).then(result => { + setDocs(result.docs); + setCrossDocs(result.crossdocs); }); } } + function handleSinglePackAdd(filesToUpload: FileWithPath[]) { + filesToUpload.forEach(f => { + const reader = new FileReader(); + reader.readAsText(f); + reader.onload = function () { + createDocument(f.name, reader.result as string, project_id).then(() => { + updateDocs(projectInfo); + }); + }; + }); + } - function handleDelete(id: string, pack_type = 'single_pack') { - if (pack_type === 'single_pack') { - deleteDocument(id).then(() => { - updateDocs(); - }); - } else if (pack_type === 'multi_pack') { - deleteCrossDoc(id).then(() => { - updateDocs(); - }); - } + function handleMultiPackAdd(filesToUpload: FileWithPath[]) { + filesToUpload.forEach(f => { + const reader = new FileReader(); + reader.readAsText(f); + reader.onload = function () { + createCrossDoc(f.name, reader.result as string, project_id).then(() => { + updateDocs(projectInfo); + }); + }; + }); + } + function handleSinglePackDelete(id: string) { + deleteDocument(id).then(() => { + updateDocs(projectInfo); + }); + } + function handleMultiPackDelete(id: string) { + deleteCrossDoc(id).then(() => { + updateDocs(projectInfo); + }); } return ( @@ -104,7 +95,7 @@ function Docs() {
  • {d.name}{' '} -
  • @@ -117,9 +108,7 @@ function Docs() {

    new pack

    - handleAdd(file, 'single_pack') - } + fileActionButtonFunc={handleSinglePackAdd} fileActionButtonText={'ADD'} mimeType="application/json" // Do not support zip now. @@ -128,39 +117,37 @@ function Docs() { /> - {projectInfo && projectInfo.project_type === 'crossdoc' ? ( -
    -

    All multi docs:

    - {crossdocs - ? crossdocs.map(d => ( -
      -
    • - {d.name}{' '} - -
    • -
    - )) - : 'Empty'} -
    - ) : null} - {projectInfo && projectInfo.project_type === 'crossdoc' ? ( -
    -

    new multi pack

    - - handleAdd(file, 'multi_pack') - } - fileActionButtonText={'ADD'} - mimeType="application/json" - // Do not support zip now. - // mimeType='application/json, application/x-rar-compressed, application/octet-stream, application/zip, application/octet-stream, application/x-zip-compressed, multipart/x-zip' - allowMultiple={true} - /> -
    - ) : null} + {projectInfo && projectInfo.project_type === 'crossdoc' + ? [ +
    +

    All multi docs:

    + {crossdocs + ? crossdocs.map(d => ( +
      +
    • + {d.name}{' '} + +
    • +
    + )) + : 'Empty'} +
    , +
    +

    new multi pack

    + +
    , + ] + : null} ); } diff --git a/src/app/pages/Projects.tsx b/src/app/pages/Projects.tsx index 7161f64..5fb9989 100644 --- a/src/app/pages/Projects.tsx +++ b/src/app/pages/Projects.tsx @@ -114,10 +114,8 @@ function Projects() { }); } - function handleProjectTypeChange(event: ChangeEvent) { - if (event.target) { - setProjectType((event.target as HTMLTextAreaElement).value); - } + function handleProjectTypeChange(type: string) { + setProjectType(type); } function userAddFiles( @@ -141,7 +139,6 @@ function Projects() { function createDefaultConfig(ontology: string): IProjectConfigs { const ontologyJson = JSON.parse(ontology); - console.log(ontologyJson); const ontologyObject: IOntology = camelCaseDeep(ontologyJson); const config: IProjectConfigs = { legendConfigs: {}, @@ -248,7 +245,9 @@ function Projects() { labelId="label" id="select" value={projectType} - onChange={handleProjectTypeChange} + onChange={e => + handleProjectTypeChange(e.target.value as string) + } > {PROJECT_TYPES.map(d => ( {d} From 657951fe4a633f248a942e612b74961aea3a5dae Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Fri, 15 Jan 2021 23:29:06 -0500 Subject: [PATCH 5/7] CrossDoc: code cleaning --- src/app/pages/Projects.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/Projects.tsx b/src/app/pages/Projects.tsx index 5fb9989..67e7767 100644 --- a/src/app/pages/Projects.tsx +++ b/src/app/pages/Projects.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, ChangeEvent} from 'react'; +import React, {useState, useEffect} from 'react'; import {fetchProjects, createProject, deleteProject} from '../lib/api'; import {Link, useHistory} from 'react-router-dom'; import {FileWithPath} from 'react-dropzone'; From e10036f645985c0c2960924daef31cda727f1ca1 Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Mon, 15 Feb 2021 18:57:45 -0800 Subject: [PATCH 6/7] CrossDoc: update package versions --- package.json | 1 - src/crossviewer/index.tsx | 4 ++-- yarn.lock | 46 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 982363a..37e05ea 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "react-modal": "^3.11.2", "react-alert": "^7.0.2", "react-alert-template-basic": "^1.0.0", - "react-progressbar": "^15.4.1", "styled-components": "^5.1.1" }, "devDependencies": { diff --git a/src/crossviewer/index.tsx b/src/crossviewer/index.tsx index a4f93c5..497d51d 100644 --- a/src/crossviewer/index.tsx +++ b/src/crossviewer/index.tsx @@ -6,7 +6,7 @@ import TextAreaA from "./components/TextAreaA"; import TextAreaB from "./components/TextAreaB"; // @ts-ignore -import Progress from 'react-progressbar'; +import { LinearProgress } from '@material-ui/core'; import {IAnnotation, ISinglePack,} from '../nlpviewer/lib/interfaces'; import { ICrossDocLink, @@ -191,7 +191,7 @@ export default function CrossViewer(props: CrossDocProp) { className={style.button_next_event} > Next event - + {/*
    */} {/* Click next event only if you have finished this event*/} {/*
    */} diff --git a/yarn.lock b/yarn.lock index 1c15755..824a45a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5096,6 +5096,11 @@ execa@^4.0.0, execa@^4.0.3: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -7645,7 +7650,7 @@ loglevel@^1.6.8: resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.0.tgz#728166855a740d59d38db01cf46f042caa041bb0" integrity sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ== -loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -9612,7 +9617,7 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9788,6 +9793,19 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-alert-template-basic@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/react-alert-template-basic/-/react-alert-template-basic-1.0.0.tgz#89bcf35095faf5ebdd25e1e7c300e6b4234b5ba1" + integrity sha512-6x5Us0oc+jj8BDNkvSWfQMESk5SdyGKitXdLb7CwIlIlecyATjCTKSWpLABg8tpKAPOSJu4Dv/fYUqxXEio/XA== + +react-alert@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/react-alert/-/react-alert-7.0.2.tgz#277b448c43a037a3ff5c82086e16867cadf98d05" + integrity sha512-oUxPk9DMaEm93Y33mdAmy4vDPZauMj30di4p4+QuZ3JOyoFSFteLSsjlhTkDjkyvJuVxToi3bbnsxehRHEPpeg== + dependencies: + prop-types "^15.7.2" + react-transition-group "^4.4.1" + react-app-polyfill@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/react-app-polyfill/-/react-app-polyfill-2.0.0.tgz#a0bea50f078b8a082970a9d853dc34b6dcc6a3cf" @@ -9871,6 +9889,21 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-modal@^3.11.2: + version "3.12.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.12.1.tgz#38c33f70d81c33d02ff1ed115530443a3dc2afd3" + integrity sha512-WGuXn7Fq31PbFJwtWmOk+jFtGC7E9tJVbFX0lts8ZoS5EPi9+WWylUJWLKKVm3H4GlQ7ZxY7R6tLlbSIBQ5oZA== + dependencies: + exenv "^1.2.0" + prop-types "^15.5.10" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -9984,7 +10017,7 @@ react-select@^3.0.8: react-input-autosize "^2.2.2" react-transition-group "^4.3.0" -react-transition-group@^4.3.0, react-transition-group@^4.4.0: +react-transition-group@^4.3.0, react-transition-group@^4.4.0, react-transition-group@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -12034,6 +12067,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack-chokidar2@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" From 06e3ab35d8cb8e130d0388ede4eaac75a85fb3b7 Mon Sep 17 00:00:00 2001 From: 132lilinwei Date: Mon, 22 Feb 2021 16:39:50 -0800 Subject: [PATCH 7/7] change quote symbols --- src/app/lib/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/lib/api.ts b/src/app/lib/api.ts index be1373a..ecfc4c3 100644 --- a/src/app/lib/api.ts +++ b/src/app/lib/api.ts @@ -227,7 +227,7 @@ export function deleteCrossLink(crossDocID: string, linkID: string) { } export function nextCrossDoc() { - return postData(`/api/crossdocs/next-crossdoc`, {}).then(r => r.json()); + return postData('/api/crossdocs/next-crossdoc', {}).then(r => r.json()); } export function loadNlpModel(modelName: string) {