├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── __init__.py ├── application ├── __init__.py ├── app.py └── utils │ ├── __init__.py │ ├── constants.py │ ├── files.py │ ├── labels.py │ ├── log.py │ ├── suggestions.py │ ├── tutorial.py │ ├── umls_authentication.py │ └── umls_retrieve.py ├── basedir.py ├── clinical-annotator-lookups └── .gitkeep ├── config.py ├── data ├── .gitkeep ├── example.csv ├── example.txt └── log │ └── .gitkeep ├── docs ├── _config.yml ├── assets │ └── css │ │ └── style.scss ├── files │ ├── annotator_demo.mov │ ├── filter.png │ ├── match_options.png │ ├── more_info.png │ ├── rec.png │ ├── search.png │ └── suggestion.png └── index.md ├── index.py ├── main.py ├── manage.py ├── package-lock.json ├── requirements.txt ├── runtime.txt ├── setup.py ├── static ├── .babelrc ├── .eslintrc ├── bin │ └── server.js ├── bootstrap.rc ├── constants.js ├── gh │ └── browser.png ├── images │ ├── example.png │ ├── search.png │ ├── selected.png │ └── suggestion.png ├── index.html ├── karma.conf.js ├── package-lock.json ├── package.json ├── server.js ├── src │ ├── actions │ │ ├── colormap.js │ │ ├── files.js │ │ ├── index.js │ │ ├── labels.js │ │ ├── log.js │ │ ├── tutorial.js │ │ └── umls.js │ ├── components │ │ ├── Annotation │ │ │ ├── AnnotatedToken.tsx │ │ │ ├── AnnotationView.js │ │ │ ├── CUIModal.tsx │ │ │ ├── InfoModal.tsx │ │ │ ├── LabelController.tsx │ │ │ ├── LabelFilter.tsx │ │ │ ├── LabelListItem.tsx │ │ │ ├── Mark.tsx │ │ │ ├── PauseModal.tsx │ │ │ ├── SearchBar.tsx │ │ │ ├── Selection.tsx │ │ │ ├── TextController.tsx │ │ │ ├── config.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ ├── annotationUtils.ts │ │ │ │ ├── colorUtils.ts │ │ │ │ ├── index.ts │ │ │ │ ├── labelUtils.ts │ │ │ │ ├── selectionUtils.ts │ │ │ │ ├── suggestionUtils.ts │ │ │ │ └── tokenUtils.ts │ │ ├── Files │ │ │ ├── FileListItem.tsx │ │ │ └── FilesViewer.js │ │ ├── Footer │ │ │ ├── index.js │ │ │ └── styles.scss │ │ ├── Header │ │ │ └── index.js │ │ ├── Home │ │ │ └── index.js │ │ ├── NotFound.js │ │ └── Tutorial │ │ │ ├── TutorialAnnotation.js │ │ │ ├── TutorialDone.js │ │ │ ├── TutorialEvaluationItem.tsx │ │ │ ├── TutorialExplanation.js │ │ │ ├── TutorialView.js │ │ │ ├── types.ts │ │ │ └── utils │ │ │ ├── evaluationUtils.ts │ │ │ └── index.ts │ ├── constants │ │ └── index.js │ ├── containers │ │ ├── App │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ ├── app.scss │ │ │ │ ├── fonts │ │ │ │ └── roboto.scss │ │ │ │ ├── index.js │ │ │ │ ├── links.scss │ │ │ │ ├── screens.scss │ │ │ │ └── typography.scss │ │ ├── HomeContainer │ │ │ └── index.js │ │ └── TutorialDoneContainer │ │ │ └── index.js │ ├── index.js │ ├── reducers │ │ ├── data.js │ │ └── index.js │ ├── routes.js │ ├── store │ │ └── configureStore.js │ ├── style.scss │ └── utils │ │ ├── http_functions.js │ │ ├── isMobileAndTablet.js │ │ ├── misc.js │ │ ├── parallax.js │ │ └── test.js ├── tsconfig.json └── webpack │ ├── common.config.js │ ├── dev.config.js │ └── prod.config.js ├── test.py ├── testing_config.py ├── tests └── test_api.py ├── tutorial ├── attempts │ └── .gitkeep └── users │ └── .gitkeep └── umls_sources.csv /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | .DS_Store 3 | venv 4 | env 5 | application/__pycache__ 6 | application/utils/__pycache__ 7 | __pycache__ 8 | blog_old.md 9 | node_modules 10 | .idea/* 11 | *.pyc 12 | *.py~ 13 | tmp 14 | clinical-annotator-lookups/* 15 | !clinical-annotator-lookups/.gitkeep 16 | static/dist 17 | tutorial/*.txt 18 | tutorial/*.json 19 | tutorial/attempts/* 20 | !tutorial/attempts/.gitkeep 21 | data/*.txt 22 | data/*.csv 23 | !data/example.txt 24 | !data/example.csv 25 | data/*.json 26 | !data/.gitkeep 27 | data/log/* 28 | !data/log/.gitkeep 29 | tutorial/users/* 30 | !tutorial/users/.gitkeep 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sontag Lab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn main:app 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PRAnCER 2 | 3 | PRAnCER (Platform enabling Rapid Annotation for Clinical Entity Recognition) is a web platform that enables the rapid annotation of medical terms within clinical notes. A user can highlight spans of text and quickly map them to concepts in large vocabularies within a single, intuitive platform. Users can use the search and recommendation features to find labels without ever needing to leave the interface. Further, the platform can take in output from existing clinical concept extraction systems as pre-annotations, which users can accept or modify in a single click. These features allow users to focus their time and energy on harder examples instead. 4 | 5 | ## Usage 6 | ### Installation Instructions 7 | Detailed installation instructions are provided below; PRAnCER can operate on Mac, Windows, and Linux machines. 8 | ### Linking to UMLS Vocabulary 9 | Use of the platform requires a UMLS license, as it requires several UMLS-derived files to surface recommendations. Please email magrawal (at) mit (dot) edu to request these files, along with your API key so we may confirm. You can sign up [here](https://uts.nlm.nih.gov/uts/signup-login). Surfacing additional information in the UI also requires you enter your UMLS API key in application/utils/constants.py. 10 | ### Loading in and Exporting Data 11 | To load in data, users directly place any clinical notes as .txt files in the /data folder; an example file is provided. The output of annotation is .json file in the /data folder with the same file prefix as the .txt. To start annotating a note from scratch, a user can just delete the corresponding .json file. 12 | ### Pre-filled Suggestions 13 | Two options exist for pre-filled suggestions; users specify which they want to use in application/utils/constants.py. The default is "MAP". 14 | Option 1 for pre-filled suggestions is "MAP", if users want to preload annotations based on a dictionary of high-precision text to CUI for their domain, e.g. {hypertension: "C0020538"}. A pre-created dictionary will be provided alongside the UMLS files described above. 15 | Option 2 for pre-filled suggestions is "CSV", if users want to load in pre-computed pre-annotations (e.g. from their own algorithm, scispacy, cTAKES, MetaMap). Users simply place a CSV of spans and CUIs, with the same prefix as the data .txt file, and our scripts will automatically incorporate those annotations. example.csv in the /data file provides an example. 16 | 17 | ## Installation 18 | 19 | The platform requires **python3.7**, **node.js**, and several other python and javascript packages. Specific installation instructions for each follow! 20 | 21 | ### Backend requirements 22 | 23 | #### 1) First check if python3 is installed. 24 | 25 | You can check to see if it is installed: 26 | ``` 27 | $ python3 --version 28 | ``` 29 | If it is installed, you should see *Python 3.7.x* 30 | 31 | If you need to install it, you can easily do that with a package manager like Homebrew: 32 | ``` 33 | $ brew install python3 34 | ``` 35 | 36 | #### 2) With python3 installed, install necessary python packages. 37 | 38 | You can install packages with the python package installer pip: 39 | ``` 40 | $ pip3 install flask flask_script flask_migrate flask_bcrypt nltk editdistance requests lxml 41 | ``` 42 | 43 | ### Frontend requirements 44 | 45 | #### 3) Check to see if npm and node.js are installed: 46 | 47 | ``` 48 | $ npm -v 49 | $ node -v 50 | ``` 51 | 52 | If they are, you can skip to Step 4. 53 | If not, to install node, first install nvm: 54 | ``` 55 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.1/install.sh | bash 56 | ``` 57 | Source: https://github.com/nvm-sh/nvm 58 | 59 | Re-start your terminal and confirm nvm installation with: 60 | ``` 61 | command -v nvm 62 | ``` 63 | Which will return ```nvm``` if successful 64 | 65 | Then install node version 10.15.1: 66 | ``` 67 | $ nvm install 10.15.1 68 | ``` 69 | 70 | #### 4) Install the node dependencies: 71 | 72 | ``` 73 | $ cd static 74 | $ npm install --save 75 | ``` 76 | 77 | For remote server applications, permissions errors may be triggered.\ 78 | If so, try adding ```--user``` to install commands. 79 | 80 | ## Run program 81 | 82 | ### Run the backend 83 | 84 | Open one terminal tab to run the backend server: 85 | ```sh 86 | $ python3 manage.py runserver 87 | ``` 88 | If all goes well, you should see `* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)` followed by a few more lines in the terminal. 89 | 90 | ### Run the frontend 91 | 92 | Open a second terminal tab to run the frontend: 93 | ```sh 94 | $ cd static 95 | $ npm start 96 | ``` 97 | 98 | After this, open your browser to http://localhost:3000 and you should see the homepage! 99 | 100 | ## Contact 101 | 102 | If you have any questions, please email Monica Agrawal [magrawal@mit.edu]. Credit belongs to Ariel Levy for the development of this platform. 103 | 104 | Based on [React-Redux-Flask boilerplate.](https://github.com/dternyak/React-Redux-Flask) 105 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/__init__.py -------------------------------------------------------------------------------- /application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/application/__init__.py -------------------------------------------------------------------------------- /application/app.py: -------------------------------------------------------------------------------- 1 | from flask import request, render_template, jsonify, url_for, redirect, g 2 | from index import app 3 | from .utils.files import save_annotations_file, get_file_data, get_filenames_from_directory 4 | from .utils.labels import get_umls_labels, get_labels_for_code, get_labels_for_keyword, get_colormap_data 5 | from .utils.umls_retrieve import retrieve_cui_info 6 | from .utils.tutorial import file_evaluation, clear_user_annotations, create_user_dir 7 | from .utils.log import add_log 8 | 9 | 10 | @app.route('/', methods=['GET']) 11 | def index(): 12 | return render_template('index.html') 13 | 14 | 15 | @app.route('/', methods=['GET']) 16 | def any_root_path(path): 17 | return render_template('index.html') 18 | 19 | 20 | @app.route("/api/get_filenames", methods=["GET"]) 21 | def get_filenames(): 22 | filenames = get_filenames_from_directory() 23 | 24 | if filenames or filenames == []: 25 | return jsonify(filenames=filenames) 26 | else: 27 | return jsonify(error=True), 403 28 | 29 | 30 | @app.route("/api/get_file", methods=["POST"]) # this needs to be POST 31 | def get_file(): 32 | incoming = request.get_json() 33 | id = incoming["id"] 34 | textDir = incoming["textDir"] 35 | annDir = incoming["annDir"] 36 | 37 | if textDir == None and annDir == None: 38 | file = get_file_data(id) 39 | else: 40 | file = get_file_data(id, textDir, annDir) 41 | 42 | if file: 43 | return jsonify(file=file) 44 | else: 45 | return jsonify(error=True), 403 46 | 47 | 48 | @app.route("/api/save_annotations", methods=["POST"]) 49 | def save_annotations(): 50 | incoming = request.get_json() 51 | dir = incoming["dir"] 52 | 53 | if dir == None: 54 | is_saved = save_annotations_file(incoming["id"]+".json", incoming["annotations"]) 55 | else: 56 | is_saved = save_annotations_file(incoming["id"]+".json", incoming["annotations"], dir) 57 | 58 | if is_saved: 59 | return jsonify(saved=True) 60 | else: 61 | return jsonify(saved=False), 403 62 | 63 | 64 | @app.route("/api/search_labels", methods=["POST"]) 65 | def search_labels(): 66 | incoming = request.get_json() 67 | labels = get_labels_for_keyword(incoming["searchTerm"]) 68 | 69 | if labels or labels == []: 70 | return jsonify(labels=labels) 71 | else: 72 | return jsonify(error=True), 403 73 | 74 | 75 | @app.route("/api/recommend_labels", methods=["POST"]) 76 | def recommend_labels(): 77 | incoming = request.get_json() 78 | if incoming["isKeyword"]: 79 | labels = get_labels_for_keyword(incoming["searchTerm"]) 80 | else: 81 | labels = get_labels_for_code(incoming["searchTerm"]) 82 | 83 | if labels or labels == []: 84 | return jsonify(labels=labels) 85 | else: 86 | return jsonify(error=True), 403 87 | 88 | 89 | @app.route("/api/get_colormap", methods=["POST"]) 90 | def get_colormap(): 91 | incoming = request.get_json() 92 | colormap = get_colormap_data() 93 | 94 | if colormap: 95 | return jsonify(colormap=colormap) 96 | else: 97 | return jsonify(error=True), 403 98 | 99 | 100 | @app.route("/api/get_umls_info", methods=["POST"]) 101 | def get_umls_info(): 102 | incoming = request.get_json() 103 | umls_info = retrieve_cui_info(incoming["cui"]) 104 | 105 | if umls_info or umls_info == []: 106 | return jsonify(umls_info=umls_info) 107 | else: 108 | return jsonify(error=True), 403 109 | 110 | 111 | @app.route("/api/start_tutorial", methods=["POST"]) 112 | def start_tutorial(): 113 | incoming = request.get_json() 114 | user_id = incoming["userId"] 115 | start = create_user_dir(user_id) 116 | 117 | if user_id: 118 | return jsonify(start=start) 119 | else: 120 | return jsonify(error=True), 403 121 | 122 | 123 | @app.route("/api/get_tutorial_evaluation", methods=["POST"]) 124 | def get_tutorial_evaluation(): 125 | incoming = request.get_json() 126 | evaluation = file_evaluation(incoming["fileId"], incoming["userId"]) 127 | 128 | if evaluation: 129 | return jsonify(evaluation=evaluation) 130 | else: 131 | return jsonify(error=True), 403 132 | 133 | 134 | @app.route("/api/restart_tutorial", methods=["POST"]) 135 | def restart_tutorial(): 136 | incoming = request.get_json() 137 | restart = clear_user_annotations(incoming['userId']) 138 | return jsonify(restart=restart) 139 | 140 | 141 | @app.route("/api/add_log_entry", methods=["POST"]) 142 | def add_log_entry(): 143 | incoming = request.get_json() 144 | id = incoming["id"] 145 | action = incoming["action"] 146 | annotation_id = incoming["annotation_id"] 147 | metadata = incoming["metadata"] 148 | return jsonify(log=add_log(id, action, annotation_id, metadata)) 149 | -------------------------------------------------------------------------------- /application/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/application/utils/__init__.py -------------------------------------------------------------------------------- /application/utils/constants.py: -------------------------------------------------------------------------------- 1 | ## LOCAL FILEPATHS ## 2 | 3 | FILES_DIRECTORY = "./data" # Annotation text files and saved annotations 4 | 5 | LABELS_FILE = "./clinical-annotator-lookups/umls_lookup_snomed.pk" # Map keyword -> UMLS code 6 | INDEX_FILE = "./clinical-annotator-lookups/index_snomed.pk" # Map UMLS code -> index 7 | TYPES_FILE = "./clinical-annotator-lookups/type_tui_lookup.pk" # Map types and tuis 8 | COLORS_FILE = "./clinical-annotator-lookups/color_lookup.pk" # Map type -> color 9 | SUGGESTIONS_FILE = "./clinical-annotator-lookups/suggestions.pk" # Map word -> CUI 10 | 11 | 12 | LOG_DIRECTORY = "./data/log" # Logs of actions 13 | 14 | SOURCES_FILE = './umls_sources.csv' # List of UMLS defn sources 15 | 16 | TUTORIAL_DIRECTORY = './tutorial' # Tutorial files 17 | USERS_DIRECTORY = './tutorial/users' # Tutorial attempts by user ID 18 | STORAGE_DIRECTORY = './tutorial/attempts' # Tutorial attempts by timestamp 19 | 20 | 21 | ## FIXED CONSTANTS ## 22 | 23 | TUTORIAL_LENGTH = 4 # Number of steps in the tutorial (also frontend) 24 | 25 | UMLS_APIKEY = "" # Key to make UMLS API calls 26 | UMLS_URI = "https://uts-ws.nlm.nih.gov" # URI to make UMLS API calls 27 | 28 | # Options for pre-filled suggestions are "NONE", "CSV", and "MAP" 29 | SUGGESTION_METHOD = "MAP" 30 | -------------------------------------------------------------------------------- /application/utils/files.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import pandas as pd 4 | 5 | from .constants import * 6 | from .suggestions import suggest_mapped_annotations, load_csv_annotations 7 | 8 | 9 | def get_filenames_from_directory(): 10 | filenames = [] 11 | for f in os.listdir(FILES_DIRECTORY): 12 | name, ext = os.path.splitext(f) 13 | if '.txt' in ext: 14 | filenames.append(name) 15 | return sorted(filenames) 16 | 17 | 18 | def get_file_data(id, textDir=FILES_DIRECTORY, annDir=FILES_DIRECTORY): 19 | filepath = textDir + '/' + id + '.txt' 20 | ann_filepath = annDir + '/' + id + '.json' 21 | csv_path = textDir + '/' + id + '.csv' 22 | try: 23 | with open(filepath, 'r') as infile: 24 | file_text = infile.read() 25 | except FileNotFoundError: 26 | print(filepath + " not found.") 27 | file_text = "" 28 | 29 | try: 30 | with open(ann_filepath, 'r') as infile: 31 | file_ann = json.load(infile) 32 | except FileNotFoundError: 33 | file_ann = [] 34 | if SUGGESTION_METHOD == "MAP": 35 | file_ann = suggest_mapped_annotations(file_text) 36 | if SUGGESTION_METHOD == "CSV": 37 | print("inside CSV annotations") 38 | try: 39 | with open(csv_path, 'r') as csvfile: 40 | anns = pd.read_csv(csvfile) 41 | file_ann = load_csv_annotations(file_text, anns) 42 | except FileNotFoundError: 43 | print(csv_path + " not found.") 44 | 45 | save_annotations_file(id + '.json', file_ann, annDir) 46 | 47 | return {"text": file_text, "annotations": file_ann} 48 | 49 | 50 | def save_annotations_file(filename, annotations, dir=FILES_DIRECTORY): 51 | filepath = dir + '/' + filename 52 | try: 53 | with open(filepath, 'w') as outfile: 54 | json.dump(annotations, outfile) 55 | except FileNotFoundError: 56 | print(filepath + " not found.") 57 | return None 58 | return filepath 59 | -------------------------------------------------------------------------------- /application/utils/labels.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | import os 3 | import pickle 4 | import string 5 | import random 6 | import time 7 | 8 | ## To install 9 | import editdistance 10 | from nltk.stem.porter import * 11 | from nltk.corpus import stopwords 12 | 13 | from .constants import * 14 | from .umls_retrieve import retrieve_labels 15 | 16 | 17 | umls, lookup = pickle.load(open(LABELS_FILE, 'rb'), encoding='bytes') 18 | index = pickle.load(open(INDEX_FILE, 'rb'), encoding='bytes') 19 | types, tuis = pickle.load(open(TYPES_FILE, 'rb'), encoding='bytes') 20 | colors = pickle.load(open(COLORS_FILE, 'rb'), encoding='bytes') 21 | 22 | cui2index = {term[0]: i for i, term in enumerate(umls)} 23 | 24 | stemmer = PorterStemmer() 25 | translator = str.maketrans(string.punctuation, ' '*len(string.punctuation)) 26 | 27 | 28 | def get_labels_for_code(code): 29 | if code in cui2index: 30 | return [label_data(cui2index[code])] 31 | else: 32 | return [] 33 | 34 | 35 | def get_labels_for_codes(codes): 36 | labels = [] 37 | for code in codes: 38 | labels += get_labels_for_code(code) 39 | return labels 40 | 41 | 42 | def label_data(label_id): 43 | label_entry = umls[label_id] 44 | cui = label_entry[0] 45 | name = label_entry[1] 46 | categories = [[type, category_lookup(type)] for type in label_entry[3]] 47 | return [cui, name, categories] 48 | 49 | 50 | def category_lookup(type): 51 | tui = types[type][0] 52 | if tui in tuis: 53 | return tuis[tui] 54 | else: 55 | return 'Other' 56 | 57 | 58 | def get_colormap_data(): 59 | return colors 60 | 61 | 62 | def get_umls_labels(keyword): 63 | api_labels = retrieve_labels(keyword) 64 | if len(api_labels) == 1 and api_labels[0]['ui'] == 'NONE': 65 | return [] 66 | api_codes = list(map(lambda l: l['ui'], api_labels)) 67 | return get_labels_for_codes(api_codes) 68 | 69 | 70 | def get_algorithm_labels(keyword_entered): 71 | keyword = keyword_entered.lower() 72 | ordered_lookups = get_lookups_ordered(keyword) 73 | ordered_indexed = get_inverted_index_ordered(keyword, index) 74 | 75 | # Get rid of repeats from the lookup table 76 | ordered_indexed = [i for i in ordered_indexed if i not in ordered_lookups] 77 | 78 | combined = ordered_lookups + ordered_indexed 79 | return combined 80 | 81 | 82 | # All modes -> best recommendation system 83 | def get_labels_for_keyword(keyword_entered): 84 | labels = get_algorithm_labels(keyword_entered) 85 | return labels 86 | 87 | 88 | def get_distance(term1, term2): 89 | sorted1 = "".join(sorted(term1)).strip() 90 | sorted2 = "".join(sorted(term2)).strip() 91 | return editdistance.distance(sorted1, sorted2) 92 | 93 | 94 | def get_inverted_index_ordered(term, inverted_index, num=15): 95 | words = term.translate(translator).split() 96 | stemmed = [] 97 | candidates = [] 98 | scores = [] 99 | for word in words: 100 | try: 101 | stemmed += [stemmer.stem(word)] 102 | except: 103 | return [] 104 | count_list = [] 105 | for stem in stemmed: 106 | count_list += list(inverted_index[stem]) 107 | counter = Counter(count_list) 108 | if len(counter) == 0: 109 | return [] 110 | max_matches = counter.most_common(1)[0][1] 111 | for label_id, count in counter.most_common(min(5000, len(counter))): 112 | if count == max_matches: 113 | entry = label_data(label_id) 114 | candidates += [entry] 115 | synonyms = [umls[label_id][1]] + umls[label_id][4] 116 | score = min([get_distance(synonym, term) for synonym in synonyms]) 117 | scores += [score] 118 | return [result for _,result in sorted(zip(scores, candidates))][:num] 119 | 120 | 121 | def get_lookups_ordered(keyword): 122 | if keyword not in lookup: 123 | return [] 124 | label_ids = lookup[keyword] 125 | scores = [] 126 | results = [] 127 | for label_id in label_ids: 128 | results.append(label_data(label_id)) 129 | score = -len(umls[label_id][4]) 130 | if keyword == umls[label_id][1]: 131 | score -= 100 132 | scores.append(score) 133 | return [result for _,result in sorted(zip(scores, results))] 134 | -------------------------------------------------------------------------------- /application/utils/log.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from .constants import * 4 | 5 | 6 | CURRENT_LOG = "" 7 | 8 | 9 | def log_entry(info_list): 10 | return ",".join([str(i) for i in info_list]) + '\n' 11 | 12 | 13 | def open_log(): 14 | global CURRENT_LOG 15 | timestamp = int(time.time()) 16 | filepath = LOG_DIRECTORY + '/' + str(timestamp) + '.csv' 17 | try: 18 | with open(filepath, 'a') as f: 19 | CURRENT_LOG = filepath 20 | print("Activity log opened: ", CURRENT_LOG) 21 | f.write(log_entry([timestamp, 'START'])) 22 | f.close() 23 | except FileNotFoundError: 24 | print(filepath + " not found.") 25 | return None 26 | return filepath 27 | 28 | 29 | def add_log(id, action, annotation_id, metadata): 30 | timestamp = int(time.time()) 31 | try: 32 | with open(CURRENT_LOG, 'a') as f: 33 | f.write(log_entry([timestamp, id, action, annotation_id] + metadata)) 34 | f.close() 35 | except FileNotFoundError: 36 | print("Log file not found: " + CURRENT_LOG) 37 | return False 38 | return True 39 | -------------------------------------------------------------------------------- /application/utils/suggestions.py: -------------------------------------------------------------------------------- 1 | import re 2 | import time 3 | import pickle 4 | from ast import literal_eval 5 | 6 | from .labels import cui2index, label_data 7 | from .constants import * 8 | 9 | 10 | suggestions = pickle.load(open(SUGGESTIONS_FILE, 'rb')) 11 | 12 | def load_csv_annotations(text, annotation_df): 13 | annotations = [] 14 | for i, row in annotation_df.iterrows(): 15 | span = (int(row['start']), int(row['end'])) 16 | cui = [row['cui']] 17 | annotations += [create_annotation(text[span[0]:span[1]], span, cui, 'high')] 18 | return annotations 19 | 20 | def suggest_mapped_annotations(text): 21 | annotations = [] 22 | for keyword in suggestions: 23 | labels = suggestions[keyword] 24 | annotations += keyword_annotations(text, keyword, labels, 'high') 25 | print(annotations) 26 | return annotations 27 | 28 | 29 | def keyword_annotations(text, keyword, labels, confidence): 30 | ## Default to separate word, case insensitive 31 | try: 32 | return [create_annotation(text, match.span(), labels, confidence) 33 | for match in re.finditer( 34 | r'(?:^|\W)' + keyword + r'(?:$|\W)', 35 | text, 36 | flags=re.IGNORECASE 37 | ) 38 | ] 39 | except: 40 | return [] 41 | 42 | 43 | def create_annotation(text, span, labels, confidence): 44 | timestamp = time.time() # Didn't round to ms to preserve uniqueness 45 | start, end = span 46 | if type(labels) is str: 47 | labels = [labels] 48 | annotation = { 49 | "annotationId": timestamp, 50 | "createdAt": timestamp, 51 | "text": text[start:end], 52 | "spans": [{"start": span[0], "end": span[1]}], 53 | "labels": create_labels(labels, confidence), 54 | "CUIMode": "normal", 55 | "experimentMode": 0, 56 | "creationType": "auto", 57 | "decision": "undecided" 58 | } 59 | 60 | return annotation 61 | 62 | ## Only creates a suggestion for a single code 63 | def create_labels(codes, confidence): 64 | print(codes, type(codes), confidence) 65 | if len(codes) > 0 and codes[0] in cui2index: 66 | print("inside here") 67 | data = label_data(cui2index[codes[0]]) 68 | print(data) 69 | return [{ 70 | "labelId": data[0], 71 | "title": data[1], 72 | "categories": [{"title": c[0], "type": c[1]} for c in data[2]], 73 | "confidence": confidence 74 | }] 75 | else: 76 | return [] 77 | -------------------------------------------------------------------------------- /application/utils/tutorial.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import time 4 | import shutil 5 | 6 | from .constants import * 7 | 8 | 9 | def load_annotations(fname): 10 | f = open(fname, 'r') 11 | return json.load(f) 12 | 13 | 14 | def get_span(ann): 15 | s = ann['spans'][0] 16 | return [s['start'], s['end']] 17 | 18 | 19 | def clean_ann(ann): 20 | text = ann['text'] 21 | start, end = ann['spans'][0]['start'], ann['spans'][0]['end'] 22 | if text.startswith((' ', '\n', '.', ',')): 23 | text = text[1:] 24 | start = start + 1 25 | if text.startswith(('per ')): 26 | text = text[4:] 27 | start = start + 4 28 | if text.endswith((' ', '\n', '.', ',')): 29 | text = text[:-1] 30 | end = end - 1 31 | if text.endswith(' of') or text.endswith('\nof'): 32 | text = text[:-3] 33 | end = end - 3 34 | ann['spans'][0]['end'] = end 35 | ann['spans'][0]['start'] = start 36 | # ann['text'] = text 37 | return ann 38 | 39 | 40 | def convert_annotations(annotations): 41 | dict_annotations = {} 42 | for ann in annotations: 43 | is_suggestion = ann['creationType'] == 'auto' or ann['creationType'] == 'dynamic' 44 | decision = ann['decision'] 45 | # Ignore rejected/undecided suggestions as annotations 46 | if not (is_suggestion and (decision == 'rejected' or decision == 'undecided')): 47 | ann = clean_ann(ann) 48 | dict_annotations[tuple(get_span(ann))] = ann 49 | return dict_annotations 50 | 51 | 52 | def span_length(span): 53 | return span[1] - span[0] 54 | 55 | 56 | def count_span_overlap(span1, span2): 57 | sorted_spans = [span1, span2] if span1[0] <= span2[0] else [span2, span1] 58 | left, right = sorted_spans[0], sorted_spans[1] 59 | return max(left[1] - right[0], 0) 60 | 61 | 62 | def count_ann_overlap(ann1, ann2): 63 | span1 = get_span(ann1) 64 | span2 = get_span(ann2) 65 | return count_span_overlap(span1, span2) 66 | 67 | 68 | def is_correct_labels(gold, user): 69 | gold_label_ids = set(map(lambda label: label["labelId"], gold["labels"])) 70 | user_label_ids = set(map(lambda label: label["labelId"], user["labels"])) 71 | return len(gold_label_ids.intersection(user_label_ids)) > 0 72 | 73 | 74 | def max_start(result): 75 | def get_start(ann): 76 | return get_span(ann)[0] 77 | max_start = -1 78 | if result['userMatches']: 79 | for u in result['userMatches']: 80 | if get_start(u) > max_start: 81 | max_start = get_start(u) 82 | if result['gold']: 83 | g = result['gold'] 84 | if get_start(g) > max_start or max_start == -1: 85 | max_start = get_start(g) 86 | return max_start 87 | 88 | 89 | def compare_ann(user_ann, gold_ann): 90 | results = [] 91 | 92 | gold_user_span_matches = {} 93 | gold_spans = set() 94 | user_spans = set() 95 | 96 | # Add all exact gold:user span matches 97 | for g in gold_ann: 98 | gold = gold_ann[g] 99 | if g in user_ann: 100 | gold_spans.add(g) 101 | user_spans.add(g) 102 | gold_user_span_matches[g] = [g] 103 | 104 | # Add all remaining user spans that have partial gold:user span matches 105 | user_spans_unmatched = set(user_ann.keys()) - user_spans 106 | gold_spans_unmatched = set(gold_ann.keys()) - gold_spans 107 | gold_spans_unmatched_sorted = sorted(gold_spans_unmatched, key=span_length) 108 | for u in user_spans_unmatched: 109 | # Compare with unmatched gold spans, prioritizing by overlap length 110 | best_match = max(gold_spans_unmatched_sorted, key=lambda g: count_span_overlap(u, g), default=None) 111 | # If partial match found, add to existing results 112 | if best_match and count_span_overlap(best_match, u) > 0: 113 | gold_spans.add(best_match) 114 | user_spans.add(u) 115 | if best_match not in gold_user_span_matches: 116 | gold_user_span_matches[best_match] = [] 117 | gold_user_span_matches[best_match].append(u) 118 | 119 | # Convert gold:user matches to JSON results 120 | for g in gold_user_span_matches: 121 | matched_spans = gold_user_span_matches[g] 122 | potential_match = matched_spans[0] 123 | span_score = 1 if g == potential_match else 0 124 | label_score = 1 if span_score and is_correct_labels(gold_ann[g], user_ann[potential_match]) else 0 125 | sorted_matched_spans = sorted(matched_spans, key=lambda s: s[0]) 126 | results.append({ 127 | 'gold': gold_ann[g], 128 | 'userMatches': [user_ann[u] for u in sorted_matched_spans], 129 | 'spanScore': span_score, 130 | 'labelScore': label_score 131 | }) 132 | 133 | # Add missing gold spans to JSON results 134 | gold_spans_unmatched = gold_spans_unmatched - gold_spans 135 | for g in gold_spans_unmatched: 136 | results.append({ 137 | 'gold': gold_ann[g], 138 | 'userMatches': None, 139 | 'spanScore': 0, 140 | 'labelScore': 0 141 | }) 142 | 143 | # Add extra user spans to JSON results 144 | user_spans_unmatched = user_spans_unmatched - user_spans 145 | for u in user_spans_unmatched: 146 | results.append({ 147 | 'gold': None, 148 | 'userMatches': [user_ann[u]], 149 | 'spanScore': 0, 150 | 'labelScore': 0 151 | }) 152 | 153 | return sorted(results, key=lambda r: max_start(r)) 154 | 155 | 156 | def create_user_dir(userId): 157 | user_dirpath = USERS_DIRECTORY + '/' + userId 158 | 159 | try: 160 | os.mkdir(user_dirpath) 161 | except OSError: 162 | print("Creation of the directory %s failed." % user_dirpath) 163 | return False 164 | else: 165 | print("Success creating directory %s." % user_dirpath) 166 | return True 167 | 168 | 169 | def file_evaluation(fileId, userId): 170 | user_filepath = USERS_DIRECTORY + '/' + userId + '/' + fileId + '.json' 171 | user_ann = convert_annotations(load_annotations(user_filepath)) 172 | gold_filepath = TUTORIAL_DIRECTORY + '/' + fileId + '-gold.json' 173 | gold_ann = convert_annotations(load_annotations(gold_filepath)) 174 | return compare_ann(user_ann, gold_ann) 175 | 176 | 177 | def clear_user_annotations(userId): 178 | timestamp = int(time.time()) 179 | for i in range(1, TUTORIAL_LENGTH + 1): 180 | filepath = USERS_DIRECTORY + '/' + userId + '/' + str(i) + '.json' 181 | storage = STORAGE_DIRECTORY + '/' + userId + '-' + str(i) + '-' + str(timestamp) + '.json' 182 | if os.path.exists(filepath): 183 | try: 184 | shutil.copyfile(filepath, storage) 185 | os.remove(filepath) 186 | except IOError: 187 | print("Tutorial storage not writeable.") 188 | return None 189 | except OSError: 190 | print("Tutorial filepath not found.") 191 | return None 192 | return True 193 | -------------------------------------------------------------------------------- /application/utils/umls_authentication.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ## Source: https://github.com/HHS/uts-rest-api/blob/master/samples/python/Authentication.py 3 | 4 | import requests 5 | import lxml.html as lh 6 | from lxml.html import fromstring 7 | 8 | uri="https://utslogin.nlm.nih.gov" 9 | auth_endpoint = "/cas/v1/api-key" 10 | 11 | class Authentication: 12 | 13 | def __init__(self, apikey): 14 | self.apikey=apikey 15 | self.service="http://umlsks.nlm.nih.gov" 16 | 17 | def gettgt(self): 18 | params = {'apikey': self.apikey} 19 | h = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "User-Agent":"python" } 20 | r = requests.post(uri+auth_endpoint,data=params,headers=h) 21 | response = fromstring(r.text) 22 | ## extract the entire URL needed from the HTML form (action attribute) returned - looks similar to https://utslogin.nlm.nih.gov/cas/v1/tickets/TGT-36471-aYqNLN2rFIJPXKzxwdTNC5ZT7z3B3cTAKfSc5ndHQcUxeaDOLN-cas 23 | ## we make a POST call to this URL in the getst method 24 | tgt = response.xpath('//form/@action')[0] 25 | return tgt 26 | 27 | def getst(self,tgt): 28 | params = {'service': self.service} 29 | h = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/plain", "User-Agent":"python" } 30 | r = requests.post(tgt,data=params,headers=h) 31 | st = r.text 32 | return st 33 | -------------------------------------------------------------------------------- /application/utils/umls_retrieve.py: -------------------------------------------------------------------------------- 1 | ################################################################################################# 2 | # source: https://github.com/HHS/uts-rest-api/blob/master/samples/python/retrieve-cui-or-code.py 3 | # TODO: get API key from user's UMLS profile? 4 | ################################################################################################# 5 | 6 | 7 | import requests 8 | import json 9 | import urllib 10 | 11 | from .umls_authentication import * 12 | from .constants import * 13 | 14 | 15 | sources_text = open(SOURCES_FILE, 'r').read() 16 | sources_set = set(sources_text.split(',')) 17 | AuthClient = Authentication(UMLS_APIKEY) 18 | 19 | 20 | ################################### 21 | #get TGT for our session 22 | ################################### 23 | 24 | 25 | def get_tgt(): 26 | tgt = AuthClient.gettgt() 27 | return tgt 28 | 29 | 30 | def retrieve_cui_info(cui): 31 | content_endpoint = "/rest/content/current/CUI/"+str(cui)+"/definitions" 32 | tgt = get_tgt() 33 | query = {'ticket':AuthClient.getst(tgt), 'sabs':sources_text} 34 | r = requests.get(UMLS_URI + content_endpoint, params=query) 35 | r.encoding = 'utf-8' 36 | try: 37 | items = json.loads(r.text) 38 | jsonData = items["result"] 39 | filteredData = [] 40 | for r in jsonData: 41 | if r['rootSource'] in sources_set: 42 | filteredData.append(r) 43 | return filteredData 44 | except json.decoder.JSONDecodeError: 45 | print("No definitions found") 46 | return [] 47 | 48 | 49 | def retrieve_labels(keyword): 50 | content_endpoint = "/rest/search/current" 51 | tgt = get_tgt() 52 | query = { 53 | 'ticket': AuthClient.getst(tgt), 54 | 'string': keyword, 55 | 'sabs': 'SNOMEDCT_US' 56 | } 57 | r = requests.get(UMLS_URI + content_endpoint, params=query) 58 | r.encoding = 'utf-8' 59 | try: 60 | items = json.loads(r.text) 61 | if 'result' in items: 62 | jsonData = items['result'] 63 | if 'results' in jsonData: 64 | return jsonData['results'] 65 | else: 66 | return [] 67 | except json.decoder.JSONDecodeError: 68 | print("No definitions found") 69 | return [] 70 | -------------------------------------------------------------------------------- /basedir.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | -------------------------------------------------------------------------------- /clinical-annotator-lookups/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/clinical-annotator-lookups/.gitkeep -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setup import basedir 4 | 5 | 6 | class BaseConfig(object): 7 | DEBUG = True 8 | 9 | 10 | class TestingConfig(object): 11 | """Development configuration.""" 12 | TESTING = True 13 | DEBUG = True 14 | WTF_CSRF_ENABLED = False 15 | DEBUG_TB_ENABLED = True 16 | PRESERVE_CONTEXT_ON_EXCEPTION = False 17 | -------------------------------------------------------------------------------- /data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/data/.gitkeep -------------------------------------------------------------------------------- /data/example.csv: -------------------------------------------------------------------------------- 1 | start,end,cui 2 | 26,30,C3539878 -------------------------------------------------------------------------------- /data/example.txt: -------------------------------------------------------------------------------- 1 | Pt given carbo ia for her TNBC. Will d/c. Also past history of hypertension 2 | -------------------------------------------------------------------------------- /data/log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/data/log/.gitkeep -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | title: PRAnCER 3 | description: Platform enabling Rapid Annotation for Clinical Entity Recognition 4 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | #header_wrap, #footer_wrap { 7 | background: #607d8b; 8 | } 9 | 10 | #forkme_banner { 11 | background: #323f45; 12 | right: 2px; 13 | box-shadow: none; 14 | } 15 | 16 | #project_title, #project_tagline { 17 | text-shadow: #111 0 0 1px; 18 | } 19 | 20 | h2, h3, h4 { 21 | color: #4c626e; 22 | } 23 | 24 | figure { 25 | padding: 1px; 26 | } 27 | 28 | figcaption { 29 | color: #607d8b; 30 | font-style: italic; 31 | padding: 0px; 32 | } 33 | -------------------------------------------------------------------------------- /docs/files/annotator_demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/annotator_demo.mov -------------------------------------------------------------------------------- /docs/files/filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/filter.png -------------------------------------------------------------------------------- /docs/files/match_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/match_options.png -------------------------------------------------------------------------------- /docs/files/more_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/more_info.png -------------------------------------------------------------------------------- /docs/files/rec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/rec.png -------------------------------------------------------------------------------- /docs/files/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/search.png -------------------------------------------------------------------------------- /docs/files/suggestion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/docs/files/suggestion.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | Our platform enables the rapid annotation of medical terms within clinical notes. A user can highlight spans of text and quickly map them to concepts in large vocabularies within a single, intuitive platform. Users can use the search and recommendation features to find labels without ever needing to leave the interface. Further, the platform can take in output from existing clinical concept extraction systems as pre-annotations, which users can accept or modify in a single click. These features allow users to focus their time and energy on harder examples instead. See the demo below! 3 | 4 | 5 | 6 | ## Why? 7 | Clinical notes document detailed patient information, including pertinent medical history, procedures performed during a visit, and response to treatment. This information can be leveraged for a variety of use cases--from more personalized decision support in electronic health record to richer retrospective research. 8 | 9 | However, this data often exists only in clinical text, and not in any structured fields; therefore, many use cases involve manual or automated structuring of relevant concepts from clinical notes. Current automated approaches of extracting structured clinical concepts are insufficiently robust for many downstream uses, and there isn't much annotated training data publicly available to improve machine learning models. The goal of our platform is to make that annotation process easier and faster: whether for a manual curation use case, or for the creation of an annotated training dataset. 10 | 11 | 12 | ## Features 13 | ### Pre-Annotations 14 | Sometimes concept mentions are simple and straightforward for algorithms to recognize. In these cases, pre-annotations can save annotators time and energy. Pre-annotations outline the suggested span of text alongside its predicted label. The user can then to choose to accept the machine suggestion in just a single click, or modify or delete. In the image below, there are 3 pre-annotations: on flagyl, BUN, and NGT. 15 |

16 | PRAnCER can flexibly set the pre-annotations to the outputs of any clinical entity normalization system (MetaMap, cTAKES, ClinicalBERT). A user just provides the spans and expected labels in a CSV file. Alternately, a user can provide a dictionary from text to the CUI they want it to pre-fill to; a sample dictionary is provided. 17 | 18 | ### Recommendations 19 | Even when a model can't settle on a single label with high-confidence, it can often surface a correct label in its top few predictions. PRAnCER comes built-in with an NLP recommendation algorithm for suggesting likely concept labels once a span of text is highlighted. Below you can see that we can correctly recommend 'vancomycin' for the highlighted vanco. The recommendation function is merely a Python call, so one can easily swap out our recommendation algorithm for any new model.
20 | 21 | 22 | ### Search 23 | When the recommendations fail to bring up the correct label(s), the annotator can easily search for their desired term without having to leave the interface. In the example below, the annotator could search 'piperacillin' and directly click to select.
24 | 25 | 26 | ### UMLS Linking 27 | Medical ontologies are large and nuanced, and sometimes it is necessary to get more information before choosing a concept label. You can link your UMLS account to our interface, and as shown below, it will automatically surface the UMLS-provided definitions for a concept in one click.
28 | 29 | 30 | ### Concept Categories 31 | We provide a color-coding for each concept type, e.g. findings, procedures, medical devices; these can be used as visual cues to quickly find the desired concept label. Further, users can choose to filter concepts by these concept types; below, the user chose to filter the 'PT' terms to anatomical terms.
32 | 33 | 34 | ### Flexible Annotation 35 | We understand that annotation is often messy and inexact, so PRAnCER provide tooling that can help support such use cases. Users can choose multiple concepts, label a concept as an 'ambiguous match', or as below, indicate there was no direct match found. PRAnCER also enables the annotation of overlapping spans. 36 | 37 | 38 | ## How to Use 39 | ### Installation Instructions 40 | PRAncer is built on python3 and Node.Js; the README of our Github provides detailed instructions on how to install PRAnCER on your machine in a few simple steps. PRAnCER can operate on Mac, Windows, and Linux machines. 41 | ### Loading in Data 42 | To load in data, users directly place any clinical notes as .txt files in the /data folder of our annotaor. If the user wants to load in pre-annotations, users can place a CSV of spans and CUIs, and our scripts will automatically incorporate those annotations. An example .txt file and .csv file are provided. 43 | ### UMLS Vocabulary 44 | Use of the platform requires a UMLS license, as it requires several UMLS-derived files to surface recommendations, as well as a UMLS API key to surface additional information. Please email magrawal (at) mit (dot) edu to request these files, along with your API key so we may confirm. You can sign up [here](https://uts.nlm.nih.gov/uts/signup-login). 45 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from config import BaseConfig 3 | from flask_bcrypt import Bcrypt 4 | 5 | app = Flask(__name__, static_folder="./static/dist", template_folder="./static") 6 | app.config.from_object(BaseConfig) 7 | bcrypt = Bcrypt(app) 8 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from application.app import app 2 | 3 | app = app 4 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | from flask_script import Manager 2 | 3 | from application.app import app 4 | from application.utils.log import open_log 5 | 6 | manager = Manager(app) 7 | 8 | 9 | if __name__ == '__main__': 10 | open_log() 11 | manager.run() 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.8.7 2 | bcrypt==3.1.0 3 | cffi==1.7.0 4 | click==6.6 5 | coverage==4.2 6 | Flask==0.11.1 7 | Flask-Bcrypt==0.7.1 8 | Flask-Migrate==2.0.0 9 | Flask-Script==2.0.5 10 | Flask-Testing==0.5.0 11 | GitHub-Flask==3.1.3 12 | gunicorn==19.6.0 13 | itsdangerous==0.24 14 | Jinja2==2.8 15 | Mako==1.0.4 16 | MarkupSafe==0.23 17 | mysql-connector==2.1.4 18 | psycopg2==2.7.1 19 | py==1.4.31 20 | py-bcrypt==0.4 21 | pycparser==2.14 22 | pytest==3.0.1 23 | pytest-cov==2.3.1 24 | pytest-flask==0.10.0 25 | python-editor==1.0.1 26 | requests==2.11.1 27 | six==1.10.0 28 | Werkzeug==0.11.10 29 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-2.7.9 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | -------------------------------------------------------------------------------- /static/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015" , "stage-0"], 3 | "plugins": [ 4 | ["transform-decorators-legacy"], 5 | ["transform-runtime", { 6 | "regenerator": true 7 | }] 8 | ], 9 | "env": { 10 | "start": { 11 | "presets": ["react-hmre"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /static/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "extends": "eslint-config-airbnb", 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "globals": { 11 | "connect": true 12 | }, 13 | "rules": { 14 | "camelcase": 0, 15 | "indent": ["error", 4], 16 | "global-require": 0, 17 | "react/jsx-filename-extension": 0, 18 | "react/jsx-indent": ["error", 4], 19 | "import/prefer-default-export": 0, 20 | "import/no-extraneous-dependencies": 0, 21 | "import/no-unresolved": 0, 22 | "react/jsx-uses-react": 2, 23 | "react/jsx-uses-vars": 2, 24 | "react/react-in-jsx-scope": 2, 25 | "block-scoped-var": 0, 26 | "padded-blocks": 0, 27 | "no-console": 0, 28 | "id-length": 0, 29 | "no-unused-expressions": 0, 30 | }, 31 | "plugins": [ 32 | "react" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /static/bin/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var babelrc = fs.readFileSync('./.babelrc'); 4 | var config; 5 | 6 | try { 7 | config = JSON.parse(babelrc); 8 | } catch (err) { 9 | console.error('==> ERROR: Error parsing your .babelrc.'); 10 | console.error(err); 11 | } 12 | 13 | require('babel-core/register')(config); 14 | require('../server'); 15 | -------------------------------------------------------------------------------- /static/bootstrap.rc: -------------------------------------------------------------------------------- 1 | --- 2 | # Output debugging info 3 | # loglevel: debug 4 | 5 | # Major version of Bootstrap: 3 or 4 6 | bootstrapVersion: 3 7 | 8 | # Webpack loaders, order matters 9 | styleLoaders: 10 | - style 11 | - css 12 | - sass 13 | 14 | # Extract styles to stand-alone css file 15 | # Different settings for different environments can be used, 16 | # It depends on value of NODE_ENV environment variable 17 | # This param can also be set in webpack config: 18 | # entry: 'bootstrap-loader/extractStyles' 19 | # extractStyles: false 20 | # env: 21 | # development: 22 | # extractStyles: false 23 | # production: 24 | # extractStyles: true 25 | 26 | 27 | # Customize Bootstrap variables that get imported before the original Bootstrap variables. 28 | # Thus, derived Bootstrap variables can depend on values from here. 29 | # See the Bootstrap _variables.scss file for examples of derived Bootstrap variables. 30 | # 31 | # preBootstrapCustomizations: ./path/to/bootstrap/pre-customizations.scss 32 | 33 | 34 | # This gets loaded after bootstrap/variables is loaded 35 | # Thus, you may customize Bootstrap variables 36 | # based on the values established in the Bootstrap _variables.scss file 37 | # 38 | # bootstrapCustomizations: ./path/to/bootstrap/customizations.scss 39 | 40 | 41 | # Import your custom styles here 42 | # Usually this endpoint-file contains list of @imports of your application styles 43 | # 44 | # appStyles: ./path/to/your/app/styles/endpoint.scss 45 | 46 | 47 | ### Bootstrap styles 48 | styles: 49 | 50 | # Mixins 51 | mixins: true 52 | 53 | # Reset and dependencies 54 | normalize: true 55 | print: true 56 | glyphicons: true 57 | 58 | # Core CSS 59 | scaffolding: true 60 | type: true 61 | code: true 62 | grid: true 63 | tables: true 64 | forms: true 65 | buttons: true 66 | 67 | # Components 68 | component-animations: true 69 | dropdowns: true 70 | button-groups: true 71 | input-groups: true 72 | navs: true 73 | navbar: true 74 | breadcrumbs: true 75 | pagination: true 76 | pager: true 77 | labels: true 78 | badges: true 79 | jumbotron: true 80 | thumbnails: true 81 | alerts: true 82 | progress-bars: true 83 | media: true 84 | list-group: true 85 | panels: true 86 | wells: true 87 | responsive-embed: true 88 | close: true 89 | 90 | # Components w/ JavaScript 91 | modals: true 92 | tooltip: true 93 | popovers: true 94 | carousel: true 95 | 96 | # Utility classes 97 | utilities: true 98 | responsive-utilities: true 99 | 100 | ### Bootstrap scripts 101 | scripts: 102 | transition: true 103 | alert: true 104 | button: true 105 | carousel: true 106 | collapse: true 107 | dropdown: true 108 | modal: true 109 | tooltip: true 110 | popover: true 111 | scrollspy: true 112 | tab: true 113 | affix: true 114 | Status API Training Shop Blog About Pricing 115 | -------------------------------------------------------------------------------- /static/constants.js: -------------------------------------------------------------------------------- 1 | // SERVER CONSTANTS 2 | 3 | export const LOCAL_SERVER_ADDRESS = 'http://localhost:5000'; 4 | export const SERVER_ADDRESS = LOCAL_SERVER_ADDRESS; 5 | 6 | export const PUBLIC_PORT = 8920; 7 | export const LOCAL_PORT = 3000; 8 | export const DEFAULT_PORT = LOCAL_PORT; 9 | 10 | 11 | // TUTORIAL CONSTANTS 12 | 13 | export const TUTORIAL_SLIDES_LINK = "https://docs.google.com/presentation/d/1b6O5E657zOmOLw5h9oZKyYPJdMmlACwp5cZg9VluaH4/edit?usp=sharing"; 14 | export const TUTORIAL_LENGTH = 4; 15 | 16 | 17 | // Enables dynamic popup suggestions based on user's previous annotations 18 | export const DYNAMIC_SUGGESTIONS_ENABLED = true; 19 | -------------------------------------------------------------------------------- /static/gh/browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/static/gh/browser.png -------------------------------------------------------------------------------- /static/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/static/images/example.png -------------------------------------------------------------------------------- /static/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/static/images/search.png -------------------------------------------------------------------------------- /static/images/selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/static/images/selected.png -------------------------------------------------------------------------------- /static/images/suggestion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/static/images/suggestion.png -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Clinical Annotator 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /static/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: 'src', 4 | singleRun: true, 5 | frameworks: ['mocha'], 6 | reporters: ['dots'], 7 | browsers: ['Chrome'], 8 | files: [ 9 | 'test/**/*.spec.js', 10 | ], 11 | preprocessors: { 12 | 'test/**/*.spec.js': ['webpack'], 13 | }, 14 | webpack: { 15 | resolve: { 16 | extensions: ['', '.js', '.ts', '.tsx'], 17 | modulesDirectories: ['node_modules', 'src'], 18 | }, 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | }, 25 | { 26 | test: /\.ts(x?)$/, 27 | loader: "ts-loader", 28 | } 29 | ], 30 | }, 31 | }, 32 | webpackMiddleware: { 33 | stats: { 34 | color: true, 35 | chunkModules: false, 36 | modules: false, 37 | }, 38 | }, 39 | }); 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /static/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-easy-boilerplate", 3 | "version": "1.3.3", 4 | "description": "", 5 | "scripts": { 6 | "clean": "rimraf dist", 7 | "build": "webpack --progress --verbose --colors --display-error-details --config webpack/common.config.js", 8 | "build:production": "npm run clean && npm run build", 9 | "lint": "eslint src", 10 | "start": "node bin/server.js", 11 | "test": "karma start" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "reactjs", 20 | "boilerplate", 21 | "redux", 22 | "hot", 23 | "reload", 24 | "hmr", 25 | "live", 26 | "edit", 27 | "webpack" 28 | ], 29 | "author": "https://github.com/anorudes, https://github.com/keske", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@material-ui/core": "^1.5.1", 33 | "@types/classnames": "^2.2.9", 34 | "@types/core-js": "^2.5.2", 35 | "@types/react": "^15.3.1", 36 | "@types/react-dom": "^15.1.0", 37 | "autoprefixer": "6.5.3", 38 | "axios": "^0.19.0", 39 | "babel-core": "^6.26.3", 40 | "babel-eslint": "^7.1.1", 41 | "babel-loader": "^6.4.1", 42 | "babel-plugin-react-transform": "^2.0.0", 43 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 44 | "babel-plugin-transform-runtime": "^6.23.0", 45 | "babel-polyfill": "^6.26.0", 46 | "babel-preset-es2015": "^6.24.1", 47 | "babel-preset-react": "^6.3.13", 48 | "babel-preset-react-hmre": "^1.0.1", 49 | "babel-preset-stage-0": "^6.24.1", 50 | "bootstrap": "^3.3.5", 51 | "bootstrap-loader": "^1.2.0-beta.1", 52 | "bootstrap-sass": "^3.3.6", 53 | "bootstrap-webpack": "0.0.5", 54 | "classnames": "^2.2.6", 55 | "css-loader": "^0.26.1", 56 | "csswring": "^5.1.0", 57 | "deep-equal": "^1.0.1", 58 | "eslint": "^3.4.0", 59 | "eslint-config-airbnb": "13.0.0", 60 | "eslint-plugin-import": "^2.2.0", 61 | "eslint-plugin-jsx-a11y": "^3.0.1", 62 | "eslint-plugin-react": "^6.1.2", 63 | "expect": "^1.13.4", 64 | "exports-loader": "^0.6.2", 65 | "expose-loader": "^0.7.1", 66 | "express": "^4.13.4", 67 | "express-open-in-editor": "^1.1.0", 68 | "extract-text-webpack-plugin": "^1.0.1", 69 | "file-loader": "^0.9.0", 70 | "gapi": "0.0.3", 71 | "history": "^4.4.1", 72 | "http-proxy": "^1.12.0", 73 | "imports-loader": "^0.6.5", 74 | "jasmine-core": "^2.4.1", 75 | "jquery": "^3.1.0", 76 | "jwt-decode": "^2.1.0", 77 | "karma": "^1.2.0", 78 | "karma-chrome-launcher": "^2.0.0", 79 | "karma-mocha": "^1.1.1", 80 | "karma-webpack": "^1.7.0", 81 | "less": "^2.5.3", 82 | "less-loader": "^2.2.2", 83 | "lodash": "^4.17.15", 84 | "mocha": "^3.0.2", 85 | "morgan": "^1.6.1", 86 | "node-sass": "^4.13.0", 87 | "postcss-import": "^9.0.0", 88 | "postcss-loader": "^1.1.1", 89 | "q": "^1.4.1", 90 | "qs": "^6.1.0", 91 | "rc-datepicker": "^4.0.1", 92 | "react": "^16.3.0", 93 | "react-addons-css-transition-group": "^15.3.1", 94 | "react-calendar-component": "^1.0.0", 95 | "react-date-picker": "^5.3.28", 96 | "react-datepicker": "^0.37.0", 97 | "react-document-meta": "^2.0.0-rc2", 98 | "react-dom": "^16.3.0", 99 | "react-forms": "^2.0.0-beta33", 100 | "react-hot-loader": "^1.3.0", 101 | "react-loading-order-with-animation": "^1.0.0", 102 | "react-onclickoutside": "^5.3.3", 103 | "react-redux": "^4.3.0", 104 | "react-router": "3.2.0", 105 | "react-router-redux": "^4.0.0", 106 | "react-transform-hmr": "^1.0.1", 107 | "redux": "^3.2.1", 108 | "redux-form": "^6.0.1", 109 | "redux-logger": "2.7.4", 110 | "redux-thunk": "^2.1.0", 111 | "resolve-url-loader": "^1.4.3", 112 | "rimraf": "^2.5.0", 113 | "sass-loader": "^4.0.0", 114 | "source-map-loader": "^0.2.4", 115 | "style-loader": "^0.13.0", 116 | "ts-loader": "3.5.0", 117 | "typescript": "^3.6.4", 118 | "url-loader": "^0.5.7", 119 | "webpack": "^1.12.11", 120 | "webpack-dev-middleware": "^1.5.0", 121 | "webpack-dev-server": "^1.14.1", 122 | "webpack-hot-middleware": "^2.6.0", 123 | "webpack-merge": "^1.0.2", 124 | "yargs": "^6.5.0" 125 | }, 126 | "dependencies": { 127 | "@material-ui/icons": "^4.5.1", 128 | "@types/lodash": "^4.14.144", 129 | "json-loader": "^0.5.7", 130 | "list-react-files": "^0.2.0" 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /static/server.js: -------------------------------------------------------------------------------- 1 | import { DEFAULT_PORT, SERVER_ADDRESS } from './constants'; 2 | 3 | const http = require('http'); 4 | const express = require('express'); 5 | const httpProxy = require('http-proxy'); 6 | const path = require('path'); 7 | require("babel-core/register"); 8 | require("babel-polyfill"); 9 | 10 | const proxy = httpProxy.createProxyServer({}); 11 | 12 | const app = express(); 13 | 14 | app.use(require('morgan')('short')); 15 | 16 | (function initWebpack() { 17 | const webpack = require('webpack'); 18 | const webpackConfig = require('./webpack/common.config'); 19 | 20 | const compiler = webpack(webpackConfig); 21 | 22 | app.use(require('webpack-dev-middleware')(compiler, { 23 | noInfo: true, publicPath: webpackConfig.output.publicPath, 24 | })); 25 | 26 | app.use(require('webpack-hot-middleware')(compiler, { 27 | log: console.log, path: '/__webpack_hmr', heartbeat: 10 * 1000, 28 | })); 29 | 30 | app.use(express.static(path.join(__dirname, '/'))); 31 | }()); 32 | 33 | app.all(/^\/api\/(.*)/, (req, res) => { 34 | proxy.web(req, res, { target: SERVER_ADDRESS }); 35 | }); 36 | 37 | app.get(/.*/, (req, res) => { 38 | res.sendFile(path.join(__dirname, '/index.html')); 39 | }); 40 | 41 | 42 | const server = http.createServer(app); 43 | server.listen(process.env.PORT || DEFAULT_PORT, () => { 44 | const address = server.address(); 45 | console.log('Listening on: %j', address); 46 | console.log(' -> that probably means: http://localhost:%d', address.port); 47 | }); 48 | -------------------------------------------------------------------------------- /static/src/actions/colormap.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_COLOR_MAP, 3 | GET_COLOR_MAP_FAILURE, 4 | GET_COLOR_MAP_REQUEST, 5 | GET_COLOR_MAP_SUCCESS, 6 | } from '../constants/index'; 7 | 8 | import { parseJSON } from '../utils/misc'; 9 | import { get_colormap } from '../utils/http_functions'; 10 | 11 | export function getColormapSuccess(token) { 12 | localStorage.setItem('token', token); 13 | return { 14 | type: GET_COLOR_MAP_SUCCESS, 15 | payload: { 16 | token, 17 | }, 18 | }; 19 | } 20 | 21 | 22 | export function getColormapFailure(error) { 23 | localStorage.removeItem('token'); 24 | return { 25 | type: GET_COLOR_MAP_FAILURE, 26 | payload: { 27 | status: error.response.status, 28 | statusText: error.response.statusText, 29 | }, 30 | }; 31 | } 32 | 33 | export function getColormapRequest() { 34 | return { 35 | type: GET_COLOR_MAP_REQUEST, 36 | }; 37 | } 38 | 39 | export function getColormap(searchTerm, isKeyword, mode) { 40 | return function (dispatch) { 41 | dispatch(getColormapRequest()); 42 | const colormap = get_colormap(); 43 | return colormap; 44 | // TODO: Look at how to get error messages back 45 | return get_color_map() 46 | .then(parseJSON) 47 | .then(response => { 48 | try { 49 | dispatch(getColormapSuccess(response.token)); 50 | } catch (e) { 51 | alert(e); 52 | dispatch(getColormapFailure({ 53 | response: { 54 | status: 403, 55 | statusText: 'Invalid colormap fetch', 56 | }, 57 | })); 58 | } 59 | }) 60 | .catch(error => { 61 | dispatch(getColormapFailure({ 62 | response: { 63 | status: 403, 64 | statusText: 'Invalid colormap fetch', 65 | }, 66 | })); 67 | }); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /static/src/actions/files.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_FILE, 3 | GET_FILE_FAILURE, 4 | GET_FILE_REQUEST, 5 | GET_FILE_SUCCESS, 6 | GET_FILENAMES, 7 | GET_FILENAMES_FAILURE, 8 | GET_FILENAMES_REQUEST, 9 | GET_FILENAMES_SUCCESS, 10 | SAVE_ANNOTATIONS, 11 | SAVE_ANNOTATIONS_FAILURE, 12 | SAVE_ANNOTATIONS_REQUEST, 13 | SAVE_ANNOTATIONS_SUCCESS, 14 | } from '../constants/index'; 15 | 16 | import { parseJSON } from '../utils/misc'; 17 | import { get_file, get_filenames, save_annotations } from '../utils/http_functions'; 18 | 19 | export function getFileSuccess(token) { 20 | localStorage.setItem('token', token); 21 | return { 22 | type: GET_FILE_SUCCESS, 23 | payload: { 24 | token, 25 | }, 26 | }; 27 | } 28 | 29 | 30 | export function getFileFailure(error) { 31 | localStorage.removeItem('token'); 32 | return { 33 | type: GET_FILE_FAILURE, 34 | payload: { 35 | status: error.response.status, 36 | statusText: error.response.statusText, 37 | }, 38 | }; 39 | } 40 | 41 | export function getFileRequest() { 42 | return { 43 | type: GET_FILE_REQUEST, 44 | }; 45 | } 46 | 47 | export function getFile(id, textDir = null, annDir = null) { 48 | return function (dispatch) { 49 | dispatch(getFileRequest()); 50 | const file = get_file(id, textDir, annDir); 51 | return file; 52 | // TODO: Look at how to get error messages back 53 | return get_file(id, textDir, annDir) 54 | .then(parseJSON) 55 | .then(response => { 56 | try { 57 | dispatch(getFileSuccess(response.token)); 58 | // browserHistory.push('/annotation'); 59 | } catch (e) { 60 | alert(e); 61 | dispatch(getFileFailure({ 62 | response: { 63 | status: 403, 64 | statusText: 'Invalid file fetch', 65 | }, 66 | })); 67 | } 68 | }) 69 | .catch(error => { 70 | dispatch(getFileFailure({ 71 | response: { 72 | status: 403, 73 | statusText: 'Invalid file fetch', 74 | }, 75 | })); 76 | }); 77 | }; 78 | } 79 | 80 | export function getFilenamesSuccess(token) { 81 | localStorage.setItem('token', token); 82 | return { 83 | type: GET_FILENAMES_SUCCESS, 84 | payload: { 85 | token, 86 | }, 87 | }; 88 | } 89 | 90 | 91 | export function getFilenamesFailure(error) { 92 | localStorage.removeItem('token'); 93 | return { 94 | type: GET_FILENAMES_FAILURE, 95 | payload: { 96 | status: error.response.status, 97 | statusText: error.response.statusText, 98 | }, 99 | }; 100 | } 101 | 102 | export function getFilenamesRequest() { 103 | return { 104 | type: GET_FILENAMES_REQUEST, 105 | }; 106 | } 107 | 108 | export function getFilenames() { 109 | return function (dispatch) { 110 | dispatch(getFilenamesRequest()); 111 | const filenames = get_filenames(); 112 | return filenames; 113 | // TODO: Look at how to get error messages back 114 | return get_filenames() 115 | .then(parseJSON) 116 | .then(response => { 117 | try { 118 | dispatch(getFilenamesSuccess(response.token)); 119 | } catch (e) { 120 | alert(e); 121 | dispatch(getFilenamesFailure({ 122 | response: { 123 | status: 403, 124 | statusText: 'Invalid file fetch', 125 | }, 126 | })); 127 | } 128 | }) 129 | .catch(error => { 130 | dispatch(getFilenamesFailure({ 131 | response: { 132 | status: 403, 133 | statusText: 'Invalid file fetch', 134 | }, 135 | })); 136 | }); 137 | }; 138 | } 139 | 140 | export function saveAnnotationsSuccess(token) { 141 | localStorage.setItem('token', token); 142 | return { 143 | type: SAVE_ANNOTATIONS_SUCCESS, 144 | payload: { 145 | token, 146 | }, 147 | }; 148 | } 149 | 150 | 151 | export function saveAnnotationsFailure(error) { 152 | localStorage.removeItem('token'); 153 | return { 154 | type: SAVE_ANNOTATIONS_FAILURE, 155 | payload: { 156 | status: error.response.status, 157 | statusText: error.response.statusText, 158 | }, 159 | }; 160 | } 161 | 162 | export function saveAnnotationsRequest() { 163 | return { 164 | type: SAVE_ANNOTATIONS_REQUEST, 165 | }; 166 | } 167 | 168 | export function saveAnnotations(id, annotations, dir) { 169 | return function (dispatch) { 170 | dispatch(saveAnnotationsRequest()); 171 | return save_annotations(id, annotations, dir) 172 | .then(parseJSON) 173 | .then(response => { 174 | try { 175 | dispatch(saveAnnotationsSuccess(response.token)); 176 | // browserHistory.push('/main'); 177 | } catch (e) { 178 | alert(e); 179 | dispatch(saveAnnotationsFailure({ 180 | response: { 181 | status: 403, 182 | statusText: 'Invalid annotations', 183 | }, 184 | })); 185 | } 186 | }) 187 | .catch(error => { 188 | dispatch(saveAnnotationsFailure({ 189 | response: { 190 | status: 403, 191 | statusText: 'Invalid file save', 192 | }, 193 | })); 194 | }); 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /static/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as fileActionCreators from './files'; 2 | import * as labelActionCreators from './labels'; 3 | import * as colormapActionCreators from './colormap'; 4 | import * as umlsActionCreators from './umls'; 5 | import * as tutorialActionCreators from './tutorial' 6 | import * as logActionCreators from './log' 7 | 8 | const actionCreators = { 9 | ...fileActionCreators, 10 | ...labelActionCreators, 11 | ...colormapActionCreators, 12 | ...umlsActionCreators, 13 | ...tutorialActionCreators, 14 | ...logActionCreators 15 | }; 16 | 17 | export default actionCreators 18 | -------------------------------------------------------------------------------- /static/src/actions/labels.js: -------------------------------------------------------------------------------- 1 | import { 2 | RECOMMEND_LABELS, 3 | RECOMMEND_LABELS_FAILURE, 4 | RECOMMEND_LABELS_REQUEST, 5 | RECOMMEND_LABELS_SUCCESS, 6 | SEARCH_LABELS, 7 | SEARCH_LABELS_FAILURE, 8 | SEARCH_LABELS_REQUEST, 9 | SEARCH_LABELS_SUCCESS, 10 | } from '../constants/index'; 11 | 12 | import { parseJSON } from '../utils/misc'; 13 | import { recommend_labels, search_labels } from '../utils/http_functions'; 14 | 15 | export function recommendLabelsSuccess(token) { 16 | localStorage.setItem('token', token); 17 | return { 18 | type: RECOMMEND_LABELS_SUCCESS, 19 | payload: { 20 | token, 21 | }, 22 | }; 23 | } 24 | 25 | 26 | export function recommendLabelsFailure(error) { 27 | localStorage.removeItem('token'); 28 | return { 29 | type: RECOMMEND_LABELS_FAILURE, 30 | payload: { 31 | status: error.response.status, 32 | statusText: error.response.statusText, 33 | }, 34 | }; 35 | } 36 | 37 | export function recommendLabelsRequest() { 38 | return { 39 | type: RECOMMEND_LABELS_REQUEST, 40 | }; 41 | } 42 | 43 | export function recommendLabels(searchTerm, isKeyword, mode) { 44 | return function (dispatch) { 45 | dispatch(recommendLabelsRequest()); 46 | const labels = recommend_labels(searchTerm, isKeyword, mode); 47 | return labels; 48 | // TODO: Look at how to get error messages back 49 | return recommend_labels(searchTerm, isKeyword, mode) 50 | .then(parseJSON) 51 | .then(response => { 52 | try { 53 | dispatch(recommendLabelsSuccess(response.token)); 54 | } catch (e) { 55 | alert(e); 56 | dispatch(recommendLabelsFailure({ 57 | response: { 58 | status: 403, 59 | statusText: 'Invalid labels fetch', 60 | }, 61 | })); 62 | } 63 | }) 64 | .catch(error => { 65 | dispatch(recommendLabelsFailure({ 66 | response: { 67 | status: 403, 68 | statusText: 'Invalid labels fetch', 69 | }, 70 | })); 71 | }); 72 | }; 73 | } 74 | 75 | 76 | export function searchLabelsSuccess(token) { 77 | localStorage.setItem('token', token); 78 | return { 79 | type: SEARCH_LABELS_SUCCESS, 80 | payload: { 81 | token, 82 | }, 83 | }; 84 | } 85 | 86 | 87 | export function searchLabelsFailure(error) { 88 | localStorage.removeItem('token'); 89 | return { 90 | type: SEARCH_LABELS_FAILURE, 91 | payload: { 92 | status: error.response.status, 93 | statusText: error.response.statusText, 94 | }, 95 | }; 96 | } 97 | 98 | export function searchLabelsRequest() { 99 | return { 100 | type: SEARCH_LABELS_REQUEST, 101 | }; 102 | } 103 | 104 | export function searchLabels(searchTerm) { 105 | return function (dispatch) { 106 | dispatch(searchLabelsRequest()); 107 | const labels = search_labels(searchTerm); 108 | return labels; 109 | // TODO: Look at how to get error messages back 110 | return search_labels(searchTerm) 111 | .then(parseJSON) 112 | .then(response => { 113 | try { 114 | dispatch(searchLabelsSuccess(response.token)); 115 | } catch (e) { 116 | alert(e); 117 | dispatch(searchLabelsFailure({ 118 | response: { 119 | status: 403, 120 | statusText: 'Invalid labels fetch', 121 | }, 122 | })); 123 | } 124 | }) 125 | .catch(error => { 126 | dispatch(searchLabelsFailure({ 127 | response: { 128 | status: 403, 129 | statusText: 'Invalid labels fetch', 130 | }, 131 | })); 132 | }); 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /static/src/actions/log.js: -------------------------------------------------------------------------------- 1 | import { 2 | ADD_LOG, 3 | ADD_LOG_FAILURE, 4 | ADD_LOG_REQUEST, 5 | ADD_LOG_SUCCESS, 6 | } from '../constants/index'; 7 | 8 | import { parseJSON } from '../utils/misc'; 9 | import { add_log_entry } from '../utils/http_functions'; 10 | 11 | export function addLogSuccess(token) { 12 | localStorage.setItem('token', token); 13 | return { 14 | type: ADD_LOG_SUCCESS, 15 | payload: { 16 | token, 17 | }, 18 | }; 19 | } 20 | 21 | 22 | export function addLogFailure(error) { 23 | localStorage.removeItem('token'); 24 | return { 25 | type: ADD_LOG_FAILURE, 26 | payload: { 27 | status: error.response.status, 28 | statusText: error.response.statusText, 29 | }, 30 | }; 31 | } 32 | 33 | export function addLogRequest() { 34 | return { 35 | type: ADD_LOG_REQUEST, 36 | }; 37 | } 38 | 39 | export function addLog(id, action, annotation_id, metadata) { 40 | return function (dispatch) { 41 | dispatch(addLogRequest()); 42 | const isLogged = add_log_entry(id, action, annotation_id, metadata); 43 | return isLogged; 44 | // TODO: Look at how to get error messages back 45 | return add_log_entry(id, action, annotation_id, metadata) 46 | .then(parseJSON) 47 | .then(response => { 48 | try { 49 | dispatch(addLogSuccess(response.token)); 50 | } catch (e) { 51 | alert(e); 52 | dispatch(addLogFailure({ 53 | response: { 54 | status: 403, 55 | statusText: 'Invalid labels fetch', 56 | }, 57 | })); 58 | } 59 | }) 60 | .catch(error => { 61 | dispatch(addLogFailure({ 62 | response: { 63 | status: 403, 64 | statusText: 'Invalid labels fetch', 65 | }, 66 | })); 67 | }); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /static/src/actions/tutorial.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_TUTORIAL_EVALUATION, 3 | GET_TUTORIAL_EVALUATION_FAILURE, 4 | GET_TUTORIAL_EVALUATION_REQUEST, 5 | GET_TUTORIAL_EVALUATION_SUCCESS, 6 | RESTART_TUTORIAL, 7 | RESTART_TUTORIAL_FAILURE, 8 | RESTART_TUTORIAL_REQUEST, 9 | RESTART_TUTORIAL_SUCCESS, 10 | START_TUTORIAL, 11 | START_TUTORIAL_FAILURE, 12 | START_TUTORIAL_REQUEST, 13 | START_TUTORIAL_SUCCESS, 14 | } from '../constants/index'; 15 | 16 | 17 | import { parseJSON } from '../utils/misc'; 18 | import { get_tutorial_evaluation, restart_tutorial, start_tutorial } from '../utils/http_functions'; 19 | 20 | 21 | export function startTutorialSuccess(token) { 22 | localStorage.setItem('token', token); 23 | return { 24 | type: START_TUTORIAL_SUCCESS, 25 | payload: { 26 | token, 27 | }, 28 | }; 29 | } 30 | 31 | 32 | export function startTutorialFailure(error) { 33 | localStorage.removeItem('token'); 34 | return { 35 | type: START_TUTORIAL_FAILURE, 36 | payload: { 37 | status: error.response.status, 38 | statusText: error.response.statusText, 39 | }, 40 | }; 41 | } 42 | 43 | export function startTutorialRequest() { 44 | return { 45 | type: START_TUTORIAL_REQUEST, 46 | }; 47 | } 48 | 49 | export function startTutorial(userId) { 50 | return function (dispatch) { 51 | dispatch(startTutorialRequest()); 52 | const filenames = start_tutorial(userId); 53 | return filenames; 54 | // TODO: Look at how to get error messages back 55 | return start_tutorial(userId) 56 | .then(parseJSON) 57 | .then(response => { 58 | try { 59 | dispatch(startTutorialSuccess(response.token)); 60 | } catch (e) { 61 | alert(e); 62 | dispatch(startTutorialFailure({ 63 | response: { 64 | status: 403, 65 | statusText: 'Invalid file fetch', 66 | }, 67 | })); 68 | } 69 | }) 70 | .catch(error => { 71 | dispatch(startTutorialFailure({ 72 | response: { 73 | status: 403, 74 | statusText: 'Invalid file fetch', 75 | }, 76 | })); 77 | }); 78 | }; 79 | } 80 | 81 | 82 | export function getTutorialEvaluationSuccess(token) { 83 | localStorage.setItem('token', token); 84 | return { 85 | type: GET_TUTORIAL_EVALUATION_SUCCESS, 86 | payload: { 87 | token, 88 | }, 89 | }; 90 | } 91 | 92 | 93 | export function getTutorialEvaluationFailure(error) { 94 | localStorage.removeItem('token'); 95 | return { 96 | type: GET_TUTORIAL_EVALUATION_FAILURE, 97 | payload: { 98 | status: error.response.status, 99 | statusText: error.response.statusText, 100 | }, 101 | }; 102 | } 103 | 104 | export function getTutorialEvaluationRequest() { 105 | return { 106 | type: GET_TUTORIAL_EVALUATION_REQUEST, 107 | }; 108 | } 109 | 110 | export function getTutorialEvaluation(fileId, userId) { 111 | return function (dispatch) { 112 | dispatch(getTutorialEvaluationRequest()); 113 | const labels = get_tutorial_evaluation(fileId, userId); 114 | return labels; 115 | // TODO: Look at how to get error messages back 116 | return get_tutorial_evaluation(fileId, userId) 117 | .then(parseJSON) 118 | .then(response => { 119 | try { 120 | dispatch(getTutorialEvaluationSuccess(response.token)); 121 | } catch (e) { 122 | alert(e); 123 | dispatch(getTutorialEvaluationFailure({ 124 | response: { 125 | status: 403, 126 | statusText: 'Invalid tutorial evaluation fetch', 127 | }, 128 | })); 129 | } 130 | }) 131 | .catch(error => { 132 | dispatch(getTutorialEvaluationFailure({ 133 | response: { 134 | status: 403, 135 | statusText: 'Invalid tutorial evaluation fetch', 136 | }, 137 | })); 138 | }); 139 | }; 140 | } 141 | 142 | export function restartTutorialSuccess(token) { 143 | localStorage.setItem('token', token); 144 | return { 145 | type: RESTART_TUTORIAL_SUCCESS, 146 | payload: { 147 | token, 148 | }, 149 | }; 150 | } 151 | 152 | 153 | export function restartTutorialFailure(error) { 154 | localStorage.removeItem('token'); 155 | return { 156 | type: RESTART_TUTORIAL_FAILURE, 157 | payload: { 158 | status: error.response.status, 159 | statusText: error.response.statusText, 160 | }, 161 | }; 162 | } 163 | 164 | export function restartTutorialRequest() { 165 | return { 166 | type: RESTART_TUTORIAL_REQUEST, 167 | }; 168 | } 169 | 170 | export function restartTutorial(userId) { 171 | return function (dispatch) { 172 | dispatch(restartTutorialRequest()); 173 | const filenames = restart_tutorial(userId); 174 | return filenames; 175 | // TODO: Look at how to get error messages back 176 | return restart_tutorial(userId) 177 | .then(parseJSON) 178 | .then(response => { 179 | try { 180 | dispatch(restartTutorialSuccess(response.token)); 181 | } catch (e) { 182 | alert(e); 183 | dispatch(restartTutorialFailure({ 184 | response: { 185 | status: 403, 186 | statusText: 'Invalid file fetch', 187 | }, 188 | })); 189 | } 190 | }) 191 | .catch(error => { 192 | dispatch(restartTutorialFailure({ 193 | response: { 194 | status: 403, 195 | statusText: 'Invalid file fetch', 196 | }, 197 | })); 198 | }); 199 | }; 200 | } 201 | -------------------------------------------------------------------------------- /static/src/actions/umls.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_UMLS_INFO, 3 | GET_UMLS_INFO_FAILURE, 4 | GET_UMLS_INFO_REQUEST, 5 | GET_UMLS_INFO_SUCCESS, 6 | } from '../constants/index'; 7 | 8 | import { parseJSON } from '../utils/misc'; 9 | import { get_umls_info } from '../utils/http_functions'; 10 | 11 | export function getUMLSInfoSuccess(token) { 12 | localStorage.setItem('token', token); 13 | return { 14 | type: GET_UMLS_INFO_SUCCESS, 15 | payload: { 16 | token, 17 | }, 18 | }; 19 | } 20 | 21 | 22 | export function getUMLSInfoFailure(error) { 23 | localStorage.removeItem('token'); 24 | return { 25 | type: GET_UMLS_INFO_FAILURE, 26 | payload: { 27 | status: error.response.status, 28 | statusText: error.response.statusText, 29 | }, 30 | }; 31 | } 32 | 33 | export function getUMLSInfoRequest() { 34 | return { 35 | type: GET_UMLS_INFO_REQUEST, 36 | }; 37 | } 38 | 39 | export function getUMLSInfo(cui) { 40 | return function (dispatch) { 41 | dispatch(getUMLSInfoRequest()); 42 | const info = get_umls_info(cui); 43 | return info; 44 | // TODO: Look at how to get error messages back 45 | return get_umls_info(cui) 46 | .then(parseJSON) 47 | .then(response => { 48 | try { 49 | dispatch(getUMLSInfoSuccess(response.token)); 50 | } catch (e) { 51 | alert(e); 52 | dispatch(getUMLSInfoFailure({ 53 | response: { 54 | status: 403, 55 | statusText: 'Invalid UMLS info fetch', 56 | }, 57 | })); 58 | } 59 | }) 60 | .catch(error => { 61 | dispatch(getUMLSInfoFailure({ 62 | response: { 63 | status: 403, 64 | statusText: 'Invalid UMLS info fetch', 65 | }, 66 | })); 67 | }); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /static/src/components/Annotation/AnnotatedToken.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Menu from '@material-ui/core/Menu'; 3 | import Tooltip from '@material-ui/core/Tooltip' 4 | import { Check, Clear, Edit } from '@material-ui/icons'; 5 | import { ACCEPTED, AUTO, DECISION_TYPE, Filtermap, MODIFIED, REJECTED, Token, DYNAMIC, MANUAL, UNDECIDED } from './types' 6 | import Mark from './Mark' 7 | import { getAnnotationTag, isTokenSelected } from './utils' 8 | 9 | interface AnnotatedTokenProps { 10 | token: Token 11 | colormap: Filtermap 12 | selectedAnnotationId: number 13 | onAnnotationSelection: (id: number) => void 14 | onSuggestionUpdate: (id: number, decision: DECISION_TYPE) => void 15 | onTextSelection: (selection: Selection) => void 16 | onMouseEnter: () => void 17 | onMouseLeave: () => void 18 | } 19 | 20 | interface AnnotatedTokenState { 21 | suggestionAnchorEl: any 22 | optionsAnchorEl: any 23 | annotationIndex: number 24 | } 25 | 26 | class AnnotatedToken extends React.Component { 27 | constructor(props: AnnotatedTokenProps) { 28 | super(props) 29 | 30 | this.state = { 31 | suggestionAnchorEl: null, 32 | optionsAnchorEl: null, 33 | annotationIndex: 0 34 | } 35 | } 36 | 37 | handleSuggestionClick = (event: any) => { 38 | this.setState({ 39 | suggestionAnchorEl: event.currentTarget 40 | }); 41 | }; 42 | 43 | handleSuggestionClose = () => { 44 | this.setState({ 45 | suggestionAnchorEl: null 46 | }); 47 | }; 48 | 49 | handleSuggestionUpdate = (result: DECISION_TYPE) => { 50 | const { token, onSuggestionUpdate, onTextSelection } = this.props 51 | 52 | const primaryAnnotation = token.annotations[this.state.annotationIndex] 53 | onSuggestionUpdate(primaryAnnotation.annotationId, result) 54 | this.handleSuggestionClose() 55 | 56 | if (result == ACCEPTED || result == REJECTED) { 57 | onTextSelection(null) 58 | } 59 | } 60 | 61 | handleOptionsClick = (event: any) => { 62 | this.setState({ 63 | optionsAnchorEl: event.currentTarget 64 | }) 65 | } 66 | 67 | handleOptionsClose = () => { 68 | this.setState({ 69 | optionsAnchorEl: null 70 | }); 71 | } 72 | 73 | handleOptionsUpdate = (option: number) => { 74 | const annotations = this.props.token.annotations; 75 | this.props.onAnnotationSelection(annotations[option].annotationId); 76 | const primaryAnnotation = annotations[option]; 77 | const isAnnotationSuggestion = ( 78 | primaryAnnotation.creationType == AUTO || primaryAnnotation.creationType == DYNAMIC 79 | ); 80 | 81 | if (isAnnotationSuggestion) { 82 | this.setState({ 83 | suggestionAnchorEl: this.state.optionsAnchorEl 84 | }) 85 | } 86 | 87 | this.setState({ 88 | annotationIndex: option 89 | }); 90 | 91 | this.handleOptionsClose() 92 | } 93 | 94 | render() { 95 | const { token, colormap, onAnnotationSelection, selectedAnnotationId } = this.props; 96 | const { annotations, span } = token; 97 | 98 | const tokenSelected = isTokenSelected(token, selectedAnnotationId); 99 | const hasSuggestion = annotations.find(a => 100 | a.creationType == AUTO || a.creationType == DYNAMIC 101 | ) || false; 102 | const hasUndecidedSuggestion = hasSuggestion && annotations.find(a => 103 | a.creationType != MANUAL && a.decision == UNDECIDED 104 | ); 105 | 106 | const primaryAnnotation = this.state.annotationIndex < annotations.length 107 | ? annotations[this.state.annotationIndex] 108 | : annotations[0]; 109 | const isAnnotationSuggestion = primaryAnnotation 110 | && (primaryAnnotation.creationType == AUTO || primaryAnnotation.creationType == DYNAMIC); 111 | const hasOptions = annotations.length > 1; 112 | 113 | const annotationClick = (event: any) => { 114 | onAnnotationSelection(primaryAnnotation.annotationId) 115 | if (isAnnotationSuggestion) 116 | this.handleSuggestionClick(event) 117 | if (hasOptions) { 118 | this.handleOptionsClick(event) 119 | } 120 | } 121 | 122 | const labels = primaryAnnotation ? primaryAnnotation.labels : []; 123 | 124 | const color = labels.length > 0 125 | ? isAnnotationSuggestion && primaryAnnotation.decision == REJECTED 126 | ? '#ffffff' 127 | : labels[0].categories.length > 0 ? colormap[labels[0].categories[0].type] : '9e9e9e' 128 | : '#fffacd'; 129 | 130 | const border = hasSuggestion ? true : false; 131 | 132 | const fill = !hasUndecidedSuggestion; 133 | 134 | return ( 135 |
141 | getAnnotationTag(a)).join(' | ')} 145 | onClick={annotationClick} 146 | color={color} 147 | opacity={tokenSelected ? 0.75 : 0.25} 148 | border={border} 149 | fill={fill} 150 | /> 151 | { 152 | isAnnotationSuggestion && ( 153 | 164 |
this.handleSuggestionUpdate(ACCEPTED)} 167 | > 168 | 169 | 170 | 171 |
172 |
this.handleSuggestionUpdate(MODIFIED)} 175 | > 176 | 177 | 178 | 179 |
180 |
this.handleSuggestionUpdate(REJECTED)} 183 | > 184 | 185 | 186 | 187 |
188 |
189 | ) 190 | } 191 | { 192 | hasOptions && ( 193 | 204 | { 205 | annotations.map((a, i) => ( 206 |
this.handleOptionsUpdate(i)} 210 | > 211 | {a.labels.length > 0 ? a.labels[0].title : 'empty'} 212 |
213 | )) 214 | } 215 |
216 | ) 217 | } 218 |
219 | ); 220 | } 221 | } 222 | 223 | export default AnnotatedToken 224 | -------------------------------------------------------------------------------- /static/src/components/Annotation/CUIModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 3 | import FormControl from '@material-ui/core/FormControl'; 4 | import FormLabel from '@material-ui/core/FormLabel'; 5 | import Modal from '@material-ui/core/Modal'; 6 | import Radio from '@material-ui/core/Radio'; 7 | import RadioGroup from '@material-ui/core/RadioGroup'; 8 | import Tooltip from '@material-ui/core/Tooltip' 9 | import { MoreHoriz } from '@material-ui/icons' 10 | import { CUI_TYPE, CUI_NORMAL, CUI_AMBIGUOUS, CUI_CODELESS } from './types' 11 | 12 | interface CUIModalProps { 13 | CUIMode: CUI_TYPE 14 | onChange: (CUIMode: CUI_TYPE) => void 15 | fullSize: boolean 16 | } 17 | 18 | interface CUIModalState { 19 | open: boolean 20 | } 21 | 22 | class CUIModal extends React.Component { 23 | constructor(props: CUIModalProps) { 24 | super(props) 25 | 26 | this.state = { 27 | open: false 28 | } 29 | } 30 | 31 | handleOpen = () => { 32 | this.setState({ open: true }) 33 | } 34 | 35 | handleClose = () => { 36 | this.setState({ open: false }) 37 | } 38 | 39 | handleChange = (event: any) => { 40 | const newCUIMode = event.target.value 41 | this.props.onChange(newCUIMode) 42 | } 43 | 44 | render() { 45 | const button = this.props.fullSize 46 | ? ( 47 |
51 | More CUI Options 52 |
53 | ) : ( 54 | 58 | ) 59 | 60 | return ( 61 |
62 | 63 |
64 | {button} 65 |
66 |
67 | 71 |
72 | 73 | CUI Options 74 | 80 | } label="Normal match" /> 81 | } label="Ambiguous match" /> 82 | } label="No match" /> 83 | 84 | 85 |
86 |
87 |
88 | ) 89 | } 90 | } 91 | 92 | export default CUIModal 93 | -------------------------------------------------------------------------------- /static/src/components/Annotation/InfoModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Modal from '@material-ui/core/Modal'; 3 | import Tooltip from '@material-ui/core/Tooltip' 4 | import { InfoOutlined } from '@material-ui/icons' 5 | import { UMLSDefinition } from './types' 6 | 7 | interface InfoModalProps { 8 | title: string 9 | cui: string 10 | onClick: () => void 11 | UMLSInfo: UMLSDefinition[] 12 | } 13 | 14 | interface InfoModalState { 15 | open: boolean 16 | } 17 | 18 | class InfoModal extends React.Component { 19 | constructor(props: InfoModalProps) { 20 | super(props) 21 | 22 | this.state = { 23 | open: false 24 | } 25 | } 26 | 27 | handleOpen = () => { 28 | this.setState({ open: true }) 29 | } 30 | 31 | handleClose = () => { 32 | this.setState({ open: false }) 33 | } 34 | 35 | render() { 36 | const { UMLSInfo } = this.props 37 | 38 | const definitions = UMLSInfo.length > 0 39 | ? this.props.UMLSInfo.map((defn, i) => ( 40 |
41 |
Source: {defn.rootSource}
42 |
43 |
44 | )) 45 | :
No further information available.
46 | 47 | return ( 48 |
49 | 50 | { 55 | this.props.onClick(); 56 | this.handleOpen(); 57 | }} 58 | /> 59 | 60 | 64 |
65 |
66 |

{this.props.title}

67 | {definitions} 68 |
69 |
70 |
71 |
72 | ) 73 | } 74 | } 75 | 76 | export default InfoModal 77 | -------------------------------------------------------------------------------- /static/src/components/Annotation/LabelController.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SearchBar from './SearchBar' 3 | import LabelListItem from './LabelListItem' 4 | import LabelFilter from './LabelFilter' 5 | import { 6 | CUI_TYPE, 7 | Label, 8 | Filtermap, 9 | UMLSDefinition, 10 | LOG_TYPE, 11 | LOG_SCROLL, 12 | LOG_LABEL_FILTER, 13 | LOG_LABEL_ADD, 14 | LOG_LABEL_REMOVE, 15 | SEARCH_TYPE, 16 | LOG_LABEL_MOUSE_ON, 17 | LOG_LABEL_MOUSE_OFF, 18 | SEARCH_AUTOMATIC 19 | } from './types' 20 | import { filterLabelsByType } from './utils' 21 | 22 | let scrollTimeout: number = null 23 | 24 | interface LabelControllerProps { 25 | selectedText: string 26 | searchedLabels: Label[] 27 | selectedLabels: Label[] 28 | colormap: Filtermap 29 | searchMode: SEARCH_TYPE 30 | CUIMode: CUI_TYPE 31 | onEnterPress: (searchTerm: string, isKeyword: boolean) => void 32 | onCUIModeChange: (mode: CUI_TYPE) => void 33 | setSelectedLabels: (labels: Label[]) => void 34 | deleteAnnotation: () => void 35 | onTextSelection: (selection: Selection) => void 36 | onUMLSClick: (cui: string) => void 37 | UMLSInfo: UMLSDefinition[] 38 | addLogEntryBound: (action: LOG_TYPE, metadata: string[]) => boolean 39 | } 40 | 41 | interface LabelControllerState { 42 | searchText: string 43 | selectedFilter: string 44 | } 45 | 46 | class LabelController extends React.Component { 47 | constructor(props: LabelControllerProps) { 48 | super(props) 49 | 50 | this.state = { 51 | searchText: '', 52 | selectedFilter: null 53 | } 54 | } 55 | 56 | componentDidUpdate(prevProps: LabelControllerProps) { 57 | const { selectedText } = this.props 58 | 59 | if (prevProps.selectedText !== selectedText) { 60 | this.setState({searchText: selectedText}) 61 | this.handleFilterChange(null) 62 | } 63 | } 64 | 65 | handleEnterPress = (searchTerm: string, isKeyword: boolean) => { 66 | this.props.onEnterPress(searchTerm, isKeyword) 67 | this.setState({searchText: searchTerm}) 68 | } 69 | 70 | addLabel = (label: Label, i: number) => { 71 | const { selectedLabels, setSelectedLabels, searchMode } = this.props 72 | if (!selectedLabels.map(l => l.labelId).includes(label.labelId)) { 73 | setSelectedLabels([...selectedLabels, label]) 74 | } 75 | 76 | this.props.addLogEntryBound(LOG_LABEL_ADD, [label.labelId, String(i), searchMode]) 77 | } 78 | 79 | removeLabel = (id: string) => { 80 | const { selectedLabels, setSelectedLabels } = this.props 81 | const i = selectedLabels.findIndex(l => l.labelId == id) 82 | if (i >= 0 && i < selectedLabels.length) { 83 | selectedLabels.splice(i, 1) 84 | setSelectedLabels(selectedLabels) 85 | } 86 | 87 | this.props.addLogEntryBound(LOG_LABEL_REMOVE, [id]) 88 | } 89 | 90 | handleFilterChange = (newFilter: string) => { 91 | const selectedFilter = newFilter == this.state.selectedFilter 92 | ? null 93 | : newFilter; 94 | 95 | if (selectedFilter !== null) 96 | this.props.addLogEntryBound(LOG_LABEL_FILTER, [selectedFilter]); 97 | 98 | this.setState({ 99 | selectedFilter 100 | }) 101 | } 102 | 103 | onLabelListScroll = () => { 104 | clearTimeout(scrollTimeout) 105 | var { addLogEntryBound } = this.props 106 | 107 | scrollTimeout = setTimeout(function() { 108 | addLogEntryBound(LOG_SCROLL, []); 109 | }, 1000) 110 | } 111 | 112 | render() { 113 | const { 114 | selectedLabels, 115 | searchedLabels, 116 | colormap, 117 | addLogEntryBound, 118 | searchMode 119 | } = this.props 120 | 121 | const { 122 | selectedFilter 123 | } = this.state 124 | 125 | const filteredLabels = filterLabelsByType(searchedLabels, selectedFilter) 126 | 127 | return ( 128 |
129 |
130 |
131 |
132 | this.handleEnterPress(searchTerm, true)} 137 | /> 138 |
139 |
140 | 141 | 146 | 147 |

156 | {searchMode === SEARCH_AUTOMATIC ? "Recommended" : "Searched"} Labels: 157 |

158 | 159 |
160 | {filteredLabels.map((label, i) => 161 | l.labelId).includes(label.labelId)} 166 | onClick={() => this.addLabel(label, i)} 167 | onDeleteClick={() => this.removeLabel(label.labelId)} 168 | onUMLSClick={() => this.props.onUMLSClick(label.labelId)} 169 | UMLSInfo={this.props.UMLSInfo} 170 | onMouseEnter={() => addLogEntryBound(LOG_LABEL_MOUSE_ON, [label.labelId])} 171 | onMouseLeave={() => addLogEntryBound(LOG_LABEL_MOUSE_OFF, [label.labelId])} 172 | /> 173 | )} 174 |
175 |
176 |
177 | ) 178 | } 179 | } 180 | 181 | export default LabelController 182 | -------------------------------------------------------------------------------- /static/src/components/Annotation/LabelFilter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { HighlightOff } from '@material-ui/icons' 4 | import { Filtermap } from './types' 5 | import { hex2rgba } from './utils' 6 | 7 | interface LabelFilterProps { 8 | colormap: Filtermap, 9 | onFilterChange: (filter: string) => void, 10 | selectedFilter: string 11 | } 12 | 13 | class LabelFilter extends React.Component { 14 | constructor(props: LabelFilterProps) { 15 | super(props) 16 | } 17 | 18 | onFilterClick(filter: string) { 19 | this.props.onFilterChange(filter); 20 | } 21 | 22 | render() { 23 | const { colormap, selectedFilter } = this.props; 24 | 25 | const filters = []; 26 | for (const c in colormap) { 27 | const backgroundColor = c == selectedFilter 28 | ? hex2rgba(colormap[c], 0.75) 29 | : hex2rgba(colormap[c], 0.25); 30 | 31 | filters.push( 32 | 33 |
this.onFilterClick(c)} 36 | style={{ 37 | backgroundColor: backgroundColor 38 | }} 39 | > 40 | {c.slice(0,2)} 41 |
42 |
43 | ); 44 | } 45 | 46 | filters.push( 47 | 48 |
this.onFilterClick(null)} 51 | style={{ 52 | backgroundColor: 'white', 53 | color: 'black' 54 | }} 55 | > 56 | 57 |
58 |
59 | ) 60 | 61 | return ( 62 |
63 | {filters} 64 |
65 | ) 66 | } 67 | } 68 | 69 | export default LabelFilter 70 | -------------------------------------------------------------------------------- /static/src/components/Annotation/LabelListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { Clear } from '@material-ui/icons' 4 | import InfoModal from './InfoModal' 5 | import { Filtermap, Label, UMLSDefinition } from './types' 6 | import { hex2rgba, createBackground } from './utils' 7 | 8 | interface LabelListItemProps { 9 | label: Label 10 | colormap: Filtermap 11 | selected?: boolean 12 | onClick?: () => void 13 | onDeleteClick?: () => void 14 | onUMLSClick: () => void 15 | UMLSInfo: UMLSDefinition[] 16 | onMouseEnter: () => void 17 | onMouseLeave: () => void 18 | } 19 | 20 | class LabelListItem extends React.Component { 21 | constructor(props: LabelListItemProps) { 22 | super(props) 23 | } 24 | 25 | render() { 26 | const { labelId, title, categories, confidence } = this.props.label 27 | const categoryText = categories 28 | ? categories.map(c => c.title).join(' | ') 29 | : 'None' 30 | const tooltipText =
31 |
{title}
32 |
CUI: {labelId}
33 |
Categories: {categoryText}
34 |
35 | 36 | // @ts-ignore 37 | const colorOpacity = .5 38 | const categoryColors = categories && categories.map( 39 | c => hex2rgba(this.props.colormap[c.type], colorOpacity) 40 | ) 41 | const background = createBackground(categoryColors) 42 | 43 | return ( 44 | 45 |
52 |
53 |
{title}
54 |
e.stopPropagation()}> 55 | 61 |
62 |
63 | {this.props.onDeleteClick && this.props.selected && 64 | { 69 | e.stopPropagation(); 70 | this.props.onDeleteClick(); 71 | }} 72 | /> 73 | } 74 |
75 |
76 |
77 |
78 | ) 79 | } 80 | } 81 | 82 | export default LabelListItem 83 | -------------------------------------------------------------------------------- /static/src/components/Annotation/Mark.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { CharacterSpan } from './types' 4 | import { INLINE_LABELS } from './config' 5 | import { hex2rgba } from './utils' 6 | 7 | export interface MarkProps { 8 | key: string 9 | text: string 10 | span: CharacterSpan 11 | tag: string 12 | color?: string 13 | opacity?: number 14 | border?: boolean 15 | fill?: boolean 16 | onClick: Function 17 | } 18 | 19 | const Mark: React.SFC = props => ( 20 | 21 |
props.onClick(event)} 32 | > 33 | {INLINE_LABELS && props.tag && ( 34 |
{props.tag}
35 | )} 36 |
{props.text}
37 |
38 |
39 | ) 40 | 41 | export default Mark 42 | -------------------------------------------------------------------------------- /static/src/components/Annotation/PauseModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Button from '@material-ui/core/Button'; 3 | import Modal from '@material-ui/core/Modal'; 4 | import { Pause, PlayArrow } from '@material-ui/icons' 5 | 6 | interface PauseModalProps { 7 | onPause: () => void 8 | onPlay: () => void 9 | } 10 | 11 | interface PauseModalState { 12 | open: boolean 13 | } 14 | 15 | class PauseModal extends React.Component { 16 | constructor(props: PauseModalProps) { 17 | super(props) 18 | 19 | this.state = { 20 | open: false 21 | } 22 | } 23 | 24 | handleOpen = () => { 25 | this.setState({ open: true }) 26 | this.props.onPause() 27 | } 28 | 29 | handleClose = () => { 30 | this.setState({ open: false }) 31 | this.props.onPlay() 32 | } 33 | 34 | render() { 35 | return ( 36 |
37 | 45 | 49 |
50 |

Annotation paused.

51 | 59 |
60 |
61 |
62 | ) 63 | } 64 | } 65 | 66 | export default PauseModal 67 | -------------------------------------------------------------------------------- /static/src/components/Annotation/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface SearchBarProps { 4 | initialText: string 5 | label: string | undefined 6 | onChange?: Function 7 | onEnter: (searchTerm: string) => void 8 | } 9 | 10 | interface SearchBarState { 11 | searchText: string 12 | } 13 | 14 | class SearchBar extends React.Component { 15 | constructor(props: SearchBarProps) { 16 | super(props) 17 | 18 | this.state = { 19 | searchText: this.props.initialText 20 | } 21 | } 22 | 23 | handleChange = (event: any) => { 24 | const { onChange } = this.props 25 | 26 | this.setState({searchText: event.target.value}) 27 | if (onChange) 28 | onChange(event.target.value) 29 | } 30 | 31 | handleKeyPress = (event: React.KeyboardEvent) => { 32 | const { onEnter } = this.props 33 | 34 | if (onEnter && event.key === 'Enter') { 35 | onEnter(this.state.searchText) 36 | } 37 | } 38 | 39 | componentDidUpdate(prevProps: SearchBarProps) { 40 | const { initialText, onChange } = this.props 41 | 42 | if (prevProps.initialText !== initialText) { 43 | this.setState({searchText: initialText}) 44 | if (onChange) 45 | onChange(initialText) 46 | } 47 | } 48 | 49 | render() { 50 | return ( 51 |
52 |
53 | {this.props.label} 54 |
55 | 61 |
62 | ) 63 | } 64 | } 65 | 66 | export default SearchBar 67 | -------------------------------------------------------------------------------- /static/src/components/Annotation/Selection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { ArrowForward, Delete, HighlightOff } from '@material-ui/icons' 4 | import LabelListItem from './LabelListItem' 5 | import { 6 | CUI_TYPE, 7 | Label, 8 | Filtermap, 9 | UMLSDefinition, 10 | LOG_TYPE, 11 | LOG_LABEL_REMOVE, 12 | LOG_LABEL_MOUSE_ON, 13 | LOG_LABEL_MOUSE_OFF, 14 | CUI_NORMAL, CUI_AMBIGUOUS, CUI_CODELESS 15 | } from './types' 16 | 17 | interface SelectionProps { 18 | selectedText: string 19 | selectedLabels: Label[] 20 | colormap: Filtermap 21 | CUIMode: CUI_TYPE 22 | onCUIModeChange: (mode: CUI_TYPE) => void 23 | setSelectedLabels: (labels: Label[]) => void 24 | deleteAnnotation: () => void 25 | onTextSelection: (selection: Selection) => void 26 | onUMLSClick: (cui: string) => void 27 | UMLSInfo: UMLSDefinition[] 28 | addLogEntryBound: (action: LOG_TYPE, metadata: string[]) => boolean 29 | } 30 | 31 | interface SelectionState { 32 | searchText: string 33 | selectedFilter: string 34 | } 35 | 36 | class Selection extends React.Component { 37 | constructor(props: SelectionProps) { 38 | super(props) 39 | 40 | this.state = { 41 | searchText: '', 42 | selectedFilter: null 43 | } 44 | } 45 | 46 | removeLabel = (id: string) => { 47 | const { selectedLabels, setSelectedLabels } = this.props 48 | const i = selectedLabels.findIndex(l => l.labelId == id) 49 | if (i >= 0 && i < selectedLabels.length) { 50 | selectedLabels.splice(i, 1) 51 | setSelectedLabels(selectedLabels) 52 | } 53 | 54 | this.props.addLogEntryBound(LOG_LABEL_REMOVE, [id]) 55 | } 56 | 57 | render() { 58 | const { 59 | selectedText, 60 | selectedLabels, 61 | colormap, 62 | CUIMode, 63 | onTextSelection, 64 | deleteAnnotation, 65 | addLogEntryBound, 66 | onCUIModeChange, 67 | onUMLSClick, 68 | UMLSInfo 69 | } = this.props 70 | 71 | return ( 72 |
73 |
74 | 75 | onTextSelection(null)} 78 | /> 79 | 80 | 81 |
82 |
83 |

Selection:

84 |

{selectedText}

85 |
86 | 87 |
88 | {selectedLabels.length > 0 && } 89 |
90 | 91 |
92 | {selectedLabels.map((label, _i) => 93 | this.removeLabel(label.labelId)} 99 | onUMLSClick={() => onUMLSClick(label.labelId)} 100 | UMLSInfo={UMLSInfo} 101 | onMouseEnter={() => addLogEntryBound(LOG_LABEL_MOUSE_ON, [label.labelId, "selected"])} 102 | onMouseLeave={() => addLogEntryBound(LOG_LABEL_MOUSE_OFF, [label.labelId, "selected"])} 103 | /> 104 | )} 105 |
106 |
107 | 108 | 109 | deleteAnnotation()} 113 | /> 114 | 115 |
116 | 117 |
118 |
onCUIModeChange(CUI_NORMAL)} 121 | > 122 | Normal CUI Match 123 |
124 | 125 |
onCUIModeChange(CUI_AMBIGUOUS)} 128 | > 129 | Ambiguous CUI Match 130 |
131 | 132 |
onCUIModeChange(CUI_CODELESS)} 135 | > 136 | No CUI Match 137 |
138 |
139 |
140 | ) 141 | } 142 | } 143 | 144 | export default Selection 145 | -------------------------------------------------------------------------------- /static/src/components/Annotation/TextController.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AnnotatedToken from './AnnotatedToken' 3 | import { createTokensWithAnnotations } from './utils' 4 | import { 5 | Annotation, 6 | DECISION_TYPE, 7 | Filtermap, 8 | Token, 9 | LOG_TYPE, 10 | LOG_ANNOTATION_MOUSE_ON, 11 | LOG_ANNOTATION_MOUSE_OFF, 12 | LOG_TOKEN_MOUSE_OFF, 13 | LOG_TOKEN_MOUSE_ON 14 | } from './types' 15 | 16 | interface TokenProps { 17 | i: number 18 | content: string 19 | onMouseEnter: () => void 20 | onMouseLeave: () => void 21 | onClick: () => void 22 | } 23 | 24 | const Token: React.SFC = props => { 25 | return ( 26 |
{props.content}
37 | ) 38 | } 39 | 40 | interface TextControllerProps { 41 | text: string 42 | colormap: Filtermap 43 | annotations: Annotation[] 44 | selectedAnnotationId: number 45 | onAnnotationCreationToken: (token: Token) => Number 46 | onAnnotationSelection: (id: number) => void 47 | onSuggestionUpdate: (id: number, decision: DECISION_TYPE) => void 48 | onTextSelection: (selection: Selection) => void 49 | addLogEntryBound: (action: LOG_TYPE, metadata: string[]) => boolean 50 | } 51 | 52 | interface TextControllerState { 53 | anchorEl: any 54 | } 55 | 56 | class TextController extends React.Component { 57 | constructor(props: TextControllerProps) { 58 | super(props) 59 | 60 | this.state = { 61 | anchorEl: null 62 | } 63 | } 64 | 65 | handleSuggestionClick = (event: any) => { 66 | this.setState({ 67 | anchorEl: event.currentTarget 68 | }); 69 | }; 70 | 71 | handleSuggestionClose = () => { 72 | this.setState({ 73 | anchorEl: null 74 | }); 75 | }; 76 | 77 | createTokenDisplay = (token: Token, key: number) => { 78 | const { span: { start, end } } = token 79 | 80 | return token.annotations && token.annotations.length > 0 81 | ? ( 82 | this.props.addLogEntryBound(LOG_ANNOTATION_MOUSE_ON, [String(start), String(end)])} 91 | onMouseLeave={() => this.props.addLogEntryBound(LOG_ANNOTATION_MOUSE_OFF, [String(start), String(end)])} 92 | /> 93 | ) 94 | : ( 95 | this.props.addLogEntryBound(LOG_TOKEN_MOUSE_ON, [String(start), String(end)])} 100 | onMouseLeave={() => this.props.addLogEntryBound(LOG_TOKEN_MOUSE_OFF, [String(start), String(end)])} 101 | onClick={() => this.props.onAnnotationCreationToken(token)} 102 | /> 103 | ) 104 | } 105 | 106 | render() { 107 | const { text, annotations } = this.props 108 | const tokens = createTokensWithAnnotations(text, annotations) 109 | 110 | return ( 111 |
112 | {tokens.map((token, i) => this.createTokenDisplay(token, i))} 113 |
114 | ) 115 | } 116 | } 117 | 118 | export default TextController 119 | -------------------------------------------------------------------------------- /static/src/components/Annotation/config.ts: -------------------------------------------------------------------------------- 1 | import { Label } from './types' 2 | 3 | const SAMPLE_LABELS: Label[] = [ 4 | { 5 | labelId: 'C000', 6 | title: 'Label0', 7 | categories: [{ title: 'category', type: 'general' }], 8 | }, 9 | { 10 | labelId: 'C001', 11 | title: 'Label1', 12 | categories: [{ title: 'category', type: 'general' }], 13 | }, 14 | { 15 | labelId: 'C002', 16 | title: 'Label2', 17 | categories: [{ title: 'category', type: 'general' }], 18 | } 19 | ] 20 | 21 | const LABELS = SAMPLE_LABELS 22 | 23 | export default LABELS 24 | 25 | export const INLINE_LABELS = false 26 | -------------------------------------------------------------------------------- /static/src/components/Annotation/types.ts: -------------------------------------------------------------------------------- 1 | export const SEARCH_AUTOMATIC = 'auto'; 2 | export const SEARCH_MANUAL = 'manual'; 3 | export type SEARCH_TYPE = typeof SEARCH_AUTOMATIC | typeof SEARCH_MANUAL; 4 | 5 | export const CUI_NORMAL = 'normal'; 6 | export const CUI_AMBIGUOUS = 'ambiguous'; 7 | export const CUI_CODELESS = 'codeless'; 8 | export type CUI_TYPE = typeof CUI_NORMAL | typeof CUI_AMBIGUOUS | typeof CUI_CODELESS; 9 | 10 | export const EXPERIMENT_0 = 0; 11 | export const EXPERIMENT_1 = 1; 12 | export const EXPERIMENT_2 = 2; 13 | export const EXPERIMENT_3 = 3; 14 | export type EXPERIMENT_TYPE = typeof EXPERIMENT_0 15 | | typeof EXPERIMENT_1 16 | | typeof EXPERIMENT_2 17 | | typeof EXPERIMENT_3; 18 | 19 | export const MANUAL = 'manual'; 20 | export const AUTO = 'auto'; 21 | export const DYNAMIC = 'dynamic'; 22 | export type CREATION_TYPE = typeof MANUAL | typeof AUTO | typeof DYNAMIC; 23 | 24 | export const UNDECIDED = 'undecided'; 25 | export const ACCEPTED = 'accepted'; 26 | export const REJECTED = 'rejected'; 27 | export const MODIFIED = 'modified'; 28 | export type DECISION_TYPE = typeof UNDECIDED | typeof ACCEPTED | typeof REJECTED | typeof MODIFIED; 29 | 30 | export const LOG_HIGHLIGHT = 'HIGHLIGHT'; 31 | export const LOG_SCROLL = 'SCROLL'; 32 | export const LOG_LABEL_ADD = 'LABEL-ADD'; 33 | export const LOG_LABEL_REMOVE = 'LABEL-REMOVE'; 34 | export const LOG_RECOMMEND = 'RECOMMEND-KEYWORD'; 35 | export const LOG_SEARCH_KEYWORD = 'SEARCH-KEYWORD'; 36 | export const LOG_SEARCH_CODE = 'SEARCH-CODE'; 37 | export const LOG_LABEL_FILTER = 'LABEL-FILTER'; 38 | export const LOG_ANNOTATION_ADD = 'ANNOTATION-ADD'; 39 | export const LOG_ANNOTATION_REMOVE = 'ANNOTATION-REMOVE'; 40 | export const LOG_SUGGESTION_ACTION = 'SUGGESTION-ACTION'; 41 | export const LOG_LABEL_INFO = 'LABEL-INFO'; 42 | export const LOG_PAUSE = 'PAUSE'; 43 | export const LOG_PLAY = 'PLAY'; 44 | export const LOG_ANNOTATION_MOUSE_ON = 'ANNOTATION-MOUSE-ON'; 45 | export const LOG_ANNOTATION_MOUSE_OFF = 'ANNOTATION-MOUSE-OFF'; 46 | export const LOG_TOKEN_MOUSE_ON = 'TOKEN-MOUSE-ON'; 47 | export const LOG_TOKEN_MOUSE_OFF = 'TOKEN-MOUSE-OFF'; 48 | export const LOG_LABEL_MOUSE_ON = 'LABEL-MOUSE-ON'; 49 | export const LOG_LABEL_MOUSE_OFF = 'LABEL-MOUSE-OFF'; 50 | export const LOG_CUI_MODE_CHANGE = 'CUI-MODE-CHANGE'; 51 | export const LOG_ANNOTATION_SELECT = 'ANNOTATION-SELECT'; 52 | export type LOG_TYPE = typeof LOG_HIGHLIGHT | typeof LOG_SCROLL 53 | | typeof LOG_LABEL_ADD | typeof LOG_LABEL_REMOVE 54 | | typeof LOG_LABEL_FILTER | typeof LOG_LABEL_INFO 55 | | typeof LOG_RECOMMEND 56 | | typeof LOG_SEARCH_CODE | typeof LOG_SEARCH_KEYWORD 57 | | typeof LOG_ANNOTATION_ADD | typeof LOG_ANNOTATION_REMOVE 58 | | typeof LOG_SUGGESTION_ACTION 59 | | typeof LOG_PAUSE | typeof LOG_PLAY 60 | | typeof LOG_ANNOTATION_MOUSE_ON | typeof LOG_ANNOTATION_MOUSE_OFF 61 | | typeof LOG_TOKEN_MOUSE_ON | typeof LOG_TOKEN_MOUSE_OFF 62 | | typeof LOG_LABEL_MOUSE_ON | typeof LOG_LABEL_MOUSE_OFF 63 | | typeof LOG_CUI_MODE_CHANGE | typeof LOG_ANNOTATION_SELECT 64 | 65 | export const HIGH_CONFIDENCE = 'high'; 66 | export const LOW_CONFIDENCE = 'low'; 67 | export type CONFIDENCE_TYPE = typeof HIGH_CONFIDENCE | typeof LOW_CONFIDENCE; 68 | 69 | export type CharacterSpan = { 70 | start: number; 71 | end: number; 72 | } 73 | 74 | export type Category = { 75 | title: string; 76 | type: string; 77 | } 78 | 79 | export type Label = { 80 | labelId: string; 81 | title: string; 82 | categories: Category[]; 83 | confidence?: CONFIDENCE_TYPE; 84 | } 85 | 86 | export type LabelCounts = { [id: string]: Label & {count: number}; } 87 | 88 | export type Annotation = { 89 | annotationId: number; 90 | createdAt: number; // UTC timestamp in milliseconds 91 | text: string; 92 | spans: CharacterSpan[]; 93 | labels: Label[]; 94 | CUIMode: CUI_TYPE; 95 | experimentMode: EXPERIMENT_TYPE; 96 | creationType: CREATION_TYPE; 97 | decision: DECISION_TYPE; 98 | } 99 | 100 | export type Token = { 101 | id: number, 102 | text: string, 103 | span: CharacterSpan, 104 | annotations: Annotation[], 105 | } 106 | 107 | export type WordToken = { 108 | id: number, 109 | text: string, 110 | span: CharacterSpan, 111 | } 112 | 113 | export interface Filtermap { 114 | [Key: string]: T; 115 | } 116 | 117 | export type UMLSDefinition = { 118 | rootSource: string, 119 | value: string 120 | } 121 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/annotationUtils.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, CUI_TYPE, EXPERIMENT_TYPE, Label, MANUAL, Token } from '../types' 2 | import { getSelectedText, getSelectionSpans } from './selectionUtils' 3 | 4 | export const createAnnotation = ( 5 | selection: Selection, 6 | text: string, 7 | labels: Label[], 8 | CUIMode: CUI_TYPE, 9 | experimentMode: EXPERIMENT_TYPE, 10 | ): Annotation => { 11 | const currentTime = Date.now(); 12 | 13 | const annotation: Annotation = { 14 | annotationId: currentTime, 15 | createdAt: currentTime, 16 | text: getSelectedText(selection, text), 17 | spans: getSelectionSpans(selection), 18 | labels, 19 | CUIMode, 20 | experimentMode, 21 | creationType: MANUAL, 22 | decision: null 23 | } 24 | 25 | return annotation 26 | } 27 | 28 | export const createAnnotationFromToken = ( 29 | token: Token, 30 | labels: Label[], 31 | CUIMode: CUI_TYPE, 32 | experimentMode: EXPERIMENT_TYPE, 33 | ): Annotation => { 34 | const currentTime = Date.now(); 35 | 36 | const annotation: Annotation = { 37 | annotationId: currentTime, 38 | createdAt: currentTime, 39 | text: token.text, 40 | spans: [token.span], 41 | labels, 42 | CUIMode, 43 | experimentMode, 44 | creationType: MANUAL, 45 | decision: null 46 | } 47 | 48 | return annotation 49 | } 50 | 51 | export const getAnnotationTag = (annotation: Annotation): string => { 52 | return annotation.labels.map(l => l.title).join(' | ') 53 | } 54 | 55 | export const isAnnotationSelected = ( 56 | annotation: Annotation, 57 | selectedAnnotationId: number 58 | ): boolean => { 59 | return selectedAnnotationId === annotation.annotationId 60 | } 61 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/colorUtils.ts: -------------------------------------------------------------------------------- 1 | export const hex2rgba = (hex: string, alpha = 1) => { 2 | const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16)); 3 | return `rgba(${r},${g},${b},${alpha})`; 4 | }; 5 | 6 | export const createBackground = (colors: string[]) => { 7 | const sectionLength = Math.floor(100 / colors.length) 8 | const sectionStrings = colors.map((c, i) => { 9 | const start = i == 0 ? 0 : i * sectionLength + 5 10 | const end = i == colors.length - 1 ? 100 : (i + 1) * sectionLength - 5 11 | return `${c} ${start}%,${c} ${end}%` 12 | }) 13 | return `linear-gradient(to right, ${sectionStrings.join(',')})` 14 | } 15 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './annotationUtils' 2 | export * from './colorUtils' 3 | export * from './labelUtils' 4 | export * from './selectionUtils' 5 | export * from './suggestionUtils' 6 | export * from './tokenUtils' 7 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/labelUtils.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, Label, LabelCounts } from '../types' 2 | 3 | export const generateLabelCounts = (annotations: Annotation[]) => { 4 | const labelsWithCounts: LabelCounts = {} 5 | for (var a of annotations) { 6 | for (var l of a.labels) { 7 | const currentEntry = labelsWithCounts[l.labelId] 8 | const newCount = currentEntry ? currentEntry.count + 1 : 1 9 | labelsWithCounts[l.labelId] = { 10 | ...l, 11 | count: newCount 12 | } 13 | } 14 | } 15 | return labelsWithCounts 16 | } 17 | 18 | export const nMostCommonLabels = (labelCounts: LabelCounts, n: number): Label[] => { 19 | const labelCountsValues = Object.keys(labelCounts).map(key => labelCounts[key]) 20 | const sortedLabelCounts = labelCountsValues.sort((a, b) => b.count - a.count) 21 | 22 | const relevantLabels = sortedLabelCounts.length <= n 23 | ? sortedLabelCounts 24 | : sortedLabelCounts.slice(0, n) 25 | 26 | return relevantLabels.map(l => ({ 27 | labelId: l.labelId, 28 | title: l.title, 29 | categories: l.categories, 30 | })) 31 | } 32 | 33 | export const filterLabelsByType = (labels: Label[], filter: string): Label[] => { 34 | if (filter == null) { 35 | return labels; 36 | } 37 | 38 | return labels.filter(l => l.categories.map(c => c.type).includes(filter)); 39 | } 40 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/selectionUtils.ts: -------------------------------------------------------------------------------- 1 | import { CharacterSpan } from '../types' 2 | 3 | export const selectionIsEmpty = (selection: Selection) => { 4 | let position = selection.anchorNode.compareDocumentPosition(selection.focusNode) 5 | return position === 0 && selection.focusOffset === selection.anchorOffset 6 | } 7 | 8 | export const selectionIsBackwards = (selection: Selection) => { 9 | if (selectionIsEmpty(selection)) return false 10 | 11 | let position = selection.anchorNode.compareDocumentPosition(selection.focusNode) 12 | 13 | let backward = false 14 | if ( 15 | (!position && selection.anchorOffset > selection.focusOffset) || 16 | position === Node.DOCUMENT_POSITION_PRECEDING 17 | ) 18 | backward = true 19 | 20 | return backward 21 | } 22 | 23 | export const copySelectionRanges = (selection: Selection) => { 24 | const ranges: Range[] = [] 25 | for (var i = 0; i < selection.rangeCount; i++) { 26 | ranges.push(selection.getRangeAt(i).cloneRange()) 27 | } 28 | return ranges 29 | } 30 | 31 | export const setSelectionRanges = (selection: Selection, ranges: Range[]) => { 32 | selection.empty() 33 | for (var i = 0; i < ranges.length; i++) { 34 | const range: Range = ranges[i] 35 | selection.addRange(range) 36 | } 37 | } 38 | 39 | export const getNodeId = (node: Node) => { 40 | return parseInt(node.parentElement.getAttribute('data-i'), 10) 41 | } 42 | 43 | export const getSelectionSpans = (selection: Selection) => { 44 | if (selection === null) 45 | return [] 46 | 47 | const spans: CharacterSpan[] = [] 48 | 49 | for (var i = 0; i < selection.rangeCount; i++) { 50 | const range: Range = selection.getRangeAt(i) 51 | let start = getNodeId(range.startContainer) + range.startOffset 52 | let end = getNodeId(range.endContainer) + range.endOffset 53 | 54 | // swap start and end variables - * unnecessary 55 | // if (selectionIsBackwards(selection)) { 56 | // ;[start, end] = [end, start] 57 | // } 58 | 59 | if (!isNaN(start) && !isNaN(end)) 60 | spans.push({ start, end }) 61 | } 62 | 63 | return spans 64 | } 65 | 66 | export const getSelectedText = (selection: Selection, text: string) => { 67 | const spans: CharacterSpan[] = getSelectionSpans(selection) 68 | return spans.map(span => text.slice(span.start, span.end)).join(' ') 69 | } 70 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/suggestionUtils.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, CUI_TYPE, EXPERIMENT_TYPE, Label, DYNAMIC, UNDECIDED, CharacterSpan, MANUAL } from '../types' 2 | 3 | 4 | export const createSuggestion = ( 5 | annotationId: number, 6 | text: string, 7 | spans: CharacterSpan[], 8 | labels: Label[], 9 | CUIMode: CUI_TYPE, 10 | experimentMode: EXPERIMENT_TYPE, 11 | ): Annotation => { 12 | const currentTime = Date.now(); 13 | const annotationText = spans.map(span => text.slice(span.start, span.end)).join(' ') 14 | 15 | const annotation: Annotation = { 16 | annotationId, 17 | createdAt: currentTime, 18 | text: annotationText, 19 | spans, 20 | labels, 21 | CUIMode, 22 | experimentMode, 23 | creationType: DYNAMIC, 24 | decision: UNDECIDED 25 | } 26 | 27 | return annotation 28 | } 29 | 30 | 31 | export const cleanAnnotation = (annotation: Annotation): Annotation => { 32 | var { text, spans } = annotation 33 | var { start, end } = spans && spans.length > 0 && spans[0] 34 | 35 | const startWhitelist = [' ', '\n', '\t', '.', ',', ':', '#', ')', '(', '[', ']', '{', '}'] 36 | startWhitelist.forEach((str) => { 37 | if (text.startsWith(str)) { 38 | text = text.slice(str.length) 39 | start += str.length 40 | } 41 | }) 42 | 43 | const endWhitelist = [' ', '\n', '\t', '.', ',', ':', '#', ')', '(', '[', ']', '{', '}', ' of', '\nof'] 44 | endWhitelist.forEach((str) => { 45 | if (text.endsWith(str)) { 46 | text = text.slice(0, text.length - str.length) 47 | end -= str.length 48 | } 49 | }) 50 | 51 | return {...annotation, text, spans: [{start, end}]} 52 | } 53 | 54 | 55 | const spanToString = (span: CharacterSpan): string => { 56 | return String(span.start) + ',' + String(span.end) 57 | } 58 | 59 | 60 | export const propagateSuggestions = ( 61 | fullText: string, 62 | annotation: Annotation, 63 | annotations: Annotation[] 64 | ): Annotation[] => { 65 | const newAnnotation = cleanAnnotation(annotation) 66 | const { labels, spans, CUIMode, experimentMode, text } = newAnnotation 67 | const { start } = spans && spans.length > 0 && spans[0] 68 | 69 | // remove 'undecided' suggestions on the same text 70 | // preserve manual / 'decided' suggestions on the same text 71 | const spansToPreserve = new Set() 72 | const suggestedAnnotations = [...annotations].filter(a => { 73 | if (a.annotationId === newAnnotation.annotationId) { 74 | return true 75 | } 76 | 77 | const clean: Annotation = cleanAnnotation(a) 78 | if (clean.text.toLowerCase() === text.toLowerCase()) { 79 | if (a.creationType !== MANUAL && a.decision === UNDECIDED) { 80 | return false 81 | } else { 82 | const cleanSpan: CharacterSpan = clean.spans && clean.spans[0] 83 | spansToPreserve.add(spanToString(cleanSpan)) 84 | return true 85 | } 86 | } 87 | 88 | return true 89 | }) 90 | 91 | const re = new RegExp(`\\b${text}\\b`, 'gi') 92 | const newSuggestions: Annotation[] = [] 93 | const currentTime = Date.now(); 94 | 95 | // add suggestions with same labels to all text matches 96 | let match 97 | while ((match = re.exec(fullText)) !== null) { 98 | if (match.index !== start) { 99 | const annotationId = currentTime + newSuggestions.length + 1 100 | 101 | const span: CharacterSpan = { 102 | start: match.index, 103 | end: match.index + match[0].length 104 | } 105 | 106 | if (!spansToPreserve.has(spanToString(span))) { 107 | // create new label array pointer 108 | const newLabels: Label[] = []; 109 | labels.forEach(l => newLabels.push(l)) 110 | 111 | newSuggestions.push(createSuggestion( 112 | annotationId, 113 | fullText, 114 | [span], 115 | newLabels, 116 | CUIMode, 117 | experimentMode 118 | )) 119 | } 120 | } 121 | } 122 | 123 | suggestedAnnotations.push(...newSuggestions) 124 | return suggestedAnnotations 125 | } 126 | -------------------------------------------------------------------------------- /static/src/components/Annotation/utils/tokenUtils.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, CharacterSpan, Token, WordToken } from '../types' 2 | 3 | export const isTokenSelected = (token: Token, id: number): boolean => { 4 | if (id === null) { 5 | return false 6 | } 7 | 8 | const { annotations } = token 9 | 10 | if (annotations) { 11 | const tokenIds = annotations.map(a => a.annotationId) 12 | return tokenIds.includes(id) 13 | } 14 | 15 | return false 16 | } 17 | 18 | const createToken = ( 19 | text: string, 20 | span: CharacterSpan, 21 | annotations: Annotation[], 22 | id: number = span.start 23 | ): Token => { 24 | return { 25 | id: id, 26 | text: text.slice(span.start, span.end), 27 | span, 28 | annotations, 29 | } 30 | } 31 | 32 | const createAnnotatedTokens = (text: string, annotations: Annotation[]) => { 33 | // create individual tokens for each annotation span 34 | const annotatedTokens: Token[] = [] 35 | for (var a of annotations) { 36 | for (var span of a.spans) { 37 | annotatedTokens.push(createToken(text, span, [a])) 38 | } 39 | } 40 | return annotatedTokens 41 | } 42 | 43 | const sortTokensByStart = (tokens: Token[]): Token[] => { 44 | return [...tokens].sort((a, b) => a.span.start - b.span.start) 45 | } 46 | 47 | const sortTokensByEnd = (tokens: Token[]): Token[] => { 48 | return [...tokens].sort((a, b) => a.span.end - b.span.end) 49 | } 50 | 51 | const addAnnotatedToken = ( 52 | text: string, 53 | span: CharacterSpan, 54 | spanningTokens: Token[], 55 | distinctTokens: Token[] 56 | ): boolean => { 57 | // add a text span corresponding to specific annotations as a token 58 | if (spanningTokens.length > 0 && span.start >= 0) { 59 | const annotations = spanningTokens.map(t => t.annotations[0]) 60 | const textSpan = text.slice(span.start, span.end) 61 | if (textSpan.replace(/\s/g, '').length) { 62 | distinctTokens.push(createToken(text, span, annotations)) 63 | return true 64 | } 65 | } 66 | return false 67 | } 68 | 69 | const createIndependentAnnotatedTokens = (text: string, annotations: Annotation[]) => { 70 | // create non-overlapping ordered array of tokens with annotations 71 | const annotatedTokens: Token[] = [] 72 | 73 | if (annotations.length === 0) { 74 | return annotatedTokens 75 | } 76 | 77 | const tokens = createAnnotatedTokens(text, annotations) 78 | const tokensByStart: Token[] = sortTokensByStart(tokens) 79 | const tokensByEnd: Token[] = sortTokensByEnd(tokens) 80 | var tokensByStartIndex = 0 81 | var tokensByEndIndex = 0 82 | 83 | var characterIndex = -1 84 | var spanningTokens: Token[] = [] 85 | 86 | while ( 87 | characterIndex < text.length 88 | && tokensByStartIndex < tokens.length 89 | && tokensByEndIndex < tokens.length 90 | ) { 91 | const minStartToken: Token = tokensByStart[tokensByStartIndex] 92 | const minEndToken: Token = tokensByEnd[tokensByEndIndex] 93 | 94 | if (minStartToken.span.start < minEndToken.span.end) { 95 | // next token starts before the current token ends 96 | const span: CharacterSpan = { 97 | start: characterIndex, 98 | end: minStartToken.span.start 99 | } 100 | 101 | addAnnotatedToken(text, span, spanningTokens, annotatedTokens) 102 | 103 | // add all annotations starting at the next span section to the relevant set 104 | characterIndex = span.end 105 | while ( 106 | tokensByStartIndex < tokens.length 107 | && characterIndex === tokensByStart[tokensByStartIndex].span.start 108 | ) { 109 | spanningTokens.push(tokensByStart[tokensByStartIndex]) 110 | tokensByStartIndex += 1 111 | } 112 | } else { 113 | // current token ends before the next token starts 114 | const span: CharacterSpan = { 115 | start: characterIndex, 116 | end: minEndToken.span.end, 117 | } 118 | 119 | addAnnotatedToken(text, span, spanningTokens, annotatedTokens) 120 | 121 | // remove all annotations ending with this span section from the relevant set 122 | characterIndex = span.end 123 | spanningTokens = spanningTokens.filter(a => a.span.end !== characterIndex) 124 | while ( 125 | tokensByEndIndex < tokens.length 126 | && characterIndex === tokensByEnd[tokensByEndIndex].span.end 127 | ) { 128 | tokensByEndIndex += 1 129 | } 130 | } 131 | } 132 | 133 | // handle all relevant annotations whose spans have not ended yet 134 | while (tokensByEndIndex < tokens.length) { 135 | const minEndToken: Token = tokensByEnd[tokensByEndIndex] 136 | const span: CharacterSpan = { 137 | start: characterIndex, 138 | end: minEndToken.span.end, 139 | } 140 | 141 | addAnnotatedToken(text, span, spanningTokens, annotatedTokens) 142 | 143 | characterIndex = span.end 144 | spanningTokens = spanningTokens.filter(a => a.span.end !== characterIndex) 145 | while ( 146 | tokensByEndIndex < tokens.length 147 | && characterIndex === tokensByEnd[tokensByEndIndex].span.end 148 | ) { 149 | tokensByEndIndex += 1 150 | } 151 | } 152 | 153 | return annotatedTokens 154 | } 155 | 156 | const createWordTokens = (text: string) => { 157 | const strings = text.replace(/\n/g, ' \n ').split(' ') 158 | // const strings = text.split(' ') 159 | var start = 0 160 | 161 | return strings.map((s, i) => { 162 | if (i > 0 && s == '\n' && strings[i - 1] != '\n') { 163 | start = start - 1 164 | } 165 | 166 | const end = start + s.length 167 | const wordToken: WordToken = { 168 | id: start, 169 | text: s, 170 | span: { start, end } 171 | } 172 | start = s == '\n' ? end : end + 1 173 | return wordToken 174 | }) 175 | } 176 | 177 | export const createTokensWithAnnotations = (text: string, annotations: Annotation[]) => { 178 | const tokens: Token[] = [] 179 | if (!text) { 180 | return tokens; 181 | } 182 | 183 | const sortedAnnotatedTokens = createIndependentAnnotatedTokens(text, annotations) 184 | const wordTokens: WordToken[] = createWordTokens(text) 185 | 186 | var tokenId = 0 187 | for (var i = 0; i < sortedAnnotatedTokens.length; i++) { 188 | const annotatedToken: Token = sortedAnnotatedTokens[i] 189 | const { start, end } = annotatedToken.span 190 | 191 | // add all full words before annotation 192 | while (wordTokens[tokenId].span.end < start) { 193 | tokens.push(createToken(text, wordTokens[tokenId].span, null)) 194 | tokenId += 1 195 | } 196 | 197 | // add part of word before annotation 198 | const prevAnnotatedToken = i > 0 && sortedAnnotatedTokens[i - 1] 199 | const prevSpanStart = prevAnnotatedToken 200 | ? Math.max(prevAnnotatedToken.span.end, wordTokens[tokenId].span.start) 201 | : wordTokens[tokenId].span.start 202 | if (prevSpanStart < start) { 203 | const span: CharacterSpan = { 204 | start: prevSpanStart, 205 | end: start 206 | } 207 | tokens.push(createToken(text, span, null)) 208 | } 209 | 210 | // add annotated token 211 | tokens.push(annotatedToken) 212 | 213 | // move tokenId to after annotation 214 | while (wordTokens[tokenId].span.end <= end) { 215 | tokenId += 1 216 | } 217 | 218 | // add part of word after annotation if exists 219 | const nextAnnotatedToken = i < sortedAnnotatedTokens.length - 1 && sortedAnnotatedTokens[i + 1] 220 | const nextSpanEnd = nextAnnotatedToken 221 | ? Math.min(nextAnnotatedToken.span.start, wordTokens[tokenId].span.end) 222 | : wordTokens[tokenId].span.end 223 | if (wordTokens[tokenId].span.start < end && end < nextSpanEnd) { 224 | const span: CharacterSpan = { 225 | start: end, 226 | end: nextSpanEnd, 227 | } 228 | tokens.push(createToken(text, span, null)) 229 | 230 | if (nextSpanEnd === wordTokens[tokenId].span.end) { 231 | tokenId += 1 232 | } 233 | } 234 | } 235 | 236 | for (var i = tokenId; i < wordTokens.length; i++) { 237 | tokens.push(createToken(text, wordTokens[i].span, null)) 238 | } 239 | 240 | return tokens 241 | } 242 | -------------------------------------------------------------------------------- /static/src/components/Files/FileListItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | interface FileListItemProps { 4 | file: string 5 | onClick: (id: string) => void 6 | } 7 | 8 | const FileListItem: React.SFC = props => { 9 | return ( 10 |
{ 11 | event.preventDefault() 12 | props.onClick(props.file) 13 | }}> 14 |
15 | {props.file} 16 |
17 |
18 | ) 19 | } 20 | 21 | export default FileListItem 22 | -------------------------------------------------------------------------------- /static/src/components/Files/FilesViewer.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import * as actionCreators from '../../actions'; 6 | import FileListItem from './FileListItem'; 7 | 8 | function mapStateToProps(state) { 9 | return { 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators(actionCreators.default, dispatch); 15 | } 16 | 17 | @connect(mapStateToProps, mapDispatchToProps) 18 | class FilesViewer extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | files: [], 24 | }; 25 | } 26 | 27 | componentDidMount() { 28 | this.fetchData() 29 | } 30 | 31 | async fetchData() { 32 | const filenames = this.props.getFilenames(); 33 | const data = await filenames.then((response) => response.data); 34 | 35 | this.setState({ 36 | files: data.filenames, 37 | }) 38 | } 39 | 40 | render() { 41 | return ( 42 |
43 |

Available Files

44 | 45 |
46 | {this.state.files.map(file => browserHistory.push(`/annotation/${file}`)} />)} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default FilesViewer; 54 | -------------------------------------------------------------------------------- /static/src/components/Footer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* component styles */ 4 | import { styles } from './styles.scss'; 5 | 6 | export const Footer = () => 7 |
8 |
9 |
10 |
11 |

MIT Clinical Machine Learning Group © 2020

12 |
13 |
14 |
15 |
; 16 | -------------------------------------------------------------------------------- /static/src/components/Footer/styles.scss: -------------------------------------------------------------------------------- 1 | :local(.styles) { 2 | padding-top: 35px; 3 | padding-bottom: 30px; 4 | text-align: center; 5 | background-color: #E0F2F1; 6 | color: black; 7 | position: absolute; 8 | bottom: -100; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /static/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | import AppBar from '@material-ui/core/AppBar'; 5 | import LeftNav from '@material-ui/core/Drawer'; 6 | import MenuItem from '@material-ui/core/MenuItem'; 7 | import Button from '@material-ui/core/Button'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import Toolbar from '@material-ui/core/Toolbar'; 10 | import Typography from '@material-ui/core/Typography'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import MenuIcon from '@material-ui/icons/Menu'; 13 | 14 | 15 | export class Header extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | open: false, 20 | }; 21 | 22 | } 23 | 24 | dispatchNewRoute(route) { 25 | browserHistory.push(route); 26 | this.setState({ 27 | open: false, 28 | }); 29 | 30 | } 31 | 32 | 33 | handleClickOutside() { 34 | this.setState({ 35 | open: false, 36 | }); 37 | } 38 | 39 | 40 | logout(e) { 41 | e.preventDefault(); 42 | this.props.logoutAndRedirect(); 43 | this.setState({ 44 | open: false, 45 | }); 46 | } 47 | 48 | openNav() { 49 | this.setState({ 50 | open: true, 51 | }); 52 | } 53 | 54 | render() { 55 | return ( 56 |
57 | 58 | { 59 |
60 | this.dispatchNewRoute('/home')}> 61 | Home 62 | 63 | this.dispatchNewRoute('/filesView')}> 64 | Files 65 | 66 | this.dispatchNewRoute('/tutorial')}> 67 | Tutorial 68 | 69 |
70 | } 71 |
72 | 73 | 74 | 75 | this.openNav()} /> 76 | 77 | 78 | Clinical Annotation 79 | 80 | 81 | 82 |
83 | 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /static/src/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Home = () => 4 |
5 |
6 |

Welcome to the clinical annotation platform!

7 | 8 |

Files Directory

9 |

10 | To get started, navigate to the Files page from the Menu on the left. 11 | All available files to annotate will be listed on that page. 12 | These files are sourced from the ./data folder in the project. 13 | Add a new .txt file to that folder to load it into the platform. 14 |

15 | 16 |

Annotating

17 |

18 | 19 | Once you have opened a file, you'll see the text on screen to the left, a section to search for labels on the right, and a bar below highlighted 20 | in yellow with the text and labels you have selected. 21 |

22 |

23 | To select text, you can manually click and drag over an area of text. 24 | To select an individual word, you can double click on that word to highlight it more quickly. 25 |

26 |

27 | Once you've highlighted a span of text, to find corresponding labels you can search through labels with the search box on the top right. 28 | The filters below the search boxes can help you narrow down your label options by 29 | selecting a desired category of label, like "Finding" or "Procedure". 30 |

31 |

32 | To select a label, just click on it. For more information about a label, click on the info button on the right side of the label. 33 | Even after labels are selected, they can be removed with the red x. 34 | Annotations in general can also be deleted with the red trash can. 35 |

36 |

37 | If you don't think there are any CUI matches for a span of text, or the matches are ambiguous, you can select that option with the buttons below the yellow area. 38 |

39 |

40 | If you need to pause annotating, click the pause button in the bottom right of the screen. 41 |

42 | 43 |

Extracting Annotations

44 |

45 | All changes to the annotations on a file are automatically saved in a 46 | json file in the same ./data folder. The json annotations file 47 | for a corresponding text file will have the same name as the file with a 48 | .json extension. The json object for an annotation includes fields 49 | for the span of text annotated, the UMLS labels selected, the timestamp of 50 | creation, and whether the annotation was manual or suggested. 51 |

52 |
53 |
; 54 | -------------------------------------------------------------------------------- /static/src/components/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | 5 | class NotFound extends React.Component { // eslint-disable-line react/prefer-stateless-function 6 | render() { 7 | return ( 8 |
9 |

Not Found

10 |
11 | ); 12 | } 13 | } 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/TutorialAnnotation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import Button from '@material-ui/core/Button'; 6 | import { NavigateNext } from '@material-ui/icons'; 7 | import * as actionCreators from '../../actions'; 8 | import AnnotationView from '../Annotation/AnnotationView'; 9 | 10 | 11 | function mapStateToProps(state) { 12 | return { 13 | }; 14 | } 15 | 16 | function mapDispatchToProps(dispatch) { 17 | return bindActionCreators(actionCreators.default, dispatch); 18 | } 19 | 20 | @connect(mapStateToProps, mapDispatchToProps) 21 | class TutorialAnnotation extends React.Component { 22 | render() { 23 | const { params } = this.props; 24 | const fileId = params.fileId; 25 | const userId = params.userId; 26 | 27 | return ( 28 |
29 |

Tutorial {fileId}

30 | 31 |
35 | 36 |
37 | 38 |
39 | 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | export default TutorialAnnotation; 54 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/TutorialDone.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TUTORIAL_SLIDES_LINK } from '../../../constants'; 3 | 4 | export const TutorialDone = () => 5 |
6 |
7 |

Thanks for finishing the tutorial!

8 | 9 |

10 | You can revisit the tutorial slides at 11 | any time or work through the tutorial sequence by navigating here in the menu. 12 |

13 | 14 |

15 | The UMLS Terminology Browser might 16 | also be helpful as you annotate. 17 |

18 | 19 |

20 | To begin providing annotations on clinical notes, click on 'Files' in the 21 | menu to select a note. 22 |

23 | 24 |
25 |
; 26 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/TutorialEvaluationItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from '@material-ui/core/Tooltip' 3 | import { CheckCircleTwoTone, ErrorTwoTone } from '@material-ui/icons'; 4 | import { TutorialEvaluationItem, EvaluationLabelAnalysis } from './types' 5 | import { Filtermap, Label, Annotation } from '../Annotation/types' 6 | import AnnotatedToken from '../Annotation/AnnotatedToken' 7 | import { 8 | isAnnotationAmbiguous, 9 | isCorrectTutorialEvaluationItem, 10 | isAnnotationCorrect, 11 | isSpanCorrect, 12 | generateLabelAnalysis, 13 | isAnnotationCUILess 14 | } from './utils' 15 | 16 | const ERROR_COLOR = (a: Number) => 'rgba(255, 176, 0, ' + a + ')' 17 | const ERROR_STYLE = { backgroundColor: ERROR_COLOR(0.25) } 18 | 19 | interface LabelItemProps { 20 | label: Label 21 | colormap: Filtermap 22 | } 23 | 24 | interface TutorialEvaluationItemProps { 25 | evaluationItem: TutorialEvaluationItem 26 | colormap: Filtermap 27 | } 28 | 29 | const LabelItem: React.SFC = props => { 30 | const { label, colormap } = props 31 | const { labelId, title, categories } = label 32 | 33 | return 34 |
CUI: {labelId}
35 |
Categor{categories.length > 1 ? 'ies' : 'y'}: {categories.map(c => c.title).join(', ')}
36 |
}> 37 | 40 | {title} 41 | 42 | 43 | } 44 | 45 | function spansToElement( 46 | spans: string[], 47 | descriptor: string 48 | ): JSX.Element { 49 | return ( 50 |
51 | Annotation on span{spans.length > 1 ? 's' : ''} 52 | {spans.join(', ')} 53 | {spans.length > 1 ? ' are' : ' is'} {descriptor}. 54 |
55 | ) 56 | } 57 | 58 | function spansToElementComposite( 59 | extraSpans: string[], 60 | missingSpans: string[] 61 | ): JSX.Element { 62 | return ( 63 |
64 | Annotation on span{extraSpans.length > 1 ? 's' : ''} 65 | {extraSpans.join(', ')} 66 | should be on span{missingSpans.length > 1 ? 's' : ''} 67 | {missingSpans.join(', ')}. 68 |
69 | ) 70 | } 71 | 72 | function generateSpanDescription( 73 | gold: Annotation, 74 | user: Annotation 75 | ): JSX.Element { 76 | const isCorrect = isSpanCorrect(gold, user) 77 | 78 | return
79 | {isCorrect 80 | ? spansToElement([user.text], 'correct') 81 | : spansToElementComposite([user.text], [gold.text]) 82 | } 83 |
84 | } 85 | 86 | function labelsToElement( 87 | labels: Label[], 88 | descriptor: string, 89 | colormap: Filtermap 90 | ): JSX.Element { 91 | return ( 92 |
93 | Label{labels.length > 1 ? 's' : ''} 94 | {labels.map(l => )} 95 | {labels.length > 1 ? 'are' : 'is'} {descriptor}. 96 |
97 | ) 98 | } 99 | 100 | function labelsToElementComposite( 101 | extraLabels: Label[], 102 | missingLabels: Label[], 103 | colormap: Filtermap 104 | ): JSX.Element { 105 | return ( 106 |
107 | Label{extraLabels.length > 1 ? 's' : ''} 108 | {extraLabels.map(l => )} 109 | should be label{missingLabels.length > 1 ? 's' : ''} 110 | {missingLabels.map(l => )}. 111 |
112 | ) 113 | } 114 | 115 | function generateLabelDescription( 116 | labelAnalysis: EvaluationLabelAnalysis, 117 | colormap: Filtermap 118 | ): JSX.Element { 119 | const { correctLabels, extraLabels, missingLabels } = labelAnalysis 120 | 121 | return
122 | {correctLabels.length > 0 && labelsToElement(correctLabels, 'correct', colormap)} 123 | {extraLabels.length > 0 && missingLabels.length > 0 124 | ? labelsToElementComposite(extraLabels, missingLabels, colormap) 125 | :
126 | {extraLabels.length > 0 && labelsToElement(extraLabels, 'unnecessary', colormap)} 127 | {missingLabels.length > 0 && labelsToElement(missingLabels, 'missing', colormap)} 128 |
129 | } 130 | 131 |
132 | } 133 | 134 | function generateItemIcon( 135 | isCorrect: boolean 136 | ): JSX.Element { 137 | if (isCorrect) { 138 | return 139 | } else { 140 | return
141 | } 142 | } 143 | 144 | function generateCompositeDescription( 145 | span: string, 146 | labels: Label[], 147 | descriptor: string, 148 | colormap: Filtermap 149 | ): JSX.Element { 150 | return ( 151 |
152 | Annotation on span 153 | {span} 154 | {labels.length > 0 && 155 | with label{labels.length > 1 ? 's' : ''} 156 | {labels.map(l => )} 157 | 158 | } 159 | is {descriptor}. 160 |
161 | ) 162 | } 163 | 164 | function generateDescription( 165 | gold: Annotation, 166 | user: Annotation, 167 | colormap: Filtermap 168 | ): JSX.Element { 169 | const labelAnalysis = generateLabelAnalysis(gold, user) 170 | const { correctLabels, extraLabels, missingLabels } = labelAnalysis 171 | 172 | const isCorrect = isAnnotationCorrect(gold, user) 173 | 174 | const description: JSX.Element = isCorrect 175 | ? generateCompositeDescription(user.text, correctLabels, 'correct', colormap) 176 | : !user 177 | ? generateCompositeDescription(gold.text, missingLabels, 'missing', colormap) 178 | : !gold 179 | ? generateCompositeDescription(user.text, extraLabels, 'unnecessary', colormap) 180 | :
181 | {generateSpanDescription(gold, user)} 182 | {generateLabelDescription(labelAnalysis, colormap)} 183 |
184 | 185 | return
186 | {description} 187 | {isCorrect && missingLabels.length > 0 && 188 | labelsToElement(missingLabels, 'also correct (select all labels that match)', colormap) 189 | } 190 | {isCorrect && extraLabels.length > 0 && 191 | labelsToElement(extraLabels, 'not necessary', colormap) 192 | } 193 |
194 | } 195 | 196 | function generateDescriptions( 197 | evaluationItem: TutorialEvaluationItem, 198 | colormap: Filtermap 199 | ): JSX.Element { 200 | const { userMatches, gold } = evaluationItem 201 | 202 | const descriptions = userMatches ? userMatches.map(u => { 203 | return generateDescription(gold, u, colormap) 204 | }) : [generateDescription(gold, null, colormap)] 205 | 206 | const isCodeless = gold && isAnnotationCUILess(gold) 207 | const isAmbiguous = gold && isAnnotationAmbiguous(gold) 208 | 209 | return
210 | {descriptions} 211 | {isCodeless && 212 |
213 | * Label CUI-less indicates that this span doesn't have an exact CUI match. 214 |
215 | } 216 | {isAmbiguous && 217 |
218 | * This match is marked as ambiguous. 219 |
220 | } 221 |
222 | } 223 | 224 | const TutorialEvaluationItem: React.SFC = props => { 225 | const { evaluationItem, colormap } = props 226 | const { gold, userMatches } = evaluationItem 227 | const annotation = gold ? gold : userMatches[0] 228 | 229 | const isCorrect = isCorrectTutorialEvaluationItem(evaluationItem) 230 | 231 | return ( 232 |
233 |
234 | {generateItemIcon(isCorrect)} 235 |
236 |
237 | 252 |
253 |
254 | {generateDescriptions(evaluationItem, colormap)} 255 |
256 |
257 | ) 258 | } 259 | 260 | export default TutorialEvaluationItem 261 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/TutorialExplanation.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import Button from '@material-ui/core/Button'; 6 | import { NavigateNext } from '@material-ui/icons'; 7 | import * as actionCreators from '../../actions'; 8 | import TextController from '../Annotation/TextController'; 9 | import TutorialEvaluationItem from './TutorialEvaluationItem'; 10 | import { TUTORIAL_LENGTH } from '../../../constants'; 11 | 12 | 13 | function mapStateToProps(state) { 14 | return {}; 15 | } 16 | 17 | function mapDispatchToProps(dispatch) { 18 | return bindActionCreators(actionCreators.default, dispatch); 19 | } 20 | 21 | @connect(mapStateToProps, mapDispatchToProps) 22 | class TutorialExplanation extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.state = { 27 | text: "", 28 | annotations: [], 29 | goldAnnotations: [], 30 | evaluation: [], 31 | colormap: {} 32 | }; 33 | } 34 | 35 | componentDidMount() { 36 | this.fetchData(); 37 | this.fetchColormap(); 38 | } 39 | 40 | async fetchData() { 41 | const { params } = this.props; 42 | const { fileId, userId } = params; 43 | 44 | const file = this.props.getFile(fileId, './tutorial', './tutorial/users/'+userId); 45 | const goldFile = this.props.getFile(fileId + '-gold', './tutorial', './tutorial'); 46 | const evaluationFunction = this.props.getTutorialEvaluation(fileId, userId); 47 | 48 | const data = await file.then((response) => response.data); 49 | const goldData = await goldFile.then((response) => response.data); 50 | const evaluationData = await evaluationFunction.then((response) => response.data); 51 | 52 | this.setState({ 53 | text: data.file.text, 54 | annotations: data.file.annotations, 55 | goldAnnotations: goldData.file.annotations, 56 | evaluation: evaluationData.evaluation 57 | }) 58 | } 59 | 60 | async fetchColormap() { 61 | const colormapPromise = this.props.getColormap(); 62 | const data = await colormapPromise.then((response) => response.data); 63 | 64 | this.setState({ 65 | colormap: data.colormap, 66 | }); 67 | } 68 | 69 | onNextClick = () => { 70 | const { params: { fileId, userId }, restartTutorial } = this.props; 71 | 72 | if (Number(fileId) < TUTORIAL_LENGTH) { 73 | browserHistory.push(`/tutorial/${userId}/${Number(fileId) + 1}`); 74 | } else { 75 | restartTutorial(userId); 76 | browserHistory.push(`/tutorial/done`); 77 | } 78 | } 79 | 80 | calculateSpanScore = () => { 81 | const { evaluation } = this.state; 82 | return evaluation.reduce((score, item) => score + item.spanScore, 0) 83 | } 84 | 85 | calculateLabelScore = () => { 86 | const { evaluation } = this.state; 87 | return evaluation.reduce((score, item) => score + item.labelScore, 0) 88 | } 89 | 90 | render() { 91 | const { params } = this.props; 92 | const { evaluation, text, colormap, annotations, goldAnnotations } = this.state; 93 | 94 | const fileId = params.fileId; 95 | 96 | const totalCount = goldAnnotations.length; 97 | const spanScore = this.calculateSpanScore(); 98 | const labelScore = this.calculateLabelScore(); 99 | 100 | return ( 101 |
102 |

Tutorial {fileId} Explanation

103 | 104 |
108 |
112 |
113 |

Standard Annotations

114 | 123 |
124 |
125 |
126 |

Your Annotations

127 | 136 |
137 |
138 | 139 |
142 |
143 | 144 |

145 |
146 | Span Score: {spanScore} / {totalCount} { 147 | spanScore == totalCount && 148 | } 149 |
150 |
151 | Label Score: {labelScore} / {spanScore} { 152 | labelScore == spanScore && 153 | } 154 |
155 |

156 | 157 |
158 |
159 | Status 160 |
161 |
162 | Correct Annotation 163 |
164 |
165 |
Description
166 |
167 |
168 | 169 | {evaluation && evaluation.map(item => ( 170 | 174 | ))} 175 | 176 |
* Hover over underlined labels in the descriptions to see further detail.
177 |
178 |
179 |
180 | 181 |
182 | 190 |
191 |
192 | ); 193 | } 194 | } 195 | 196 | export default TutorialExplanation; 197 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/TutorialView.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { browserHistory } from 'react-router'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import Button from '@material-ui/core/Button'; 6 | import TextField from '@material-ui/core/TextField'; 7 | import { NavigateNext } from '@material-ui/icons'; 8 | import * as actionCreators from '../../actions'; 9 | import { TUTORIAL_SLIDES_LINK } from '../../../constants'; 10 | 11 | function mapStateToProps(state) { 12 | return {}; 13 | } 14 | 15 | function mapDispatchToProps(dispatch) { 16 | return bindActionCreators(actionCreators.default, dispatch); 17 | } 18 | 19 | @connect(mapStateToProps, mapDispatchToProps) 20 | class TutorialView extends React.Component { 21 | constructor(props) { 22 | super(props); 23 | 24 | this.state = { 25 | errorText: "", 26 | userId: "" 27 | }; 28 | } 29 | 30 | handleChange = (event) => { 31 | const newValue = event.target.value; 32 | 33 | if (this.validateUserId(newValue)) 34 | this.setState({ errorText: '', userId: newValue }); 35 | else 36 | this.setState({ errorText: "Only alphanumeric characters are valid." }) 37 | } 38 | 39 | validateUserId = (str) => { 40 | const regex = RegExp(/^[a-z0-9]+$/i); 41 | return regex.test(str); 42 | } 43 | 44 | handleSubmit = () => { 45 | const { userId } = this.state; 46 | 47 | if (this.validateUserId(userId)) { 48 | this.props.startTutorial(userId) 49 | browserHistory.push(`/tutorial/${userId}/1`) 50 | } 51 | } 52 | 53 | render() { 54 | return ( 55 |
56 |
57 |

Tutorial

58 | 59 |

60 | Welcome to the tutorial for the clinical annotation platform! Before you 61 | begin annotating notes, we recommend reading through 62 | these quick slides introducing 63 | the problem we're tackling and how the platform works. 64 |

65 | 66 |

67 | Next, we will walk through a few 68 | example sentences and how they should be annotated. A sentence 69 | will appear in the annotation text box and you will be able to try 70 | annotating it as if it is a real sample. When you are done, you can 71 | hit the 'Next' button and see a report of how your annotations compare 72 | to the gold standard. When you are ready, click below to begin the first 73 | example! 74 |

75 | 76 |
77 | 86 |
87 | 88 |
89 | 97 |
98 | 99 |
100 |
101 | ); 102 | } 103 | } 104 | 105 | export default TutorialView; 106 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/types.ts: -------------------------------------------------------------------------------- 1 | import { Annotation, Label } from '../Annotation/types' 2 | 3 | export const EQUAL = 'equal' 4 | export const NONE = 'none' 5 | export const OVERLAP = 'overlap' 6 | export type SPANS_TYPE = typeof EQUAL | typeof NONE | typeof OVERLAP; 7 | 8 | export const CORRECT = 'correct' 9 | export const INCORRECT = 'incorrect' 10 | export const PARTIAL = 'partial' 11 | export type LABELS_TYPE = typeof CORRECT | typeof INCORRECT | typeof PARTIAL; 12 | 13 | export type TutorialEvaluationItem = { 14 | gold: Annotation, 15 | userMatches: Annotation[], 16 | spanScore: Number, 17 | labelScore: Number 18 | } 19 | 20 | export type EvaluationLabelAnalysis = { 21 | correctLabels: Label[], 22 | extraLabels: Label[], 23 | missingLabels: Label[] 24 | } 25 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/utils/evaluationUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TutorialEvaluationItem, 3 | EvaluationLabelAnalysis 4 | } from '../types' 5 | 6 | import { Annotation, CUI_CODELESS, CUI_AMBIGUOUS } from '../../Annotation/types' 7 | 8 | export const isSpanCorrect = (gold: Annotation, user: Annotation): boolean => { 9 | return gold && user 10 | && gold.spans[0].start === user.spans[0].start 11 | && gold.spans[0].end === user.spans[0].end 12 | } 13 | 14 | export const isAnnotationAmbiguous = (annotation: Annotation): boolean => { 15 | const { CUIMode, labels } = annotation 16 | 17 | return CUIMode === CUI_AMBIGUOUS || ( 18 | labels.length > 1 && labels.map(l => l.title).includes('CUI-less') 19 | ) 20 | } 21 | 22 | export const isAnnotationCUILess = (annotation: Annotation): boolean => { 23 | const { CUIMode, labels } = annotation 24 | 25 | return CUIMode === CUI_CODELESS || ( 26 | labels.length == 1 && labels[0].title === 'CUI-less' 27 | ) 28 | } 29 | 30 | export const generateLabelAnalysis = ( 31 | gold: Annotation, 32 | user: Annotation 33 | ): EvaluationLabelAnalysis => { 34 | const userLabels = user ? user.labels : [] 35 | const goldLabels = gold ? gold.labels : [] 36 | 37 | const userLabelIds = new Set(userLabels.map(l => l.labelId)) 38 | const goldLabelIds = new Set(goldLabels.map(l => l.labelId)) 39 | 40 | const correctLabels = [...userLabels].filter(l => goldLabelIds.has(l.labelId)) 41 | const extraLabels = [...userLabels].filter(l => !goldLabelIds.has(l.labelId)) 42 | const missingLabels = [...goldLabels].filter(l => goldLabels.length > 1 43 | ? !userLabelIds.has(l.labelId) && l.title !== 'CUI-less' 44 | : !userLabelIds.has(l.labelId) 45 | ) 46 | 47 | return { correctLabels, extraLabels, missingLabels } 48 | } 49 | 50 | export const isAnnotationCorrect = ( 51 | gold: Annotation, 52 | user: Annotation 53 | ): boolean => { 54 | const { correctLabels } = generateLabelAnalysis(gold, user) 55 | 56 | return isSpanCorrect(gold, user) && correctLabels.length >= 1 57 | } 58 | 59 | export const isCorrectTutorialEvaluationItem = ( 60 | evaluationItem: TutorialEvaluationItem 61 | ): boolean => { 62 | const { gold, userMatches } = evaluationItem 63 | 64 | return gold && userMatches && userMatches.some(u => isAnnotationCorrect(gold, u)) 65 | } 66 | -------------------------------------------------------------------------------- /static/src/components/Tutorial/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './evaluationUtils' 2 | -------------------------------------------------------------------------------- /static/src/constants/index.js: -------------------------------------------------------------------------------- 1 | export const GET_FILE_SUCCESS = 'GET_FILE_SUCCESS'; 2 | export const GET_FILE_FAILURE = 'GET_FILE_FAILURE'; 3 | export const GET_FILE_REQUEST = 'GET_FILE_REQUEST'; 4 | export const GET_FILE = 'GET_FILE'; 5 | 6 | export const GET_FILENAMES_SUCCESS = 'GET_FILENAMES_SUCCESS'; 7 | export const GET_FILENAMES_FAILURE = 'GET_FILENAMES_FAILURE'; 8 | export const GET_FILENAMES_REQUEST = 'GET_FILENAMES_REQUEST'; 9 | export const GET_FILENAMES = 'GET_FILENAMES'; 10 | 11 | export const SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS'; 12 | export const SAVE_ANNOTATIONS_FAILURE = 'SAVE_ANNOTATIONS_FAILURE'; 13 | export const SAVE_ANNOTATIONS_REQUEST = 'SAVE_ANNOTATIONS_REQUEST'; 14 | export const SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS'; 15 | 16 | export const RECOMMEND_LABELS_SUCCESS = 'RECOMMEND_LABELS_SUCCESS'; 17 | export const RECOMMEND_LABELS_FAILURE = 'RECOMMEND_LABELS_FAILURE'; 18 | export const RECOMMEND_LABELS_REQUEST = 'RECOMMEND_LABELS_REQUEST'; 19 | export const RECOMMEND_LABELS = 'RECOMMEND_LABELS'; 20 | 21 | export const SEARCH_LABELS_SUCCESS = 'SEARCH_LABELS_SUCCESS'; 22 | export const SEARCH_LABELS_FAILURE = 'SEARCH_LABELS_FAILURE'; 23 | export const SEARCH_LABELS_REQUEST = 'SEARCH_LABELS_REQUEST'; 24 | export const SEARCH_LABELS = 'SEARCH_LABELS'; 25 | 26 | export const GET_COLOR_MAP = 'GET_COLOR_MAP'; 27 | export const GET_COLOR_MAP_FAILURE = 'GET_COLOR_MAP_FAILURE'; 28 | export const GET_COLOR_MAP_REQUEST = 'GET_COLOR_MAP_REQUEST'; 29 | export const GET_COLOR_MAP_SUCCESS = 'GET_COLOR_MAP_SUCCESS'; 30 | 31 | export const GET_UMLS_INFO = 'GET_UMLS_INFO'; 32 | export const GET_UMLS_INFO_FAILURE = 'GET_UMLS_INFO_FAILURE'; 33 | export const GET_UMLS_INFO_REQUEST = 'GET_UMLS_INFO_REQUEST'; 34 | export const GET_UMLS_INFO_SUCCESS = 'GET_UMLS_INFO_SUCCESS'; 35 | 36 | export const GET_TUTORIAL_EVALUATION = 'GET_TUTORIAL_EVALUATION'; 37 | export const GET_TUTORIAL_EVALUATION_FAILURE = 'GET_TUTORIAL_EVALUATION_FAILURE'; 38 | export const GET_TUTORIAL_EVALUATION_REQUEST = 'GET_TUTORIAL_EVALUATION_REQUEST'; 39 | export const GET_TUTORIAL_EVALUATION_SUCCESS = 'GET_TUTORIAL_EVALUATION_SUCCESS'; 40 | 41 | export const RESTART_TUTORIAL_SUCCESS = 'RESTART_TUTORIAL_SUCCESS'; 42 | export const RESTART_TUTORIAL_FAILURE = 'RESTART_TUTORIAL_FAILURE'; 43 | export const RESTART_TUTORIAL_REQUEST = 'RESTART_TUTORIAL_REQUEST'; 44 | export const RESTART_TUTORIAL = 'RESTART_TUTORIAL'; 45 | 46 | export const START_TUTORIAL_SUCCESS = 'START_TUTORIAL_SUCCESS'; 47 | export const START_TUTORIAL_FAILURE = 'START_TUTORIAL_FAILURE'; 48 | export const START_TUTORIAL_REQUEST = 'START_TUTORIAL_REQUEST'; 49 | export const START_TUTORIAL = 'START_TUTORIAL'; 50 | 51 | export const ADD_LOG_SUCCESS = 'ADD_LOG_SUCCESS'; 52 | export const ADD_LOG_FAILURE = 'ADD_LOG_FAILURE'; 53 | export const ADD_LOG_REQUEST = 'ADD_LOG_REQUEST'; 54 | export const ADD_LOG = 'ADD_LOG'; 55 | -------------------------------------------------------------------------------- /static/src/containers/App/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import createMuiTheme from '@material-ui/core/styles/createMuiTheme'; 4 | import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider'; 5 | import indigo from '@material-ui/core/colors/indigo'; 6 | import teal from '@material-ui/core/colors/teal'; 7 | 8 | 9 | /* application components */ 10 | import { Header } from '../../components/Header'; 11 | import { Footer } from '../../components/Footer'; 12 | 13 | /* global styles for app */ 14 | import './styles/app.scss'; 15 | 16 | class App extends React.Component { // eslint-disable-line react/prefer-stateless-function 17 | render() { 18 | const theme = { 19 | palette: { 20 | primary: { 21 | main: '#607D8B' 22 | }, 23 | secondary: { 24 | main: '#E0F2F1' 25 | } 26 | }, 27 | typography: { 28 | fontSize: 24, 29 | } 30 | }; 31 | 32 | return ( 33 | 34 |
35 |
36 |
40 | {this.props.children} 41 |
42 |
43 |
44 |
45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | export { App }; 52 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/app.scss: -------------------------------------------------------------------------------- 1 | /* global styles */ 2 | @import 'fonts/roboto'; 3 | @import 'typography'; 4 | @import 'links'; 5 | 6 | :global(body) { 7 | position: relative; 8 | font-family: 'Roboto', sans-serif !important; 9 | h1, h2, h3, h4 { 10 | font-weight: 300; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/fonts/roboto.scss: -------------------------------------------------------------------------------- 1 | /* 2 | Fonts 3 | */ 4 | 5 | @import url(//fonts.googleapis.com/css?family=Roboto:400,100,300,500,700,900&subset=latin,cyrillic-ext); 6 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/index.js: -------------------------------------------------------------------------------- 1 | import 'style!./styles.scss'; 2 | export default require('./styles.scss').locals.styles; 3 | -------------------------------------------------------------------------------- /static/src/containers/App/styles/links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | text-decoration: none; 3 | 4 | &:hover { 5 | text-decoration: none; 6 | } 7 | } -------------------------------------------------------------------------------- /static/src/containers/App/styles/screens.scss: -------------------------------------------------------------------------------- 1 | // Phone 2 | // 3 | // @media (#{$phone}) { 4 | // code-here 5 | // } 6 | // 7 | $phone: "max-width: 768px"; 8 | 9 | 10 | // Tablet 11 | // 12 | // @media (#{$tablet}) { 13 | // code-here 14 | // } 15 | // 16 | $tablet: "min-width: 768px"; 17 | 18 | 19 | // Desktop 20 | // 21 | // @media (#{$desktop}) { 22 | // code-here 23 | // } 24 | $desktop: "min-width: 992px"; 25 | 26 | 27 | // Phone 28 | // 29 | // @media (#{$large-desktop}) { 30 | // code-here 31 | // } 32 | $large-desktop: "min-width: 1200px"; -------------------------------------------------------------------------------- /static/src/containers/App/styles/typography.scss: -------------------------------------------------------------------------------- 1 | /* Typography */ 2 | 3 | body { 4 | font-family: 'Roboto', sans-serif; 5 | font-weight: 300; 6 | } 7 | 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6 { 14 | font-weight: 300; 15 | } 16 | 17 | p { 18 | font-size: 16px; 19 | } -------------------------------------------------------------------------------- /static/src/containers/HomeContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* components */ 4 | import { Home } from '../../components/Home'; 5 | 6 | export const HomeContainer = () => 7 |
8 | 9 |
; 10 | -------------------------------------------------------------------------------- /static/src/containers/TutorialDoneContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | /* components */ 4 | import { TutorialDone } from '../../components/Tutorial/TutorialDone'; 5 | 6 | export const TutorialDoneContainer = () => 7 |
8 | 9 |
; 10 | -------------------------------------------------------------------------------- /static/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, Redirect, browserHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | 7 | import configureStore from './store/configureStore'; 8 | import routes from './routes'; 9 | import './style.scss'; 10 | 11 | require('expose?$!expose?jQuery!jquery'); 12 | require('bootstrap-webpack'); 13 | 14 | const store = configureStore(); 15 | const history = syncHistoryWithStore(browserHistory, store); 16 | 17 | ReactDOM.render( 18 | 19 | 20 | 21 | {routes} 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | -------------------------------------------------------------------------------- /static/src/reducers/data.js: -------------------------------------------------------------------------------- 1 | import { RECEIVE_PROTECTED_DATA, FETCH_PROTECTED_DATA_REQUEST } from '../constants'; 2 | import { createReducer } from '../utils/misc'; 3 | 4 | const initialState = { 5 | data: null, 6 | isFetching: false, 7 | loaded: false, 8 | }; 9 | 10 | export default createReducer(initialState, { 11 | [RECEIVE_PROTECTED_DATA]: (state, payload) => 12 | Object.assign({}, state, { 13 | data: payload.data, 14 | isFetching: false, 15 | loaded: true, 16 | }), 17 | [FETCH_PROTECTED_DATA_REQUEST]: (state) => 18 | Object.assign({}, state, { 19 | isFetching: true, 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /static/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import data from './data'; 4 | 5 | const rootReducer = combineReducers({ 6 | routing: routerReducer, 7 | /* your reducers */ 8 | data, 9 | }); 10 | 11 | export default rootReducer; 12 | -------------------------------------------------------------------------------- /static/src/routes.js: -------------------------------------------------------------------------------- 1 | /* eslint new-cap: 0 */ 2 | 3 | import React from 'react'; 4 | import { Route } from 'react-router'; 5 | 6 | /* containers */ 7 | import { App } from './containers/App'; 8 | import { HomeContainer } from './containers/HomeContainer'; 9 | import { TutorialDoneContainer } from './containers/TutorialDoneContainer'; 10 | 11 | /* components */ 12 | import AnnotationView from './components/Annotation/AnnotationView'; 13 | import FilesViewer from './components/Files/FilesViewer'; 14 | import NotFound from './components/NotFound'; 15 | import TutorialView from './components/Tutorial/TutorialView'; 16 | import TutorialAnnotation from './components/Tutorial/TutorialAnnotation'; 17 | import TutorialExplanation from './components/Tutorial/TutorialExplanation'; 18 | 19 | export default ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | -------------------------------------------------------------------------------- /static/src/store/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunkMiddleware from 'redux-thunk'; 3 | import rootReducer from '../reducers'; 4 | 5 | const debugware = []; 6 | if (process.env.NODE_ENV !== 'production') { 7 | const createLogger = require('redux-logger'); 8 | 9 | debugware.push(createLogger({ 10 | collapsed: true, 11 | })); 12 | } 13 | 14 | export default function configureStore(initialState) { 15 | const store = createStore( 16 | rootReducer, 17 | initialState, 18 | applyMiddleware(thunkMiddleware, ...debugware) 19 | ); 20 | 21 | if (module.hot) { 22 | // Enable Webpack hot module replacement for reducers 23 | module.hot.accept('../reducers', () => { 24 | const nextRootReducer = require('../reducers/index').default; 25 | 26 | store.replaceReducer(nextRootReducer); 27 | }); 28 | } 29 | 30 | return store; 31 | } 32 | -------------------------------------------------------------------------------- /static/src/style.scss: -------------------------------------------------------------------------------- 1 | :global(body) { 2 | position: relative; 3 | } 4 | 5 | .flex { 6 | flex-grow: 1; 7 | } 8 | 9 | .text-body { 10 | line-height: 1.8; 11 | font-size: 20px; 12 | } 13 | 14 | .text-body h1 { 15 | text-align: center; 16 | } 17 | 18 | .text-body h3 { 19 | width: 700px; 20 | margin: 25px auto 0; 21 | } 22 | 23 | .text-body p { 24 | width: 700px; 25 | margin: 20px auto; 26 | text-align: justify; 27 | } 28 | 29 | .home .code { 30 | background-color: #ddd; 31 | color: #333; 32 | border: 1px solid #ccc; 33 | border-radius: 4px; 34 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 35 | padding: 0 5px; 36 | margin: 0 5px; 37 | font-size: 14px; 38 | display: inline-block; 39 | } 40 | 41 | .home img { 42 | display: block; 43 | text-align: center; 44 | margin: 10px auto; 45 | height: 150px; 46 | } 47 | 48 | .home img.large { 49 | height: 350px; 50 | } 51 | 52 | .menu-button { 53 | margin-left: -12; 54 | margin-right: 20; 55 | } 56 | 57 | .file-text { 58 | margin: 100px 0 0; 59 | padding: 50px; 60 | background-color: #ddd; 61 | } 62 | 63 | .annotation-view { 64 | height: 100%; 65 | } 66 | 67 | .experiment-modal { 68 | position: absolute; 69 | margin-top: 20px; 70 | right: 20px; 71 | bottom: 170px; 72 | } 73 | 74 | .pause-modal { 75 | position: absolute; 76 | margin-top: 20px; 77 | right: 20px; 78 | bottom: 120px; 79 | } 80 | 81 | .text-controller { 82 | position: absolute; 83 | margin: 20px 0 0; 84 | padding: 50px 25px 25px; 85 | background-color: #f9f9f9; 86 | border: 1px solid #dddddd; 87 | height: 100%; 88 | width: 100%; 89 | min-height: 200px; 90 | overflow-y: scroll; 91 | } 92 | 93 | .label-controller { 94 | margin: 20px 0 0; 95 | height: 100%; 96 | } 97 | 98 | .bordered-section { 99 | border: 1px solid #dddddd; 100 | padding: 25px; 101 | } 102 | 103 | .divided-section { 104 | // border-bottom: 1px solid black; 105 | overflow-y: scroll; 106 | } 107 | 108 | .token { 109 | margin-right: 4px; 110 | } 111 | 112 | .search-bar { 113 | display: flex; 114 | flex-direction: row; 115 | } 116 | 117 | .search-icon { 118 | // width: 100px; 119 | min-width: 70px; 120 | height: 100%; 121 | // position: absolute; 122 | pointer-events: none; 123 | // display: flex; 124 | align-items: center; 125 | align-content: center; 126 | } 127 | 128 | .search-bar input { 129 | // display: flex; 130 | width: 100%; 131 | } 132 | 133 | .label-filter { 134 | width: 100%; 135 | float: left; 136 | } 137 | 138 | .label-filter-item { 139 | float: left; 140 | width: 10%; 141 | padding: 5px 0; 142 | color: white; 143 | text-align: center; 144 | border-radius: 5px; 145 | } 146 | 147 | .label-item { 148 | margin: 10px 0 0; 149 | float: left; 150 | width: 100%; 151 | } 152 | 153 | .label-item { 154 | margin-top: 25px; 155 | } 156 | 157 | .label-item:last-child { 158 | margin-bottom: 25px; 159 | } 160 | 161 | .selection-section { 162 | width: 100%; 163 | margin-top: 20px; 164 | padding: 10px 25px; 165 | border: 1px solid #dddddd; 166 | height: 100px; 167 | } 168 | 169 | .selection-clear { 170 | margin-top: calc(40px - 12px); 171 | float: left; 172 | } 173 | 174 | .selection-delete { 175 | margin-top: calc(40px - 12px); 176 | float: right; 177 | } 178 | 179 | .selection-main { 180 | width: calc(100% - 48px); 181 | padding: 0 20px; 182 | display: flex; 183 | float: left; 184 | } 185 | 186 | .selection-text { 187 | text-align: center; 188 | max-width: 500px; 189 | text-overflow: 'ellipsis'; 190 | overflow: 'hidden'; 191 | white-space: 'nowrap'; 192 | } 193 | 194 | .selection-arrow { 195 | margin-top: calc(40px - 12px); 196 | width: calc(24px + 20px); 197 | padding: 0 10px; 198 | } 199 | 200 | .selection-labels { 201 | overflow-y: hidden; 202 | white-space: nowrap; 203 | margin: 0; 204 | width: 100%; 205 | height: 80px; 206 | flex-grow: 1; 207 | } 208 | 209 | .selection-section .label-item { 210 | float: none; 211 | display: inline-block; 212 | width: 330px; 213 | margin: calc(40px - 22px) 10px; 214 | } 215 | 216 | .label-item-suggestion .label-title { 217 | width: calc(100% - 30px); 218 | } 219 | 220 | .cui-menu { 221 | display: flex; 222 | justify-content: center; 223 | margin-top: 10px; 224 | } 225 | 226 | .cui-menu-option { 227 | width: 200px; 228 | padding: 5px; 229 | margin-left: 15px; 230 | text-align: center; 231 | vertical-align: middle; 232 | font-size: 10px; 233 | border-radius: 10px; 234 | color: rgba(96, 125, 139, 1); 235 | } 236 | 237 | .cui-menu-option.selected { 238 | border: 1px solid; 239 | background-color: rgba(96, 125, 139, .1); 240 | } 241 | 242 | .label-title { 243 | padding: 10px; 244 | float: left; 245 | width: 100%; 246 | } 247 | 248 | .label-title-text { 249 | width: calc(100% - 45px); 250 | white-space: nowrap; 251 | overflow: hidden; 252 | text-overflow: ellipsis; 253 | float: left; 254 | } 255 | 256 | .label-link { 257 | float: right; 258 | } 259 | 260 | .label-delete-button { 261 | float: right; 262 | } 263 | 264 | .label-confidence { 265 | font-size: 20px; 266 | float: right; 267 | padding: 12px 5px; 268 | } 269 | 270 | input[type=submit] { 271 | text-align: center; 272 | margin: 10px 0 10px; 273 | } 274 | 275 | .file-item { 276 | border: 1px solid black; 277 | font-size: 15px; 278 | padding: 10px; 279 | margin-top: 20px; 280 | } 281 | 282 | .label-display { 283 | margin: 20px 0 0; 284 | padding-top: 10px; 285 | height: 500px; 286 | overflow-y: scroll; 287 | } 288 | 289 | .annotation-item { 290 | border: 1px solid black; 291 | margin: 5px 0 0; 292 | } 293 | 294 | .annotation-title { 295 | padding: 5px; 296 | } 297 | 298 | .annotation-selected { 299 | background-color: lemonchiffon; 300 | } 301 | 302 | .annotation-subtitle { 303 | padding: 5px; 304 | color: dimgray; 305 | } 306 | 307 | .annotation-text { 308 | max-width: 100%; 309 | text-overflow: ellipsis; 310 | overflow: hidden; 311 | } 312 | 313 | .annotation-num { 314 | float: left; 315 | } 316 | 317 | .annotation-menu { 318 | float: right; 319 | } 320 | 321 | .selection { 322 | border: 1px solid black 323 | } 324 | 325 | .hover-state:hover { 326 | filter: brightness(90%); 327 | cursor: pointer; 328 | } 329 | 330 | .switch-label { 331 | font-size: 1.2rem !important; 332 | } 333 | 334 | .switch-base { 335 | width: 30px !important; 336 | height: 30px !important; 337 | } 338 | 339 | .switch-icon-checked { 340 | transform: translateX(32px) !important; 341 | } 342 | 343 | .modal-content { 344 | position: absolute; 345 | width: 600px; 346 | height: 90vh; 347 | left: 0; 348 | right: 0; 349 | margin-top: 5vh; 350 | margin-left: auto; 351 | margin-right: auto; 352 | background-color: white; 353 | padding: 50px; 354 | text-align: center; 355 | } 356 | 357 | .pause-modal-content { 358 | width: 90vw; 359 | padding-top: 300px; 360 | } 361 | 362 | .info-modal-content { 363 | text-align: left; 364 | font-size: 1.5rem; 365 | overflow-y: scroll; 366 | height: calc(90vh - 100px); 367 | } 368 | 369 | .info-modal-content p { 370 | font-size: 1.5rem; 371 | } 372 | 373 | .info-modal-content .defn { 374 | padding: 10px 0; 375 | } 376 | 377 | .suggestion-menu-item { 378 | color: white; 379 | background-color: #607d8b; 380 | opacity: 0.9; 381 | padding: 5px; 382 | float: left; 383 | } 384 | 385 | .suggestion-menu-icon { 386 | font-size: 14px; 387 | float: left; 388 | } 389 | 390 | .suggestion-menu-text { 391 | float: left; 392 | } 393 | 394 | .suggestion-menu-selected { 395 | background-color: #37474f !important; 396 | } 397 | 398 | .next-button { 399 | text-align: center; 400 | } 401 | 402 | .tutorial-evaluation-item { 403 | padding: 10px 0; 404 | width: 100%; 405 | float: left; 406 | border-bottom: 1px solid #9e9e9e; 407 | } 408 | 409 | .tutorial-evaluation-icon { 410 | width: 80px; 411 | float: left; 412 | text-align: left; 413 | } 414 | 415 | .tutorial-evaluation-token { 416 | width: calc(30% - 40px); 417 | float: left; 418 | } 419 | 420 | .tutorial-evaluation-description { 421 | width: calc(70% - 40px); 422 | float: left; 423 | } 424 | 425 | .tutorial-evaluation-description div { 426 | padding: 0 5px; 427 | } 428 | 429 | .tutorial-evaluation-description-singular:not(:first-child) { 430 | padding-top: 5px; 431 | border-top: 1px solid #aeaeae; 432 | } 433 | 434 | .tutorial-evaluation-description-singular:not(:last-child) { 435 | padding-bottom: 5px; 436 | } 437 | 438 | .tutorial-label-item { 439 | display: inline-block; 440 | padding: 0 5px; 441 | } 442 | 443 | .tutorial-score { 444 | text-align: center; 445 | font-weight: bolder; 446 | padding: 5px; 447 | background-color: rgba(96, 125, 139, 0.25); 448 | border: 5px solid rgba(96, 125, 139, 1); 449 | color: rgba(96, 125, 139, 1); 450 | } 451 | 452 | .tutorial-score span:not(:last-child) { 453 | margin-right: 50px; 454 | } 455 | 456 | .text-field { 457 | text-align: center; 458 | margin-bottom: 25px; 459 | } 460 | 461 | .cui-modal-button { 462 | width: 100%; 463 | clear: both; 464 | border: 1px solid; 465 | border-radius: 10px; 466 | padding: 5px; 467 | margin-top: 0px; 468 | margin-bottom: 20px; 469 | font-size: 10px; 470 | text-align: center; 471 | color: rgba(96, 125, 139, 1); 472 | background-color: rgba(96, 125, 139, .1); 473 | } 474 | -------------------------------------------------------------------------------- /static/src/utils/http_functions.js: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 3 | import axios from 'axios'; 4 | 5 | export function get_filenames() { 6 | return axios.get('/api/get_filenames', {}); 7 | } 8 | 9 | export function get_file(id, textDir, annDir) { 10 | return axios.post('/api/get_file', { id, textDir, annDir }); 11 | } 12 | 13 | export function save_annotations(id, annotations, dir) { 14 | return axios.post('/api/save_annotations', { id, annotations, dir }); 15 | } 16 | 17 | export function recommend_labels(searchTerm, isKeyword, mode) { 18 | return axios.post('/api/recommend_labels', { searchTerm, isKeyword, mode }); 19 | } 20 | 21 | export function search_labels(searchTerm) { 22 | return axios.post('/api/search_labels', { searchTerm }); 23 | } 24 | 25 | export function get_colormap() { 26 | return axios.post('/api/get_colormap', {}); 27 | } 28 | 29 | export function get_umls_info(cui) { 30 | return axios.post('/api/get_umls_info', { cui }); 31 | } 32 | 33 | export function start_tutorial(userId) { 34 | return axios.post('/api/start_tutorial', { userId }); 35 | } 36 | 37 | export function get_tutorial_evaluation(fileId, userId) { 38 | return axios.post('/api/get_tutorial_evaluation', { fileId, userId }); 39 | } 40 | 41 | export function restart_tutorial(userId) { 42 | return axios.post('/api/restart_tutorial', { userId }); 43 | } 44 | 45 | export function add_log_entry(id, action, annotation_id, metadata) { 46 | return axios.post('/api/add_log_entry', {id, action, annotation_id, metadata}); 47 | } 48 | -------------------------------------------------------------------------------- /static/src/utils/isMobileAndTablet.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Is mobile or tablet? 3 | * 4 | * @return {Boolean} 5 | */ 6 | export function isMobileAndTablet() { 7 | return window.innerWidth <= 800 && window.innerHeight <= 600; 8 | } 9 | -------------------------------------------------------------------------------- /static/src/utils/misc.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0, no-param-reassign: 0 */ 2 | 3 | export function createConstants(...constants) { 4 | return constants.reduce((acc, constant) => { 5 | acc[constant] = constant; 6 | return acc; 7 | }, {}); 8 | } 9 | 10 | export function createReducer(initialState, reducerMap) { 11 | return (state = initialState, action) => { 12 | const reducer = reducerMap[action.type]; 13 | 14 | 15 | return reducer 16 | ? reducer(state, action.payload) 17 | : state; 18 | }; 19 | } 20 | 21 | 22 | export function parseJSON(response) { 23 | return response.data; 24 | } 25 | 26 | export function validateEmail(email) { 27 | const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 28 | return re.test(email); 29 | } 30 | -------------------------------------------------------------------------------- /static/src/utils/parallax.js: -------------------------------------------------------------------------------- 1 | import { isMobileAndTablet } from './isMobileAndTablet'; 2 | 3 | /* 4 | * Add parallax effect to element 5 | * 6 | * @param {Object} DOM element 7 | * @param {Integer} Animation speed, default: 30 8 | */ 9 | export function setParallax(elem, speed = 30) { 10 | const top = (window.pageYOffset - elem.offsetTop) / speed; 11 | 12 | isMobileAndTablet 13 | ? elem.style.backgroundPosition = `0px ${top}px` // eslint-disable-line no-param-reassign 14 | : null; 15 | } 16 | -------------------------------------------------------------------------------- /static/src/utils/test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | fs.readdir('.', function(err, files) { 3 | console.log(files); 4 | }); 5 | -------------------------------------------------------------------------------- /static/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "noImplicitAny": true, 7 | "esModuleInterop": true, 8 | "target": "es6", 9 | "lib": [ 10 | "es2019", 11 | "dom" 12 | ], 13 | "jsx": "react", 14 | "allowSyntheticDefaultImports": true, 15 | "allowJs": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /static/webpack/common.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const autoprefixer = require('autoprefixer'); 3 | const postcssImport = require('postcss-import'); 4 | const merge = require('webpack-merge'); 5 | 6 | const development = require('./dev.config'); 7 | const production = require('./prod.config'); 8 | 9 | require('babel-polyfill').default; 10 | 11 | const TARGET = process.env.npm_lifecycle_event; 12 | 13 | const PATHS = { 14 | app: path.join(__dirname, '../src'), 15 | build: path.join(__dirname, '../dist'), 16 | }; 17 | 18 | process.env.BABEL_ENV = TARGET; 19 | 20 | const common = { 21 | entry: [ 22 | 'babel-polyfill', 23 | PATHS.app, 24 | ], 25 | 26 | output: { 27 | path: PATHS.build, 28 | filename: 'bundle.js', 29 | }, 30 | 31 | externals: { 32 | fs: require('fs'), 33 | }, 34 | 35 | resolve: { 36 | extensions: ['', '.jsx', '.js', '.ts', '.tsx', '.json', '.scss'], 37 | modulesDirectories: ['node_modules', PATHS.app], 38 | }, 39 | 40 | module: { 41 | loaders: [{ 42 | test: /bootstrap-sass\/assets\/javascripts\//, 43 | loader: 'imports?jQuery=jquery', 44 | }, { 45 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/, 46 | loader: 'url?limit=10000&mimetype=application/font-woff', 47 | }, { 48 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/, 49 | loader: 'url?limit=10000&mimetype=application/font-woff2', 50 | }, { 51 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, 52 | loader: 'url?limit=10000&mimetype=application/octet-stream', 53 | }, { 54 | test: /\.otf(\?v=\d+\.\d+\.\d+)?$/, 55 | loader: 'url?limit=10000&mimetype=application/font-otf', 56 | }, { 57 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, 58 | loader: 'file', 59 | }, { 60 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 61 | loader: 'url?limit=10000&mimetype=image/svg+xml', 62 | }, { 63 | test: /\.js$/, 64 | loaders: ['babel-loader'], 65 | exclude: /node_modules/, 66 | // include: __dirname, 67 | }, 68 | { 69 | test: /\.ts|\.tsx$/, 70 | loaders: ['babel-loader', 'ts-loader'], 71 | // include: __dirname, 72 | }, { 73 | test: /\.png$/, 74 | loader: 'file?name=[name].[ext]', 75 | }, { 76 | test: /\.jpg$/, 77 | loader: 'file?name=[name].[ext]', 78 | }], 79 | }, 80 | 81 | postcss: (webpack) => ( 82 | [ 83 | autoprefixer({ 84 | browsers: ['last 2 versions'], 85 | }), 86 | postcssImport({ 87 | addDependencyTo: webpack, 88 | }), 89 | ] 90 | ), 91 | }; 92 | 93 | if (TARGET === 'start' || !TARGET) { 94 | module.exports = merge(development, common); 95 | } 96 | 97 | if (TARGET === 'build' || !TARGET) { 98 | module.exports = merge(production, common); 99 | } 100 | -------------------------------------------------------------------------------- /static/webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'bootstrap-loader', 8 | 'webpack-hot-middleware/client', 9 | './src/index', 10 | ], 11 | 12 | output: { 13 | publicPath: '/dist/', 14 | }, 15 | 16 | module: { 17 | loaders: [{ 18 | test: /\.scss$/, 19 | loader: 'style!css?localIdentName=[path][name]--[local]!postcss-loader!sass', 20 | }], 21 | }, 22 | 23 | plugins: [ 24 | new webpack.DefinePlugin({ 25 | 'process.env': { 26 | NODE_ENV: '"development"', 27 | }, 28 | __DEVELOPMENT__: true, 29 | }), 30 | new ExtractTextPlugin('bundle.css'), 31 | new webpack.optimize.OccurenceOrderPlugin(), 32 | new webpack.HotModuleReplacementPlugin(), 33 | new webpack.NoErrorsPlugin(), 34 | new webpack.ProvidePlugin({ 35 | jQuery: 'jquery', 36 | }), 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /static/webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | 7 | entry: ['bootstrap-loader/extractStyles'], 8 | 9 | output: { 10 | publicPath: 'dist/', 11 | }, 12 | 13 | module: { 14 | loaders: [{ 15 | test: /\.scss$/, 16 | loader: 'style!css!postcss-loader!sass', 17 | }], 18 | }, 19 | 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': { 23 | NODE_ENV: '"production"', 24 | }, 25 | __DEVELOPMENT__: false, 26 | }), 27 | new ExtractTextPlugin('bundle.css'), 28 | new webpack.optimize.DedupePlugin(), 29 | new webpack.optimize.OccurenceOrderPlugin(), 30 | new webpack.optimize.UglifyJsPlugin({ 31 | compress: { 32 | warnings: false, 33 | }, 34 | }), 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from basedir import basedir 3 | import os 4 | import shutil 5 | import sys 6 | 7 | 8 | def main(): 9 | argv = [] 10 | 11 | argv.extend(sys.argv[1:]) 12 | 13 | pytest.main(argv) 14 | 15 | try: 16 | os.remove(os.path.join(basedir, '.coverage')) 17 | 18 | except OSError: 19 | pass 20 | 21 | try: 22 | shutil.rmtree(os.path.join(basedir, '.cache')) 23 | 24 | except OSError: 25 | pass 26 | 27 | try: 28 | shutil.rmtree(os.path.join(basedir, 'tests/.cache')) 29 | except OSError: 30 | pass 31 | 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /testing_config.py: -------------------------------------------------------------------------------- 1 | from flask_testing import TestCase 2 | from application.app import app 3 | import os 4 | from setup import basedir 5 | import json 6 | 7 | 8 | class BaseTestConfig(TestCase): 9 | 10 | def create_app(self): 11 | app.config.from_object('config.TestingConfig') 12 | return app 13 | 14 | def setUp(self): 15 | self.app = self.create_app().test_client() 16 | 17 | def tearDown(self): 18 | return 19 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from testing_config import BaseTestConfig 2 | import json 3 | 4 | 5 | class TestAPI(BaseTestConfig): 6 | def test_get_spa_from_index(self): 7 | result = self.app.get("/") 8 | self.assertIn('', result.data.decode("utf-8")) 9 | -------------------------------------------------------------------------------- /tutorial/attempts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/tutorial/attempts/.gitkeep -------------------------------------------------------------------------------- /tutorial/users/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clinicalml/prancer/dfeca10e9bc5aa8de938d718bfffefe623048d55/tutorial/users/.gitkeep -------------------------------------------------------------------------------- /umls_sources.csv: -------------------------------------------------------------------------------- 1 | AIR,AOD,ALT,ATC,AOT,BI,NCI_BioC,NCI_BRIDG,NCI_BRIDG_3_0_3,NCI_CRCH,NCI_CTEP-SDC,NCI_CDISC-GLOSS,NCI_CDISC,CDT,HCDT,NCI_CBDD,CCC,CCS,CCS_10,CCSR_10,RAM,CCPSS,NCI_CPTAC,NCI_CTRP,NCI_CTCAE_3,NCI_CTCAE,NCI_CTCAE_5,JABL,CHV,NCI_CareLex,COSTAR,CST,CPT,HCPT,CSP,DSM-5,UWDA,NCI_DICOM,DDB,DRUGBANK,DXP,NCI_EDQM-HC,MTHSPL,NCI_FDA,NDDF,FMA,GO,NCI_GENC,NCI_GAIA,MCM,GS,HCPCS,HL7V2.5,HL7V3.0,HGNC,HPO,ICD10PCS,ICD10AE,ICD10AM,ICD10AMAE,MTHICD9,ICPC2P,ICPC2ICD10ENG,MTHICPC2EAE,MTHICPC2ICD10AE,ICNP,ICD10,ICD9CM,ICD10CM,ICF,ICF-CY,ICPC,ICPC2EENG,NCI_ICH,NCI_INC,NCI_JAX,NCI_KEGG,LCH,LCH_NW,LNC,LNC_SPECIAL_USE,MVX,MEDCIN,MDR,CPM,MED-RT,MEDLINEPLUS,MSH,MTHCMSFRF,MTH,MMX,MTHMST,MMSL,NANDA-I,NCI_PID,VANDF,NUCCPT,NCBI,NCI_DTP,NCI_NCI-GLOSS,NCI_DCP,NCI_NCI-HL7,NCI_NCI-HGNC,NCI,NCI_NCPDP,NEU,NCI_NICHD,NIC,NOC,OMS,OMIM,PCDS,PNDS,PPAC,PDQ,NCI_PI-RADS,PSY,QMR,RCD,RCDAE,RCDSA,RCDSY,NCI_RENI,RXNORM,SNM,SNOMEDCT_US,SNOMEDCT_VET,SNMI,SOP,SRC,SPN,NCI_CDC,ULT,UMD,NCI_UCUM,USP,USPMG,CVX,WHO,NCI_ZFin 2 | --------------------------------------------------------------------------------