diff --git a/.eslintignore b/.eslintignore index ecab6d5..f6cb226 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,7 @@ 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 + diff --git a/package.json b/package.json index c7801b0..37e05ea 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "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", "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..6d5a800 100644 --- a/simple-backend/nlpviewer_backend/handlers/crossdoc.py +++ b/simple-backend/nlpviewer_backend/handlers/crossdoc.py @@ -11,6 +11,93 @@ 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 +124,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..8079a9a 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( @@ -55,6 +56,8 @@ class Document(models.Model): 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..a123e7b 100644 --- a/simple-backend/nlpviewer_backend/urls.py +++ b/simple-backend/nlpviewer_backend/urls.py @@ -53,6 +53,12 @@ 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 29e3649..ecfc4c3 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/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..497d51d --- /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 { LinearProgress } from '@material-ui/core'; +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" ] } 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"