├── .gitignore ├── LICENSE.txt ├── README.md ├── package-lock.json ├── server ├── api │ ├── idea2.py │ ├── project.py │ ├── server.py │ ├── swagger.yml │ └── templates │ │ └── home.html ├── datasets │ ├── README.md │ ├── bc5cdr │ │ └── .gitkeep │ ├── bc5cdr_example │ │ ├── processed.bert │ │ ├── processed.csv │ │ ├── processed.elmo │ │ ├── processed.nlp │ │ └── processed.sbert │ └── data_process_example │ │ ├── datareader.py │ │ ├── dataset.csv │ │ └── process_data.py ├── environment.yml ├── requirements.txt ├── synthesizer │ ├── __init__.py │ ├── gll.py │ ├── gll_.py │ ├── parser.py │ └── synthesizer.py └── verifier │ ├── DB.py │ ├── __init__.py │ ├── label_models.py │ ├── modeler.py │ ├── modeler_.py │ ├── translator.py │ └── util.py ├── tagruler-teaser.gif └── ui ├── package.json ├── public ├── favicon.png ├── index.html ├── manifest.json └── robots.txt └── src ├── AnnotationBuilder.js ├── AnnotationDisplay.js ├── AnnotationDisplayCollapse.js ├── App.js ├── App.test.js ├── ClassLabelsCollection.js ├── ColorPalette.js ├── Concept.js ├── ConceptCollection.js ├── ConceptElement.js ├── Dataset.js ├── Footer.js ├── GithubIcon.js ├── LFPanel.js ├── LabelCreation.js ├── LabelingFunctionsSelected.js ├── LabelingFunctionsSuggested.js ├── LeftDrawer.js ├── Link.js ├── Main.js ├── Navigation.js ├── NavigationBar.js ├── NavigationButtons.js ├── ProjectCreation.js ├── ProjectGrid.js ├── RichTextUtils.js ├── SaveButton.js ├── SelectedSpan.js ├── SortingTableUtils.js ├── Span.js ├── StatisticsCRFPane.js ├── StatisticsPane.js ├── actions ├── ConceptStyler.js ├── annotate.js ├── api.js ├── concepts.js ├── connectivesAndKeyTypes.js ├── datasets.js ├── getStatistics.js ├── getText.js ├── interaction.js ├── labelAndSuggestLF.js ├── labelClasses.js ├── loadingBar.js ├── save.js ├── spanAnnotate.js └── submitLFs.js ├── errorSnackbar.js ├── index.js ├── reducers.js ├── reducers ├── ConnectivesKeyTypesReducer.js ├── annotationsReducer.js ├── conceptsReducer.js ├── datasetsReducer.js ├── interactionHistoryReducer.js ├── labelAndSuggestLFReducer.js ├── labelClassesReducer.js ├── labelExampleReducer.js ├── loadingBarReducer.js ├── reducers.js ├── selectedLFReducer.js ├── statisticsReducer.js └── textReducer.js ├── serviceWorker.js └── store.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | */node_modules/* 3 | venv/* 4 | */.idea/* 5 | *.ipynb 6 | .vscode/ 7 | server/*/__pycache__/* 8 | ui/.eslintrc.js 9 | ui/package-lock.json 10 | server/venv/* 11 | .idea/* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TagRuler: Interactive Tool for Span-Level Data Programming by Demonstration 2 | This repo contains the source code and the user evaluation data for TagRuler, a data programming by demonstration system for span-level annotation. 3 | Check out our [demo video](https://youtu.be/MRc2elPaZKs) to see TagRuler in action! 4 | 5 | Demonstration Video: https://youtu.be/MRc2elPaZKs 6 | 7 | 8 |

9 | TagRuler synthesizes labeling functions based on your annotations, allowing you to quickly and easily generate large amounts of training data for span annotation, without the need to program.
10 | 11 |

12 | 13 | 14 | # What is TagRuler? 15 | 16 | In 2020, we introduced [Ruler](https://github.com/megagonlabs/ruler), a novel data programming by demonstration system that allows domain experts to leverage data programming without the need for coding. Ruler generates document classification rules, but we knew that there was a bigger challenge left to tackle: span-level annotations. This is one of the more time-consuming labelling tasks, and creating a DPBD system for this proved to be a challenge because of the sheer magnitude of the space of labeling functions over spans. 17 | 18 | We feel that this is a critical extension of the DPBD paradigm, and that by open-sourcing it, we can help with all kinds of labelling needs. 19 | 20 | # How to use the source code in this repo 21 | 22 | Follow these instructions to run the system on your own, where you can plug in your own data and save the resulting labels, models, and annotations. 23 | 24 | ## 1. Server 25 | 26 | ### 1-1. Install Dependencies :wrench: 27 | 28 | ```shell 29 | cd server 30 | pip install -r requirements.txt 31 | python -m spacy download en_core_web_sm 32 | ``` 33 | 34 | ### 1-2. (Optional) Download Data Files 35 | 36 | - **BC5CDR** ([Download Preprocessed Data](https://drive.google.com/file/d/1kKeINUOjtCVGr1_L3aC3qDo3-O-jr5hR/view?usp=sharing)): PubMed articles for Chemical-Disease annotation 37 | Li, Jiao & Sun, Yueping & Johnson, Robin & Sciaky, Daniela & Wei, Chih-Hsuan & Leaman, Robert & Davis, Allan Peter & Mattingly, Carolyn & Wiegers, Thomas & lu, Zhiyong. (2016). Original database URL: http://www.biocreative.org/tasks/biocreative-v/track-3-cdr/ 38 | 39 | - **Your Own Data** See instructions in [server/datasets](server/datasets) 40 | 41 | ### 1-3. Run :runner: 42 | 43 | ``` 44 | python api/server.py 45 | ``` 46 | 47 | ## 2. User Interface 48 | 49 | ### 2-1. Install Node.js 50 | 51 | [You can download node.js here.](https://nodejs.org/en/) 52 | 53 | To confirm that you have node.js installed, run `node - v` 54 | 55 | ### 2-2. Run 56 | 57 | ```shell 58 | cd ui 59 | npm install 60 | npm start 61 | ``` 62 | 63 | By default, the app will make calls to `localhost:5000`, assuming that you have the server running on your machine. (See the [instructions above](#Engine)). 64 | 65 | Once you have both of these running, navigate to `localhost:3000`. 66 | 67 | 68 | # Issues? 69 | 70 | ...or other inquiries, contact and/or . 71 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /server/api/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Main module of the server file 3 | """ 4 | 5 | from flask import render_template 6 | import connexion 7 | from flask_cors import CORS 8 | 9 | 10 | # create the application instance 11 | app = connexion.App(__name__, specification_dir="./") 12 | 13 | # Cead the swagger.yml file to configure the endpoints 14 | app.add_api("swagger.yml") 15 | 16 | CORS(app.app, resources={r"/*": {"origins": "*"}}) 17 | 18 | # Create a URL route in our application for "/" 19 | @app.route("/") 20 | def home(): 21 | """ 22 | This function just responds to the browser URL 23 | localhost:5000/ 24 | 25 | :return: the rendered templates "home.html" 26 | """ 27 | return render_template("home.html") 28 | 29 | 30 | if __name__ == "__main__": 31 | try: 32 | app.run(debug=False, threaded=False) 33 | except KeyboardInterrupt: 34 | pass -------------------------------------------------------------------------------- /server/api/templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEA2 API 6 | 7 | 8 |

9 | Welcome to IDEA2 API page! 10 |

11 | 12 | Please negative to /api/ui for more information. 13 | 14 | -------------------------------------------------------------------------------- /server/datasets/README.md: -------------------------------------------------------------------------------- 1 | # Using Your Own Data 2 | 3 | We'll release some preprocessing code soon! Until then, you need to replicate the following file structure: 4 | 5 | ``` 6 | . 7 | +-- datasets 8 | | +-- your_dataset_here 9 | | | +-- processed.bert 10 | | | +-- processed.csv 11 | | | +-- processed.elmo 12 | | | +-- processed.nlp 13 | | | +-- processed.sbert 14 | | +-- example_dataset_1 15 | | +-- example_dataset_2 16 | ``` 17 | 18 | Where each file contains preprocessed data that follows the following schema: 19 | 20 | `processed.csv` (csv format) 21 | | | text | labels | split | 22 | |----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|-------| 23 | | 85 | angioedema due to ace inhibitors : common and inadequately diagnosed . the estimated incidence of angioedema during angiotensin - converting enzyme ( ace ) inhibitor treatment is between 1 and 7 per thousand patients . this potentially serious adverse effect is often preceded by minor manifestations that may serve as a warning . | I-DI,O,O,I-CH,I-CH,O,O,O,O,O,O,O,O,O,O,I-DI,O,I-CH,I-CH,I-CH,I-CH,I-CH,I-CH,I-CH,I-CH,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O,O | train | 24 | 25 | 'split' is one of 'train', 'dev', 'test', 'valid', where the latter three have labels (for train, labels can be empty). 26 | 27 | `processed.bert` ([npy](https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html#module-numpy.lib.format) format)\ 28 | A Numpy array of 2-D Numpy arrays. Each 2-D Numpy array is an array of BERT representations of tokens in a text data sample. 29 | ``` 30 | array([array([[ 0.024, -0.004, ..., -0.002, 0.061 ], 31 | [ 0.059, -0.004, ..., -0.003, 0.044 ], 32 | ..., 33 | [ 0.048, 0.006, ..., 0.011, -0.016]], dtype=float32), 34 | ..., 35 | array([[-0.039, 0.090, ..., -0.002, -0.002 ], 36 | ..., 37 | [-0.019, 0.027, ..., -0.011, 0.045 ]], dtype=float32)], dtype=object) 38 | 39 | ``` 40 | 41 | `processed.elmo` ([npy](https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html#module-numpy.lib.format) format)\ 42 | A Numpy array of 2-D Numpy arrays. Each 2-D Numpy array is an array of ELMo representations of tokens in a text data sample. 43 | ``` 44 | array([array([[ 0.024, -0.004, ..., -0.002, 0.061 ], 45 | [ 0.059, -0.004, ..., -0.003, 0.044 ], 46 | ..., 47 | [ 0.048, 0.006, ..., 0.011, -0.016]], dtype=float32), 48 | ..., 49 | array([[-0.039, 0.090, ..., -0.002, -0.002 ], 50 | ..., 51 | [-0.019, 0.027, ..., -0.011, 0.045 ]], dtype=float32)], dtype=object) 52 | 53 | ``` 54 | 55 | `processed.sbert` ([npy](https://numpy.org/doc/stable/reference/generated/numpy.lib.format.html#module-numpy.lib.format) format)\ 56 | A 2-D Numpy array which contains Sentence-BERT representations of text data samples. The shape of this array should be (N, V) where N is the number of text samples and V is the length of the Sentence-BERT representation. 57 | ``` 58 | array([[ 0.039, 0.011, 0.063, ..., -0.007, -0.004], 59 | [-0.047, -0.048, 0.023, ..., -0.026, -0.054], 60 | ..., 61 | [-0.025, -0.024, 0.054, ..., -0.017, -0.048]], dtype=float32) 62 | ``` 63 | 64 | `processed.nlp`\ 65 | \\TODO 66 | 67 | 68 | # Data Attribution 69 | 70 | 71 | ## BC5CDR 72 | Li, Jiao & Sun, Yueping & Johnson, Robin & Sciaky, Daniela & Wei, Chih-Hsuan & Leaman, Robert & Davis, Allan Peter & Mattingly, Carolyn & Wiegers, Thomas & lu, Zhiyong. (2016). 73 | 74 | BioCreative V CDR task corpus: a resource for chemical disease relation extraction. Database. 2016. baw068. 10.1093/database/baw068. Community-run, formal evaluations and manually annotated text corpora are critically important for advancing biomedical text-mining research. Recently in BioCreative V, a new challenge was organized for the tasks of disease named entity recognition (DNER) and chemical-induced disease (CID) relation extraction. Given the nature of both tasks, a test collection is required to contain both disease/chemical annotations and relation annotations in the same set of articles. Despite previous efforts in biomedical corpus construction, none was found to be sufficient for the task. Thus, we developed our own corpus called BC5CDR during the challenge by inviting a team of Medical Subject Headings (MeSH) indexers for disease/chemical entity annotation and Comparative Toxicogenomics Database (CTD) curators for CID relation annotation. To ensure high annotation quality and productivity, detailed annotation guidelines and automatic annotation tools were provided. The resulting BC5CDR corpus consists of 1500 PubMed articles with 4409 annotated chemicals, 5818 diseases and 3116 chemical-disease interactions. Each entity annotation includes both the mention text spans and normalized concept identifiers, using MeSH as the controlled vocabulary. To ensure accuracy, the entities were first captured independently by two annotators followed by a consensus annotation: The average inter-annotator agreement (IAA) scores were 87.49% and 96.05% for the disease and chemicals, respectively, in the test set according to the Jaccard similarity coefficient. Our corpus was successfully used for the BioCreative V challenge tasks and should serve as a valuable resource for the text-mining research community. 75 | 76 | Database URL: http://www.biocreative.org/tasks/biocreative-v/track-3-cdr/ 77 | 78 | -------------------------------------------------------------------------------- /server/datasets/bc5cdr/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/datasets/bc5cdr/.gitkeep -------------------------------------------------------------------------------- /server/datasets/bc5cdr_example/processed.bert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/datasets/bc5cdr_example/processed.bert -------------------------------------------------------------------------------- /server/datasets/bc5cdr_example/processed.elmo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/datasets/bc5cdr_example/processed.elmo -------------------------------------------------------------------------------- /server/datasets/bc5cdr_example/processed.nlp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/datasets/bc5cdr_example/processed.nlp -------------------------------------------------------------------------------- /server/datasets/bc5cdr_example/processed.sbert: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/datasets/bc5cdr_example/processed.sbert -------------------------------------------------------------------------------- /server/datasets/data_process_example/datareader.py: -------------------------------------------------------------------------------- 1 | from allennlp.data import Instance 2 | from allennlp.data.dataset_readers import DatasetReader 3 | from allennlp.data.fields import TextField, SequenceLabelField 4 | from allennlp.data.token_indexers import TokenIndexer, SingleIdTokenIndexer 5 | from allennlp.data.tokenizers import Token 6 | from allennlp.data.tokenizers.word_tokenizer import SpacyWordSplitter, WordTokenizer 7 | from allennlp.data.tokenizers.word_splitter import JustSpacesWordSplitter 8 | from tqdm.auto import tqdm 9 | from typing import Iterator, List, Dict 10 | from xml.etree import ElementTree 11 | import pandas as pd 12 | 13 | @DatasetReader.register('text') 14 | class TextDatasetReader(DatasetReader): 15 | """ 16 | DatasetReader for Laptop Reviews corpus available at 17 | http://alt.qcri.org/semeval2014/task4/. 18 | """ 19 | def __init__(self, token_indexers: Dict[str, TokenIndexer] = None) -> None: 20 | super().__init__(lazy=False) 21 | self.token_indexers = token_indexers or {"tokens": SingleIdTokenIndexer()} 22 | 23 | def text_to_instance(self, doc_id: str, tokens: List[Token], tags: List[str] = None) -> Instance: 24 | tokens_field = TextField(tokens, self.token_indexers) 25 | fields = {"tokens": tokens_field} 26 | 27 | if tags: 28 | tags_field = SequenceLabelField(labels=tags, sequence_field=tokens_field) 29 | fields["tags"] = tags_field 30 | 31 | return Instance(fields) 32 | 33 | def _read(self, file_path: str) -> Iterator[Instance]: 34 | #splitter = JustSpacesWordSplitter() 35 | splitter = SpacyWordSplitter('en_core_web_sm', True, True, True) 36 | tokenizer = WordTokenizer(word_splitter=splitter) 37 | df = pd.read_csv(file_path) 38 | #TODO: fix based on correct index 39 | for i,text in tqdm(enumerate(df['text'])): 40 | # Tokenizes the sentence 41 | tokens = tokenizer.tokenize(text) 42 | space_split = text.split(' ') 43 | tokens_merged = [] 44 | ii=0 45 | jj=0 46 | while ii 1: 23 | mul_tok_link_key += 1 24 | for crnt_token_text in crnt_token_texts: 25 | # crnt_token: text 26 | #crnt_token_text = origin_text[annotation["start_offset"]:annotation["end_offset"]] 27 | # initialize a new Token class 28 | crnt_token = Token(crnt_token_text, annotation['isPositive']) 29 | token_list.append(crnt_token) 30 | 31 | # Find the concept from annotation 32 | crnt_concept_name = None 33 | if ("label" in annotation) and (annotation["label"]): 34 | crnt_concept_name = annotation["label"] 35 | 36 | crnt_token.assign_concept(crnt_concept_name) 37 | 38 | # Find the relationship from annotation 39 | """ 40 | crnt_rel_code = None 41 | if "link" in annotation: 42 | if not annotation["link"] is None: 43 | crnt_rel_code = int(annotation["link"]) 44 | """ 45 | 46 | crnt_rel_code = None 47 | if len(crnt_token_texts) > 1: 48 | crnt_rel_code = mul_tok_link_key 49 | rel_code_list.append(crnt_rel_code) 50 | 51 | # Find the index of current annotation 52 | flag: bool = False 53 | # print(annotated_text) 54 | for crnt_sent in sentences: 55 | sent_start = origin_doc[crnt_sent.start].idx 56 | sent_end = origin_doc[crnt_sent.end-1].idx + len(origin_doc[crnt_sent.end-1]) 57 | if (annotation["start_offset"] >= sent_start) and (annotation["end_offset"]<=sent_end): 58 | crnt_token.assign_sent_idx(sentences.index(crnt_sent)) 59 | flag = True 60 | break 61 | if not flag: 62 | print("No sentence found for the annotation: \"{}\"\nsentences: {}".format(annotation, sentences)) 63 | 64 | # Find the named entity of current annotation 65 | #TODO if this is too slow, this can be done O(n) out of the loop 66 | crnt_token.assign_span_label(annotation['spanLabel']) 67 | crnt_tokens.append(crnt_token) 68 | offset = 0 69 | for crnt_token in crnt_tokens: 70 | for i,tk in enumerate(origin_doc): 71 | #TODO handle cases where selected span is not a token 72 | if tk.idx <= annotation['start_offset'] + offset and tk.idx+len(tk.text) >= annotation['start_offset'] + offset: 73 | if len(tk.ent_type_)>0: crnt_token.assign_ner_type(tk.ent_type_) 74 | crnt_token.assign_pos_type(tk.pos_) 75 | crnt_token.assign_dep_rel(tk.dep_) 76 | crnt_token.assign_tok_id(i) 77 | offset += (len(crnt_token.text) + 1) #TODO what is there's double spaces? 78 | break 79 | 80 | 81 | # Match existing concepts 82 | augment_concept(token_list, concepts) 83 | 84 | return token_list, rel_code_list 85 | 86 | 87 | def augment_concept(token_list, concepts: dict): 88 | for crnt_token in token_list: 89 | if crnt_token.concept_name is not None: 90 | continue 91 | 92 | for key in concepts.keys(): 93 | if crnt_token.text in concepts[key]: 94 | crnt_token.assign_concept(key) 95 | break 96 | 97 | 98 | # remove stop word and punct 99 | def simple_parse(text: str, concepts: dict): 100 | token_list = [] 101 | doc = nlp(text) 102 | tokens = [token.text for token in doc if not token.is_stop and not token.is_punct] 103 | #print(tokens) 104 | 105 | if len(doc) == len(tokens): 106 | # early return 107 | return token_list 108 | 109 | ner_dict = dict() 110 | 111 | # merge multiple tokens if falling into one ent.text 112 | for ent in doc.ents: 113 | matched_text = [] 114 | for i in range(len(tokens)): 115 | if tokens[i] in ent.text: 116 | matched_text.append(tokens[i]) 117 | 118 | if len(matched_text) > 1: 119 | new_text = "" 120 | for crnt_text in matched_text: 121 | new_text += crnt_text 122 | tokens.remove(crnt_text) 123 | 124 | tokens.append(ent.text) 125 | ner_dict[ent.text] = ent.label_ 126 | 127 | for crnt_text in tokens: 128 | crnt_token = Token(crnt_text) 129 | if crnt_text in ner_dict.keys(): 130 | crnt_token.assign_ner_type(ner_dict[crnt_text]) 131 | token_list.append(crnt_token) 132 | 133 | augment_concept(token_list, concepts) 134 | 135 | return token_list 136 | 137 | -------------------------------------------------------------------------------- /server/synthesizer/synthesizer.py: -------------------------------------------------------------------------------- 1 | from synthesizer.parser import parse, simple_parse 2 | from synthesizer.gll import * 3 | 4 | 5 | class Synthesizer: 6 | 7 | token_list = [] 8 | rel_code_list = [] 9 | 10 | def __init__(self, t_origin, annots, t_label, de, cs: dict, sent_id, label_dict): 11 | self.origin_text = t_origin 12 | self.annotations = annots 13 | self.delimiter = de 14 | self.concepts = cs 15 | self.label = t_label 16 | self.instances = [] 17 | self.sent_id = sent_id 18 | self.label_dict= label_dict 19 | 20 | def print(self): 21 | print(self.instances) 22 | 23 | def run(self): 24 | """ 25 | Based on one label, suggest LFs 26 | 27 | Returns: 28 | List(Dict): labeling functions represented as dicts with fields: 29 | 'Conditions', 'Connective', 'Direction', 'Label', and 'Weight' 30 | """ 31 | self.token_list, self.rel_code_list = \ 32 | parse(self.annotations, self.origin_text, self.delimiter, self.concepts) 33 | 34 | assert len(self.token_list) == len(self.rel_code_list) 35 | 36 | relationship_set = None 37 | relationship_undirected = dict() 38 | relationship_directed = dict() 39 | 40 | for i in range(len(self.rel_code_list)): 41 | crnt_rel_code = self.rel_code_list[i] 42 | crnt_token = self.token_list[i] 43 | 44 | if crnt_rel_code is None: 45 | if relationship_set is None: 46 | relationship_set = Relationship(RelationshipType.SET) 47 | relationship_set.add(crnt_token, crnt_rel_code) 48 | 49 | else: 50 | if abs(crnt_rel_code) < 100: 51 | # no direction relationship 52 | if crnt_rel_code not in relationship_undirected: 53 | relationship_undirected[crnt_rel_code] = Relationship(RelationshipType.UNDIRECTED) 54 | 55 | relationship_undirected[crnt_rel_code].add(crnt_token, crnt_rel_code) 56 | 57 | else: 58 | # directed relationship 59 | abs_code = abs(crnt_rel_code) 60 | if abs_code not in relationship_directed: 61 | relationship_directed[abs_code] = Relationship(RelationshipType.DIRECTED) 62 | 63 | relationship_directed[abs_code].add(crnt_token, crnt_rel_code) 64 | 65 | # for each relationship, generate instances 66 | if relationship_set is not None: # TODO: utilize this code for merging rules later 67 | self.instances.extend(relationship_set.get_instances(self.concepts, self.sent_id, self.label_dict)) 68 | for k, v in relationship_undirected.items(): 69 | self.instances.extend(v.get_instances(self.concepts, self.sent_id, self.label_dict)) 70 | 71 | # add label to each instance 72 | for i, crnt_instance in enumerate(self.instances): 73 | # remove repeated conditions 74 | conditions = crnt_instance[CONDS] 75 | #crnt_instance[CONDS] = [dict(t) for t in {tuple(d.items()) for d in conditions}] 76 | 77 | # sort instances based on weight 78 | calc_weight(self.instances, self.concepts) 79 | self.instances.sort(key=lambda x: x[WEIGHT], reverse=True) 80 | return self.instances#[:20] 81 | 82 | def single_condition(self): 83 | extended_instances = [] 84 | crnt_instance = self.instances[0] 85 | if len(crnt_instance[CONDS]) == 1: 86 | single_cond = crnt_instance[CONDS][0] 87 | crnt_text = list(single_cond.keys())[0] 88 | crnt_type = single_cond[crnt_text] 89 | 90 | # only process when the highlighted is token 91 | if crnt_type == KeyType[TOKEN]: 92 | # pipeline to process the crnt_text 93 | # remove stopwords and punct 94 | token_list = simple_parse(crnt_text, self.concepts) 95 | 96 | if len(token_list) == 0: 97 | return 98 | 99 | # relationship set 100 | relationship_set = Relationship(RelationshipType.SET) 101 | for crnt_token in token_list: 102 | relationship_set.add(crnt_token, None) 103 | extended_instances.extend( relationship_set.get_instances(self.concepts) ) 104 | else: 105 | for condition in crnt_instance[CONDS]: 106 | new_inst = crnt_instance.copy() 107 | new_inst[CONDS] = [condition] 108 | extended_instances.extend([new_inst]) 109 | self.instances = extended_instances 110 | 111 | 112 | 113 | def calc_weight(instances, concepts): 114 | # TODO better weight calc 115 | for crnt_instance in instances: 116 | crnt_weight = 1 117 | for crnt_cond in crnt_instance[CONDS]: 118 | k = crnt_cond.get("string") 119 | v = crnt_cond.get("type") 120 | 121 | KeyType = {TOKEN: 0, CONCEPT: 1, NER: 2, REGEXP: 3, POS: 4, DEP: 5, ELMO_SIMILAR: 6, BERT_SIMILAR: 7, 122 | SIM: 8} 123 | 124 | if v == KeyType[POS]: 125 | crnt_weight += 2 126 | elif v == KeyType[DEP]: 127 | crnt_weight += 1 128 | crnt_instance[WEIGHT] = crnt_weight 129 | 130 | 131 | def test_synthesizer(): 132 | text_origin = "the room is clean." 133 | annotations = [{"id":5, "start_offset": 5, "end_offset":9}, {"id":12, "start_offset": 12, "end_offset":17}] 134 | label = 1.0 135 | de = '#' 136 | concepts = dict() 137 | 138 | concepts['Hotel'] = ['room'] 139 | 140 | crnt_syn = Synthesizer(text_origin, annotations, label, de, concepts) 141 | 142 | 143 | 144 | #test_synthesizer() 145 | -------------------------------------------------------------------------------- /server/verifier/DB.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import json 3 | import os 4 | import pandas as pd 5 | from verifier.translator import make_lf 6 | 7 | 8 | class InteractionDBSingleton: 9 | filename = "interactionDB.json" 10 | def __init__(self): 11 | self.db = {} 12 | self.count = 0 13 | 14 | def add(self, interaction: dict): 15 | if "index" in interaction: 16 | index = interaction["index"] 17 | interaction['time_submitted'] = datetime.now() 18 | self.db[index].update(interaction) 19 | else: 20 | index = self.count 21 | interaction["index"] = index 22 | interaction['time_first_seen'] = datetime.now() 23 | self.db[index] = interaction 24 | self.count += 1 25 | return index 26 | 27 | def update(self, index: int, selected_lf_ids: list): 28 | self.db[index]["lfs"] = selected_lf_ids 29 | 30 | def get(self, index: int): 31 | return self.db[index] 32 | 33 | def save(self, dirname): 34 | with open(os.path.join(dirname, self.filename), "w+") as file: 35 | json.dump({ 36 | "db": self.db, 37 | "count": self.count 38 | }, file, default=str) 39 | 40 | def load(self, dirname): 41 | with open(os.path.join(dirname, self.filename), "r") as file: 42 | data = json.load(file) 43 | self.db = data['db'] 44 | self.count = data['count'] 45 | 46 | interactionDB = InteractionDBSingleton() 47 | 48 | class LFDBSingleton: 49 | filename = "LF_DB.json" 50 | def __init__(self): 51 | self.db = {} 52 | self.lf_index = 0 53 | 54 | def get(self, lf_id): 55 | return self.db[lf_id] 56 | 57 | def add_lfs(self, lf_dicts: dict, all_concepts, emb_dict, ui_label_dict): 58 | new_lfs = {} 59 | for lf_hash, lf_explanation in lf_dicts.items(): 60 | if not lf_hash in self.db: 61 | lf_explanation["time_submitted"] = datetime.now() 62 | lf_explanation["ID"] = self.lf_index 63 | lf_explanation["active"] = True 64 | self.lf_index += 1 65 | 66 | crnt_lf = make_lf(lf_explanation, all_concepts.get_dict(), emb_dict, ui_label_dict) 67 | self.db[lf_hash] = lf_explanation 68 | new_lfs[lf_hash] = crnt_lf 69 | else: 70 | self.db[lf_hash]["active"] = True 71 | new_lfs[lf_hash] = make_lf(self.db[lf_hash], all_concepts.get_dict(), emb_dict, ui_label_dict) 72 | 73 | return new_lfs 74 | 75 | def delete(self, lf_id: str): 76 | return self.db.pop(lf_id) 77 | 78 | def deactivate(self, lf_id: str): 79 | self.db[lf_id]["active"] = False 80 | return self.db[lf_id] 81 | 82 | def update(self, stats: dict): 83 | for lf_id, stats_dict in stats.items(): 84 | self.db[lf_id].update(stats_dict) 85 | return self.db.copy() 86 | 87 | def __contains__(self, item: str): 88 | return item in self.db 89 | 90 | def __len__(self): 91 | return len(self.db) 92 | 93 | def save(self, dirname): 94 | with open(os.path.join(dirname, self.filename), "w+") as file: 95 | json.dump({ 96 | "db": self.db, 97 | "lf_index": self.lf_index 98 | }, file, default=str) 99 | 100 | def load(self, dirname, all_concepts): 101 | with open(os.path.join(dirname, self.filename), "r") as file: 102 | data = json.load(file) 103 | lfs = data['db'] 104 | self.db.update({k:v for k,v in lfs.items() if not v['active']}) 105 | self.lf_index = data['lf_index'] 106 | return self.add_lfs({k:v for k,v in lfs.items() if v['active']}, all_concepts) 107 | 108 | 109 | LF_DB = LFDBSingleton() -------------------------------------------------------------------------------- /server/verifier/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/server/verifier/__init__.py -------------------------------------------------------------------------------- /server/verifier/translator.py: -------------------------------------------------------------------------------- 1 | """Translate a dictionary explaining a labeling rule into a function 2 | """ 3 | import re 4 | from wiser.rules import TaggingRule # A TaggingRule instance is defined for a tagging LF. 5 | from snorkel.labeling import LabelingFunction 6 | from synthesizer.gll import * 7 | import numpy as np 8 | 9 | 10 | def raw_stringify(s): 11 | """From a regex create a regular expression that finds occurences of the string as entire words 12 | 13 | Args: 14 | s (string): the string to look for 15 | 16 | Returns: 17 | string: a regular expession that looks for this string surrounded by non word characters 18 | """ 19 | return "(?:(?<=\W)|(?<=^))({})(?=\W|$)".format(re.escape(s)) 20 | 21 | 22 | def find_indices(cond_dict: dict, text: str): 23 | """Find all instances of this condition in the text 24 | """ 25 | v = cond_dict.get("type") 26 | k = cond_dict.get("string") 27 | case_sensitive = True if cond_dict.get("case_sensitive") else False 28 | 29 | if v == KeyType[NER]: 30 | doc = nlp(text) 31 | for ent in doc.ents: 32 | if ent.label_ == k: 33 | return [(doc[ent.start].idx, doc[ent.end-1].idx + len(doc[ent.end-1].text))] 34 | return [] 35 | elif v == KeyType[POS] or v == KeyType[DEP] or v == KeyType[ELMO_SIMILAR] or v == KeyType[BERT_SIMILAR]: 36 | return [(0,0)] 37 | elif case_sensitive: 38 | return [(m.start(), m.end()) for m in re.finditer(k, text)] 39 | else: 40 | return [(m.start(), m.end()) for m in re.finditer(k, text, re.IGNORECASE)] 41 | 42 | 43 | def make_lf(instance, concepts, emb_dict, ui_label_dict): 44 | def apply_instance(self, instance): 45 | """ 46 | An ``Instance`` is a collection of :class:`~allennlp.data.fields.field.Field` objects, 47 | specifying the inputs and outputs to 48 | some model. We don't make a distinction between inputs and outputs here, though - all 49 | operations are done on all fields, and when we return arrays, we return them as dictionaries 50 | keyed by field name. A model can then decide which fields it wants to use as inputs as which 51 | as outputs. 52 | 53 | The ``Fields`` in an ``Instance`` can start out either indexed or un-indexed. During the data 54 | processing pipeline, all fields will be indexed, after which multiple instances can be combined 55 | into a ``Batch`` and then converted into padded arrays. 56 | 57 | Parameters 58 | ---------- 59 | instance : An ``Instance`` is a collection of :class:`~allennlp.data.fields.field.Field` objects. 60 | instance['fields'] : ``Dict[str, Field]`` 61 | ex: 62 | instance['fields']['tags'] = ['ABS', 'I-OP', 'ABS', 'ABS'] 63 | """ 64 | label = self.lf_dict.get(LABEL) 65 | direction = bool(self.lf_dict.get(DIRECTION)) 66 | conn = self.lf_dict.get(CONNECTIVE) 67 | conds = self.lf_dict.get(CONDS) 68 | types = [cond.get("TYPE_") for cond in conds] 69 | strs_ = [cond.get("string") for cond in conds] 70 | #labels = np.array([np.array(['ABS'] * len(instance['tokens']), dtype=object)] * len(conds)) 71 | 72 | type_ = conds[0].get("TYPE_") 73 | str_ = conds[0].get("string") 74 | is_pos = conds[0].get("positive") 75 | labels = np.array(['ABS'] * len(instance['tokens']), dtype=object) 76 | if type_ == BERT_SIMILAR or type_ == ELMO_SIMILAR: 77 | emb_type = 'bert' if type_ == BERT_SIMILAR else 'elmo' 78 | emb_thres = BERT_THRESHOLD if type_ == BERT_SIMILAR else ELMO_THRESHOLD 79 | sent_id = self.lf_dict.get(SENT_ID) 80 | tok_id = self.lf_dict.get(TOK_ID) 81 | if type(tok_id) == list: 82 | target_emb = self.emb_dict.emb_dict[emb_type][sent_id][tok_id] 83 | emb = instance[emb_type] 84 | cos_scores = target_emb @ emb.T 85 | if is_pos: 86 | similar_inds = (cos_scores > emb_thres) 87 | else: 88 | similar_inds = (cos_scores <= emb_thres) 89 | similar_ind = similar_inds[0][:-len(tok_id)+1] 90 | for i in range(1,len(tok_id)): 91 | similar_ind = (similar_ind) & (similar_inds[i][i:len(similar_inds[i])-len(tok_id)+1+i]) 92 | for i in range(len(tok_id)): 93 | labels[i:len(labels)-len(tok_id)+1+i][similar_ind] = ui_label_dict[label] 94 | else: 95 | # target_emb is the emb vec corresponding to the similarity target word 96 | target_emb = self.emb_dict.emb_dict[emb_type][sent_id][tok_id] 97 | # emb is an N x M matrix containing token embeddings in a sentence 98 | emb = instance[emb_type] 99 | cos_scores = np.dot(emb, target_emb) 100 | if is_pos: 101 | labels[cos_scores > emb_thres] = ui_label_dict[label] 102 | else: 103 | labels[cos_scores <= emb_thres] = ui_label_dict[label] 104 | # try: 105 | # except: 106 | # print('error') 107 | elif type_ == POS: 108 | for i, pos in enumerate([token.pos_ for token in instance['tokens']]): 109 | if (pos in str_ and is_pos) or (pos not in str_ and not is_pos): 110 | labels[i] = ui_label_dict[label] 111 | elif type_ == DEP: 112 | for i, dep in enumerate([token.dep_ for token in instance['tokens']]): 113 | if (dep in str_ and is_pos) or (dep not in str_ and not is_pos): 114 | labels[i] = ui_label_dict[label] 115 | elif type_ == NER: 116 | for i, ner in enumerate([token.ent_type_ for token in instance['tokens']]): 117 | if (ner in str_ and is_pos) or (ner not in str_ and not is_pos): 118 | labels[i] = ui_label_dict[label] 119 | return list(labels) 120 | 121 | 122 | def lf_init(self): 123 | pass 124 | 125 | 126 | LF_class = type(instance['name'], (TaggingRule,), {"__init__":lf_init, "lf_dict":instance, "apply_instance":apply_instance, "emb_dict":emb_dict, "name":instance['name']} ) 127 | return LF_class() 128 | -------------------------------------------------------------------------------- /tagruler-teaser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/tagruler-teaser.gif -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dpbd", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.4.0", 7 | "@material-ui/icons": "^4.2.1", 8 | "axios": "^0.19.0", 9 | "clsx": "^1.0.4", 10 | "d3": "^5.12.0", 11 | "jquery": "^3.4.0", 12 | "material-ui-dropzone": "^3.3.0", 13 | "prop-types": "^15.7.2", 14 | "react": "^16.9.0", 15 | "react-dom": "^16.9.0", 16 | "react-redux": "^7.1.1", 17 | "react-router-dom": "^5.2.0", 18 | "react-scripts": "^3.4.1", 19 | "redux": "^4.0.4", 20 | "redux-thunk": "^2.3.0", 21 | "typeface-roboto": "0.0.75" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/megagonlabs/tagruler/f680478e82ffc913a05af35b5837eee477e7d322/ui/public/favicon.png -------------------------------------------------------------------------------- /ui/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 17 | 26 | Interactive Span-level Annotation 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Ruler", 3 | "name": "Ruler: Data Programming by Demonstration for Text", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64", 8 | "type": "image/x-png" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /ui/src/AnnotationDisplayCollapse.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AnnotationDisplay from './AnnotationDisplay' 5 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 6 | import ExpandLessIcon from '@material-ui/icons/ExpandLess'; 7 | import Grid from '@material-ui/core/Grid'; 8 | 9 | class AnnotationDisplayCollapse extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | open: false 14 | } 15 | this.collapseText(this.props.text); 16 | } 17 | 18 | collapseText(text, MAX_COLLAPSED_MARGIN = 100) { 19 | const max_start = this.props.annotations[0].start_offset || 0; 20 | const min_end = this.props.annotations[0].end_offset || 0; 21 | 22 | var coll_annotations = this.props.annotations; 23 | if (max_start > MAX_COLLAPSED_MARGIN) { 24 | const start = text.indexOf(" ", max_start - MAX_COLLAPSED_MARGIN); 25 | if (start > 30) { 26 | coll_annotations = this.props.annotations.map((ann) => { 27 | return ({ 28 | ...ann, 29 | start_offset: ann.start_offset - start + 3, 30 | end_offset: ann.end_offset - start + 3 31 | }) 32 | }) 33 | text = "..." + text.slice(start); 34 | } 35 | } 36 | 37 | if (min_end < text.length - MAX_COLLAPSED_MARGIN) { 38 | const end = text.lastIndexOf(" ", min_end + MAX_COLLAPSED_MARGIN); 39 | if (text.length - end > 30){ 40 | text = text.slice(0, end) + "..."; 41 | } 42 | } 43 | 44 | this.coll_text = text; 45 | this.coll_annotations = coll_annotations; 46 | } 47 | 48 | toggleOpen() { 49 | this.setState({ 50 | open: !this.state.open 51 | }) 52 | } 53 | 54 | toggleIcon() { 55 | if (this.state.open) { 56 | return 57 | } else { 58 | return 59 | } 60 | 61 | } 62 | 63 | render() { 64 | 65 | var collapsedText = this.coll_text; 66 | var coll_annotations = this.coll_annotations; 67 | 68 | return( 69 | 74 | 79 | { this.props.text !== collapsedText ? this.toggleIcon() : null} 80 | 81 | ) 82 | } 83 | } 84 | 85 | AnnotationDisplayCollapse.propTypes = { 86 | text: PropTypes.string, 87 | annotations: PropTypes.array 88 | } 89 | 90 | export default AnnotationDisplayCollapse -------------------------------------------------------------------------------- /ui/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createMuiTheme } from '@material-ui/core/styles'; 3 | import { makeStyles } from '@material-ui/core/styles'; 4 | import { ThemeProvider } from '@material-ui/styles'; 5 | import ColorPalette from './ColorPalette'; 6 | import Main from './Main'; 7 | 8 | const windowHeight = Math.max( 9 | document.documentElement.clientHeight, 10 | window.innerHeight || 0); 11 | 12 | const useStyles = makeStyles(theme => ({ 13 | root: { 14 | width: '100%', 15 | height: windowHeight, 16 | } 17 | })); 18 | 19 | 20 | const App = () => { 21 | const theme = createMuiTheme(ColorPalette), 22 | classes = useStyles(); 23 | 24 | return ( 25 | 26 | 27 |
28 |
29 |
30 |
31 | ); 32 | }; 33 | 34 | export default App; 35 | -------------------------------------------------------------------------------- /ui/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /ui/src/ClassLabelsCollection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import ButtonGroup from '@material-ui/core/ButtonGroup'; 4 | import Grid from '@material-ui/core/Grid'; 5 | import {connect} from "react-redux"; 6 | 7 | class ClassLabelsCollection extends React.Component { 8 | constructor(props) { 9 | super(props) 10 | this.keyHandling = this.keyHandling.bind(this); 11 | this.assignLabel = this.assignLabel.bind(this); 12 | } 13 | 14 | defaultSelections 15 | 16 | keyHandling(e) { 17 | const key = parseInt(e.key); 18 | if (key in this.props.hotKeys) { 19 | this.assignLabel(key); 20 | } 21 | } 22 | 23 | assignLabel(key) { 24 | this.props.onClick(key); 25 | } 26 | 27 | componentDidMount() { 28 | window.addEventListener("keyup", this.keyHandling); 29 | } 30 | 31 | componentWillUnmount() { 32 | window.removeEventListener("keyup", this.keyHandling); 33 | } 34 | 35 | render(){ 36 | const classes = this.props.classes; 37 | //console.log(this.props.labelClasses) 38 | return ( 39 | 40 | { this.props.labelClasses.map( (labelClass) => 41 | 56 | ) 57 | } 58 | ) 59 | } 60 | } 61 | 62 | function mapStateToProps(state, ownProps?) { 63 | return { 64 | labelClasses: state.labelClasses.data, 65 | hotKeys: state.labelClasses.data.map(lClass => lClass.key), 66 | annotations: state.annotations, 67 | label: state.label 68 | }; 69 | } 70 | function mapDispatchToProps(state){ 71 | return {}; 72 | } 73 | 74 | export default connect(mapStateToProps, mapDispatchToProps)(ClassLabelsCollection); -------------------------------------------------------------------------------- /ui/src/ColorPalette.js: -------------------------------------------------------------------------------- 1 | // 2 | // Labeler color theme 3 | // 4 | const ColorPalette = { 5 | props: { 6 | MuiGrid: { 7 | spacing: 3 8 | } 9 | }, 10 | 11 | "palette": { 12 | "common": { 13 | "black": "#000", 14 | "white": "#fff" 15 | }, 16 | "background": { 17 | "paper": "#fff", 18 | "default": "#fafafa" 19 | }, 20 | "primary": { 21 | "light": "rgba(150, 150, 150, 1)", 22 | "main": "rgba(50, 50, 50, 1)", 23 | "dark": "rgba(0, 0, 0, 1)", 24 | "contrastText": "#fff" 25 | }, 26 | "secondary": { 27 | "light": "rgba(107, 174, 214, 1)", 28 | "main": "rgba(33, 113, 181, 1)", 29 | "dark": "rgba(8, 81, 156, 1)", 30 | "contrastText": "#fff" 31 | }, 32 | "error": { 33 | "light": "#e57373", 34 | "main": "#f44336", 35 | "dark": "#d32f2f", 36 | "contrastText": "#fff" 37 | }, 38 | "warning": { 39 | "light": "#e57373", 40 | "main": "#f44336", 41 | "dark": "#d32f2f", 42 | "contrastText": "#fff" 43 | }, 44 | "success": { 45 | "light": "rgba(150, 150, 150, 1)", 46 | "main": "rgba(50, 50, 50, 1)", 47 | "dark": "rgba(0, 0, 0, 1)", 48 | "contrastText": "#fff" 49 | }, 50 | "info": { 51 | "light": "rgba(107, 174, 214, 1)", 52 | "main": "rgba(33, 113, 181, 1)", 53 | "dark": "rgba(8, 81, 156, 1)", 54 | "contrastText": "#fff" 55 | }, 56 | "text": { 57 | "primary": "rgba(0, 0, 0, 0.87)", 58 | "secondary": "rgba(0, 0, 0, 0.54)", 59 | "disabled": "rgba(0, 0, 0, 0.38)", 60 | "hint": "rgba(0, 0, 0, 0.38)" 61 | } 62 | } 63 | }; 64 | 65 | export default ColorPalette; -------------------------------------------------------------------------------- /ui/src/Concept.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from "react-redux"; 4 | 5 | import Avatar from '@material-ui/core/Avatar'; 6 | import Badge from '@material-ui/core/Badge'; 7 | import Card from '@material-ui/core/Card'; 8 | import CardHeader from '@material-ui/core/CardHeader'; 9 | import Collapse from '@material-ui/core/Collapse'; 10 | import DeleteForeverIcon from '@material-ui/icons/DeleteForever'; 11 | import IconButton from '@material-ui/core/IconButton'; 12 | import Table from '@material-ui/core/Table'; 13 | import TableBody from '@material-ui/core/TableBody'; 14 | import TableCell from '@material-ui/core/TableCell'; 15 | import TableHead from '@material-ui/core/TableHead'; 16 | import TableRow from '@material-ui/core/TableRow'; 17 | import Typography from '@material-ui/core/Typography'; 18 | 19 | import ConceptElement from './ConceptElement' 20 | import { conceptEditors, select_concept } from './actions/concepts' 21 | import { annotate, highlight } from './actions/annotate' 22 | 23 | import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; 24 | import ExpandLessIcon from '@material-ui/icons/ExpandLess'; 25 | 26 | class Concept extends React.Component { 27 | constructor(props) { 28 | super(props); 29 | this.toggleOpen = this.toggleOpen.bind(this); 30 | this.handleRemove = this.handleRemove.bind(this); 31 | this.handleClick = this.handleClick.bind(this); 32 | } 33 | 34 | changeToken(token_idx, new_token) { 35 | if (this.props.tokens[token_idx] !== new_token) { 36 | var current_tokens = this.props.tokens; 37 | current_tokens[token_idx] = new_token; 38 | this.updateConcept(current_tokens); 39 | } 40 | } 41 | 42 | handleRemove(old_token_idx) { 43 | const old_token = this.props.tokens[old_token_idx]; 44 | var current_tokens = this.props.tokens; 45 | current_tokens = current_tokens.filter((elt, idx) => idx !== old_token_idx); 46 | this.updateConcept(current_tokens); 47 | 48 | //Remove annotations that came from this element 49 | var new_annotations = this.props.annotations.filter( 50 | annotation => { 51 | if (annotation.origin == old_token.string) { 52 | return false; 53 | } return true; 54 | }) 55 | this.props.annotate(new_annotations); 56 | } 57 | 58 | updateConcept(tokens) { 59 | this.props.conceptEditors.updateConcept( 60 | this.props.concept, 61 | tokens 62 | ); 63 | } 64 | 65 | toggleOpen(event){ 66 | if (this.props.isOpen) { 67 | this.props.close(); 68 | } else { 69 | this.props.open(); 70 | } 71 | } 72 | 73 | handleClick(event){ 74 | if (this.props.selectedConcept===this.props.concept) { 75 | //this.props.select_concept(null); 76 | } else { 77 | this.props.select_concept(this.props.concept); 78 | } 79 | } 80 | 81 | deleteSelf() { 82 | this.props.select_concept(null); 83 | this.props.conceptEditors.deleteConcept(this.props.concept); 84 | } 85 | 86 | render() { 87 | const classes = this.props.classes; 88 | const concept = this.props.concept; 89 | const color = this.props.color; 90 | 91 | const isSelected = (this.props.selectedConcept===this.props.concept); 92 | let style = {padding: 5, border: "2px solid " + color} 93 | if (isSelected) { 94 | style["backgroundColor"] = color; 95 | style["color"] = "white"; 96 | } else { 97 | style["backgroundColor"] = "white"; 98 | style["color"] = "black"; 99 | } 100 | 101 | const new_token_idx = this.props.tokens.length; 102 | 103 | return( 104 |
105 | 106 | 113 | 114 | 115 | 119 | {this.props.isOpen ? : } 120 | 121 | } 122 | title={{concept}} 123 | avatar={ 126 | {new_token_idx} 127 | } 128 | /> 129 | 130 | 131 | 132 | 133 | value 134 | {this.props.view ? null : type} 135 | case 136 | 137 | 138 | 139 | 140 | {this.props.tokens.map((token, idx) => { 141 | let key = token.string + String(token.type) + String(token.case_sensitive) + String(idx); 142 | return ( this.handleRemove(idx)} 145 | changeToken={(new_token) => this.changeToken(idx, new_token)} 146 | key={key} 147 | idx={idx} 148 | concept={concept} 149 | addAnnotations={this.props.addAnnotations} 150 | classes={classes} 151 | view={this.props.view} 152 | />) 153 | })} 154 | this.changeToken(new_token_idx, new_token)} 157 | key={new_token_idx} 158 | idx={new_token_idx} 159 | classes={classes} 160 | concept={concept} 161 | view={this.props.view} 162 | addAnnotations={this.props.addAnnotations} 163 | new_entry={true} 164 | /> 165 |
166 |
167 |
168 |
169 | ); 170 | } 171 | } 172 | 173 | function mapStateToProps(state, ownProps?) { 174 | let concept = state.concepts.data[ownProps.concept]; 175 | return { 176 | ...concept, 177 | selectedConcept: state.selectedConcept, 178 | text: state.text.data, 179 | error: state.highlights.error, 180 | highlights: state.highlights.data, 181 | annotations: state.annotations 182 | }; 183 | } 184 | 185 | function mapDispatchToProps(dispatch) { 186 | return { 187 | annotate: bindActionCreators(annotate, dispatch), 188 | conceptEditors: bindActionCreators(conceptEditors, dispatch), 189 | select_concept: bindActionCreators(select_concept, dispatch), 190 | highlight: bindActionCreators(highlight, dispatch) 191 | }; 192 | } 193 | 194 | export default connect(mapStateToProps, mapDispatchToProps)(Concept); 195 | -------------------------------------------------------------------------------- /ui/src/ConceptCollection.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {conceptEditors, select_concept} from './actions/concepts' 4 | import { highlight } from './actions/annotate' 5 | import {bindActionCreators} from 'redux'; 6 | import {connect} from "react-redux"; 7 | 8 | import AddIcon from '@material-ui/icons/Add'; 9 | import Box from '@material-ui/core/Box'; 10 | import Button from '@material-ui/core/Button'; 11 | import Grid from '@material-ui/core/Grid'; 12 | import MenuItem from '@material-ui/core/MenuItem'; 13 | import Select from '@material-ui/core/Select'; 14 | import Typography from '@material-ui/core/Typography'; 15 | 16 | import Concept from './Concept' 17 | 18 | import { TOKEN_VIEW, REGEX_VIEW } from './ConceptElement' 19 | 20 | 21 | import TextField from '@material-ui/core/TextField'; 22 | 23 | 24 | class ConceptCollection extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | 28 | this.state = { 29 | conceptName: "", 30 | fetched: false, 31 | view: TOKEN_VIEW, //view 0 is token view, view 1 is regex view 32 | openConcept: null, 33 | }; 34 | this.handleInput = this.handleInput.bind(this); 35 | this.handleKeyPress = this.handleKeyPress.bind(this); 36 | this.handleAdd = this.handleAdd.bind(this); 37 | this.changeView = this.changeView.bind(this); 38 | this.openConcept = this.openConcept.bind(this); 39 | this.closeConcept = this.closeConcept.bind(this); 40 | 41 | } 42 | 43 | componentDidMount() { 44 | if (!this.state.fetched) { 45 | this.props.conceptEditors.fetchConcepts(); 46 | this.setState({fetched: true}); 47 | } 48 | const childRef = this.props.childRef; 49 | childRef(this); 50 | } 51 | componentWillUnmount() { 52 | const childRef = this.props.childRef; 53 | childRef(undefined); 54 | } 55 | 56 | openConcept(cname) { 57 | this.resetHighlights(); 58 | this.setState({openConcept: cname}); 59 | } 60 | 61 | closeConcept() { 62 | this.resetHighlights(); 63 | this.setState({openConcept: null}); 64 | } 65 | 66 | resetHighlights() { 67 | this.props.highlight([]); 68 | } 69 | 70 | changeView(new_view) { 71 | this.setState({...this.state, view: new_view}) 72 | } 73 | 74 | handleInput(event) { 75 | this.setState({ 76 | conceptName: event.target.value 77 | }); 78 | } 79 | 80 | handleAdd() { 81 | const newConcept = this.state.conceptName.trim() 82 | if (newConcept !== "") { 83 | this.setState({conceptName: ""}); 84 | this.props.conceptEditors.addConcept(newConcept); 85 | this.props.select_concept(newConcept); 86 | this.openConcept(newConcept); 87 | } 88 | } 89 | 90 | handleKeyPress(event){ 91 | if (event.key === 'Enter') { 92 | this.handleAdd(); 93 | } 94 | } 95 | 96 | render() { 97 | const conceptNames = Object.keys(this.props.concepts); 98 | const classes = this.props.classes; 99 | 100 | return ( 101 | 102 | 103 | Concepts 104 | 108 | 109 |
110 | 111 | { conceptNames.map( (concept) => 112 | this.openConcept(concept)} close={this.closeConcept} isOpen={this.state.openConcept===concept} mustClose={this.props.closeAll} view={this.state.view} key={concept} concept={concept} classes={classes} addAnnotations={this.props.addAnnotations}/> 113 | ) 114 | } 115 | 116 | 128 | 129 | }} 130 | /> 131 | 132 | 133 |
134 | ); 135 | } 136 | } 137 | 138 | ConceptCollection.propTypes = { 139 | addAnnotations: PropTypes.func, 140 | classes: PropTypes.object.isRequired, 141 | conceptEditors: PropTypes.object.isRequired 142 | }; 143 | 144 | function mapStateToProps(state, ownProps?) { 145 | return { concepts: state.concepts.data }; 146 | } 147 | 148 | function mapDispatchToProps(dispatch) { 149 | return { 150 | conceptEditors: bindActionCreators(conceptEditors, dispatch), 151 | select_concept: bindActionCreators(select_concept, dispatch) , 152 | highlight: bindActionCreators(highlight, dispatch) 153 | }; 154 | } 155 | 156 | export default connect(mapStateToProps, mapDispatchToProps)(ConceptCollection); 157 | -------------------------------------------------------------------------------- /ui/src/ConceptElement.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from "react-redux"; 4 | import PropTypes from 'prop-types'; 5 | 6 | 7 | import AddIcon from '@material-ui/icons/Add'; 8 | import CloseIcon from '@material-ui/icons/Close'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import MenuItem from '@material-ui/core/MenuItem'; 11 | import Select from '@material-ui/core/Select'; 12 | import Switch from '@material-ui/core/Switch'; 13 | import TableCell from '@material-ui/core/TableCell'; 14 | import TableRow from '@material-ui/core/TableRow'; 15 | import TextField from '@material-ui/core/TextField'; 16 | import Tooltip from '@material-ui/core/Tooltip'; 17 | import Typography from '@material-ui/core/Typography'; 18 | 19 | import { highlight_regex, highlight, annotate, TokenToRegex, isToken } from './actions/annotate' 20 | 21 | export const TOKEN_VIEW = 0; 22 | export const REGEX_VIEW = 3; 23 | 24 | class ConceptElement extends React.Component { 25 | constructor(props) { 26 | super(props); 27 | var token_text = this.props.token.string; 28 | if (isToken(this.props.token.string)) { 29 | token_text = isToken(this.props.token.string) 30 | } 31 | this.state = { 32 | type: this.props.view===TOKEN_VIEW ? this.props.keyType["TOKEN"] : this.props.keyType["REGEXP"], //default type. will be overrided if a type is provided. 33 | case_sensitive: false, 34 | ...this.props.token, 35 | token_text 36 | } 37 | 38 | this.availableKeyTypes = {}; // create a mapping {key_code: key_name} 39 | for (var i = ["TOKEN", "REGEXP"].length - 1; i >= 0; i--) { 40 | let keytype_name = ["TOKEN", "REGEXP"][i]; 41 | this.availableKeyTypes[this.props.keyType[keytype_name]] = keytype_name; 42 | } 43 | 44 | } 45 | 46 | handleKeyPress(event){ 47 | if (event.key === 'Enter') { 48 | this.submit(); 49 | } 50 | } 51 | 52 | resetHighlights() { 53 | this.props.highlight([]); 54 | } 55 | 56 | handleInput(event) { 57 | const raw_str = event.target.value; 58 | var regex = raw_str; 59 | var newState = {}; 60 | if (this.state.type === this.props.keyType["TOKEN"]) { // it's a token 61 | if (this.props.view === TOKEN_VIEW) { // token view 62 | newState = { 63 | string: TokenToRegex(regex), 64 | token_text: event.target.value 65 | } 66 | } else { //regex view 67 | newState = { 68 | type: this.props.keyType["REGEXP"], 69 | string: event.target.value, 70 | token_text: event.target.value 71 | } 72 | } 73 | } else { // it's a regexp. view doesn't matter. 74 | newState = { 75 | string: event.target.value, 76 | token_text: event.target.value 77 | } 78 | } 79 | // annotate 80 | this.refreshHighlights(newState.string); 81 | this.setState(newState); 82 | } 83 | 84 | changeKeytype(new_type) { 85 | var newState = {}; 86 | if (new_type===0) { // regexp change to token 87 | newState = { 88 | type: this.props.keyType[new_type], 89 | token_text: this.state.string, 90 | string: TokenToRegex(this.state.string) 91 | }; 92 | } else { // token change to regexp 93 | newState = { 94 | type: this.props.keyType[new_type], 95 | string: this.state.token_text, 96 | token_text: this.state.token_text 97 | }; 98 | } 99 | this.refreshHighlights(newState.string); 100 | this.setState(newState); 101 | } 102 | 103 | refreshHighlights(regex_string=this.state.string, case_sensitive=this.state.case_sensitive) { 104 | if (!this.isEmpty()) { 105 | this.props.highlight_regex(regex_string, this.props.text, case_sensitive, this.props.idx); 106 | } else { 107 | this.resetHighlights(); 108 | } 109 | } 110 | 111 | changeCaseSensitivity() { 112 | const bool = this.state.case_sensitive; 113 | this.refreshHighlights(this.state.string, !bool); 114 | this.setState({case_sensitive: !bool}); 115 | } 116 | 117 | submit() { 118 | if (this.isValid()) { 119 | if (this.state!==this.props.token) { 120 | this.props.changeToken(this.state); 121 | // annotate the text with the new condition, if applicable 122 | const hlights = this.props.highlights.map(hlight => { 123 | hlight.id = hlight.start_offset; 124 | hlight.label = this.props.concept; 125 | hlight.origin = this.state.string; 126 | return hlight; 127 | }); 128 | this.props.annotate(this.props.addAnnotations(hlights)); 129 | } 130 | } 131 | this.resetHighlights(); 132 | } 133 | 134 | isEmpty(state=null){ 135 | if (state===null) { 136 | state = this.state; 137 | } 138 | if (state.string==="") { 139 | return true; 140 | } 141 | if (state.token_text==="") { 142 | return true; 143 | } 144 | return false 145 | } 146 | 147 | onBlur() { // Event triggered when textfield loses focus (click outside, for example) 148 | if (!this.props.new_entry) { 149 | this.submit(); 150 | } 151 | } 152 | 153 | isValid() { 154 | if (this.isEmpty()) { 155 | return false 156 | } 157 | if (this.props.error) { 158 | return false; 159 | } 160 | return true; 161 | } 162 | 163 | render() { 164 | const keytype = this.state.type; 165 | var SelectField = null; 166 | if (this.props.view===TOKEN_VIEW) { 167 | SelectField = 176 | } 177 | return ( 178 | 179 | 180 | 190 | 191 | 192 | {this.props.view===TOKEN_VIEW ? 193 | {SelectField} 194 | : null} 195 | 196 | 197 | 198 | 199 | 200 | 201 | 206 | {this.props.delete ? : } 207 | 208 | 209 | 210 | ) 211 | } 212 | } 213 | 214 | ConceptElement.propTypes = { 215 | idx: PropTypes.number.isRequired, 216 | token: PropTypes.object.isRequired, 217 | changeToken: PropTypes.func.isRequired, 218 | view: PropTypes.number.isRequired, 219 | } 220 | 221 | function mapStateToProps(state, ownProps?) { 222 | const error = state.highlights.error[ownProps.idx]; 223 | return { 224 | text: state.text.data, 225 | error: error, 226 | highlights: state.highlights.data, 227 | keyType: state.gll.keyType, 228 | }; 229 | } 230 | 231 | function mapDispatchToProps(dispatch) { 232 | return { 233 | annotate: bindActionCreators(annotate, dispatch), 234 | highlight: bindActionCreators(highlight, dispatch), 235 | highlight_regex: bindActionCreators(highlight_regex, dispatch) 236 | }; 237 | } 238 | 239 | export default connect(mapStateToProps, mapDispatchToProps)(ConceptElement); -------------------------------------------------------------------------------- /ui/src/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core/styles'; 3 | import FavoriteIcon from '@material-ui/icons/Favorite'; 4 | import GithubIcon from './GithubIcon'; 5 | import Toolbar from '@material-ui/core/Toolbar'; 6 | import clsx from 'clsx'; 7 | import IconButton from "@material-ui/core/IconButton"; 8 | 9 | const drawerWidth = 200; 10 | 11 | const useStyles = makeStyles(theme => ({ 12 | root: { 13 | position:'fixed', 14 | bottom:0, 15 | left:0, 16 | width:'100%', 17 | justifyContent: 'center', 18 | alignItems: 'center', 19 | background:"rgba(50, 50, 50, 1)" 20 | }, 21 | footer: { 22 | transition: theme.transitions.create(['margin', 'width'], { 23 | easing: theme.transitions.easing.sharp, 24 | duration: theme.transitions.duration.leavingScreen, 25 | }), 26 | }, 27 | footerShift: { 28 | width: `calc(100% - ${drawerWidth}px)`, 29 | marginLeft: drawerWidth, 30 | transition: theme.transitions.create(['margin', 'width'], { 31 | easing: theme.transitions.easing.easeOut, 32 | duration: theme.transitions.duration.enteringScreen, 33 | }) 34 | }, 35 | menuButton: { 36 | marginRight: theme.spacing(2), 37 | }, 38 | icon:{ 39 | color:'white' 40 | } 41 | 42 | })); 43 | 44 | 45 | function Footer(props) { 46 | 47 | const classes = useStyles(), 48 | isDrawerOpen = props.isDrawerOpen; 49 | 50 | function handleClick(url) { 51 | const win = window.open(url, '_blank'); 52 | win.focus(); 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default Footer; 65 | -------------------------------------------------------------------------------- /ui/src/GithubIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SvgIcon from '@material-ui/core/SvgIcon'; 3 | 4 | const GithubIcon = props => ( 5 | 6 | 9 | 10 | ); 11 | 12 | export default GithubIcon; 13 | -------------------------------------------------------------------------------- /ui/src/LFPanel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from "react-redux"; 4 | 5 | import Button from '@material-ui/core/Button'; 6 | import CircularProgress from '@material-ui/core/CircularProgress'; 7 | import Container from '@material-ui/core/Container'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import Grid from '@material-ui/core/Grid'; 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import InfoIcon from '@material-ui/icons/Info'; 12 | import Paper from '@material-ui/core/Paper'; 13 | import Popover from '@material-ui/core/Popover'; 14 | import Table from '@material-ui/core/Table'; 15 | import TableBody from '@material-ui/core/TableBody'; 16 | import TableCell from '@material-ui/core/TableCell'; 17 | import TableRow from '@material-ui/core/TableRow'; 18 | import Typography from '@material-ui/core/Typography'; 19 | import WarningIcon from '@material-ui/icons/Warning'; 20 | 21 | import AnnotationDisplayCollapse from './AnnotationDisplayCollapse' 22 | import { style } from './SortingTableUtils' 23 | import { getLFexamples } from './actions/submitLFs' 24 | import { getInteraction, deleteInteraction, setAsCurrentInteraction } from './actions/interaction' 25 | 26 | class LFPanel extends React.Component { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | anchorEl: null, 31 | }; 32 | } 33 | 34 | handleOpen(event) { 35 | this.props.getLFexamples(this.props.lf.id); 36 | this.setState({anchorEl: event.currentTarget}); 37 | } 38 | 39 | handleClose() { 40 | this.setState({ 41 | anchorEl: null, 42 | }); 43 | this.props.getLFexamples(null); 44 | } 45 | 46 | componentDidUpdate(prevProps) { 47 | if (prevProps.pending && !this.props.pending) { 48 | this.setState({pending: false}); 49 | } 50 | } 51 | 52 | returnToInteraction() { 53 | this.props.setAsCurrentInteraction(this.props.lf.interaction_idx); 54 | } 55 | 56 | render() { 57 | var classes = this.props.classes; 58 | const anchorEl = this.state.anchorEl; 59 | const open = (Boolean(anchorEl) && !this.props.pending); 60 | 61 | var fields_to_display = ["conditions", "label", "Coverage Training", "Conflicts Training", "Emp. Acc.", "Recall","Correct", "Incorrect"] 62 | var names_to_display = ["Conditions", "Label", "Train Set Coverage", "Train Set Conflicts", "Precision", "Recall","Correct", "Incorrect", "Context"] 63 | 64 | if (this.props.lf.context) { 65 | fields_to_display.splice(0, "Context", 1); 66 | names_to_display.splice(0, "Context", 1); 67 | } 68 | return ( 69 | 70 | 71 | 72 | 82 | 83 | 93 | 94 | 95 | 96 | Statistics 97 | Computed over the training data unless otherwise noted. 98 | 99 | {fields_to_display.map((key, idx) => 100 | 101 | {names_to_display[idx]} 102 | {isNaN(this.props.lf[key]) ? this.props.lf[key] : style(this.props.lf[key])} 103 | 104 | )} 105 |
106 |
107 | 108 | False Positives ({this.props.mistakes.length}) 109 | Up to 5 examples from the development set that are INCORRECTLY labeled by this function. 110 |
111 | {this.props.pending ? < CircularProgress/> : this.props.mistakes.map((example_dict, idx) => 112 | 113 | 114 | 115 | 120 | 121 | )} 122 |
123 |
124 | 125 | 126 | Matches ({this.props.examples.length}) 127 | Examples from the training data that are labeled by this function. 128 |
129 | {this.props.pending ? < CircularProgress/> : this.props.examples.map((example_dict, idx) => 130 | 131 | 132 | 137 | )} 138 |
139 |
140 | 141 |
142 |
143 |
144 |
145 | ); 146 | } 147 | } 148 | 149 | function mapStateToProps(state, ownProps?) { 150 | return { 151 | pending: !Boolean(state.labelExamples.examples), //TODO add pending field to labelExamples 152 | examples: state.labelExamples.examples || [], 153 | mistakes: state.labelExamples.mistakes || [] 154 | } 155 | } 156 | 157 | function mapDispatchToProps(dispatch) { 158 | return { 159 | getInteraction: bindActionCreators(getInteraction, dispatch), 160 | deleteInteraction: bindActionCreators(deleteInteraction, dispatch), 161 | setAsCurrentInteraction: bindActionCreators(setAsCurrentInteraction, dispatch), 162 | getLFexamples: bindActionCreators(getLFexamples, dispatch) 163 | }; 164 | } 165 | 166 | export default connect(mapStateToProps, mapDispatchToProps)(LFPanel); -------------------------------------------------------------------------------- /ui/src/LabelCreation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from "react-redux"; 4 | 5 | import fetchClasses, {addLabelClass} from './actions/labelClasses'; 6 | 7 | import AddIcon from '@material-ui/icons/Add'; 8 | import Button from '@material-ui/core/Button'; 9 | import TextField from '@material-ui/core/TextField'; 10 | import Typography from '@material-ui/core/Typography'; 11 | 12 | const colorTheme = [ 13 | { 14 | backgroundColor: 'rgb(255,236,153)', 15 | textColor: "rgb(239,140,2)", 16 | }, 17 | { 18 | backgroundColor: "rgb(183,217,179)", 19 | textColor: "rgb(43,138,62)", 20 | }, 21 | { 22 | backgroundColor: "rgb(201,231,239)", 23 | textColor: "rgb(12,152,173)", 24 | }, 25 | { 26 | backgroundColor: "rgb(227,217,237)", 27 | textColor: "rgb(83,75,154)", 28 | }, 29 | { 30 | backgroundColor: "rgb(248,200,201)", 31 | textColor: "rgb(225,49,49)", 32 | }, 33 | ] 34 | 35 | class LabelCreation extends React.Component { 36 | constructor(props) { 37 | super(props); 38 | if (this.props.labelClassesPending) { 39 | this.props.fetchClasses(); 40 | } 41 | var new_label_key = Object.keys(props.labelClasses).length; 42 | 43 | this.state = { 44 | newLabel: "", 45 | fetched: false, 46 | new_label_key: new_label_key 47 | }; 48 | this.handleInput = this.handleInput.bind(this); 49 | this.handleKeyPress = this.handleKeyPress.bind(this); 50 | this.handleAdd = this.handleAdd.bind(this); 51 | 52 | } 53 | 54 | handleInput(event) { 55 | this.setState({ 56 | newLabel: event.target.value 57 | }); 58 | } 59 | 60 | 61 | handleAdd() { 62 | const newLabel = this.state.newLabel.trim() 63 | if (newLabel !== "") { 64 | let lkey = Object.keys(this.props.labelClasses).length % colorTheme.length 65 | this.props.addLabel({key: lkey, 66 | name:newLabel, 67 | backgroundColor:colorTheme[lkey].backgroundColor, 68 | textColor:colorTheme[lkey].textColor 69 | }); 70 | this.setState({ 71 | newLabel: "", 72 | new_label_key: Object.keys(this.props.labelClasses).length + 1 73 | }); 74 | } 75 | } 76 | 77 | handleKeyPress(event){ 78 | if (event.key === 'Enter') { 79 | this.handleAdd(); 80 | } 81 | } 82 | 83 | render() { 84 | const labels = Object.values(this.props.labelClasses); 85 | const classes = this.props.classes; 86 | 87 | return( 88 |
89 | What would you like to name this project? 90 |
91 | 97 |
98 | Name your label classes. 99 |
100 | { labels.map( (item) => { 101 | console.log(item); 102 | var lname = item.name; 103 | var value = item.key; 104 | return( 105 |
106 | 114 |
115 | ) 116 | }) 117 | } 118 | 128 | 129 | }} 130 | /> 131 |
132 |
133 | ) 134 | } 135 | } 136 | 137 | function mapStateToProps(state, ownProps?) { 138 | return { 139 | labelClasses: state.labelClasses.data, 140 | hotKeys: state.labelClasses.data, 141 | labelClassesPending: state.labelClasses.pending, 142 | }; 143 | } 144 | 145 | function mapDispatchToProps(dispatch){ 146 | // TODO action ADDLABEL 147 | return { 148 | addLabel: bindActionCreators(addLabelClass, dispatch), 149 | fetchClasses: bindActionCreators(fetchClasses, dispatch) 150 | }; 151 | } 152 | 153 | export default connect(mapStateToProps, mapDispatchToProps)(LabelCreation); -------------------------------------------------------------------------------- /ui/src/LabelingFunctionsSuggested.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import {connect} from "react-redux"; 4 | import {bindActionCreators} from 'redux'; 5 | import PropTypes from 'prop-types'; 6 | 7 | import Checkbox from '@material-ui/core/Checkbox'; 8 | import InfoIcon from '@material-ui/icons/Info'; 9 | import Paper from '@material-ui/core/Paper'; 10 | import Table from '@material-ui/core/Table'; 11 | import TableBody from '@material-ui/core/TableBody'; 12 | import TableCell from '@material-ui/core/TableCell'; 13 | import TableHead from '@material-ui/core/TableHead'; 14 | import TableRow from '@material-ui/core/TableRow'; 15 | import Tooltip from '@material-ui/core/Tooltip'; 16 | import Typography from '@material-ui/core/Typography'; 17 | import WarningIcon from '@material-ui/icons/Warning'; 18 | 19 | import { set_selected_LF } from './actions/labelAndSuggestLF' 20 | 21 | class LabelingFunctionsSuggested extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | this.state = { 25 | all_selected: false 26 | }; 27 | 28 | } 29 | 30 | 31 | componentDidUpdate(prevProps) { 32 | if (this.state.all_selected) { 33 | for (var i = Object.values(this.props.labelingFunctions).length - 1; i >= 0; i--) { 34 | let lf = Object.values(this.props.labelingFunctions)[i]; 35 | if (lf.selected !== true) { 36 | this.setState({all_selected: false}) 37 | } 38 | } 39 | } 40 | } 41 | 42 | label(lf) { 43 | return ( 44 | this.props.labelClasses 45 | .filter(c => c.key === lf.Label)[0].name 46 | ); 47 | } 48 | 49 | conditionToString(condition) { 50 | let string = condition["string"]; 51 | if (condition["case_sensitive"]) { 52 | string = ""+string+""; 53 | } 54 | if (condition.type === this.props.keyType["CONTEXT_SIMILAR"]){ 55 | string = "\""+string +"\"" 56 | } 57 | 58 | let condition_type_text = {condition.TYPE_} 59 | if ("explanation" in condition) { // Add mouseover explanation, if available 60 | condition_type_text = {condition_type_text} 61 | } 62 | 63 | if (condition.positive) { 64 | return <>{string} ({condition_type_text}); 65 | } else { 66 | return <>NOT({string}) ({condition_type_text}); 67 | } 68 | } 69 | 70 | conditions(lf) { 71 | const conditions = lf.Conditions.map(cond => this.conditionToString(cond)); 72 | return conditions 73 | .reduce((prev, curr) => [prev, " " + lf.CONNECTIVE_ + " ", curr]) 74 | } 75 | 76 | LFtoStrings(key, lf) { 77 | const stringsDict = { 78 | id: key, 79 | conditions: this.conditions(lf), 80 | context: lf.CONTEXT_, 81 | label: this.label(lf), 82 | order: lf.Direction.toString(), 83 | weight: lf.Weight, 84 | target: lf.Target 85 | }; 86 | return stringsDict; 87 | } 88 | 89 | selectAllLF(bool_selected) { 90 | // (de)select all LFs, depending on value of bool_selected 91 | const LF_names = Object.keys(this.props.labelingFunctions); 92 | 93 | let newLFs = {}; 94 | for (var i = LF_names.length - 1; i >= 0; i--) { 95 | let LF_key = LF_names[i]; 96 | newLFs[LF_key] = this.props.labelingFunctions[LF_key]; 97 | newLFs[LF_key]['selected'] = bool_selected; 98 | } 99 | 100 | this.setState({all_selected: bool_selected}); 101 | this.props.set_selected_LF(newLFs); 102 | } 103 | 104 | handleChange(name, event) { 105 | let updatedLF = this.props.labelingFunctions[name]; 106 | updatedLF['selected'] = !(updatedLF['selected']); 107 | const newLFs = { 108 | ...this.props.labelingFunctions, 109 | [name]: updatedLF 110 | }; 111 | this.props.set_selected_LF(newLFs); 112 | } 113 | 114 | 115 | 116 | render() { 117 | const classes = this.props.classes; 118 | 119 | var show_context = false; 120 | const LFList = Object.keys(this.props.labelingFunctions).map((lf_key) => { 121 | var lf_dict = this.LFtoStrings(lf_key, this.props.labelingFunctions[lf_key]) 122 | if (lf_dict.context) { 123 | show_context = true; 124 | } 125 | return lf_dict; 126 | }); 127 | var LF_content = 128 | 129 | 130 | 131 | this.selectAllLF(!this.state.all_selected)} 133 | checked={this.state.all_selected} 134 | /> 135 | { this.state.all_selected ? "Deselect All" : "Select All"} 136 | 137 | Token 138 | Conditions 139 | { show_context ? Context : null} 140 | Label 141 | {/*Reliability*/} 142 | 143 | 144 | 145 | {LFList.map(row => ( 146 | 147 | 148 | this.handleChange(row.id, event)} 151 | checked={this.props.labelingFunctions[row.id].selected===true}/> 152 | 153 | {row.target} 154 | {row.conditions} 155 | {/*show_context ? {row.context} : null*/} 156 | {/*{row.order}*/} 157 | {/*{row.label}*/} 158 | 159 | 172 | 173 | {/*{(row.weight).toFixed(2)}*/} 174 | 175 | ))} 176 | 177 |
178 | 179 | return( 180 | 181 | 182 | Suggested Labeling Functions 183 | 184 | { this.props.no_label ? {"You must assign a label in order to generate labeling functions!"} : "" } 185 | { (this.props.no_annotations && !(this.props.no_label)) ? {"TIP: to improve function suggestions, annotate the parts of the text that guided your decision."} : "" } 186 | {LF_content} 187 | 188 | ); 189 | } 190 | } 191 | 192 | LabelingFunctionsSuggested.propTypes = { 193 | all_selected: PropTypes.bool 194 | }; 195 | 196 | function mapStateToProps(state, ownProps?) { 197 | 198 | return { 199 | labelingFunctions: state.suggestedLF, 200 | labelClasses:state.labelClasses.data, 201 | no_annotations: (state.annotations.length < 1), 202 | no_label: (state.label === null), 203 | keyType: state.gll.keyType 204 | }; 205 | } 206 | 207 | function mapDispatchToProps(dispatch) { 208 | return { 209 | set_selected_LF: bindActionCreators(set_selected_LF, dispatch) 210 | }; 211 | } 212 | 213 | export default connect(mapStateToProps, mapDispatchToProps)(LabelingFunctionsSuggested); -------------------------------------------------------------------------------- /ui/src/LeftDrawer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Drawer from '@material-ui/core/Drawer'; 3 | import List from '@material-ui/core/List'; 4 | import Divider from '@material-ui/core/Divider'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 7 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 8 | import ListItem from '@material-ui/core/ListItem'; 9 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 10 | import ListItemText from '@material-ui/core/ListItemText'; 11 | import { makeStyles, useTheme } from '@material-ui/core/styles'; 12 | import {default as TaskIcon} from '@material-ui/icons/Assignment' ; 13 | import {default as DashboardIcon} from '@material-ui/icons/BarChart'; 14 | import {default as LabelsIcon} from '@material-ui/icons/Label'; 15 | import {default as DatasetIcon} from '@material-ui/icons/TextFormat'; 16 | import {default as SatisfiedIcon} from '@material-ui/icons/SentimentSatisfied'; 17 | import {default as VerySatisfiedIcon} from '@material-ui/icons/SentimentVerySatisfied'; 18 | import {default as MoodIcon} from '@material-ui/icons/Mood'; 19 | 20 | 21 | const drawerWidth = 200; 22 | 23 | const useStyles = makeStyles(theme => ({ 24 | root: { 25 | display: 'flex', 26 | }, 27 | menuButton: { 28 | marginRight: theme.spacing(2), 29 | }, 30 | hide: { 31 | display: 'none', 32 | }, 33 | drawer: { 34 | width: drawerWidth, 35 | flexShrink: 0, 36 | }, 37 | drawerPaper: { 38 | width: drawerWidth, 39 | }, 40 | drawerHeader: { 41 | display: 'flex', 42 | alignItems: 'center', 43 | padding: '0 8px', 44 | ...theme.mixins.toolbar, 45 | justifyContent: 'flex-end', 46 | }, 47 | content: { 48 | flexGrow: 1, 49 | padding: theme.spacing(3), 50 | transition: theme.transitions.create('margin', { 51 | easing: theme.transitions.easing.sharp, 52 | duration: theme.transitions.duration.leavingScreen, 53 | }), 54 | marginLeft: -drawerWidth, 55 | }, 56 | contentShift: { 57 | transition: theme.transitions.create('margin', { 58 | easing: theme.transitions.easing.easeOut, 59 | duration: theme.transitions.duration.enteringScreen, 60 | }), 61 | marginLeft: 0, 62 | }, 63 | })); 64 | 65 | 66 | const LeftDrawer = (props) => { 67 | 68 | const theme = useTheme(), 69 | classes = useStyles(); 70 | 71 | 72 | 73 | return( 74 | 83 |
84 | 85 | {theme.direction === 'ltr' ? : } 86 | 87 |
88 | 89 | 90 | {['Dataset', 'Labels', 'Task Guidelines', 'Dashboard'].map((text, index) => ( 91 | 92 | {index === 0 ? : 93 | index === 1 ? : 94 | index === 2 ? : 95 | } 96 | 97 | 98 | 99 | ))} 100 | 101 | 102 | 103 | {['Other', 'Labeling', 'Tasks'].map((text, index) => ( 104 | 105 | {index === 0 ? : 106 | index === 1 ? : 107 | } 108 | 109 | 110 | 111 | ))} 112 | 113 | 114 |
115 | ); 116 | 117 | }; 118 | 119 | export default LeftDrawer; 120 | 121 | -------------------------------------------------------------------------------- /ui/src/Link.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import CancelIcon from '@material-ui/icons/Cancel'; 5 | import Badge from '@material-ui/core/Badge'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | 8 | const defaultAnchor = { x: 0.5, y: 0 }; 9 | const defaultBorderColor = '#6A747E'; 10 | const defaultBorderWidth = 2; 11 | const defaultOffset = 30; 12 | const defaultOpacity = 0.25 13 | 14 | 15 | export default class Link extends React.Component { 16 | constructor(props){ 17 | super(props); 18 | 19 | this.state = { 20 | isHovering: true, 21 | width: 0, 22 | height: 0 23 | }; 24 | 25 | this.updateDimensions = this.updateDimensions.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | window.addEventListener('resize', this.updateDimensions); 30 | } 31 | 32 | componentWillUnmount() { 33 | window.removeEventListener('resize', this.updateDimensions); 34 | } 35 | 36 | findElement(id) { 37 | return document.getElementById(id); 38 | } 39 | 40 | updateDimensions() { 41 | this.setState({ width: window.innerWidth, height: window.innerHeight }); 42 | } 43 | 44 | detect() { 45 | const { from, to} = this.props; 46 | 47 | const a = this.findElement(from); 48 | const b = this.findElement(to); 49 | 50 | if (!a || !b) { 51 | console.error(`Elements with ids ${from} and ${to} were not found.`); 52 | return false; 53 | } 54 | 55 | const anchor0 = defaultAnchor; 56 | const anchor1 = defaultAnchor; 57 | 58 | const box0 = a.getBoundingClientRect(); 59 | const box1 = b.getBoundingClientRect(); 60 | 61 | let offsetX = window.pageXOffset; 62 | let offsetY = window.pageYOffset; 63 | 64 | let x0 = box0.left + box0.width * anchor0.x + offsetX; 65 | let x1 = box1.left + box1.width * anchor1.x + offsetX; 66 | const y0 = box0.top + box0.height * anchor0.y + offsetY; 67 | const y1 = box1.top + box1.height * anchor1.y + offsetY; 68 | 69 | if (Math.abs(x0-x1) < 30) { 70 | x0 = box0.left + offsetX; 71 | x1 = box1.left + box1.width + offsetX; 72 | } 73 | return { x0, y0, x1, y1 }; 74 | } 75 | 76 | handleMouseHover(newState) { 77 | this.setState({isHovering: newState}); 78 | } 79 | 80 | render() { 81 | this.props.classes.anchorOriginTopRightRectangle = { 82 | bottom: 0, 83 | right: 0, 84 | transform: 'scale(1) translate(0%, -50%)', 85 | transformOrigin: '100% 100%', 86 | '&$invisible': { 87 | transform: 'scale(0) translate(50%, 50%)', 88 | }, 89 | } 90 | 91 | const classes = this.props.classes; 92 | let offset = this.props.offset || defaultOffset; 93 | 94 | const points = this.detect(); 95 | let {x0, y0, x1, y1} = points; 96 | if (!points) { 97 | return( 98 | 99 | ) 100 | } 101 | 102 | const leftOffset = Math.min(x0, x1); 103 | const topOffset = Math.min(y0, y1) - offset; 104 | 105 | x0 -= leftOffset; 106 | x1 -= leftOffset; 107 | y0 -= topOffset; 108 | y1 -= topOffset; 109 | 110 | const width = Math.abs(x0 - x1); 111 | 112 | const positionStyle = { 113 | position: 'absolute', 114 | top: `${topOffset}px`, 115 | left: `${leftOffset}px`, 116 | width: width, 117 | height: Math.abs(y0 - y1) + offset 118 | } 119 | 120 | const color = this.props.color || defaultBorderColor; 121 | const strokeWidth = this.props.width || defaultBorderWidth; 122 | const strokeOpacity = this.props.opacity || defaultOpacity; 123 | 124 | return ( 125 | this.handleMouseHover(true)} 130 | onMouseLeave={() => this.handleMouseHover(false)} 131 | onClick={this.props.onDelete}> 132 | 133 | 134 | } 135 | invisible={this.props.onDelete ? false : true} 136 | style={positionStyle}> 137 | 138 | this.handleMouseHover(true)} 146 | onMouseLeave={() => this.handleMouseHover(false)}/> 147 | 148 | 149 | ) 150 | } 151 | } 152 | 153 | Link.propTypes = { 154 | from: PropTypes.string.isRequired, 155 | to: PropTypes.string.isRequired, 156 | width: PropTypes.number, 157 | color: PropTypes.string, 158 | onDelete: PropTypes.func, 159 | offSet: PropTypes.number, 160 | classes: PropTypes.object 161 | } -------------------------------------------------------------------------------- /ui/src/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { withStyles } from '@material-ui/core/styles'; 3 | import CssBaseline from '@material-ui/core/CssBaseline'; 4 | import NavigationBar from './NavigationBar'; 5 | import LeftDrawer from './LeftDrawer'; 6 | import ProjectGrid from './ProjectGrid'; 7 | import Footer from './Footer'; 8 | import ProjectCreation from './ProjectCreation' 9 | 10 | import { 11 | BrowserRouter as Router, 12 | Switch, 13 | Route, 14 | } from "react-router-dom"; 15 | 16 | const styles = { 17 | grow: { 18 | flexGrow: 1, 19 | } 20 | }; 21 | 22 | class Main extends React.Component { 23 | 24 | constructor(props) { 25 | super(props); 26 | 27 | this.handleDrawerClose = this.handleDrawerClose.bind(this); 28 | this.handleDrawerOpen = this.handleDrawerOpen.bind(this); 29 | 30 | this.drawerWidth = 200; 31 | this.state = {isDrawerOpen:false}; 32 | } 33 | 34 | handleDrawerClose(){ 35 | this.setState({isDrawerOpen:false} ) 36 | } 37 | 38 | handleDrawerOpen(){ 39 | this.setState({isDrawerOpen:true} ) 40 | } 41 | 42 | render() { 43 | const classes = this.props.classes, 44 | isDrawerOpen = this.state.isDrawerOpen 45 | 46 | return ( 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 |
66 | ); 67 | } 68 | } 69 | 70 | export default withStyles(styles)(Main); 71 | -------------------------------------------------------------------------------- /ui/src/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {bindActionCreators} from 'redux'; 3 | import {connect} from "react-redux"; 4 | 5 | // actions 6 | import getStatistics from './actions/getStatistics' 7 | import submitLFs, { getLFstats } from './actions/submitLFs' 8 | import { getText } from './actions/getText' 9 | import {clear_suggestions} from "./actions/labelAndSuggestLF"; 10 | import { setAsCurrentInteraction } from './actions/interaction' 11 | 12 | // presentational component 13 | import NavigationButtons from './NavigationButtons' 14 | 15 | class Navigation extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | queued_interactions: [] 20 | }; 21 | } 22 | 23 | selected_LFs(del=false) { 24 | const LF_ids = Object.keys(this.props.suggestedLF); 25 | var suggestedLF = this.props.suggestedLF; 26 | const selected_LFs = LF_ids.reduce( function (selected_LFs, id) { 27 | let LF = suggestedLF[id]; 28 | if (LF.selected) { 29 | if (del) { 30 | delete LF.selected; 31 | } 32 | selected_LFs[id] = LF; 33 | } 34 | return selected_LFs; 35 | }, {}); 36 | return selected_LFs; 37 | } 38 | 39 | clickNext() { 40 | const selected_LFs = this.selected_LFs(true); 41 | const LFS_WILL_UPDATE = (Object.keys(selected_LFs).length > 0); 42 | if (LFS_WILL_UPDATE) { 43 | for (var i = selected_LFs.length - 1; i >= 0; i--) { 44 | let lf = selected_LFs[i]; 45 | delete lf.selected; 46 | } 47 | this.props.submitLFs(selected_LFs); 48 | } 49 | 50 | if (this.state.queued_interactions.length===0) { 51 | this.props.fetchNextText(); 52 | } else { 53 | this.props.setAsCurrentInteraction(this.state.queued_interactions.pop()); 54 | } 55 | this.props.clear_suggestions(); 56 | this.props.getStatistics(); 57 | } 58 | 59 | clickPrevious() { 60 | this.state.queued_interactions.push(this.props.index); 61 | this.props.setAsCurrentInteraction(this.props.index - 1); 62 | } 63 | 64 | render() { 65 | const selected_LFs = this.selected_LFs(); 66 | const LFS_WILL_UPDATE = (Object.keys(selected_LFs).length > 0); 67 | const disableNext = LFS_WILL_UPDATE && this.props.LFLoading; 68 | const FabText = ((this.props.currentLabel === null) && (this.props.text.length !== 0)) ? "Skip" : "Next"; 69 | 70 | return( 0 ? this.clickPrevious.bind(this) : null} 74 | />) 75 | } 76 | } 77 | 78 | function mapStateToProps(state, ownProps?) { 79 | return { 80 | LFLoading: state.selectedLF.pending, 81 | text: state.text.data, 82 | suggestedLF: state.suggestedLF, 83 | currentLabel: state.label, 84 | index: state.text.index 85 | }; 86 | } 87 | function mapDispatchToProps(dispatch) { 88 | return { 89 | fetchNextText: bindActionCreators(getText, dispatch), 90 | getStatistics: bindActionCreators(getStatistics, dispatch), 91 | submitLFs: bindActionCreators(submitLFs, dispatch), 92 | clear_suggestions: bindActionCreators(clear_suggestions, dispatch), 93 | getLFstats: bindActionCreators(getLFstats, dispatch), 94 | setAsCurrentInteraction: bindActionCreators(setAsCurrentInteraction, dispatch) 95 | }; 96 | } 97 | 98 | export default connect(mapStateToProps, mapDispatchToProps)(Navigation); 99 | -------------------------------------------------------------------------------- /ui/src/NavigationBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {makeStyles} from '@material-ui/core/styles'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Toolbar from '@material-ui/core/Toolbar'; 5 | import IconButton from '@material-ui/core/IconButton'; 6 | import Typography from '@material-ui/core/Typography'; 7 | import Badge from '@material-ui/core/Badge'; 8 | import MenuItem from '@material-ui/core/MenuItem'; 9 | import Menu from '@material-ui/core/Menu'; 10 | import AccountCircle from '@material-ui/icons/AccountCircle'; 11 | import MailIcon from '@material-ui/icons/Mail'; 12 | import NotificationsIcon from '@material-ui/icons/Notifications'; 13 | import MoreIcon from '@material-ui/icons/MoreVert'; 14 | import clsx from 'clsx'; 15 | import SaveButton from './SaveButton' 16 | 17 | const drawerWidth = 200; 18 | 19 | const useStyles = makeStyles(theme => ({ 20 | grow: { 21 | flexGrow: 1, 22 | }, 23 | title: { 24 | display: 'none', 25 | [theme.breakpoints.up('sm')]: { 26 | display: 'block', 27 | }, 28 | }, 29 | appBar: { 30 | transition: theme.transitions.create(['margin', 'width'], { 31 | easing: theme.transitions.easing.sharp, 32 | duration: theme.transitions.duration.leavingScreen, 33 | }), 34 | }, 35 | appBarShift: { 36 | width: `calc(100% - ${drawerWidth}px)`, 37 | marginLeft: drawerWidth, 38 | transition: theme.transitions.create(['margin', 'width'], { 39 | easing: theme.transitions.easing.easeOut, 40 | duration: theme.transitions.duration.enteringScreen, 41 | }) 42 | }, 43 | inputRoot: { 44 | color: 'inherit', 45 | }, 46 | hide: { 47 | display: 'none', 48 | }, 49 | inputInput: { 50 | padding: theme.spacing(1, 1, 1, 7), 51 | transition: theme.transitions.create('width'), 52 | width: '100%', 53 | [theme.breakpoints.up('md')]: { 54 | width: 200, 55 | }, 56 | }, 57 | sectionDesktop: { 58 | display: 'none', 59 | [theme.breakpoints.up('md')]: { 60 | display: 'flex', 61 | }, 62 | }, 63 | sectionMobile: { 64 | display: 'flex', 65 | [theme.breakpoints.up('md')]: { 66 | display: 'none', 67 | }, 68 | }, 69 | })); 70 | 71 | const NavigationBar = (props)=> { 72 | 73 | const classes = useStyles(), 74 | open = props.isDrawerOpen; 75 | 76 | const [anchorEl, setAnchorEl] = React.useState(null); 77 | const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null); 78 | 79 | const isMenuOpen = Boolean(anchorEl); 80 | const isMobileMenuOpen = Boolean(mobileMoreAnchorEl); 81 | 82 | 83 | function handleProfileMenuOpen(event) { 84 | setAnchorEl(event.currentTarget); 85 | } 86 | 87 | function handleMobileMenuClose() { 88 | setMobileMoreAnchorEl(null); 89 | } 90 | 91 | function handleMenuClose() { 92 | setAnchorEl(null); 93 | handleMobileMenuClose(); 94 | } 95 | 96 | function handleMobileMenuOpen(event) { 97 | setMobileMoreAnchorEl(event.currentTarget); 98 | } 99 | const menuId = 'primary-search-account-menu'; 100 | const renderMenu = ( 101 | 110 | Profile 111 | My account 112 | Logout 113 | 114 | ); 115 | 116 | const mobileMenuId = 'primary-search-account-menu-mobile'; 117 | const renderMobileMenu = ( 118 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |

Messages

134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 |

Notifications

142 |
143 | 144 | 145 | 146 | 147 | 153 | 154 | 155 |

Profile

156 |
157 |
158 | ); 159 | 160 | return ( 161 | //
162 | 167 | 168 | 169 | TagŘuler: Data Programming By Demonstration for Span-Level Annotation in Text 170 | 171 | 172 |
173 |
174 | {/**/} 175 | {/* */} 176 | {/* */} 177 | {/* */} 178 | {/**/} 179 | {/**/} 180 | {/* */} 181 | {/* */} 182 | {/* */} 183 | {/**/} 184 | 192 | 193 | 194 | 195 |
196 |
197 | 204 | 205 | 206 |
207 | {renderMobileMenu} 208 | {renderMenu} 209 | 210 | 211 | ); 212 | }; 213 | 214 | export default NavigationBar; 215 | 216 | -------------------------------------------------------------------------------- /ui/src/NavigationButtons.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // material UI 4 | import ArrowIcon from '@material-ui/icons/ArrowForward'; 5 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 6 | import ButtonGroup from '@material-ui/core/ButtonGroup'; 7 | import Button from '@material-ui/core/Button'; 8 | 9 | export default class NavigationButtons extends React.Component { 10 | render() { 11 | return( 12 | 13 | 22 | 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /ui/src/ProjectCreation.js: -------------------------------------------------------------------------------- 1 | // material ui 2 | import Box from '@material-ui/core/Box'; 3 | import Button from '@material-ui/core/Button'; 4 | import LinearProgress from '@material-ui/core/LinearProgress'; 5 | import Paper from '@material-ui/core/Paper'; 6 | import Step from '@material-ui/core/Step'; 7 | import StepContent from '@material-ui/core/StepContent'; 8 | import StepLabel from '@material-ui/core/StepLabel'; 9 | import Stepper from '@material-ui/core/Stepper'; 10 | import Typography from '@material-ui/core/Typography'; 11 | 12 | import clsx from 'clsx'; 13 | import React, {useRef, useEffect} from 'react'; 14 | import {connect} from "react-redux"; 15 | import { Link as RouteLink } from "react-router-dom"; 16 | import { Redirect } from "react-router-dom"; 17 | import {bindActionCreators} from 'redux'; 18 | 19 | // actions 20 | import {submitLabelsAndName} from './actions/labelClasses'; 21 | import launch, { launchStatus } from './actions/loadingBar'; 22 | import fetchClasses, {addLabelClass} from './actions/labelClasses'; 23 | 24 | // components 25 | import Dataset from './Dataset'; 26 | import LabelCreation from './LabelCreation'; 27 | import { useStyles } from './ProjectGrid'; 28 | 29 | function LinearProgressWithLabel(props) { 30 | return ( 31 | 32 | 33 | 34 | 35 | 36 | {`${Math.round( 37 | props.value, 38 | )}%`} 39 | 40 | 41 | ); 42 | } 43 | 44 | function VerticalLinearStepper(props) { 45 | 46 | const [activeStep, setActiveStep] = React.useState(0); 47 | const steps = getSteps(); 48 | 49 | const classes = useStyles(), 50 | isDrawerOpen = props.isDrawerOpen; 51 | 52 | // this is used for getIdToken for a dummy user object 53 | const promise1 = new Promise((resolve, reject) => { 54 | resolve(true); 55 | }); 56 | 57 | const handleNext = () => { 58 | //TODO If we're on the label creation step, send the labels to server when next is clicked 59 | setActiveStep((prevActiveStep) => prevActiveStep + 1); 60 | }; 61 | 62 | const handleBack = () => { 63 | setActiveStep((prevActiveStep) => prevActiveStep - 1); 64 | }; 65 | 66 | const handleReset = () => { 67 | setActiveStep(0); 68 | }; 69 | 70 | function getSteps() { 71 | return ['Select Dataset', 'Create Project']; 72 | } 73 | 74 | function getStepContent(step) { 75 | switch (step) { 76 | case 0: 77 | return ( promise1}} 79 | classes={classes} 80 | />); 81 | case 1: 82 | return (); 83 | case 2: 84 | return (Continue to Task); 85 | default: 86 | return 'Unknown step'; 87 | } 88 | } 89 | 90 | function getStepButton(step) { 91 | 92 | switch (step) { 93 | case 1: 94 | function goToProject() { 95 | props.submitLabelsAndName(props.labelClasses); 96 | props.launch(); 97 | props.fetchClasses(); 98 | } 99 | return () 108 | default: 109 | return () 118 | } 119 | } 120 | 121 | 122 | const intervalRef = useRef(); 123 | 124 | useEffect(() => { 125 | const id = setInterval(() => { 126 | if (props.inProgress){ 127 | props.getLaunchProgress(props.launchThread); 128 | } 129 | }, 1000); 130 | intervalRef.current = id; 131 | return () => { 132 | clearInterval(intervalRef.current); 133 | }; 134 | }); 135 | 136 | if (props.launchProgress >= 100) { 137 | return ( ) 138 | } 139 | 140 | if (props.inProgress) { 141 | return( 142 |
143 | Your project is loading. You will be automatically redirected when it is complete. 144 |
145 | 146 |
147 | ) 148 | } 149 | 150 | return ( 151 |
152 | 153 | {steps.map((label, index) => ( 154 | 155 | {label} 156 | 157 | {getStepContent(index)} 158 |
159 |
160 | 167 | {getStepButton(index)} 168 |
169 |
170 |
171 |
172 | ))} 173 |
174 | {activeStep === steps.length && ( 175 | 176 | All steps completed - you're finished 177 | 180 | 181 | )} 182 |
183 | ); 184 | } 185 | 186 | function mapStateToProps(state, ownProps?) { 187 | return { 188 | selected_dataset: state.datasets.selected, 189 | labelClasses: state.labelClasses.data, 190 | launchProgress: state.launchProgress.progress*100, //convert to a percentage, not a fraction 191 | inProgress: (!(state.launchProgress.thread === null)), 192 | launchThread: state.launchProgress.thread, 193 | }; 194 | } 195 | function mapDispatchToProps(dispatch) { 196 | return { 197 | submitLabelsAndName: bindActionCreators(submitLabelsAndName, dispatch), 198 | launch: bindActionCreators(launch, dispatch), 199 | getLaunchProgress: bindActionCreators(launchStatus, dispatch), 200 | fetchClasses: bindActionCreators(fetchClasses, dispatch) 201 | }; 202 | } 203 | 204 | export default connect(mapStateToProps, mapDispatchToProps)(VerticalLinearStepper); -------------------------------------------------------------------------------- /ui/src/RichTextUtils.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { styled, withStyles } from '@material-ui/core/styles'; 4 | import Badge from '@material-ui/core/Badge'; 5 | import Box from '@material-ui/core/Box'; 6 | import Tooltip from '@material-ui/core/Tooltip'; 7 | 8 | export const InlineBox = styled(Box)({ 9 | width: "fit-content", 10 | display: "inline" 11 | }); 12 | 13 | const StyledBadge = withStyles((theme) => ({ 14 | badge: { 15 | right: "50%", 16 | top: 10, 17 | borderRadius: "50%", 18 | //background: `${theme.palette.background.paper}`, 19 | //padding: '0 4px', 20 | }, 21 | }))(Badge); 22 | 23 | const InlineBadge = styled(StyledBadge)({ 24 | width: "fit-content", 25 | display: "inline" 26 | }); 27 | 28 | export function NERSpan(text, NER, start_offset) { 29 | return( 30 | 34 | 35 | {NER.label}} //should be NER.label but that breaks the annotation... 41 | >{text} 42 | 43 | 44 | ) 45 | } 46 | 47 | export function MatchedSpan(text, matched_span_data, start_offset) { 48 | return( 49 | 53 | {text} 54 | 55 | ) 56 | } -------------------------------------------------------------------------------- /ui/src/SaveButton.js: -------------------------------------------------------------------------------- 1 | import IconButton from '@material-ui/core/IconButton'; 2 | import SaveAltIcon from '@material-ui/icons/SaveAlt'; 3 | 4 | import React from 'react'; 5 | import {bindActionCreators} from 'redux'; 6 | import {connect} from "react-redux"; 7 | import {saveModel} from './actions/save' 8 | 9 | const SaveButton = (props) => { 10 | return ( 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | 18 | function mapStateToProps(state, ownProps?) { 19 | return { 20 | }; 21 | } 22 | 23 | function mapDispatchToProps(dispatch) { 24 | return { 25 | save: bindActionCreators(saveModel, dispatch) 26 | }; 27 | } 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(SaveButton); 30 | -------------------------------------------------------------------------------- /ui/src/SelectedSpan.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from "react-redux"; 4 | 5 | import CancelIcon from '@material-ui/icons/Cancel'; 6 | import AddBoxIcon from '@material-ui/icons/AddBox'; 7 | import IndeterminateBoxIcon from '@material-ui/icons/IndeterminateCheckBox'; 8 | import IconButton from '@material-ui/core/IconButton'; 9 | import LinkIcon from '@material-ui/icons/Link'; 10 | import LinkOffIcon from '@material-ui/icons/LinkOff'; 11 | import Typography from '@material-ui/core/Typography'; 12 | 13 | import { InlineBox } from './RichTextUtils' 14 | import { DIR_LINK, UNDIR_LINK } from './AnnotationBuilder' 15 | 16 | 17 | import { styled } from '@material-ui/core/styles'; 18 | import Badge from '@material-ui/core/Badge'; 19 | 20 | import { colors } from '@material-ui/core'; 21 | import { MuiThemeProvider} from '@material-ui/core/styles'; 22 | import { createMuiTheme } from '@material-ui/core'; 23 | import { shadows } from '@material-ui/system'; 24 | 25 | const DeleteBadge = styled(Badge)({ 26 | width: "fit-content", 27 | display: "inline", 28 | left: 10 29 | }); 30 | 31 | const PositiveBadge = styled(Badge)({ 32 | width: "fit-content", 33 | display: "inline", 34 | top: -10, 35 | left: 9, 36 | }); 37 | 38 | const NegativeBadge = styled(Badge)({ 39 | width: "fit-content", 40 | display: "inline", 41 | top: -10, 42 | left: 30 43 | }); 44 | 45 | const LinkBadge = styled(Badge)({ 46 | width: "fit-content", 47 | display: "inline", 48 | //top: 5 49 | }); 50 | 51 | class SelectedSpan extends React.Component { 52 | constructor(props){ 53 | super(props); 54 | 55 | this.handleClick = this.handleClick.bind(this); 56 | this.handleMouseHover = this.handleMouseHover.bind(this); 57 | this.handlePositiveMouseHover = this.handlePositiveMouseHover.bind(this); 58 | this.handleNegativeMouseHover = this.handleNegativeMouseHover.bind(this); 59 | 60 | this.state = { 61 | isHovering: false, 62 | isPosHovering: false, 63 | isNegHovering: false, 64 | isSpanPositive: true, 65 | }; 66 | } 67 | 68 | handleBadgeClick(newState) { 69 | for (let i = 0; i < this.props.annotations.length; i++){ 70 | if (this.props.annotations[i].start_offset == this.props.sid){ 71 | this.props.annotations[i].isPositive = newState; 72 | } 73 | } 74 | this.setState({isSpanPositive: newState}); 75 | } 76 | 77 | handleMouseHover(newState) { 78 | this.setState({isHovering: newState}); 79 | } 80 | 81 | handlePositiveMouseHover(newState) { 82 | this.setState({isHovering: newState}); 83 | this.setState({isPosHovering: newState}); 84 | } 85 | 86 | handlePositiveMouseLeave(newState) { 87 | this.setState({isPosHovering: newState}); 88 | } 89 | 90 | handleNegativeMouseHover(newState) { 91 | this.setState({isHovering: newState}); 92 | this.setState({isNegHovering: newState}); 93 | } 94 | 95 | handleNegativeMouseLeave(newState) { 96 | this.setState({isNegHovering: newState}); 97 | } 98 | 99 | delayMouseLeave() { 100 | setTimeout(function() { 101 | this.setState({isHovering: false}); 102 | }.bind(this), 1000); 103 | } 104 | 105 | handleClick(){ 106 | this.props.annotate(); 107 | } 108 | 109 | render(){ 110 | const classes = this.props.classes; 111 | const linkVisible = ((this.state.isHovering) || (this.props.selectedLink.type===UNDIR_LINK)); 112 | let style = this.props.style; 113 | 114 | const text = this.props.text; 115 | 116 | const innerSpan = ( 117 | this.handleMouseHover(true)} 119 | onMouseLeave={() => this.handleMouseHover(false)} 120 | onClick={ ("clickSegment" in this.props) ? this.props.clickSegment : ()=>{} } 121 | > 122 | 128 | {text} 129 | 130 | 131 | ); 132 | 133 | if ((this.props.clickLinkButton) && (this.props.onDelete)) { 134 | const themePosHover = createMuiTheme({ 135 | palette:{ 136 | primary:{ 137 | main: "#228be6", 138 | }, 139 | secondary:{ 140 | main: "#d0ebff", 141 | } 142 | } 143 | }) 144 | const themeNegHover = createMuiTheme({ 145 | palette:{ 146 | primary:{ 147 | main: "#f03e3e" 148 | }, 149 | secondary:{ 150 | main: "#ffc9c9" 151 | } 152 | } 153 | }) 154 | return ( 155 | <> 156 | 157 | this.handlePositiveMouseHover(true)} 163 | onMouseLeave={() => this.handlePositiveMouseLeave(false)} 164 | color={this.state.isPosHovering? "primary":(this.state.isSpanPositive? "primary":"secondary")} 165 | onClick={() => this.handleBadgeClick(true)}> 166 | }>{""} 167 | 168 | 169 | this.handleNegativeMouseHover(true)} 175 | onMouseLeave={() => this.handleNegativeMouseLeave(false)} 176 | color={this.state.isNegHovering? "primary":(!this.state.isSpanPositive? "primary":"secondary")} 177 | //onMouseLeave={() => this.handleMouseHover(false)} 178 | onClick={() => this.handleBadgeClick(false)}> 179 | }>{""} 180 | 181 | {innerSpan} 182 | this.handleMouseHover(true)} 188 | //onMouseLeave={() => this.handleMouseHover(false)} 189 | onClick={this.props.onDelete}> 190 | }>{""} 191 | ); 192 | } else { 193 | return(innerSpan); 194 | } 195 | } 196 | } 197 | 198 | SelectedSpan.propTypes = { 199 | annotate: PropTypes.func, 200 | clickLinkButton: PropTypes.func, 201 | onDelete: PropTypes.func, 202 | classes: PropTypes.object, 203 | clickSegment: PropTypes.func 204 | 205 | } 206 | 207 | function mapStateToProps(state, ownProps?) { 208 | return { selectedLink: state.selectedLink, 209 | annotations: state.annotations }; 210 | } 211 | function mapDispatchToProps(dispatch) { 212 | return {}; 213 | } 214 | 215 | export default connect(mapStateToProps, mapDispatchToProps)(SelectedSpan); -------------------------------------------------------------------------------- /ui/src/SortingTableUtils.js: -------------------------------------------------------------------------------- 1 | 2 | export function desc(a, b, orderBy) { 3 | if (b[orderBy] < a[orderBy]) { 4 | return -1; 5 | } 6 | if (b[orderBy] > a[orderBy]) { 7 | return 1; 8 | } 9 | return 0; 10 | } 11 | 12 | export function stableSort(array, cmp) { 13 | const stabilizedThis = array.map((el, index) => [el, index]); 14 | stabilizedThis.sort((a, b) => { 15 | const order = cmp(a[0], b[0]); 16 | if (order !== 0) return order; 17 | return a[1] - b[1]; 18 | }); 19 | return stabilizedThis.map(el => el[0]); 20 | } 21 | 22 | export function getSorting(order, orderBy) { 23 | return order === 'desc' ? (a, b) => desc(a, b, orderBy) : (a, b) => -desc(a, b, orderBy); 24 | } 25 | 26 | export function style(number) { 27 | if (isNaN(number)) { 28 | return null; 29 | } else if (number <= 1.0) { 30 | return parseFloat(Math.round(number * 1000) / 1000).toFixed(3); 31 | } else { 32 | return number; 33 | } 34 | } -------------------------------------------------------------------------------- /ui/src/Span.js: -------------------------------------------------------------------------------- 1 | let Span = function(startOffset, endOffset, text, label=null) { 2 | const span = { 3 | id: startOffset, 4 | label: label, 5 | start_offset: startOffset, 6 | end_offset: endOffset, 7 | text: text.slice(startOffset, endOffset), 8 | link: null, 9 | spanAnnotated: false, 10 | spanLabel: null, 11 | isPositive: true 12 | }; 13 | return span; 14 | } 15 | 16 | export default Span; -------------------------------------------------------------------------------- /ui/src/StatisticsCRFPane.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Button from '@material-ui/core/Button'; 3 | import {connect} from "react-redux"; 4 | import {bindActionCreators} from 'redux'; 5 | 6 | import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp'; 7 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import LinearProgress from '@material-ui/core/LinearProgress'; 10 | import Paper from '@material-ui/core/Paper'; 11 | import RefreshIcon from '@material-ui/icons/Refresh'; 12 | import Table from '@material-ui/core/Table'; 13 | import TableBody from '@material-ui/core/TableBody'; 14 | import TableCell from '@material-ui/core/TableCell'; 15 | import TableRow from '@material-ui/core/TableRow'; 16 | import Typography from '@material-ui/core/Typography'; 17 | 18 | import { getLRStatistics } from './actions/getStatistics' 19 | import { style } from './SortingTableUtils' 20 | 21 | 22 | class CRFStatisticsPane extends React.Component { 23 | componentDidUpdate(prevProps, prevState) { 24 | if ((prevProps.statistics !== this.props.statistics) && (Object.keys(prevProps.statistics).length > 0)) { 25 | this.setState({prevStats: prevProps.statistics}); 26 | } 27 | } 28 | 29 | statDelta(key) { 30 | if ((this.state) && ("prevStats" in this.state)) { 31 | const delta = this.props.statistics[key] - this.state.prevStats[key]; 32 | let cellContent = delta; 33 | if (delta > 0.0){ 34 | cellContent = {style(delta)} 35 | } else if (delta < -0.0) { 36 | cellContent = {style(delta)} 37 | } 38 | return({cellContent}) 39 | } 40 | } 41 | 42 | render(){ 43 | const classes = this.props.classes; 44 | const prevStats = ((this.state) && ("prevStats" in this.state)); 45 | const labelKeyMap = { 46 | 'Precision': 'Precision', 47 | 'Recall': 'Recall', 48 | 'F1-score': 'F1-score' 49 | } 50 | const labelClasses = this.props.labelClasses 51 | return( 52 | 53 | 54 | 55 | Trained Model Statistics 56 | 57 | Train a conditional random field (CRF) model on your training set. 58 | 59 | {this.props.pending ? : } 60 | 61 | 62 | 63 | {["Precision","Recall", "F1-score"].map(key => { 64 | if (key in this.props.statistics) { 65 | return ( 66 | {labelKeyMap[key]} 67 | {style(this.props.statistics[key])} 68 | {this.statDelta(key)} 69 | ) 70 | } else { return null} 71 | })} 72 |
73 |
74 | 75 | Class-Specific Statistics 76 | 77 | 78 | 79 | 80 | 81 | 82 | {this.props.labelClasses.map(label => ( 83 | prevStats ? 84 | [label.name !== "O" ? 85 | 98 | : null, 99 | 100 | ]: [label.name !== "O" ? 101 | 114 | : null] 115 | ))} 116 | 117 | 118 | {"Precision0" in this.props.statistics ? Precision : null} 119 | {"Precision0" in this.props.statistics ? this.props.labelClasses.map(label => { 120 | return ( 121 | label.name !== 'O'? [{style(this.props.statistics["Precision"+label.key])} 122 | ,this.statDelta("Precision"+label.key)] : null 123 | ) 124 | }) : null} 125 | 126 | 127 | {"Recall0" in this.props.statistics ? Recall : null} 128 | {"Recall0" in this.props.statistics ? this.props.labelClasses.map(label => { 129 | return ( 130 | label.name !== "O"? [{style(this.props.statistics["Recall"+label.key])} 131 | ,this.statDelta("Recall"+label.key)] : null 132 | ) 133 | }) : null} 134 | 135 | 136 | {"F10" in this.props.statistics ? F1 : null} 137 | {"F10" in this.props.statistics ? this.props.labelClasses.map(label => { 138 | return ( 139 | label.name !== "O"? [{style(this.props.statistics["F1"+label.key])} 140 | ,this.statDelta("F1"+label.key)] : null 141 | ) 142 | }) : null} 143 | 144 | 145 |
146 |
) 147 | } 148 | } 149 | 150 | function mapStateToProps(state, ownProps?) { 151 | return { 152 | statistics: state.statistics_LRmodel.data, 153 | pending: state.statistics_LRmodel.pending, 154 | labelClasses: state.labelClasses.data 155 | }; 156 | } 157 | function mapDispatchToProps(dispatch) { 158 | return { 159 | getLRStatistics: bindActionCreators(getLRStatistics, dispatch) 160 | }; 161 | } 162 | export default connect(mapStateToProps, mapDispatchToProps)(CRFStatisticsPane); -------------------------------------------------------------------------------- /ui/src/StatisticsPane.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {connect} from "react-redux"; 3 | 4 | import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp'; 5 | import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; 6 | import Divider from '@material-ui/core/Divider'; 7 | import LinearProgress from '@material-ui/core/LinearProgress'; 8 | import Paper from '@material-ui/core/Paper'; 9 | import Table from '@material-ui/core/Table'; 10 | import TableBody from '@material-ui/core/TableBody'; 11 | import TableCell from '@material-ui/core/TableCell'; 12 | import TableRow from '@material-ui/core/TableRow'; 13 | import Typography from '@material-ui/core/Typography'; 14 | 15 | import { style } from './SortingTableUtils' 16 | 17 | 18 | class StatisticsPane extends React.Component { 19 | componentDidUpdate(prevProps, prevState) { 20 | if (Object.keys(prevProps.statistics).length > 0) { 21 | if (!this.statsSame(prevProps.statistics, this.props.statistics)) { 22 | this.setState({prevStats: prevProps.statistics}); 23 | } 24 | } 25 | } 26 | 27 | statsSame(oldStats, newStats) { 28 | const stat_list = Object.keys(oldStats); 29 | for (var i = stat_list.length - 1; i >= 0; i--) { 30 | let stat_key = stat_list[i]; 31 | if (oldStats[stat_key] !== newStats[stat_key]) { 32 | return false; 33 | } 34 | } return true; 35 | } 36 | 37 | statDelta(key) { 38 | if ((this.state) && ("prevStats" in this.state)) { 39 | const delta = this.props.statistics[key] - this.state.prevStats[key]; 40 | let cellContent = delta; 41 | if (delta > 0){ 42 | cellContent = {style(delta)} 43 | } else if (delta < 0) { 44 | cellContent = {style(delta)} 45 | } 46 | return({cellContent}) 47 | } 48 | } 49 | 50 | render(){ 51 | const classes = this.props.classes; 52 | 53 | const statNameMap = { 54 | 'f1': 'F1-score', 55 | 'precision': 'Precision', 56 | 'recall': 'Recall', 57 | 'label_coverage': 'Coverage' 58 | } 59 | return( 60 | 61 | 62 | 63 | Labeling Statistics 64 | 65 | Your labeling functions are agreggated by a labeling model then evaluated on the labeled development set. More information about these metrics is available via Scikit-Learn. 66 | 67 | {this.props.pending ? : } 68 | 69 | 70 | 71 | {Object.keys(this.props.statistics).map(key => 72 | 73 | {statNameMap[key]} 74 | {style(this.props.statistics[key])} 75 | { this.statDelta(key) } 76 | )} 77 | 78 |
79 |
) 80 | } 81 | } 82 | 83 | function mapStateToProps(state, ownProps?) { 84 | return { 85 | statistics: state.statistics.data, 86 | pending: state.statistics.pending 87 | }; 88 | } 89 | function mapDispatchToProps(dispatch) { 90 | return {}; 91 | } 92 | export default connect(mapStateToProps, mapDispatchToProps)(StatisticsPane); -------------------------------------------------------------------------------- /ui/src/actions/ConceptStyler.js: -------------------------------------------------------------------------------- 1 | class ConceptStyler{ 2 | 3 | constructor() { 4 | this.colorIndex = 0; 5 | this.colorAssignments = {}; 6 | this.hotKeys = {}; 7 | this.availableLetters = "abcdefghijklmnopqrstuvwxyz-=[];',."; 8 | } 9 | 10 | hotkey(conceptString) { 11 | if (conceptString in this.hotKeys) { 12 | return this.hotKeys[conceptString]; 13 | } 14 | 15 | let letter = conceptString[0]; 16 | if (this.availableLetters.indexOf(letter) === -1) { 17 | letter = this.availableLetters[0]; 18 | } 19 | this.availableLetters = this.availableLetters.replace(letter, ""); 20 | this.hotKeys[conceptString] = letter; 21 | return letter; 22 | } 23 | 24 | color(conceptString) { 25 | return this.nextColor(conceptString); 26 | } 27 | 28 | nextColor(string){ 29 | if (string in this.colorAssignments){ 30 | return this.colorAssignments[string]; 31 | } 32 | const colorPalette = ["#2CA02C", "#E377C2", "#17BECF", "#8C564B", "#D62728", "#BCBD22", "#9467BD", "#FF7F0E", "#1F77B4"]; 33 | let color = colorPalette[this.colorIndex]; 34 | this.colorAssignments[string] = color; 35 | this.colorIndex = (this.colorIndex + 1) % colorPalette.length; 36 | return color 37 | } 38 | 39 | stringToColor(string) { 40 | let hash = 0; 41 | let i; 42 | 43 | /* eslint-disable no-bitwise */ 44 | for (i = 0; i < string.length; i += 1) { 45 | hash = string.charCodeAt(i) + ((hash << 5) - hash); 46 | } 47 | 48 | let colour = '#'; 49 | 50 | for (i = 0; i < 3; i += 1) { 51 | const value = (hash >> (i * 8)) & 0xff; 52 | colour += `00${value.toString(16)}`.substr(-2); 53 | } 54 | /* eslint-enable no-bitwise */ 55 | 56 | return colour; 57 | } 58 | } 59 | 60 | let conceptStyler = new ConceptStyler(); 61 | export default conceptStyler; -------------------------------------------------------------------------------- /ui/src/actions/annotate.js: -------------------------------------------------------------------------------- 1 | export const ANNOTATE = "ANNOTATE"; 2 | export const HIGHLIGHT = "HIGHLIGHT"; 3 | export const HIGHLIGHT_ERROR = "HIGHLIGHT_ERROR" 4 | export const SELECT_LINK = "SELECT_LINK"; 5 | export const ADD_NER = "ADD_NER"; 6 | 7 | export function annotate(data) { 8 | /* data is an array of annotations, for example 9 | [{ 10 | id: 11, 11 | label: 0, 12 | start_offset: 11, 13 | end_offset: 17, 14 | text: 'Russia', 15 | link: null 16 | }] 17 | ] */ 18 | return dispatch => { 19 | dispatch({ 20 | type: ANNOTATE, 21 | data 22 | }) 23 | } 24 | } 25 | 26 | export function highlight(data) { 27 | /* data is an array of annotations, for example 28 | [{ 29 | id: 11, 30 | label: 0, 31 | start_offset: 11, 32 | end_offset: 17, 33 | text: 'Russia', 34 | link: null 35 | }] 36 | ] */ 37 | return dispatch => { 38 | dispatch({ 39 | type: HIGHLIGHT, 40 | data 41 | }) 42 | } 43 | } 44 | 45 | export function NER(data) { 46 | return dispatch => { 47 | dispatch({ 48 | type: ADD_NER, 49 | data 50 | }) 51 | } 52 | } 53 | 54 | export function highlight_regex(regex_str, text, case_sensitive, idx) { 55 | var data = []; 56 | var matches = []; 57 | try { 58 | var regex = null; 59 | if (!case_sensitive) { 60 | regex = new RegExp(regex_str, 'ig'); 61 | } else { 62 | regex = new RegExp(regex_str, 'g'); 63 | } 64 | matches = text.matchAll(regex); 65 | } 66 | catch (err) { 67 | return dispatch => dispatch({ 68 | type: HIGHLIGHT_ERROR, error: err.toString(), idx: idx}); 69 | } 70 | for (const match of matches) { 71 | data.push({ 72 | id: `${match.index}_pending`, // 'pending' because this annotation will not necessarily be submitted 73 | label: 'pending', 74 | start_offset: match.index, 75 | end_offset: match.index + match[0].length, 76 | link: null, 77 | }); 78 | } 79 | return highlight(data); 80 | } 81 | 82 | export function isToken(string) { 83 | if (string.startsWith("(?:(?<=\\W)|(?<=^))(")) { 84 | string = string.slice("(?:(?<=\\W)|(?<=^))(".length); 85 | if (string.endsWith(")(?=\\W|$)")) { 86 | string = string.slice(0, string.length - ")(?=\\W|$)".length); 87 | return string; 88 | } 89 | } return false; 90 | } 91 | 92 | export function TokenToRegex(token) { 93 | const regex = "(?:(?<=\\W)|(?<=^))(" + token + ")(?=\\W|$)"; 94 | return regex; 95 | } 96 | 97 | export function highlight_string(string, text, case_sensitive=false) { 98 | if (!case_sensitive) { 99 | text = text.toLowerCase(); 100 | } 101 | const regex = TokenToRegex(string, case_sensitive); 102 | 103 | return highlight_regex(regex, text) 104 | } 105 | 106 | export function select_link(data){ 107 | /* EXAMPLE DATA 108 | { 109 | type: 'Undirected Link', 110 | segment: { 111 | id: 11, 112 | label: 0, 113 | start_offset: 11, 114 | end_offset: 15, 115 | text: 'Ever', 116 | link: null 117 | } 118 | } 119 | */ 120 | return dispatch => { 121 | dispatch({ 122 | type: SELECT_LINK, 123 | data 124 | }) 125 | } 126 | } -------------------------------------------------------------------------------- /ui/src/actions/api.js: -------------------------------------------------------------------------------- 1 | //const api = 'http://54.83.150.235:3000/'; 2 | const api = 'http://localhost:5000/api'; 3 | 4 | export {api as default} -------------------------------------------------------------------------------- /ui/src/actions/concepts.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | import conceptStyler from './ConceptStyler' 4 | 5 | export const GET_CONCEPTS_PENDING = "GET_CONCEPTS_PENDING"; 6 | export const UPDATE_CONCEPT_PENDING = "UPDATE_CONCEPT_PENDING"; 7 | export const GET_CONCEPTS_SUCCESS = "GET_CONCEPTS_SUCCESS"; 8 | export const GET_CONCEPTS_ERROR = "GET_CONCEPTS_ERROR"; 9 | export const SELECT_CONCEPT = 'SELECT_CONCEPT' 10 | 11 | function getConceptsPending() { 12 | return { 13 | type: GET_CONCEPTS_PENDING, 14 | } 15 | } 16 | 17 | function updateConceptPending(conceptName) { 18 | return { 19 | type: UPDATE_CONCEPT_PENDING, 20 | conceptName: conceptName 21 | } 22 | } 23 | 24 | function getConceptsSuccess(data) { 25 | return { 26 | type: GET_CONCEPTS_SUCCESS, 27 | data: data 28 | } 29 | } 30 | 31 | function getConceptsError(error) { 32 | return { 33 | type: GET_CONCEPTS_ERROR, 34 | error: error 35 | } 36 | } 37 | 38 | function fetchConcepts() { 39 | return dispatch => { 40 | dispatch(getConceptsPending()); 41 | axios.get(`${api}/concept`) 42 | .then(response => { 43 | if(response.error) { 44 | throw(response.error); 45 | } 46 | const conceptNames = Object.keys(response.data); 47 | let data = {}; 48 | for (let i=0; i { 61 | dispatch(getConceptsError(error)); 62 | }) 63 | } 64 | } 65 | 66 | function addConcept(conceptName){ 67 | const data = { 68 | name: conceptName, 69 | tokens: [] 70 | }; 71 | 72 | return dispatch => { 73 | dispatch(getConceptsPending()); 74 | axios.post(`${api}/concept`, data) 75 | .then(response => { 76 | if(response.error) { 77 | throw(response.error); 78 | } 79 | dispatch(fetchConcepts()) 80 | }); 81 | } 82 | } 83 | 84 | function deleteConcept(conceptName){ 85 | return dispatch => { 86 | dispatch(updateConceptPending(conceptName)); 87 | axios.delete(`${api}/concept/${conceptName}`) 88 | .then(response => { 89 | if(response.error) { 90 | throw(response.error); 91 | } 92 | dispatch(fetchConcepts()) 93 | }); 94 | } 95 | } 96 | 97 | // add string to concept tokens list 98 | function updateConcept(conceptName, data){ 99 | return dispatch => { 100 | dispatch(updateConceptPending(conceptName)); 101 | axios.put(`${api}/concept/${conceptName}`, data) 102 | .then(response => { 103 | if(response.error) { 104 | throw(response.error); 105 | } 106 | dispatch(fetchConcepts()) 107 | }); 108 | } 109 | } 110 | 111 | // select a concept for annotating spans 112 | export function select_concept(data){ 113 | return dispatch => dispatch({type: SELECT_CONCEPT, data: data}) 114 | } 115 | 116 | export const conceptEditors = { 117 | addConcept, 118 | deleteConcept, 119 | fetchConcepts, 120 | updateConcept 121 | } -------------------------------------------------------------------------------- /ui/src/actions/connectivesAndKeyTypes.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const KEYTYPE = "KEYTYPE" 5 | export const CONNECTIVE = "CONNECTIVE" 6 | 7 | export function getKeyTypes() { 8 | return dispatch => { 9 | axios.get(`${api}/keytype`) 10 | .then(response => { 11 | if(response.error) { 12 | throw(response.error); 13 | } 14 | dispatch({type: KEYTYPE, data: response.data}); 15 | }) 16 | } 17 | } 18 | 19 | export function getConnectives() { 20 | return dispatch => { 21 | axios.get(`${api}/connective`) 22 | .then(response => { 23 | if(response.error) { 24 | throw(response.error); 25 | } 26 | dispatch({type: CONNECTIVE, data: response.data}); 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /ui/src/actions/datasets.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const SUCCESS = "UPLOAD_SUCCESS" 5 | export const PENDING = "UPLOAD_PENDING" 6 | export const ERROR = "UPLOAD_ERROR" 7 | export const SELECT = "SELECT_DATASET" 8 | 9 | 10 | function UploadError(error) { 11 | return { 12 | type: ERROR, 13 | error: error 14 | } 15 | } 16 | 17 | function selectDataset(data) { 18 | return { 19 | type:SELECT, 20 | data: data 21 | } 22 | } 23 | 24 | function UploadPending() { 25 | return { 26 | type: PENDING 27 | } 28 | } 29 | 30 | function UploadSuccess(data) { 31 | return { 32 | type: SUCCESS, 33 | data: data 34 | } 35 | } 36 | 37 | export function fetchDatasets(idToken) { 38 | return dispatch => { 39 | axios.get(`${api}/datasets`, 40 | { 41 | headers: { 42 | 'Authorization': 'Bearer ' + idToken 43 | } 44 | } 45 | ) 46 | .then(response => { 47 | if(response.error) { 48 | dispatch(UploadError()); 49 | throw(response.error); 50 | } 51 | dispatch(UploadSuccess(response.data)); 52 | return response.data; 53 | }) 54 | .catch(error => { 55 | dispatch(UploadError(error)); 56 | }) 57 | } 58 | 59 | } 60 | 61 | export function setSelected(dataset_uuid, idToken=0) { 62 | return dispatch => { 63 | axios.post(`${api}/datasets`, 64 | {dataset_uuid: dataset_uuid[0]}, 65 | { 66 | headers: { 67 | 'Authorization': 'Bearer ' + idToken 68 | } 69 | } 70 | ) 71 | .then(response => { 72 | if (response.error) { 73 | throw(response.error); 74 | } 75 | dispatch(selectDataset(dataset_uuid)); 76 | }) 77 | } 78 | } 79 | 80 | export default function uploadDataset(formData, dataset_uuid=0, idToken=0) { 81 | return dispatch => { 82 | dispatch(UploadPending()); 83 | console.log(formData); 84 | axios.post(`${api}/datasets/${dataset_uuid}`, 85 | formData, 86 | { 87 | headers: { 88 | 'Content-Type': 'multipart/form-data', 89 | 'Authorization': 'Bearer ' + idToken 90 | } 91 | } 92 | ) 93 | .then(response => { 94 | if(response.error) { 95 | dispatch(UploadError()); 96 | throw(response.error); 97 | } 98 | dispatch(UploadSuccess(response.data)); 99 | setSelected(dataset_uuid); 100 | return response.data; 101 | }) 102 | .catch(error => { 103 | dispatch(UploadError(error)); 104 | }) 105 | } 106 | } -------------------------------------------------------------------------------- /ui/src/actions/getStatistics.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const GET_STATS_PENDING = "GET_STATS_PENDING"; 5 | export const GET_STATS_SUCCESS = "GET_STATS_SUCCESS"; 6 | export const GET_STATS_ERROR = "GET_STATS_ERROR"; 7 | 8 | export function getStatsPending() { 9 | return { 10 | type: GET_STATS_PENDING 11 | } 12 | } 13 | 14 | function getStatsSuccess(data) { 15 | return { 16 | type: GET_STATS_SUCCESS, 17 | data: data 18 | } 19 | } 20 | 21 | function getStatsError(error) { 22 | return { 23 | type: GET_STATS_ERROR, 24 | error: error 25 | } 26 | } 27 | 28 | function getStatistics() { 29 | return dispatch => { 30 | dispatch(getStatsPending()); 31 | axios.get(`${api}/statistics`) 32 | .then(response => { 33 | if(response.error) { 34 | throw(response.error); 35 | } 36 | dispatch(getStatsSuccess(response.data)); 37 | return response.data; 38 | }) 39 | .catch(error => { 40 | dispatch(getStatsError(error)); 41 | }) 42 | } 43 | } 44 | 45 | export default getStatistics; 46 | 47 | 48 | export const GET_LRSTATS_PENDING = "GET_LRSTATS_PENDING"; 49 | export const GET_LRSTATS_SUCCESS = "GET_LRSTATS_SUCCESS"; 50 | export const GET_LRSTATS_ERROR = "GET_LRSTATS_ERROR"; 51 | 52 | function getLRStatsPending() { 53 | return { 54 | type: GET_LRSTATS_PENDING 55 | } 56 | } 57 | 58 | function getLRStatsSuccess(data) { 59 | return { 60 | type: GET_LRSTATS_SUCCESS, 61 | data: data 62 | } 63 | } 64 | 65 | function getLRStatsError(error) { 66 | return { 67 | type: GET_LRSTATS_ERROR, 68 | error: error 69 | } 70 | } 71 | 72 | export function getLRStatistics() { 73 | return dispatch => { 74 | dispatch(getLRStatsPending()); 75 | axios.get(`${api}/lr_statistics`) 76 | .then(response => { 77 | if(response.error) { 78 | throw(response.error); 79 | } 80 | dispatch(getLRStatsSuccess(response.data)); 81 | return response.data; 82 | }) 83 | .catch(error => { 84 | dispatch(getLRStatsError(error)); 85 | }) 86 | } 87 | } -------------------------------------------------------------------------------- /ui/src/actions/getText.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | import { annotate, select_link, NER } from './annotate' 4 | import { reset_label, label } from "./labelAndSuggestLF"; 5 | 6 | export const GET_TEXT_PENDING = "GET_TEXT_PENDING"; 7 | export const GET_TEXT_SUCCESS = 'GET_TEXT_SUCCESS'; 8 | export const GET_TEXT_ERROR = "GET_TEXT_ERROR"; 9 | 10 | export function getTextPending() { 11 | return { 12 | type: GET_TEXT_PENDING, 13 | } 14 | } 15 | 16 | function newText(data, index){ 17 | return { 18 | type: GET_TEXT_SUCCESS, 19 | data, 20 | index 21 | } 22 | } 23 | 24 | function getTextError(error) { 25 | return { 26 | type: GET_TEXT_ERROR, 27 | error 28 | } 29 | } 30 | 31 | export function getText(){ 32 | return dispatch => { 33 | dispatch(getTextPending()); 34 | axios.get(`${api}/interaction`) 35 | .then(response => { 36 | if (response.error) { 37 | throw(response.error); 38 | } 39 | setInteraction(response, dispatch); 40 | return response.data.text 41 | }) 42 | .catch(error => { 43 | dispatch(getTextError(error)); 44 | }) 45 | } 46 | } 47 | 48 | // Set the new text, and reset all state related to the previous text 49 | export function setInteraction(response, dispatch){ 50 | //change the text 51 | dispatch(newText(response.data.text, response.data.index)); 52 | 53 | //reset annotations 54 | let annotations = []; 55 | if ("annotations" in response.data) { 56 | annotations = response.data.annotations; 57 | } 58 | dispatch(annotate(annotations)); 59 | let ners = []; 60 | if ("NER" in response.data) { 61 | ners = response.data.NER; 62 | } 63 | dispatch(NER(ners)); 64 | 65 | //reset selected span to link 66 | dispatch(select_link({type: null})); 67 | 68 | if ("label" in response.data) { 69 | dispatch(label(response.data)); 70 | } else { 71 | //reset selected label 72 | dispatch(reset_label()); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /ui/src/actions/interaction.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | import {setInteraction, getTextPending} from './getText' 4 | 5 | export const DELETE_INTERACTION = "DELETE_INTERACTION"; 6 | export const GET_INTERACTION_SUCCESS = "GET_INTERACTION_SUCCESS" 7 | export const GET_INTERACTION_PENDING = "GET_INTERACTION_PENDING" 8 | export const GET_INTERACTION_ERROR = "GET_INTERACTION_ERROR" 9 | 10 | export function deleteInteraction(index) { 11 | return { 12 | type: DELETE_INTERACTION, 13 | index: index 14 | } 15 | } 16 | 17 | function getInteractionError() { 18 | return { 19 | type: GET_INTERACTION_ERROR 20 | } 21 | } 22 | 23 | function getInteractionPending() { 24 | return { 25 | type: GET_INTERACTION_PENDING 26 | } 27 | } 28 | 29 | function getInteractionSuccess(data) { 30 | return { 31 | type: GET_INTERACTION_SUCCESS, 32 | data: data 33 | } 34 | } 35 | 36 | export function getInteraction(index) { 37 | return dispatch => { 38 | dispatch(getInteractionPending()); 39 | axios.get(`${api}/interaction/${index}`) 40 | .then(response => { 41 | if(response.error) { 42 | dispatch(getInteractionError()); 43 | throw(response.error); 44 | } 45 | dispatch(getInteractionSuccess(response.data)); 46 | return response.data; 47 | }) 48 | .catch(error => { 49 | dispatch(getInteractionError(error)); 50 | }) 51 | } 52 | } 53 | 54 | export function setAsCurrentInteraction(index) { 55 | return dispatch => { 56 | dispatch(getTextPending()) 57 | axios.get(`${api}/interaction/${index}`) 58 | .then(response => { 59 | if(response.error) { 60 | dispatch(getInteractionError()); 61 | throw(response.error); 62 | } 63 | setInteraction(response, dispatch); 64 | dispatch(getInteractionSuccess(response.data)); 65 | return response.data; 66 | }) 67 | } 68 | } -------------------------------------------------------------------------------- /ui/src/actions/labelAndSuggestLF.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const LABEL = 'LABEL' 5 | export const NEW_LF = 'NEW_LF' 6 | export const SPANLABEL = 'SPANLABEL' 7 | 8 | export function reset_label(){ 9 | return dispatch => { 10 | dispatch({type: LABEL, data: {label: null}}); 11 | } 12 | } 13 | 14 | export function label(data){ 15 | return dispatch => { 16 | dispatch({type: LABEL, data: data}); 17 | axios.post(`${api}/interaction`, data) 18 | .then( response => { 19 | dispatch({type: NEW_LF, data: response.data}) 20 | }) 21 | } 22 | } 23 | 24 | export function set_selected_LF(data){ 25 | return dispatch => dispatch({type: NEW_LF, data: data}); 26 | } 27 | 28 | export function clear_suggestions(){ 29 | return dispatch => dispatch({type: NEW_LF, data: {}}) 30 | } -------------------------------------------------------------------------------- /ui/src/actions/labelClasses.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const GET_CLASSES_SUCCESS="GET_CLASSES_SUCCESS"; 5 | export const GET_CLASSES_PENDING="GET_CLASSES_PENDING"; 6 | export const GET_CLASSES_ERROR="GET_CLASSES_ERROR"; 7 | export const ADD_CLASS_SUCCESS="ADD_CLASS_SUCCESS"; 8 | 9 | const colorTheme = [ 10 | { 11 | backgroundColor: 'rgb(255,236,153)', 12 | textColor: "rgb(239,140,2)", 13 | }, 14 | { 15 | backgroundColor: "rgb(183,217,179)", 16 | textColor: "rgb(43,138,62)", 17 | }, 18 | { 19 | backgroundColor: "rgb(201,231,239)", 20 | textColor: "rgb(12,152,173)", 21 | }, 22 | { 23 | backgroundColor: "rgb(227,217,237)", 24 | textColor: "rgb(83,75,154)", 25 | }, 26 | { 27 | backgroundColor: "rgb(248,200,201)", 28 | textColor: "rgb(225,49,49)", 29 | }, 30 | ] 31 | 32 | function pending() { 33 | return { 34 | type: GET_CLASSES_PENDING 35 | } 36 | } 37 | 38 | function getClassesSuccess(data) { 39 | return { 40 | type: GET_CLASSES_SUCCESS, 41 | data: data 42 | } 43 | } 44 | 45 | function addClassSuccess(data) { 46 | return { 47 | type: ADD_CLASS_SUCCESS, 48 | data: data 49 | } 50 | } 51 | 52 | function raiseError(error) { 53 | return { 54 | type: GET_CLASSES_ERROR, 55 | error: error 56 | } 57 | } 58 | 59 | function dataFromResponse(response) { 60 | return Object.keys(response.data).map(k => { 61 | return { 62 | key: parseInt(k), 63 | name: response.data[k] 64 | } 65 | }) 66 | } 67 | 68 | 69 | export function submitLabelsAndName(labelClasses, project_name=null) { 70 | return dispatch => { 71 | dispatch(pending()); 72 | axios.post(`${api}/label`, 73 | { 74 | labels: labelClasses, 75 | name: project_name 76 | } 77 | ) 78 | .then(response => { 79 | if (response.error) { 80 | throw(response.error); 81 | } 82 | const data = dataFromResponse(response.data); 83 | dispatch(getClassesSuccess(data)); 84 | }) 85 | .catch(error => { 86 | dispatch(raiseError(error)); 87 | }) 88 | } 89 | } 90 | 91 | export function addLabelClass(labelClassObj) { 92 | return dispatch => { 93 | dispatch(addClassSuccess(labelClassObj)); 94 | } 95 | } 96 | 97 | function fetchClasses() { 98 | return dispatch => { 99 | dispatch(pending()); 100 | axios.get(`${api}/label`) 101 | .then(response => { 102 | if(response.error) { 103 | throw(response.error); 104 | } 105 | const data = dataFromResponse(response); 106 | for(let i=0;i { 113 | dispatch(raiseError(error)); 114 | }) 115 | } 116 | } 117 | 118 | export default fetchClasses; -------------------------------------------------------------------------------- /ui/src/actions/loadingBar.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | function updateLoadingBar(value) { 5 | return { 6 | type: "LOADING_BAR", 7 | data: value 8 | } 9 | } 10 | 11 | function setThread(thread) { 12 | return { 13 | type: "SET_LAUNCH_THREAD", 14 | data: thread 15 | } 16 | } 17 | 18 | export function launchStatus(thread=0){ 19 | return dispatch => { 20 | console.log("getting launch status"); 21 | axios.get(`${api}/launch`) 22 | .then(response => { 23 | console.log(response); 24 | dispatch(updateLoadingBar(response.data)); 25 | }) 26 | } 27 | } 28 | 29 | export default function launch(){ 30 | return dispatch => { 31 | dispatch(updateLoadingBar(0)); 32 | dispatch(setThread(0)); 33 | axios.post(`${api}/launch`, {}) 34 | .then(response => { 35 | dispatch(setThread(response.data)); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/actions/save.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | 4 | export const SAVE_ERROR = "SAVE_ERROR" 5 | export const SAVE_PENDING = "SAVE_PENDING" 6 | export const SAVE_SUCCESS = "SAVE_SUCCESS" 7 | 8 | 9 | function saveSuccess(data) { 10 | return { 11 | type: SAVE_SUCCESS, 12 | data: data 13 | } 14 | } 15 | 16 | export function savePending() { 17 | return { 18 | type: SAVE_PENDING 19 | } 20 | } 21 | 22 | function saveError(error) { 23 | return { 24 | type: SAVE_ERROR, 25 | error: error 26 | } 27 | } 28 | 29 | export function saveModel() { 30 | return dispatch => { 31 | window.open(`${api}/save`); 32 | } 33 | } -------------------------------------------------------------------------------- /ui/src/actions/spanAnnotate.js: -------------------------------------------------------------------------------- 1 | export const SPANANNOTATE = "SPANANNOTATE"; 2 | 3 | export function spanAnnotate(data) { 4 | return dispatch => { 5 | dispatch({ 6 | type: SPANANNOTATE, 7 | data 8 | }) 9 | } 10 | } -------------------------------------------------------------------------------- /ui/src/actions/submitLFs.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import api from './api' 3 | import getStatistics from './getStatistics' 4 | 5 | 6 | export const SUBMIT_LF_PENDING = "SUBMIT_LF_PENDING"; 7 | export const ONE_LF_PENDING = "ONE_LF_PENDING"; 8 | export const SUBMIT_LF_SUCCESS = "SUBMIT_LF_SUCCESS"; 9 | export const SUBMIT_LF_ERROR = "SUBMIT_LF_ERROR"; 10 | export const LF_STATS = "LF_STATS"; 11 | export const LF_STATS_ERROR = "LF_STATS_ERROR"; 12 | export const LF_LABEL_EXAMPLES = "LF_LABEL_EXAMPLES"; 13 | 14 | 15 | function allLFPending(data) { 16 | if (data) { 17 | return { 18 | type: SUBMIT_LF_PENDING, 19 | data: data 20 | } 21 | } return { type: SUBMIT_LF_PENDING } 22 | 23 | } 24 | 25 | function oneLFPending(lf_id) { 26 | return { 27 | type: ONE_LF_PENDING, 28 | lf_id: lf_id 29 | } 30 | } 31 | 32 | function submitLFSuccess(data) { 33 | return { 34 | type: SUBMIT_LF_SUCCESS, 35 | data: data 36 | } 37 | } 38 | 39 | function submitLFError(error) { 40 | return { 41 | type: SUBMIT_LF_ERROR, 42 | error: error 43 | } 44 | } 45 | 46 | function lfStats(data) { 47 | return { 48 | type: LF_STATS, 49 | data: data 50 | } 51 | } 52 | 53 | function lfStatsError(error) { 54 | return { 55 | type: LF_STATS_ERROR, 56 | error: error 57 | } 58 | } 59 | 60 | export function deleteLF(lf_ids) { 61 | return dispatch => { 62 | dispatch(allLFPending()); 63 | axios.put(`${api}/labelingfunctions`, lf_ids) 64 | .then(response => { 65 | dispatch(getLFstats()); 66 | }) 67 | } 68 | } 69 | 70 | function submitLFs(data) { 71 | return dispatch => { 72 | dispatch(allLFPending(data)); 73 | axios.put(`${api}/interaction`, data) 74 | .then(response => { 75 | if(response.error) { 76 | throw(response.error); 77 | } 78 | dispatch(submitLFSuccess(response.data)); 79 | dispatch(getStatistics()); 80 | return response.data; 81 | }) 82 | .catch(error => { 83 | dispatch(submitLFError(error)); 84 | }) 85 | } 86 | } 87 | 88 | export function getLFstats() { 89 | return dispatch => { 90 | dispatch(allLFPending({})); 91 | axios.get(`${api}/labelingfunctions`) 92 | .then(response => { 93 | if(response.error) { 94 | throw(response.error); 95 | } 96 | dispatch(lfStats(response.data)); 97 | dispatch(getStatistics()); 98 | return response.data; 99 | }) 100 | .catch(error => { 101 | dispatch(lfStatsError(error)); 102 | }) 103 | } 104 | } 105 | 106 | export function getLFexamples(lf_id) { 107 | if (lf_id === null) { 108 | return dispatch => { dispatch({ 109 | type: LF_LABEL_EXAMPLES, 110 | data: {} 111 | })}; 112 | } 113 | return dispatch => { 114 | dispatch(oneLFPending(lf_id)); 115 | axios.get(`${api}/labelingfunctions/${lf_id}`) 116 | .then(response => { 117 | if(response.error) { 118 | throw(response.error); 119 | } 120 | dispatch({ 121 | type: LF_LABEL_EXAMPLES, 122 | data: response.data 123 | }); 124 | return response.data; 125 | }) 126 | .catch(error => { 127 | dispatch(lfStatsError(error)); 128 | }) 129 | } 130 | } 131 | 132 | export default submitLFs; -------------------------------------------------------------------------------- /ui/src/errorSnackbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import clsx from 'clsx'; 4 | import CheckCircleIcon from '@material-ui/icons/CheckCircle'; 5 | import ErrorIcon from '@material-ui/icons/Error'; 6 | import InfoIcon from '@material-ui/icons/Info'; 7 | import CloseIcon from '@material-ui/icons/Close'; 8 | import { amber, green } from '@material-ui/core/colors'; 9 | import IconButton from '@material-ui/core/IconButton'; 10 | import Snackbar from '@material-ui/core/Snackbar'; 11 | import SnackbarContent from '@material-ui/core/SnackbarContent'; 12 | import WarningIcon from '@material-ui/icons/Warning'; 13 | import { makeStyles } from '@material-ui/core/styles'; 14 | 15 | const variantIcon = { 16 | success: CheckCircleIcon, 17 | warning: WarningIcon, 18 | error: ErrorIcon, 19 | info: InfoIcon, 20 | }; 21 | 22 | const useStyles1 = makeStyles(theme => ({ 23 | success: { 24 | backgroundColor: green[600], 25 | }, 26 | error: { 27 | backgroundColor: theme.palette.error.dark, 28 | }, 29 | info: { 30 | backgroundColor: theme.palette.primary.main, 31 | }, 32 | warning: { 33 | backgroundColor: amber[700], 34 | }, 35 | icon: { 36 | fontSize: 20, 37 | }, 38 | iconVariant: { 39 | opacity: 0.9, 40 | marginRight: theme.spacing(1), 41 | }, 42 | message: { 43 | display: 'flex', 44 | alignItems: 'center', 45 | }, 46 | })); 47 | 48 | function MySnackbarContentWrapper(props) { 49 | const classes = useStyles1(); 50 | const { className, message, onClose, variant, ...other } = props; 51 | const Icon = variantIcon[variant]; 52 | 53 | return ( 54 | 59 | 60 | {message} 61 | 62 | } 63 | action={[ 64 | 65 | 66 | , 67 | ]} 68 | {...other} 69 | /> 70 | ); 71 | } 72 | 73 | MySnackbarContentWrapper.propTypes = { 74 | className: PropTypes.string, 75 | message: PropTypes.string, 76 | onClose: PropTypes.func, 77 | variant: PropTypes.oneOf(['error', 'info', 'success', 'warning']).isRequired, 78 | }; 79 | 80 | export default function ErrorSnackbar(props) { 81 | let {open, setOpen} = props; 82 | 83 | const handleClose = (event, reason) => { 84 | if (reason === 'clickaway') { 85 | return; 86 | } 87 | 88 | setOpen(false); 89 | }; 90 | 91 | return ( 92 |
93 | 102 | 107 | 108 |
109 | ); 110 | } -------------------------------------------------------------------------------- /ui/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux' 4 | import configureStore from './store'; 5 | //import './index.css'; 6 | import App from './App'; 7 | import * as serviceWorker from './serviceWorker'; 8 | 9 | // const store = configureStore(); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , document.getElementById('root')); 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://bit.ly/CRA-PWA 19 | serviceWorker.unregister(); 20 | -------------------------------------------------------------------------------- /ui/src/reducers.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import annotations, { selectedLink } from './annotationsReducer' 4 | import concepts, { selectedConcept } from './conceptsReducer' 5 | import connectivesKeyTypes from './ConnectivesKeyTypesReducer' 6 | import interactionHistory from './interactionHistoryReducer' 7 | import { label, suggestedLF } from './labelAndSuggestLFReducer' 8 | import labelClasses from './labelClassesReducer' 9 | import labelExamples from './labelExampleReducer' 10 | import selectedLF from './selectedLFReducer' 11 | import statistics from './statisticsReducer' 12 | import text from './textReducer' 13 | 14 | const rootReducer = combineReducers({ 15 | annotations, 16 | concepts, 17 | gll: connectivesKeyTypes, 18 | interactionHistory, 19 | label, 20 | labelClasses, 21 | labelExamples, 22 | selectedConcept, 23 | selectedLF, 24 | selectedLink, 25 | suggestedLF, 26 | statistics, 27 | text 28 | }); 29 | 30 | export default rootReducer; 31 | 32 | -------------------------------------------------------------------------------- /ui/src/reducers/ConnectivesKeyTypesReducer.js: -------------------------------------------------------------------------------- 1 | import { CONNECTIVE, KEYTYPE } from '../actions/connectivesAndKeyTypes' 2 | 3 | const initialState = { 4 | fetched_conn: false, 5 | fetched_keytype: false, 6 | connective: {}, 7 | keyType: {}, 8 | } 9 | 10 | export default function connectivesKeyTypesReducer(state=initialState, action) { 11 | switch (action.type) { 12 | case CONNECTIVE: 13 | return {...state, connective: action.data, fetched_conn: true} 14 | case KEYTYPE: 15 | return {...state, keyType: action.data, fetched_keytype: true} 16 | default: 17 | return state 18 | } 19 | } -------------------------------------------------------------------------------- /ui/src/reducers/annotationsReducer.js: -------------------------------------------------------------------------------- 1 | import { ANNOTATE, HIGHLIGHT, HIGHLIGHT_ERROR, SELECT_LINK, ADD_NER } from '../actions/annotate'; 2 | import { SPANANNOTATE } from '../actions/spanAnnotate'; 3 | 4 | export function spanAnnotations(state=[], action){ 5 | switch (action.type){ 6 | case SPANANNOTATE: 7 | return action.data 8 | default: 9 | return state; 10 | } 11 | } 12 | 13 | function annotations(state=[], action ){ 14 | switch (action.type) { 15 | case ANNOTATE: 16 | return action.data 17 | default: 18 | return state 19 | } 20 | } 21 | 22 | export default annotations; 23 | 24 | export function highlights(state={data: [], error: {}}, action ){ 25 | switch (action.type) { 26 | case HIGHLIGHT: 27 | return { 28 | ...state, 29 | data: action.data, 30 | error: {}, 31 | idx: null 32 | } 33 | case HIGHLIGHT_ERROR: 34 | var newErr = state.error; 35 | newErr[action.idx] = action.error; 36 | return { 37 | ...state, 38 | error: newErr 39 | } 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | export function ners(state=[], action) { 46 | switch (action.type) { 47 | case ADD_NER: 48 | return action.data 49 | default: 50 | return state 51 | } 52 | } 53 | 54 | export function selectedLink(state={type: null}, action) { 55 | switch (action.type) { 56 | case SELECT_LINK: 57 | return action.data 58 | default: 59 | return state 60 | } 61 | } -------------------------------------------------------------------------------- /ui/src/reducers/conceptsReducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | GET_CONCEPTS_PENDING, 4 | UPDATE_CONCEPT_PENDING, 5 | GET_CONCEPTS_SUCCESS, 6 | GET_CONCEPTS_ERROR, 7 | SELECT_CONCEPT 8 | } from '../actions/concepts' 9 | 10 | const initialState = { 11 | pending: false, 12 | data: {}, 13 | error: null 14 | } 15 | 16 | function concepts (state = initialState, action ){ 17 | switch (action.type) { 18 | case GET_CONCEPTS_PENDING: 19 | return { 20 | ...state, 21 | data: {...state.data, ...action.data}, 22 | pending: true 23 | } 24 | case UPDATE_CONCEPT_PENDING: 25 | const conceptName = action.conceptName; 26 | state.data[conceptName].pending = true; 27 | return { 28 | ...state, 29 | data: state.data, 30 | pending: false 31 | } 32 | case GET_CONCEPTS_SUCCESS: 33 | return { 34 | ...state, 35 | data: action.data, 36 | pending: false, 37 | } 38 | case GET_CONCEPTS_ERROR: 39 | return { 40 | ...state, 41 | pending: false, 42 | error: action.error 43 | } 44 | default: 45 | return state 46 | } 47 | } 48 | 49 | export default concepts; 50 | 51 | export function selectedConcept(state=null, action) { 52 | switch (action.type) { 53 | case SELECT_CONCEPT: 54 | return action.data 55 | //{...state, ...action.data} 56 | default: 57 | return state 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /ui/src/reducers/datasetsReducer.js: -------------------------------------------------------------------------------- 1 | import { SUCCESS, ERROR, SELECT } from '../actions/datasets' 2 | 3 | const initialState = { 4 | error: null, 5 | data: [], 6 | selected: undefined 7 | } 8 | 9 | export default function datasetsReducer(state=initialState, action) { 10 | switch(action.type) { 11 | case SUCCESS: 12 | return {...state, 13 | data: action.data} 14 | case ERROR: 15 | return { 16 | ...state, 17 | error: action.error} 18 | case SELECT: 19 | return { 20 | ...state, 21 | selected: action.data 22 | } 23 | default: 24 | return state; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/reducers/interactionHistoryReducer.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | GET_INTERACTION_SUCCESS, 4 | GET_INTERACTION_PENDING, 5 | GET_INTERACTION_ERROR 6 | } from '../actions/interaction' 7 | 8 | const initialState = { 9 | pending: false, 10 | data: {}, 11 | error: null 12 | } 13 | 14 | function interactionHistory(state = initialState, action ){ 15 | switch (action.type) { 16 | case GET_INTERACTION_PENDING: 17 | return { 18 | ...state, 19 | data: {}, 20 | pending: true 21 | } 22 | case GET_INTERACTION_SUCCESS: 23 | return { 24 | ...state, 25 | data: action.data, 26 | pending: false, 27 | } 28 | case GET_INTERACTION_ERROR: 29 | return { 30 | ...state, 31 | pending: false, 32 | error: action.error 33 | } 34 | default: 35 | return state 36 | } 37 | } 38 | 39 | export default interactionHistory; -------------------------------------------------------------------------------- /ui/src/reducers/labelAndSuggestLFReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | NEW_LF, 3 | LABEL, 4 | SPANLABEL 5 | } from '../actions/labelAndSuggestLF'; 6 | import { annotate, select_link, NER } from '../actions/annotate' 7 | 8 | export function label (state = null, action ) { 9 | switch (action.type) { 10 | case LABEL: 11 | return action.data.label 12 | default: 13 | return state 14 | } 15 | } 16 | 17 | export function suggestedLF (state = {}, action) { 18 | switch (action.type) { 19 | case NEW_LF: 20 | /* Keep already selected LFs */ 21 | const LF_names = Object.keys(state); 22 | var already_selected_lfs = {}; 23 | for (var i = LF_names.length -1; i >= 0; i--) { 24 | let lf_id = LF_names[i]; 25 | let lf = state[lf_id]; 26 | if (lf.selected) { 27 | already_selected_lfs[lf_id] = lf 28 | } 29 | } 30 | return { 31 | ...action.data, 32 | ...already_selected_lfs 33 | }; 34 | default: 35 | return state; 36 | } 37 | } -------------------------------------------------------------------------------- /ui/src/reducers/labelClassesReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CLASSES_SUCCESS, 3 | GET_CLASSES_PENDING, 4 | GET_CLASSES_ERROR, 5 | ADD_CLASS_SUCCESS 6 | } from '../actions/labelClasses' 7 | 8 | const initialState = { 9 | pending: true, 10 | data: [], 11 | error: null 12 | } 13 | 14 | function labelClassesReducer(state=initialState, action){ 15 | switch (action.type) { 16 | case GET_CLASSES_SUCCESS: 17 | return { 18 | ...state, 19 | data: action.data, 20 | pending: false 21 | } 22 | case GET_CLASSES_ERROR: 23 | return { 24 | ...state, 25 | error: action.error, 26 | pending: false 27 | } 28 | case GET_CLASSES_PENDING: 29 | return { 30 | ...state, 31 | pending: true 32 | } 33 | case ADD_CLASS_SUCCESS: 34 | return { 35 | ...state, 36 | data: [...state.data, action.data], 37 | pending: false 38 | } 39 | default: 40 | return state; 41 | } 42 | } 43 | 44 | export default labelClassesReducer; -------------------------------------------------------------------------------- /ui/src/reducers/labelExampleReducer.js: -------------------------------------------------------------------------------- 1 | import { LF_LABEL_EXAMPLES } from '../actions/submitLFs' 2 | 3 | const initialState = { 4 | } 5 | 6 | export default function labelExamples(state=initialState, action) { 7 | switch(action.type) { 8 | case LF_LABEL_EXAMPLES: 9 | return action.data 10 | default: 11 | return state; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/reducers/loadingBarReducer.js: -------------------------------------------------------------------------------- 1 | export default function loadingBarReducer(state={progress: 0, thread: null}, action) { 2 | switch (action.type) { 3 | case "LOADING_BAR": 4 | if (action.data < 1.0) { 5 | return { 6 | ...state, 7 | progress: action.data, 8 | } 9 | } else { 10 | return { 11 | ...state, 12 | progress: action.data, 13 | thread: null 14 | } 15 | } 16 | case "SET_LAUNCH_THREAD": 17 | return { 18 | ...state, 19 | thread: action.data 20 | } 21 | default: 22 | return state 23 | } 24 | } -------------------------------------------------------------------------------- /ui/src/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | 3 | import annotations, { spanAnnotations,highlights, selectedLink, ners } from './annotationsReducer' 4 | import concepts, { selectedConcept } from './conceptsReducer' 5 | import connectivesKeyTypes from './ConnectivesKeyTypesReducer' 6 | import datasets from './datasetsReducer' 7 | import { label, suggestedLF } from './labelAndSuggestLFReducer' 8 | import labelClasses from './labelClassesReducer' 9 | import labelExamples from './labelExampleReducer' 10 | import loadingBarReducer from './loadingBarReducer' 11 | import selectedLF from './selectedLFReducer' 12 | import statistics, {statistics_LRmodel} from './statisticsReducer' 13 | import text from './textReducer' 14 | 15 | const rootReducer = combineReducers({ 16 | annotations, 17 | spanAnnotations, 18 | concepts, 19 | datasets, 20 | gll: connectivesKeyTypes, 21 | highlights, 22 | label, 23 | labelClasses, 24 | labelExamples, 25 | launchProgress: loadingBarReducer, 26 | ners, 27 | selectedConcept, 28 | selectedLF, 29 | selectedLink, 30 | suggestedLF, 31 | statistics, 32 | statistics_LRmodel, 33 | text 34 | }); 35 | 36 | export default rootReducer; 37 | 38 | -------------------------------------------------------------------------------- /ui/src/reducers/selectedLFReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SUBMIT_LF_PENDING, 3 | SUBMIT_LF_SUCCESS, 4 | SUBMIT_LF_ERROR, 5 | ONE_LF_PENDING, 6 | LF_STATS, 7 | LF_STATS_ERROR 8 | } from '../actions/submitLFs' 9 | 10 | const initialState = { 11 | pending: false, 12 | data: {}, 13 | error: null 14 | } 15 | 16 | function updateData(old_data, new_data) { 17 | old_data = {...old_data, ...new_data} 18 | return Object.keys(old_data).reduce(function(updated_data, key) { 19 | updated_data[key] = old_data[key]; 20 | return updated_data; 21 | }, {}); 22 | } 23 | 24 | export default function selectedLFReducer(state=initialState, action) { 25 | switch (action.type) { 26 | case SUBMIT_LF_PENDING: 27 | return { 28 | ...state, 29 | data: {...state.data, ...action.data}, 30 | pending: true 31 | } 32 | case ONE_LF_PENDING: 33 | return state // TODO tell redux it's loading 34 | case SUBMIT_LF_SUCCESS: 35 | return { 36 | ...state, 37 | data: updateData(state.data, action.data), 38 | pending: false, 39 | stats: false 40 | } 41 | case LF_STATS: 42 | return { 43 | data: action.data, 44 | pending: false, 45 | stats: true 46 | } 47 | case LF_STATS_ERROR: 48 | return { 49 | ...state, 50 | pending: false, 51 | error: action.error 52 | } 53 | case SUBMIT_LF_ERROR: 54 | return { 55 | ...state, 56 | pending: false, 57 | error: action.error 58 | } 59 | default: 60 | return state; 61 | } 62 | } -------------------------------------------------------------------------------- /ui/src/reducers/statisticsReducer.js: -------------------------------------------------------------------------------- 1 | import { GET_STATS_PENDING, GET_STATS_SUCCESS, GET_STATS_ERROR } from '../actions/getStatistics' 2 | import { GET_LRSTATS_PENDING, GET_LRSTATS_SUCCESS, GET_LRSTATS_ERROR } from '../actions/getStatistics' 3 | 4 | 5 | const initialState = { 6 | pending: false, 7 | data: {}, 8 | error: null 9 | } 10 | 11 | export default function statisticsReducer(state=initialState, action) { 12 | switch (action.type) { 13 | case GET_STATS_PENDING: 14 | return { 15 | ...state, 16 | pending: true 17 | } 18 | case GET_STATS_SUCCESS: 19 | return { 20 | ...state, 21 | data: action.data, 22 | pending: false 23 | } 24 | case GET_STATS_ERROR: 25 | return { 26 | ...state, 27 | pending: false, 28 | error: action.error 29 | } 30 | default: 31 | return state; 32 | } 33 | } 34 | 35 | 36 | export function statistics_LRmodel(state=initialState, action) { 37 | switch (action.type) { 38 | case GET_LRSTATS_PENDING: 39 | return { 40 | ...state, 41 | pending: true 42 | } 43 | case GET_LRSTATS_SUCCESS: 44 | return { 45 | ...state, 46 | data: action.data, 47 | pending: false 48 | } 49 | case GET_LRSTATS_ERROR: 50 | return { 51 | ...state, 52 | pending: false, 53 | error: action.error 54 | } 55 | default: 56 | return state; 57 | } 58 | } -------------------------------------------------------------------------------- /ui/src/reducers/textReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_TEXT_PENDING, 3 | GET_TEXT_SUCCESS, 4 | GET_TEXT_ERROR 5 | } from '../actions/getText'; 6 | 7 | const initialState = { 8 | pending: false, 9 | data: "", 10 | error: null 11 | } 12 | 13 | function text (state =initialState, action ){ 14 | switch (action.type) { 15 | case GET_TEXT_PENDING: 16 | return { 17 | ...state, 18 | pending: true 19 | } 20 | case GET_TEXT_SUCCESS: 21 | return { 22 | ...state, 23 | data: action.data, 24 | index: action.index, 25 | pending: false 26 | } 27 | case GET_TEXT_ERROR: 28 | return { 29 | ...state, 30 | pending: false, 31 | error: action.error 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default text; -------------------------------------------------------------------------------- /ui/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /ui/src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers/reducers'; 4 | 5 | 6 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 7 | 8 | export default function configureStore() { 9 | return createStore( 10 | rootReducer, 11 | composeEnhancers(applyMiddleware(thunk)) 12 | ); 13 | } 14 | --------------------------------------------------------------------------------