diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..25d1394cd53003659e76d92d30ed71c570fc0872 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM node:14 + +RUN apt-get update && \ + apt-get install -y nginx gettext-base && \ + rm -rf /var/lib/apt/lists/* && \ + chown -R 1000:1000 /etc/nginx && \ + chown -R 1000:1000 /var/log/nginx && \ + chown -R 1000:1000 /var/lib/nginx + +WORKDIR /app/transformer-autocomplete +ADD . . + +RUN cd front && npm install && npx tsc && npm run build:prod +RUN cd grunt && npm install && npx grunt +RUN cd server && npm install && npx tsc + +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 31bae5517794e68e9765e9dbab220dc69eb69049..a033414cb62360dd9a84955f31756ce3cefdffaf 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,17 @@ emoji: 🏆 colorFrom: green colorTo: gray sdk: docker +app_port: 8080 pinned: false --- -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +# transformer-autocomplete + +Autocompletion based on GPT-2 + +#### How to compile the front (to test the front with any server) + +1. Update the API endpoint in `front/js-src/Api.ts` +2. compile the TS to pure JS with `cd front; tsc` or through vscode (you can launch it in watch mode if needed) +3. pack the js into a single file (we use rollup) with `npm run watch` + diff --git a/backend/API.py b/backend/API.py new file mode 100644 index 0000000000000000000000000000000000000000..8dac9065188b4d30348bdd7e8549af6256ccf9e7 --- /dev/null +++ b/backend/API.py @@ -0,0 +1,165 @@ +from threading import Thread +import falcon +from falcon.http_status import HTTPStatus +import json +import requests +import time +from Model import generate_completion +import sys + + +class AutoComplete(object): + def on_post(self, req, resp, single_endpoint=True, x=None, y=None): + json_data = json.loads(req.bounded_stream.read()) + + resp.status = falcon.HTTP_200 + + start = time.time() + + try: + context = json_data["context"].rstrip() + except KeyError: + resp.body = "The context field is required" + resp.status = falcon.HTTP_422 + return + + try: + n_samples = json_data['samples'] + except KeyError: + n_samples = 3 + + try: + length = json_data['gen_length'] + except KeyError: + length = 20 + + try: + max_time = json_data['max_time'] + except KeyError: + max_time = -1 + + try: + model_name = json_data['model_size'] + except KeyError: + model_name = "small" + + try: + temperature = json_data['temperature'] + except KeyError: + temperature = 0.7 + + try: + max_tokens = json_data['max_tokens'] + except KeyError: + max_tokens = 256 + + try: + top_p = json_data['top_p'] + except KeyError: + top_p = 0.95 + + try: + top_k = json_data['top_k'] + except KeyError: + top_k = 40 + + + # CTRL + try: + repetition_penalty = json_data['repetition_penalty'] + except KeyError: + repetition_penalty = 0.02 + + # PPLM + try: + stepsize = json_data['step_size'] + except KeyError: + stepsize = 0.02 + + try: + gm_scale = json_data['gm_scale'] + except KeyError: + gm_scale = None + + try: + kl_scale = json_data['kl_scale'] + except KeyError: + kl_scale = None + + try: + num_iterations = json_data['num_iterations'] + except KeyError: + num_iterations = None + + try: + use_sampling = json_data['use_sampling'] + except KeyError: + use_sampling = None + + try: + bag_of_words_or_discrim = json_data['bow_or_discrim'] + except KeyError: + bag_of_words_or_discrim = "kitchen" + + print(json_data) + + sentences = generate_completion( + context, + length=length, + max_time=max_time, + model_name=model_name, + temperature=temperature, + max_tokens=max_tokens, + top_p=top_p, + top_k=top_k, + + # CTRL + repetition_penalty=repetition_penalty, + + # PPLM + stepsize=stepsize, + bag_of_words_or_discrim=bag_of_words_or_discrim, + gm_scale=gm_scale, + kl_scale=kl_scale, + num_iterations=num_iterations, + use_sampling=use_sampling + ) + + resp.body = json.dumps({"sentences": sentences, 'time': time.time() - start}) + + resp.status = falcon.HTTP_200 + sys.stdout.flush() + + +class Request(Thread): + def __init__(self, end_point, data): + Thread.__init__(self) + self.end_point = end_point + self.data = data + self.ret = None + + def run(self): + print("Requesting with url", self.end_point) + self.ret = requests.post(url=self.end_point, json=self.data) + + def join(self): + Thread.join(self) + return self.ret.text + + +class HandleCORS(object): + def process_request(self, req, resp): + resp.set_header('Access-Control-Allow-Origin', '*') + resp.set_header('Access-Control-Allow-Methods', '*') + resp.set_header('Access-Control-Allow-Headers', '*') + if req.method == 'OPTIONS': + raise HTTPStatus(falcon.HTTP_200, body='\n') + + +autocomplete = AutoComplete() +app = falcon.API(middleware=[HandleCORS()]) +app.add_route('/autocomplete', autocomplete) +app.add_route('/autocomplete/{x}', autocomplete) +app.add_route('/autocomplete/{x}/{y}', autocomplete) + +application = app diff --git a/backend/GPUHandler.py b/backend/GPUHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..f35111e9e6da8000d883eb797d769260d49dfdcf --- /dev/null +++ b/backend/GPUHandler.py @@ -0,0 +1,159 @@ +import time +import torch +from transformers import (GPT2LMHeadModel, GPT2Tokenizer, GPT2Config, + OpenAIGPTLMHeadModel, OpenAIGPTTokenizer, + XLNetLMHeadModel, XLNetTokenizer, + TransfoXLLMHeadModel, TransfoXLTokenizer, + CTRLLMHeadModel, CTRLTokenizer) + +model_metadata = { + "gpt2/small": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 550, + "checkpoint": "gpt2", + "identifier": "gpt2/small" + }, "gpt": { + "tokenizer": OpenAIGPTTokenizer, + "model": OpenAIGPTLMHeadModel, + "size": 550, + "checkpoint": "openai-gpt", + "identifier": "gpt" + }, "xlnet": { + "tokenizer": XLNetTokenizer, + "model": XLNetLMHeadModel, + "size": 550, + "checkpoint": "xlnet-base-cased", + "identifier": "xlnet" + }, "gpt2/arxiv-nlp": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 550, + "checkpoint": "arxiv-nlp-v1", + "identifier": "gpt2/arxiv-nlp" + }, "gpt2/medium": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 1500, + "checkpoint": "gpt2-medium", + "identifier": "gpt2/medium" + }, "gpt2/large": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 3300, + "checkpoint": "gpt2-large", + "identifier": "gpt2/large" + }, "distilgpt2/small": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 350, + "checkpoint": "distilgpt2", + "identifier": "distilgpt2/small" + }, "ctrl": { + "tokenizer": CTRLTokenizer, + "model": CTRLLMHeadModel, + "size": 6300, + "checkpoint": "ctrl", + "identifier": "ctrl" + }, "pplm": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 3000, + "checkpoint": "gpt2-large", + "identifier": "pplm" + }, "gpt2/xl": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 7000, + "checkpoint": "gpt2-xl", + "identifier": "gpt2/xl" + }, "pplm": { + "tokenizer": GPT2Tokenizer, + "model": GPT2LMHeadModel, + "size": 4000, + "checkpoint": "gpt2-medium", + "identifier": "pplm", + "configuration_options": { + "config": GPT2Config, + "options": { + "output_hidden_states": True + } + } + } +} + +memory_overhead = 500 + +class GPU: + def __init__(self, id): + self.id = id + self.models = [] + self.total_memory = torch.cuda.get_device_properties( + "cuda:{}".format(id)).total_memory / 1_000_000 - 1_000 + + print("INIT GPU WITH DEVICE", "cuda:{}".format(id)) + + def register_model(self, model, cached_path=None): + if self.total_memory_used() + model["size"] < self.total_memory: + model["device"] = "cuda:{}".format(self.id) + + if cached_path: + model["cached_path"] = cached_path + + self.models.append(model) + return True + else: + return False + + def total_memory_used(self): + return sum([model["size"] for model in self.models]) + memory_overhead + + def __repr__(self): + return str( + [(model["checkpoint"], model["size"]) for model in self.models] + + [str(round(100 * (self.total_memory_used() / self.total_memory))) + "%"] + + ["cuda:{}".format(self.id)] + ) + + +class GPUHandler: + def __init__(self, ids, model_list, gpu_ids, cached_models=None): + if cached_models is None: + cached_models = {} + + self.gpus = [GPU(id) for id in gpu_ids] + print("GPU handler initiated with {} gpus.".format(len(self.gpus))) + + self.sanity_check([model_metadata[model] for model in model_list]) + + for model in model_list: + self.register_model(model_metadata[model], cached_models.get(model)) + + def register_model(self, model, cached_path=None): + for index, gpu in enumerate(self.gpus): + if gpu.register_model(model, cached_path): + print("Registered model", model, "in GPU", gpu) + break + + if index >= len(self.gpus): + raise ValueError("Could not load model", model["checkpoint"]) + + def sanity_check(self, model_list): + temp_gpus = [GPU(id) for id in range(len(self.gpus))] + + for model in model_list: + + current_gpu_index = 0 + while current_gpu_index < len(temp_gpus): + if not temp_gpus[current_gpu_index].register_model(model): + current_gpu_index += 1 + else: + break + + if current_gpu_index >= len(temp_gpus): + raise RuntimeError("SANITY CHECK FAILED") + + print("Current layout", temp_gpus) + + def __repr__(self): + return f"NO. GPUS: {len(self.gpus)}.\n{self.gpus}" diff --git a/backend/Model.py b/backend/Model.py new file mode 100644 index 0000000000000000000000000000000000000000..e0b0ca3e874e3f7598988199dca5b92fcbdea570 --- /dev/null +++ b/backend/Model.py @@ -0,0 +1,299 @@ +import time +from transformers import (GPT2LMHeadModel, GPT2Tokenizer, + OpenAIGPTLMHeadModel, OpenAIGPTTokenizer, + XLNetLMHeadModel, XLNetTokenizer, + TransfoXLLMHeadModel, TransfoXLTokenizer, + CTRLLMHeadModel, CTRLTokenizer) + +from Utils import forward, create_context +import torch +import torch.nn.functional as F +from math import floor +import requests +import json +import os +from PPLM import run_model as run_pplm, DISCRIMINATOR_MODELS_PARAMS +from GPUHandler import GPUHandler + +PADDING_TEXT = """With eyes for the most part downcast and, if ever they lighted on a fellow creature, at once and +furtively averted, Bernard hastened across the roof. He was like a man pursued, but pursued by enemies he does not +wish to see, lest they should seem more hostile even than he had supposed, and he himself be made to feel guiltier +and even more helplessly alone. That horrible Benito Hoover!’ And yet the man had meant well enough. Which only made +it, in a way, much worse. Those who meant well behaved in the same way as those who meant badly. Even Lenina was making +him suffer. He remembered those weeks of timid indecision, during which he had looked and longed and despaired of ever +having the courage to ask her. Dared he face the risk of being humiliated by a contemptuous refusal? But if she were to +say yes, what rapture! Well, now she had said it and he was still wretched—wretched that she should have thought it +such a perfect afternoon for Obstacle Golf, that she should have trotted away to join Henry Foster, that she should +have found him funny for not wanting to talk of their most private affairs in public. Wretched, in a word, because she +had behaved as any healthy and virtuous English girl ought to behave and not in some other, abnormal, extraordinary +way. """ + +try: + PID = int(requests.get(url="http://localhost:3000").json()) + N_GPU = torch.cuda.device_count() + GPU_PER_WORKER = int(os.getenv("GPU_PER_WORKER")) + GPU_IDS = list(range(PID * GPU_PER_WORKER, (PID + 1) * GPU_PER_WORKER)) + print("Successfully init thread with id {}. The GPU ids attributed are: {}".format(PID, GPU_IDS)) + + with open(os.getenv("FILE")) as json_file: + data = json.load(json_file) + models = data["models_to_load"] + cached_models = data.get("cached_models") +except requests.exceptions.ConnectionError or TypeError: + if __name__ == "__main__": + PID = 0 + N_GPU = torch.cuda.device_count() + GPU_PER_WORKER = 1 + GPU_IDS = [0] + print("Successfully init development thread with id {}. The GPU ids attributed are: {}".format(PID, GPU_IDS)) + models = ["pplm"] + cached_models = None + pass + else: + raise requests.exceptions.ConnectionError("The PID server is not running.") + + +handler = GPUHandler(int(), models, GPU_IDS, cached_models) +models = {} + +for gpu in handler.gpus: + for model in gpu.models: + model_name = model["identifier"] + print(f"Loading {model_name} model and tokenizer") + models[model_name] = model + + if model.get("cached_path"): + print("Loading {} from local path.".format(model_name)) + model_checkpoint_path = model["cached_path"] + else: + model_checkpoint_path = model["checkpoint"] + + if "configuration_options" in models[model_name]: + configuration_options = models[model_name]["configuration_options"] + print("Specific configuration options", configuration_options["options"]) + + config = configuration_options["config"].from_pretrained(model_checkpoint_path) + + for option_key, option_value in configuration_options["options"].items(): + setattr(config, option_key, option_value) + + models[model_name]["model"] = models[model_name]["model"].from_pretrained(model_checkpoint_path, config=config).to(models[model_name]["device"]) + else: + models[model_name]["model"] = models[model_name]["model"].from_pretrained(model_checkpoint_path).to(models[model_name]["device"]) + + models[model_name]["tokenizer"] = models[model_name]["tokenizer"].from_pretrained(models[model_name]["checkpoint"]) + models[model_name]["model"].eval() + +print("All models successfully loaded.") + + +def top_k_top_p_filtering(batch_logits, top_k=0, top_p=0.0, filter_value=-float('Inf')): + """ + Filter a distribution of logits using top-k and/or nucleus (top-p) filtering + + :param batch_logits: logits output by the model + :param top_k: >0: keep only top k tokens with highest probability (top-k filtering). + :param top_p: >0.0: keep the top tokens with cumulative probability >= top_p (nucleus filtering). + :param filter_value: + :return: A top_p/top_k filtered tensor of logits + """ + + for i in range(batch_logits.size(0)): + logits = batch_logits[i] + assert logits.dim() == 1 # batch size 1 for now - could be updated for more but the code would be less clear + top_k = min(top_k, logits.size(-1)) # Safety check + if top_k and top_k > 0: + # Remove all tokens with a probability less than the last token of the top-k + indices_to_remove = logits < torch.topk(logits, top_k)[0][..., -1, None] + logits[indices_to_remove] = filter_value + + if top_p and top_p > 0.0: + sorted_logits, sorted_indices = torch.sort(logits, descending=True) + cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1) + + # Remove tokens with cumulative probability above the threshold + sorted_indices_to_remove = cumulative_probs > top_p + # Shift the indices to the right to keep also the first token above the threshold + sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone() + sorted_indices_to_remove[..., 0] = 0 + + indices_to_remove = sorted_indices[sorted_indices_to_remove] + logits[indices_to_remove] = filter_value + + if 'batched_logits' in locals(): + batched_logits = torch.cat((batched_logits, logits.unsqueeze(0)), dim=0) + else: + batched_logits = logits.unsqueeze(0) + + return batched_logits + + +def check_tensor_for_eot(output, eot_token, dot_token): + return all([(eot_token in output_item or dot_token in output_item) for output_item in output.tolist()]) + + +def truncate_after_eot(output, eot_tokens): + result = [] + for i in range(output.size(0)): + if any([eot_token in output[i] for eot_token in eot_tokens]): + item = output[i].tolist() + index = find_min_value_in_array(item, eot_tokens) + result.append(item[:index] + [eot_tokens[0]]) + else: + result.append(output[i].tolist()) + return result + + +def find_min_value_in_array(array, values): + indexes = [] + for value in values: + try: + indexes.append(array.index(value)) + except ValueError: + "" # Couldn't find value in array + + return min(indexes) + + +# @lru_cache() +def generate_completion( + raw_text, + length=-1, + max_time=-1, + model_name="small", + temperature=1, + max_tokens=256, + top_p=0.0, + top_k=0, + batch_size=3, + repetition_penalty=1.2, + + # PPLM + bag_of_words_or_discrim=None, + stepsize=0.02, + gamma=1.5, + num_iterations=3, + window_length=5, + kl_scale=0.01, + gm_scale=0.95, + use_sampling=False +): + start = time.time() + + try: + print("Running with model", model_name) + model, tokenizer, device = models[model_name]["model"], models[model_name]["tokenizer"], models[model_name]["device"] + except KeyError: + print("Error. Defaulting to small model.") + model, tokenizer, device = models["gpt2/small"]["model"], models["gpt2/small"]["tokenizer"], models["gpt2/small"]["device"] + + if "pplm" in model_name: + if ":" in bag_of_words_or_discrim: + discrim, discrim_label = bag_of_words_or_discrim.split(":") + discrim_label = DISCRIMINATOR_MODELS_PARAMS[discrim]["class_id"][int(discrim_label)] + bag_of_words = None + + # Hardcoded parameters for the discriminator + gamma = 1.0 + + print("Running PPLM with discriminator:", discrim, discrim_label) + else: + bag_of_words = bag_of_words_or_discrim + discrim = None + discrim_label = None + + # Hardcoded parameters for the BOW + gamma = 1.5 + window_length = 5 + + print("Running PPLM with bag of words:", bag_of_words) + + print("kl", kl_scale, "gm", gm_scale, "sampling", use_sampling, "window length", window_length, "gamma", gamma, "temperature", temperature) + + return run_pplm( + model, tokenizer, device, raw_text, + max_time=max_time, + discrim=discrim, + discrim_label=discrim_label, + num_samples=batch_size, + bag_of_words=bag_of_words, + length=length, + temperature=temperature, + top_k=top_k, + stepsize=stepsize, + gamma=gamma, + num_iterations=num_iterations, + window_length=window_length, + kl_scale=kl_scale, + gm_scale=gm_scale, + use_sampling=use_sampling + ) + + + context_tokens, eot_token, dot_token = create_context(model_name, tokenizer, raw_text, PADDING_TEXT, max_tokens=max_tokens) + + if length == -1: + length = 100 + + context = torch.tensor(context_tokens, device=device, dtype=torch.long).unsqueeze(0).repeat(batch_size, 1) + prev = context + past = None + + with torch.no_grad(): + for _ in range(length): + try: + output = forward(model_name, model, prev, past, device=device) + except RuntimeError: + return "ERROR 500: OOM. TransfoXL asked for too much memory." + + logits, past = output if len(output) > 2 else output[0], None + + logits = logits[:, -1, :] / max(temperature, 0.001) + + if "ctrl" in model_name: + for i in range(batch_size): + for j in set(prev[i].tolist()): + logits[i, j] /= repetition_penalty + + logits = top_k_top_p_filtering(logits, top_p=top_p, top_k=top_k) + log_probs = F.softmax(logits, dim=-1) + token = torch.multinomial(log_probs, num_samples=1) + + prev = torch.cat((prev, token), dim=1) + + # Check that there is no eot token in all of the sentence, else breaks. + if check_tensor_for_eot(prev[:, len(context_tokens):], eot_token, dot_token) or (max_time != -1 and time.time() - start + 0.1 > max_time): + break + + out = prev[:, len(context_tokens):] + # Remove the words following the eot tokens. + out = truncate_after_eot(out, list(filter(lambda t: t is not None, [dot_token, eot_token]))) + end = time.time() + + # Remove empty sentences and duplicates + generations = list(set(filter(lambda x: len(x) > 0, [" " + tokenizer.decode(single_generation).strip() for single_generation in out]))) + + sentences = [ + {"value": generations[i], "time": end - start, "tokens": len(out[i])} for i in range(len(generations)) + ] + + + # print(end - start, [len(out[i]) for i in range(len(generations))]) + + return sentences + + +if __name__ == "__main__": + print(generate_completion( + "My dog died", + length=30, model_name="pplm", batch_size=3, top_k=10, top_p=0.9, + bag_of_words_or_discrim="sentiment:2", + stepsize=0.03, + gamma=1, + num_iterations=3, + window_length=5, + kl_scale=0.01, + gm_scale=0.95, + max_time=-1, + use_sampling=False + )) diff --git a/backend/PPLM.py b/backend/PPLM.py new file mode 100644 index 0000000000000000000000000000000000000000..2c00b9311416aa3afa4f3d5ba663ddaf02a134e7 --- /dev/null +++ b/backend/PPLM.py @@ -0,0 +1,723 @@ +#! /usr/bin/env python3 +# coding=utf-8 +# Copyright 2018 The Uber AI Team Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Example command with bag of words: +python examples/run_pplm.py -B space --cond_text "The president" --length 100 --gamma 1.5 --num_iterations 3 --num_samples 10 --stepsize 0.01 --window_length 5 --kl_scale 0.01 --gm_scale 0.95 + +Example command with discriminator: +python examples/run_pplm.py -D sentiment --class_label 3 --cond_text "The lake" --length 10 --gamma 1.0 --num_iterations 30 --num_samples 10 --stepsize 0.01 --kl_scale 0.01 --gm_scale 0.95 +""" + +import json +from operator import add +from typing import List, Optional, Tuple, Union + +import numpy as np +import torch +import torch.nn.functional as F +from torch.autograd import Variable +from tqdm import trange +from transformers.file_utils import cached_path +import time + +from run_pplm_discrim_train import ClassificationHead + +PPLM_BOW = 1 +PPLM_DISCRIM = 2 +PPLM_BOW_DISCRIM = 3 +SMALL_CONST = 1e-15 +BIG_CONST = 1e10 + +BAG_OF_WORDS_ARCHIVE_MAP = { + 'kitchen': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/kitchen.txt", + 'legal': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/legal.txt", + 'military': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/military.txt", + 'monsters': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/monsters.txt", + 'politics': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/politics.txt", + 'positive_words': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/positive_words.txt", + 'religion': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/religion.txt", + 'science': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/science.txt", + 'space': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/space.txt", + 'technology': "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/bow/technology.txt", +} + +DISCRIMINATOR_MODELS_PARAMS = { + "clickbait": { + "url": "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/discriminators/clickbait_classifierhead.pt", + "class_size": 2, + "embed_size": 1024, + "class_vocab": {"non_clickbait": 0, "clickbait": 1}, + "class_id": {0: "non_clickbait", 1: "clickbait"}, + "default_class": 1, + "pretrained_model": "gpt2-medium", + }, + "sentiment": { + "url": "http://s.yosinski.com/SST_classifier_head.pt", + "class_size": 5, + "embed_size": 1024, + "class_vocab": {"very_positive": 2, "very_negative": 3}, + "class_id": {2: "very_positive", 3: "very_negative"}, + "default_class": 3, + "pretrained_model": "gpt2-medium", + }, + "toxicity": { + "url": "https://s3.amazonaws.com/models.huggingface.co/bert/pplm/discriminators/toxicity_classifierhead.pt", + "class_size": 2, + "embed_size": 1024, + "class_vocab": {"non_toxic": 0, "toxic": 1}, + "class_id": {0: "non_toxic", 1: "toxic"}, + "default_class": 0, + "pretrained_model": "gpt2-medium", + }, +} + + +def to_var(x, requires_grad=False, volatile=False, device='cuda'): + if torch.cuda.is_available() and device == 'cuda': + x = x.cuda() + elif device != 'cuda': + x = x.to(device) + return Variable(x, requires_grad=requires_grad, volatile=volatile) + + +def top_k_filter(logits, k, probs=False): + """ + Masks everything but the k top entries as -infinity (1e10). + Used to mask logits such that e^-infinity -> 0 won't contribute to the + sum of the denominator. + """ + if k == 0: + return logits + else: + values = torch.topk(logits, k)[0] + batch_mins = values[:, -1].view(-1, 1).expand_as(logits) + if probs: + return torch.where(logits < batch_mins, + torch.ones_like(logits) * 0.0, logits) + return torch.where(logits < batch_mins, + torch.ones_like(logits) * -BIG_CONST, + logits) + + +def perturb_past( + past, + model, + last, + unpert_past=None, + unpert_logits=None, + accumulated_hidden=None, + grad_norms=None, + stepsize=0.01, + one_hot_bows_vectors=None, + classifier=None, + class_label=None, + loss_type=0, + num_iterations=3, + horizon_length=1, + window_length=0, + decay=False, + gamma=1.5, + kl_scale=0.01, + device='cuda', +): + # Generate inital perturbed past + grad_accumulator = [ + (np.zeros(p.shape).astype("float32")) + for p in past + ] + + if accumulated_hidden is None: + accumulated_hidden = 0 + + if decay: + decay_mask = torch.arange( + 0., + 1.0 + SMALL_CONST, + 1.0 / (window_length) + )[1:] + else: + decay_mask = 1.0 + + # TODO fix this comment (SUMANTH) + # Generate a mask is gradient perturbated is based on a past window + _, batch_size, _, curr_length, _ = past[0].shape + + if curr_length > window_length and window_length > 0: + ones_key_val_shape = ( + tuple(past[0].shape[:-2]) + + tuple([window_length]) + + tuple(past[0].shape[-1:]) + ) + + zeros_key_val_shape = ( + tuple(past[0].shape[:-2]) + + tuple([curr_length - window_length]) + + tuple(past[0].shape[-1:]) + ) + + ones_mask = torch.ones(ones_key_val_shape) + ones_mask = decay_mask * ones_mask.permute(0, 1, 2, 4, 3) + ones_mask = ones_mask.permute(0, 1, 2, 4, 3) + + window_mask = torch.cat( + (ones_mask, torch.zeros(zeros_key_val_shape)), + dim=-2 + ).to(device) + else: + window_mask = torch.ones_like(past[0]).to(device) + + # accumulate perturbations for num_iterations + loss_per_iter = [] + losses_per_iter = [] + new_accumulated_hidden = None + for i in range(num_iterations): + curr_perturbation = [ + to_var(torch.from_numpy(p_), requires_grad=True, device=device) + for p_ in grad_accumulator + ] + + # Compute hidden using perturbed past + perturbed_past = list(map(add, past, curr_perturbation)) + _, _, _, curr_length, _ = curr_perturbation[0].shape + all_logits, _, all_hidden = model(last, past=perturbed_past) + hidden = all_hidden[-1] + new_accumulated_hidden = accumulated_hidden + torch.sum( + hidden, + dim=1 + ).detach() + # TODO: Check the layer-norm consistency of this with trained discriminator (Sumanth) + logits = all_logits[:, -1, :] + probs = F.softmax(logits, dim=-1) + + loss = 0.0 + losses = torch.zeros(batch_size, device=device) + loss_list = [] + if loss_type == PPLM_BOW or loss_type == PPLM_BOW_DISCRIM: + for one_hot_bow in one_hot_bows_vectors: + bow_logits = torch.mm(probs, torch.t(one_hot_bow)) + bow_losses = -torch.log(torch.sum(bow_logits, dim=-1)) + losses += bow_losses + bow_loss = torch.sum(bow_losses) # sum over batches + loss += bow_loss + loss_list.append(bow_loss) + + if loss_type == 2 or loss_type == 3: + ce_loss = torch.nn.CrossEntropyLoss(reduction='none') + # TODO why we need to do this assignment and not just using unpert_past? (Sumanth) + curr_unpert_past = unpert_past + curr_probs = torch.unsqueeze(probs, dim=1) + wte = model.resize_token_embeddings() + for _ in range(horizon_length): + inputs_embeds = torch.matmul(curr_probs, wte.weight.data) + _, curr_unpert_past, curr_all_hidden = model( + past=curr_unpert_past, + inputs_embeds=inputs_embeds + ) + curr_hidden = curr_all_hidden[-1] + new_accumulated_hidden = new_accumulated_hidden + torch.sum( + curr_hidden, dim=1) + + prediction = classifier(new_accumulated_hidden / + (curr_length + 1 + horizon_length)) + + label = torch.tensor(batch_size * [class_label], + device=device, + dtype=torch.long) + discrim_losses = ce_loss(prediction, label) + losses += discrim_losses + discrim_loss = discrim_losses.sum(-1) + loss += discrim_loss + loss_list.append(discrim_loss) + + kl_loss = 0.0 + if kl_scale > 0.0: + unpert_probs = F.softmax(unpert_logits[:, -1, :], dim=-1) + unpert_probs = ( + unpert_probs + SMALL_CONST * + (unpert_probs <= SMALL_CONST).float().to(device).detach() + ) + correction = SMALL_CONST * (probs <= SMALL_CONST).float().to( + device).detach() + corrected_probs = probs + correction.detach() + kl_losses = kl_scale * ( + (corrected_probs * (corrected_probs / unpert_probs).log()).sum(-1) + ) + losses += kl_losses + kl_loss = kl_losses.sum() + loss += kl_loss + + loss_per_iter.append(loss.data.cpu().numpy()) + losses_per_iter.append(losses.data.cpu().numpy()) + + # compute gradients + loss.backward() + + # calculate gradient norms + if grad_norms is not None and loss_type == PPLM_BOW: + grad_norms = [ + torch.max(grad_norms[index], + torch.norm_except_dim(p_.grad * window_mask, dim=1)) + #torch.norm(p_.grad * window_mask)) + for index, p_ in enumerate(curr_perturbation) + ] + else: + grad_norms = [ + (torch.norm_except_dim(p_.grad * window_mask, dim=1) + SMALL_CONST) + for index, p_ in enumerate(curr_perturbation) + ] + + # normalize gradients + grad = [ + -stepsize * + (p_.grad * window_mask / grad_norms[ + index] ** gamma).data.cpu().numpy() + for index, p_ in enumerate(curr_perturbation) + ] + + # accumulate gradient + grad_accumulator = list(map(add, grad, grad_accumulator)) + + # reset gradients, just to make sure + for p_ in curr_perturbation: + p_.grad.data.zero_() + + # removing past from the graph + new_past = [] + for p_ in past: + new_past.append(p_.detach()) + past = new_past + + # apply the accumulated perturbations to the past + grad_accumulator = [ + to_var(torch.from_numpy(p_), requires_grad=True, device=device) + for p_ in grad_accumulator + ] + pert_past = list(map(add, past, grad_accumulator)) + + return pert_past, new_accumulated_hidden, grad_norms, losses_per_iter + + +def get_classifier( + name: Optional[str], class_label: Union[str, int], + device: str +) -> Tuple[Optional[ClassificationHead], Optional[int]]: + if name is None: + return None, None + + params = DISCRIMINATOR_MODELS_PARAMS[name] + classifier = ClassificationHead( + class_size=params['class_size'], + embed_size=params['embed_size'] + ).to(device) + if "url" in params: + resolved_archive_file = cached_path(params["url"]) + elif "path" in params: + resolved_archive_file = params["path"] + else: + raise ValueError("Either url or path have to be specified " + "in the discriminator model parameters") + classifier.load_state_dict( + torch.load(resolved_archive_file, map_location=device)) + classifier.eval() + + if isinstance(class_label, str): + if class_label in params["class_vocab"]: + label_id = params["class_vocab"][class_label] + else: + label_id = params["default_class"] + + + elif isinstance(class_label, int): + if class_label in set(params["class_vocab"].values()): + label_id = class_label + else: + label_id = params["default_class"] + + else: + label_id = params["default_class"] + + return classifier, label_id + + +def get_bag_of_words_indices(bag_of_words_ids_or_paths: List[str], tokenizer) -> \ + List[List[List[int]]]: + bow_indices = [] + for id_or_path in bag_of_words_ids_or_paths: + if id_or_path in BAG_OF_WORDS_ARCHIVE_MAP: + filepath = cached_path(BAG_OF_WORDS_ARCHIVE_MAP[id_or_path]) + else: + filepath = id_or_path + with open(filepath, "r") as f: + words = f.read().strip().split("\n") + bow_indices.append( + [tokenizer.encode(word.strip(), add_prefix_space=True, + add_special_tokens=False) for word in + words]) + return bow_indices + + +def build_bows_one_hot_vectors(bow_indices, tokenizer, device='cuda'): + if bow_indices is None: + return None + + one_hot_bows_vectors = [] + for single_bow in bow_indices: + single_bow = list(filter(lambda x: len(x) <= 1, single_bow)) + single_bow = torch.tensor(single_bow).to(device) + num_words = single_bow.shape[0] + one_hot_bow = torch.zeros(num_words, tokenizer.vocab_size).to(device) + one_hot_bow.scatter_(1, single_bow, 1) + one_hot_bows_vectors.append(one_hot_bow) + return one_hot_bows_vectors + + +def full_text_generation( + model, + tokenizer, + context=None, + num_samples=1, + device="cuda", + max_time=5, + sample=False, + discrim=None, + class_label=None, + bag_of_words=None, + length=100, + grad_length=10000, + stepsize=0.02, + num_iterations=3, + temperature=1.0, + gm_scale=0.9, + kl_scale=0.01, + top_k=10, + window_length=0, + horizon_length=1, + decay=False, + gamma=1.5, +): + classifier, class_id = get_classifier( + discrim, + class_label, + device + ) + + bow_indices = [] + if bag_of_words: + bow_indices = get_bag_of_words_indices(bag_of_words.split(";"), + tokenizer) + + if bag_of_words and classifier: + loss_type = PPLM_BOW_DISCRIM + + elif bag_of_words: + loss_type = PPLM_BOW + + elif classifier is not None: + loss_type = PPLM_DISCRIM + + else: + raise Exception("Specify either a bag of words or a discriminator") + + # unpert_gen_tok_text = generate_text_pplm( + # model=model, + # tokenizer=tokenizer, + # context=context, + # device=device, + # length=length, + # perturb=False + # ) + # if device == 'cuda': + # torch.cuda.empty_cache() + + print(context, bow_indices, top_k, gm_scale, kl_scale) + + pert_gen_tok_text, last_losses = generate_text_pplm( + model=model, + context=context, + tokenizer=tokenizer, + device=device, + max_time=max_time, + sample=sample, + perturb=True, + bow_indices=bow_indices, + classifier=classifier, + class_label=class_id, + loss_type=loss_type, + length=length, + grad_length=grad_length, + stepsize=stepsize, + num_iterations=num_iterations, + temperature=temperature, + gm_scale=gm_scale, + kl_scale=kl_scale, + top_k=top_k, + window_length=window_length, + horizon_length=horizon_length, + decay=decay, + gamma=gamma, + ) + + if device == 'cuda': + torch.cuda.empty_cache() + + return pert_gen_tok_text, last_losses + + +def generate_text_pplm( + model, + tokenizer, + context=None, + past=None, + device="cuda", + max_time=5, + perturb=True, + bow_indices=None, + classifier=None, + class_label=None, + loss_type=0, + length=100, + stepsize=0.02, + temperature=1.0, + top_k=10, + sample=False, + num_iterations=3, + grad_length=10000, + horizon_length=1, + window_length=0, + decay=False, + gamma=1.5, + gm_scale=0.9, + kl_scale=0.01, +): + output_so_far = None + if context: + context_t = torch.tensor(context, device=device, dtype=torch.long) + while len(context_t.shape) < 2: + context_t = context_t.unsqueeze(0) + output_so_far = context_t + + # collect one hot vectors for bags of words + one_hot_bows_vectors = build_bows_one_hot_vectors(bow_indices, tokenizer, + device) + + start = time.time() + + grad_norms = None + last = None + losses_this_iter = None + losses_in_time = [] + for i in trange(length, ascii=True): + + # Get past/probs for current output, except for last word + # Note that GPT takes 2 inputs: past + current_token + + # run model forward to obtain unperturbed + if past is None and output_so_far is not None: + last = output_so_far[:, -1:] + if output_so_far.shape[1] > 1: + _, past, _ = model(output_so_far[:, :-1]) + + unpert_logits, unpert_past, unpert_all_hidden = model(output_so_far) + unpert_last_hidden = unpert_all_hidden[-1] + + # check if we are abowe grad max length + if i >= grad_length: + current_stepsize = stepsize * 0 + else: + current_stepsize = stepsize + + # modify the past if necessary + if not perturb or num_iterations == 0: + pert_past = past + + else: + accumulated_hidden = unpert_last_hidden[:, :-1, :] + accumulated_hidden = torch.sum(accumulated_hidden, dim=1) + + if past is not None: + pert_past, _, grad_norms, losses_this_iter = perturb_past( + past, + model, + last, + unpert_past=unpert_past, + unpert_logits=unpert_logits, + accumulated_hidden=accumulated_hidden, + grad_norms=grad_norms, + stepsize=current_stepsize, + one_hot_bows_vectors=one_hot_bows_vectors, + classifier=classifier, + class_label=class_label, + loss_type=loss_type, + num_iterations=num_iterations, + horizon_length=horizon_length, + window_length=window_length, + decay=decay, + gamma=gamma, + kl_scale=kl_scale, + device=device, + ) + losses_in_time.append(losses_this_iter) + else: + pert_past = past + + pert_logits, past, pert_all_hidden = model(last, past=pert_past) + pert_logits = pert_logits[:, -1, :] / temperature # + SMALL_CONST + pert_probs = F.softmax(pert_logits, dim=-1) + + # Fuse the modified model and original model + if perturb: + + unpert_probs = F.softmax(unpert_logits[:, -1, :], dim=-1) + + pert_probs = ((pert_probs ** gm_scale) * ( + unpert_probs ** (1 - gm_scale))) # + SMALL_CONST + pert_probs = top_k_filter(pert_probs, k=top_k, + probs=True) # + SMALL_CONST + + # rescale + if torch.sum(pert_probs) <= 1: + pert_probs = pert_probs / torch.sum(pert_probs) + + else: + pert_logits = top_k_filter(pert_logits, k=top_k) # + SMALL_CONST + pert_probs = F.softmax(pert_logits, dim=-1) + + # sample or greedy + if sample: + last = torch.multinomial(pert_probs, num_samples=1) + + else: + _, last = torch.topk(pert_probs, k=1, dim=-1) + + # update context/output_so_far appending the new token + output_so_far = ( + last if output_so_far is None + else torch.cat((output_so_far, last), dim=1) + ) + + if time.time() - start > max_time and max_time != -1: + break + + final_losses = losses_this_iter[-1] if losses_this_iter else None + return output_so_far, final_losses + + +def set_generic_model_params(discrim_weights, discrim_meta): + if discrim_weights is None: + raise ValueError('When using a generic discriminator, ' + 'discrim_weights need to be specified') + if discrim_meta is None: + raise ValueError('When using a generic discriminator, ' + 'discrim_meta need to be specified') + + with open(discrim_meta, 'r') as discrim_meta_file: + meta = json.load(discrim_meta_file) + meta['path'] = discrim_weights + DISCRIMINATOR_MODELS_PARAMS['generic'] = meta + + +def run_model( + model, + tokenizer, + device, + raw_text, + max_time, + bag_of_words=None, + discrim=None, + discrim_weights=None, + discrim_meta=None, + discrim_label=-1, + stepsize=0.02, + length=10, + seed=None, + temperature=1.0, + top_k=10, + gm_scale=0.9, + kl_scale=0.01, + uncond=False, + num_iterations=3, + grad_length=10000, + num_samples=1, + horizon_length=1, + window_length=0, + decay=False, + gamma=1.5, + use_sampling=False +): + print(seed) + if seed is not None: + # set Random seed + torch.manual_seed(seed) + np.random.seed(seed) + + if discrim == 'generic': + set_generic_model_params(discrim_weights, discrim_meta) + + tokenized_cond_text = [tokenizer.encode( + tokenizer.bos_token + raw_text, max_length=512 - length - 1)] * num_samples + + # Freeze GPT-2 weights + for param in model.parameters(): + param.requires_grad = False + + # generate unperturbed and perturbed texts + + # full_text_generation returns: + # unpert_gen_tok_text, pert_gen_tok_texts, discrim_losses, losses_in_time + + pert_gen_tok_text, last_losses = full_text_generation( + model=model, + tokenizer=tokenizer, + context=tokenized_cond_text, + device=device, + max_time=max_time, + num_samples=num_samples, + discrim=discrim, + class_label=discrim_label, + bag_of_words=bag_of_words, + length=length, + grad_length=grad_length, + stepsize=stepsize, + num_iterations=num_iterations, + temperature=temperature, + gm_scale=gm_scale, + kl_scale=kl_scale, + top_k=top_k, + window_length=window_length, + horizon_length=horizon_length, + decay=decay, + gamma=gamma, + sample=use_sampling + ) + + generated_texts = [] + + # iterate through the perturbed texts + for sample, loss in zip(pert_gen_tok_text.tolist(), last_losses.tolist()): + generated_part = sample[len(tokenized_cond_text[0]):] + pert_gen_text = tokenizer.decode(generated_part) + + # keep the prefix, perturbed seq, original seq for each index + generated_texts.append( + { + "value": pert_gen_text, + "tokens": len(generated_part), + "loss": loss + } + ) + + return generated_texts diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..344e70db3d893a1168bbddf1df2d2c1c81e3974f --- /dev/null +++ b/backend/README.md @@ -0,0 +1,69 @@ +# Python backend + +## Setup + +``` +pip install -r requirements.txt +chmod +x launch.sh +``` + +## Execution + + +`./launch.sh` + +## Usage + +The API listens to the port `6006` and the route `autocomplete`. It listens to `POST` requests. +Query it like this: `{POST}http://:6006/autocomplete` + +The necessary argument is `context` which is a string of characters (ideally a sentence) which will be converted in tokens and fed to GPT-2. + +The optional arguments are detailed below: + +`length` is an unsigned int which sets the maximum length (in tokens) of the generated sentence __default: 100__ + +`n_samples` is an int `0 < n_samples <= 3` which sets the maximum amount of samples generated. __default: 3__ + +`max_time` is an unsigned float which sets an heuristic for the maximum time spent generating sentences. It is a heuristic because it is not exact, it can slightly overflow. __default: infinite__ + +`model_size` takes `"small"` or `"medium"` as input and corresponds to the GPT model size __default: small__ + +`temperature` float - temperature of the model __default: 1__ + +`max_tokens` int - maximum amount of tokens that will be fed into the model. __default: 256__ + +`top_p` float - 0 < top_p < 1, nucleus sampling; only tokens with a cumulative probability of top_p will be selected for multinomial sampling __default: 0.9__ + +`top_k` int - Only top k tokens will be selected for multinomial sampling. __default: 256__ + +## Return format + +The server returns a set of sentences according to the context. Their format is: +``` +{sentences: {value: string, time: number}[], time: number} +``` + +Example: + +With POST parameters as: + +```json +{ + "context": "That man is just another", + "samples": 3 +} +``` + +The response is as follows: + +```json +{ + "sentences": [ + {"value": " handicapped working man.", "time": 0.15415167808532715}, + {"value": " guy, doing everything his manly nature requires.", "time": 0.2581148147583008}, + {"value": " guy, Mohr said.", "time": 0.17547011375427246} + ], + "time": 0.264873743057251 +} +``` \ No newline at end of file diff --git a/backend/Utils.py b/backend/Utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7f6765eeffc51ec16248d88bdbc583427f156646 --- /dev/null +++ b/backend/Utils.py @@ -0,0 +1,57 @@ +import torch + + +def forward(model_name, model, input_ids, past, device='cpu'): + if "gpt2" in model_name or "ctrl" in model_name: + if past is not None: + return model(input_ids[:, -1], past=past) + return model(input_ids) + elif "xlnet" in model_name: + input_ids = torch.cat(( + input_ids, + torch.zeros((input_ids.shape[0], 1), dtype=torch.long, device=device) + ), dim=1) + + perm_mask = torch.zeros( + (input_ids.shape[0], input_ids.shape[1], input_ids.shape[1]), + dtype=torch.float, + device=device + ) + perm_mask[:, :, -1] = 1.0 + + target_mapping = torch.zeros( + (input_ids.shape[0], 1, input_ids.shape[1]), + dtype=torch.float, + device=device) + target_mapping[:, 0, -1] = 1.0 + + return model(input_ids, perm_mask=perm_mask, target_mapping=target_mapping) + elif "transfo-xl" in model_name: + return model(input_ids, mems=past) + else: + return model(input_ids) + + +def create_context(model_name, tokenizer, initial_text="", padding_text=None, max_tokens=512): + if not len(initial_text) and "gpt2" in model_name: + initial_text = "<|endoftext|>" + if 'xlnet' in model_name or "transfo-xl" in model_name: + initial_text = padding_text + initial_text + + if 'transfo-xl' in model_name: + max_tokens = int(max_tokens / 2) + + context_tokens = tokenizer.encode(initial_text)[-max_tokens:] + + if "gpt2" in model_name: + eot_token = tokenizer.encoder["<|endoftext|>"] + if len(context_tokens) == 0: + context_tokens = [tokenizer.encoder["<|endoftext|>"]] + elif "xlnet" in model_name: + eot_token = tokenizer.convert_tokens_to_ids('') + else: + eot_token = None + dot_token = tokenizer.encode(".")[-1] + + return context_tokens, eot_token, dot_token + diff --git a/backend/id/.gitignore b/backend/id/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..b2d59d1f7578b05e96f3b3f8a7d5039df3d06fb6 --- /dev/null +++ b/backend/id/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/dist \ No newline at end of file diff --git a/backend/id/id.ts b/backend/id/id.ts new file mode 100644 index 0000000000000000000000000000000000000000..37cdbccdecc5aa5744674ef2f9a000838d680c32 --- /dev/null +++ b/backend/id/id.ts @@ -0,0 +1,17 @@ +import * as Koa from "koa"; +import * as Router from "koa-router"; + +let id = 0; + +const app = new Koa(); +const router = new Router(); + +router.get("/*", async ctx => { + ctx.body = id++; +}); + +app.use(router.routes()); + +app.listen(3000); + +console.log("Server running on port 3000"); diff --git a/backend/id/package.json b/backend/id/package.json new file mode 100644 index 0000000000000000000000000000000000000000..85d316ec5e4919f204f9f0f8ccebfd1e8758da4f --- /dev/null +++ b/backend/id/package.json @@ -0,0 +1,12 @@ +{ + "dependencies": { + "@types/koa": "^2.0.48", + "@types/koa-router": "^7.0.40", + "koa": "^2.7.0", + "koa-router": "^7.4.0", + "typescript": "^3.5.1" + }, + "scripts": { + "start": "tsc && node dist/id.js" + } +} diff --git a/backend/id/tsconfig.json b/backend/id/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..439840f6eb84627e8bcf96d8c2df7dc6c2bcf33e --- /dev/null +++ b/backend/id/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "commonjs", + "outDir": "dist/", + "strictNullChecks": true, + "strict": true, + "lib": ["esnext", "dom", "es6", "es2016", "es2017", "es2018"] + } +} diff --git a/backend/id/yarn.lock b/backend/id/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..44419c9e4f85e165db2132422fd096a65f57a4dd --- /dev/null +++ b/backend/id/yarn.lock @@ -0,0 +1,426 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/accepts@*": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== + dependencies: + "@types/node" "*" + +"@types/body-parser@*": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/cookies@*": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.2.tgz#5e0560d46ed9998082dce799af1058dd6a49780a" + integrity sha512-jnihWgshWystcJKrz8C9hV+Ot9lqOUyAh2RF+o3BEo6K6AS2l4zYCb9GYaBuZ3C6Il59uIGqpE3HvCun4KKeJA== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.16.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz#50ba6f8a691c08a3dd9fa7fba25ef3133d298049" + integrity sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@*": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287" + integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/http-assert@*": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.4.0.tgz#41d173466e396e99a14d75f7160cc997f2f9ed8b" + integrity sha512-TZDqvFW4nQwL9DVSNJIJu4lPLttKgzRF58COa7Vs42Ki/MrhIqUbeIw0MWn4kGLiZLXB7oCBibm7nkSjPkzfKQ== + +"@types/keygrip@*": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.1.tgz#ff540462d2fb4d0a88441ceaf27d287b01c3d878" + integrity sha1-/1QEYtL7TQqIRBzq8n0oewHD2Hg= + +"@types/koa-compose@*": + version "3.2.4" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.4.tgz#76a461634a59c3e13449831708bb9b355fb1548e" + integrity sha512-ioou0rxkuWL+yBQYsHUQAzRTfVxAg8Y2VfMftU+Y3RA03/MzuFL0x/M2sXXj3PkfnENbHsjeHR1aMdezLYpTeA== + dependencies: + "@types/koa" "*" + +"@types/koa-router@^7.0.40": + version "7.0.40" + resolved "https://registry.yarnpkg.com/@types/koa-router/-/koa-router-7.0.40.tgz#9654dbc43375a0380c44c49c4504b4dbfc3e4e6a" + integrity sha512-YK4+WGXch6Ig9PreZ9jlHZb2onm0S1szGw0oQxWvPhoyjSHo1Tq+CpjxMmthEUIQUc9KznOGgehFarOx8XwsFw== + dependencies: + "@types/koa" "*" + +"@types/koa@*", "@types/koa@^2.0.48": + version "2.0.48" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.0.48.tgz#29162783029d3e5df8b58c55f6bf0d35f78fc39f" + integrity sha512-CiIUYhHlOFJhSCTmsFoFkV2t9ij1JwW26nt0W9XZoWTvmAw6zTE0+k3IAoGICtjzIfhZpZcO323NHmI1LGmdDw== + dependencies: + "@types/accepts" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/node@*": + version "12.0.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.0.7.tgz#4f2563bad652b2acb1722d7e7aae2b0ff62d192c" + integrity sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +accepts@^1.3.5: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +any-promise@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= + +cache-content-type@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" + integrity sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA== + dependencies: + mime-types "^2.1.18" + ylru "^1.2.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +content-disposition@~0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookies@~0.7.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" + integrity sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A== + dependencies: + depd "~1.1.2" + keygrip "~1.0.3" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +deep-equal@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +error-inject@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" + integrity sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc= + +escape-html@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +fresh@~0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +http-assert@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.1.tgz#c5f725d677aa7e873ef736199b89686cceb37878" + integrity sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw== + dependencies: + deep-equal "~1.0.1" + http-errors "~1.7.2" + +http-errors@^1.3.1, http-errors@^1.6.3, http-errors@~1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +keygrip@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" + integrity sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g== + +koa-compose@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" + integrity sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec= + dependencies: + any-promise "^1.1.0" + +koa-compose@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" + integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw== + +koa-convert@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" + integrity sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA= + dependencies: + co "^4.6.0" + koa-compose "^3.0.0" + +koa-is-json@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" + integrity sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ= + +koa-router@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" + integrity sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g== + dependencies: + debug "^3.1.0" + http-errors "^1.3.1" + koa-compose "^3.0.0" + methods "^1.0.1" + path-to-regexp "^1.1.1" + urijs "^1.19.0" + +koa@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" + integrity sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q== + dependencies: + accepts "^1.3.5" + cache-content-type "^1.0.0" + content-disposition "~0.5.2" + content-type "^1.0.4" + cookies "~0.7.1" + debug "~3.1.0" + delegates "^1.0.0" + depd "^1.1.2" + destroy "^1.0.4" + error-inject "^1.0.0" + escape-html "^1.0.3" + fresh "~0.5.2" + http-assert "^1.3.0" + http-errors "^1.6.3" + is-generator-function "^1.0.7" + koa-compose "^4.1.0" + koa-convert "^1.2.0" + koa-is-json "^1.0.0" + on-finished "^2.3.0" + only "~0.0.2" + parseurl "^1.3.2" + statuses "^1.5.0" + type-is "^1.6.16" + vary "^1.1.2" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +methods@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.18, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +on-finished@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +only@~0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" + integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q= + +parseurl@^1.3.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-to-regexp@^1.1.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30= + dependencies: + isarray "0.0.1" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +"statuses@>= 1.5.0 < 2", statuses@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +type-is@^1.6.16: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.1.tgz#ba72a6a600b2158139c5dd8850f700e231464202" + integrity sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw== + +urijs@^1.19.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" + integrity sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg== + +vary@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +ylru@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" + integrity sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ== diff --git a/backend/install.sh b/backend/install.sh new file mode 100644 index 0000000000000000000000000000000000000000..b477f846cf1c32a8d7b7b8abc0c556d0dcf63dcd --- /dev/null +++ b/backend/install.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +sudo apt install jq -y +pip install -r requirements.txt +cd id +npm install \ No newline at end of file diff --git a/backend/launch.sh b/backend/launch.sh new file mode 100755 index 0000000000000000000000000000000000000000..a9f2cfa3ff785744453be27335d581eef4c3c3d2 --- /dev/null +++ b/backend/launch.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +pgrep -f gunicorn | xargs kill -9 +kill $(lsof -t -i:3000) + +cd id +npm run start & +sleep 5 +cd - + +if [[ "$1" == "" ]]; then + echo "JSON file argument not supplied. Exiting." 1>&2 + exit 1 +fi + +# Number of GPUs +N_GPU=$(nvidia-smi -L | wc -l) + +export FILE=$1 +export GPU_PER_WORKER=`cat "$FILE" | jq -r .gpu_per_worker` + +# Are there enough GPUs ? +if [[ $(($N_GPU / $GPU_PER_WORKER)) -eq 0 ]]; then + echo "Not enough GPUs to run this." 1>&2 + exit 1 +fi + +N_WORKERS=$(($N_GPU / $GPU_PER_WORKER)) + +echo "File $FILE" +echo "Available GPUs $N_GPU" +echo "GPUs per worker $GPU_PER_WORKER" +echo "Total workers $N_WORKERS" + +function sys_exit () +{ + echo "Ctrl-C caught...performing clean up" + echo "Cleaning up the servers." + echo $INST1 + kill -9 $INST1 + exit 2 + +} + +trap "sys_exit" INT + +echo "Running server with" ${N_WORKERS} "workers." +gunicorn --statsd-host=localhost:8125 -w ${N_WORKERS} API --bind=0.0.0.0:6006 --statsd-prefix=transformer-autocomplete -t 600 & +INST1=$! + +while true; do sleep 1000; done diff --git a/backend/machine_configurations/neuralgenv2.json b/backend/machine_configurations/neuralgenv2.json new file mode 100644 index 0000000000000000000000000000000000000000..c3ca5072144163dd1d08630caba80ce4d6f92f4f --- /dev/null +++ b/backend/machine_configurations/neuralgenv2.json @@ -0,0 +1,13 @@ +{ + "models_to_load": [ + "gpt2/small", + "gpt2/medium", + "gpt2/large", + "gpt2/arxiv-nlp", + + "gpt", + "xlnet", + "distilgpt2/small" + ], + "gpu_per_worker": 1 +} \ No newline at end of file diff --git a/backend/machine_configurations/transformer-autocomplete.json b/backend/machine_configurations/transformer-autocomplete.json new file mode 100644 index 0000000000000000000000000000000000000000..54143d644eb92975b077272c0c6f0b50ad7992aa --- /dev/null +++ b/backend/machine_configurations/transformer-autocomplete.json @@ -0,0 +1,11 @@ +{ + "models_to_load": [ + "ctrl", + "gpt2/xl" + ], + "gpu_per_worker": 2, + "cached_models": { + "gpt2/xl": "/datadrive/transformer-autocomplete/backend/gpt2-xl-local", + "ctrl": "/datadrive/transformer-autocomplete/backend/ctrl-local" + } +} diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d8d411ec7c07d7af57dc352164eaa7e851916c16 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +falcon +gunicorn +torch +transformers diff --git a/backend/run_pplm_discrim_train.py b/backend/run_pplm_discrim_train.py new file mode 100644 index 0000000000000000000000000000000000000000..fccfb144268bd373f6b57c031ec1fa33130cbb4b --- /dev/null +++ b/backend/run_pplm_discrim_train.py @@ -0,0 +1,582 @@ +#! /usr/bin/env python3 +# coding=utf-8 + +# This code is licensed under a non-commercial license. + +import argparse +import csv +import json +import math +import time + +import numpy as np +import torch +import torch.nn.functional as F +import torch.optim +import torch.optim as optim +import torch.utils.data as data +from nltk.tokenize.treebank import TreebankWordDetokenizer +from torchtext import data as torchtext_data +from torchtext import datasets +from tqdm import tqdm, trange + +from transformers import GPT2Tokenizer, GPT2LMHeadModel + +torch.manual_seed(0) +np.random.seed(0) +EPSILON = 1e-10 +device = "cpu" +example_sentence = "This is incredible! I love it, this is the best chicken I have ever had." +max_length_seq = 100 + + +class ClassificationHead(torch.nn.Module): + """Classification Head for transformer encoders""" + + def __init__(self, class_size, embed_size): + super(ClassificationHead, self).__init__() + self.class_size = class_size + self.embed_size = embed_size + # self.mlp1 = torch.nn.Linear(embed_size, embed_size) + # self.mlp2 = (torch.nn.Linear(embed_size, class_size)) + self.mlp = torch.nn.Linear(embed_size, class_size) + + def forward(self, hidden_state): + # hidden_state = F.relu(self.mlp1(hidden_state)) + # hidden_state = self.mlp2(hidden_state) + logits = self.mlp(hidden_state) + return logits + + +class Discriminator(torch.nn.Module): + """Transformer encoder followed by a Classification Head""" + + def __init__( + self, + class_size, + pretrained_model="gpt2-medium", + cached_mode=False + ): + super(Discriminator, self).__init__() + self.tokenizer = GPT2Tokenizer.from_pretrained(pretrained_model) + self.encoder = GPT2LMHeadModel.from_pretrained(pretrained_model) + self.embed_size = self.encoder.transformer.config.hidden_size + self.classifier_head = ClassificationHead( + class_size=class_size, + embed_size=self.embed_size + ) + self.cached_mode = cached_mode + + def get_classifier(self): + return self.classifier_head + + def train_custom(self): + for param in self.encoder.parameters(): + param.requires_grad = False + self.classifier_head.train() + + def avg_representation(self, x): + mask = x.ne(0).unsqueeze(2).repeat( + 1, 1, self.embed_size + ).float().to(device).detach() + hidden, _ = self.encoder.transformer(x) + masked_hidden = hidden * mask + avg_hidden = torch.sum(masked_hidden, dim=1) / ( + torch.sum(mask, dim=1).detach() + EPSILON + ) + return avg_hidden + + def forward(self, x): + if self.cached_mode: + avg_hidden = x.to(device) + else: + avg_hidden = self.avg_representation(x.to(device)) + + logits = self.classifier_head(avg_hidden) + probs = F.log_softmax(logits, dim=-1) + + return probs + + +class Dataset(data.Dataset): + def __init__(self, X, y): + """Reads source and target sequences from txt files.""" + self.X = X + self.y = y + + def __len__(self): + return len(self.X) + + def __getitem__(self, index): + """Returns one data pair (source and target).""" + data = {} + data["X"] = self.X[index] + data["y"] = self.y[index] + return data + + +def collate_fn(data): + def pad_sequences(sequences): + lengths = [len(seq) for seq in sequences] + + padded_sequences = torch.zeros( + len(sequences), + max(lengths) + ).long() # padding value = 0 + + for i, seq in enumerate(sequences): + end = lengths[i] + padded_sequences[i, :end] = seq[:end] + + return padded_sequences, lengths + + item_info = {} + for key in data[0].keys(): + item_info[key] = [d[key] for d in data] + + x_batch, _ = pad_sequences(item_info["X"]) + y_batch = torch.tensor(item_info["y"], dtype=torch.long) + + return x_batch, y_batch + + +def cached_collate_fn(data): + item_info = {} + for key in data[0].keys(): + item_info[key] = [d[key] for d in data] + + x_batch = torch.cat(item_info["X"], 0) + y_batch = torch.tensor(item_info["y"], dtype=torch.long) + + return x_batch, y_batch + + +def train_epoch(data_loader, discriminator, optimizer, + epoch=0, log_interval=10): + samples_so_far = 0 + discriminator.train_custom() + for batch_idx, (input_t, target_t) in enumerate(data_loader): + input_t, target_t = input_t.to(device), target_t.to(device) + + optimizer.zero_grad() + + output_t = discriminator(input_t) + loss = F.nll_loss(output_t, target_t) + loss.backward(retain_graph=True) + optimizer.step() + + samples_so_far += len(input_t) + + if batch_idx % log_interval == 0: + print( + "Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}".format( + epoch + 1, + samples_so_far, len(data_loader.dataset), + 100 * samples_so_far / len(data_loader.dataset), loss.item() + ) + ) + + +def evaluate_performance(data_loader, discriminator): + discriminator.eval() + test_loss = 0 + correct = 0 + with torch.no_grad(): + for input_t, target_t in data_loader: + input_t, target_t = input_t.to(device), target_t.to(device) + output_t = discriminator(input_t) + # sum up batch loss + test_loss += F.nll_loss(output_t, target_t, reduction="sum").item() + # get the index of the max log-probability + pred_t = output_t.argmax(dim=1, keepdim=True) + correct += pred_t.eq(target_t.view_as(pred_t)).sum().item() + + test_loss /= len(data_loader.dataset) + + print( + "Performance on test set: " + "Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)".format( + test_loss, correct, len(data_loader.dataset), + 100. * correct / len(data_loader.dataset) + ) + ) + + +def predict(input_sentence, model, classes, cached=False): + input_t = model.tokenizer.encode(input_sentence) + input_t = torch.tensor([input_t], dtype=torch.long, device=device) + if cached: + input_t = model.avg_representation(input_t) + + log_probs = model(input_t).data.cpu().numpy().flatten().tolist() + print("Input sentence:", input_sentence) + print("Predictions:", ", ".join( + "{}: {:.4f}".format(c, math.exp(log_prob)) for c, log_prob in + zip(classes, log_probs) + )) + + +def get_cached_data_loader(dataset, batch_size, discriminator, shuffle=False): + data_loader = torch.utils.data.DataLoader(dataset=dataset, + batch_size=batch_size, + collate_fn=collate_fn) + + xs = [] + ys = [] + for batch_idx, (x, y) in enumerate(tqdm(data_loader, ascii=True)): + with torch.no_grad(): + x = x.to(device) + avg_rep = discriminator.avg_representation(x).cpu().detach() + avg_rep_list = torch.unbind(avg_rep.unsqueeze(1)) + xs += avg_rep_list + ys += y.cpu().numpy().tolist() + + data_loader = torch.utils.data.DataLoader( + dataset=Dataset(xs, ys), + batch_size=batch_size, + shuffle=shuffle, + collate_fn=cached_collate_fn) + + return data_loader + + +def train_discriminator( + dataset, dataset_fp=None, pretrained_model="gpt2-medium", + epochs=10, batch_size=64, log_interval=10, + save_model=False, cached=False, no_cuda=False): + global device + device = "cuda" if torch.cuda.is_available() and not no_cuda else "cpu" + + print("Preprocessing {} dataset...".format(dataset)) + start = time.time() + + if dataset == "SST": + idx2class = ["positive", "negative", "very positive", "very negative", + "neutral"] + class2idx = {c: i for i, c in enumerate(idx2class)} + + discriminator = Discriminator( + class_size=len(idx2class), + pretrained_model=pretrained_model, + cached_mode=cached + ).to(device) + + text = torchtext_data.Field() + label = torchtext_data.Field(sequential=False) + train_data, val_data, test_data = datasets.SST.splits( + text, + label, + fine_grained=True, + train_subtrees=True, + ) + + x = [] + y = [] + for i in trange(len(train_data), ascii=True): + seq = TreebankWordDetokenizer().detokenize( + vars(train_data[i])["text"] + ) + seq = discriminator.tokenizer.encode(seq) + seq = torch.tensor([50256] + seq, device=device, dtype=torch.long) + x.append(seq) + y.append(class2idx[vars(train_data[i])["label"]]) + train_dataset = Dataset(x, y) + + test_x = [] + test_y = [] + for i in trange(len(test_data), ascii=True): + seq = TreebankWordDetokenizer().detokenize( + vars(test_data[i])["text"] + ) + seq = discriminator.tokenizer.encode(seq) + seq = torch.tensor([50256] + seq, device=device, dtype=torch.long) + test_x.append(seq) + test_y.append(class2idx[vars(test_data[i])["label"]]) + test_dataset = Dataset(test_x, test_y) + + discriminator_meta = { + "class_size": len(idx2class), + "embed_size": discriminator.embed_size, + "pretrained_model": pretrained_model, + "class_vocab": class2idx, + "default_class": 2, + } + + elif dataset == "clickbait": + idx2class = ["non_clickbait", "clickbait"] + class2idx = {c: i for i, c in enumerate(idx2class)} + + discriminator = Discriminator( + class_size=len(idx2class), + pretrained_model=pretrained_model, + cached_mode=cached + ).to(device) + + with open("datasets/clickbait/clickbait_train_prefix.txt") as f: + data = [] + for i, line in enumerate(f): + try: + data.append(eval(line)) + except: + print("Error evaluating line {}: {}".format( + i, line + )) + continue + x = [] + y = [] + with open("datasets/clickbait/clickbait_train_prefix.txt") as f: + for i, line in enumerate(tqdm(f, ascii=True)): + try: + d = eval(line) + seq = discriminator.tokenizer.encode(d["text"]) + + if len(seq) < max_length_seq: + seq = torch.tensor( + [50256] + seq, device=device, dtype=torch.long + ) + else: + print("Line {} is longer than maximum length {}".format( + i, max_length_seq + )) + continue + x.append(seq) + y.append(d["label"]) + except: + print("Error evaluating / tokenizing" + " line {}, skipping it".format(i)) + pass + + full_dataset = Dataset(x, y) + train_size = int(0.9 * len(full_dataset)) + test_size = len(full_dataset) - train_size + train_dataset, test_dataset = torch.utils.data.random_split( + full_dataset, [train_size, test_size] + ) + + discriminator_meta = { + "class_size": len(idx2class), + "embed_size": discriminator.embed_size, + "pretrained_model": pretrained_model, + "class_vocab": class2idx, + "default_class": 1, + } + + elif dataset == "toxic": + idx2class = ["non_toxic", "toxic"] + class2idx = {c: i for i, c in enumerate(idx2class)} + + discriminator = Discriminator( + class_size=len(idx2class), + pretrained_model=pretrained_model, + cached_mode=cached + ).to(device) + + x = [] + y = [] + with open("datasets/toxic/toxic_train.txt") as f: + for i, line in enumerate(tqdm(f, ascii=True)): + try: + d = eval(line) + seq = discriminator.tokenizer.encode(d["text"]) + + if len(seq) < max_length_seq: + seq = torch.tensor( + [50256] + seq, device=device, dtype=torch.long + ) + else: + print("Line {} is longer than maximum length {}".format( + i, max_length_seq + )) + continue + x.append(seq) + y.append(int(np.sum(d["label"]) > 0)) + except: + print("Error evaluating / tokenizing" + " line {}, skipping it".format(i)) + pass + + full_dataset = Dataset(x, y) + train_size = int(0.9 * len(full_dataset)) + test_size = len(full_dataset) - train_size + train_dataset, test_dataset = torch.utils.data.random_split( + full_dataset, [train_size, test_size] + ) + + discriminator_meta = { + "class_size": len(idx2class), + "embed_size": discriminator.embed_size, + "pretrained_model": pretrained_model, + "class_vocab": class2idx, + "default_class": 0, + } + + else: # if dataset == "generic": + # This assumes the input dataset is a TSV with the following structure: + # class \t text + + if dataset_fp is None: + raise ValueError("When generic dataset is selected, " + "dataset_fp needs to be specified aswell.") + + classes = set() + with open(dataset_fp) as f: + csv_reader = csv.reader(f, delimiter="\t") + for row in tqdm(csv_reader, ascii=True): + if row: + classes.add(row[0]) + + idx2class = sorted(classes) + class2idx = {c: i for i, c in enumerate(idx2class)} + + discriminator = Discriminator( + class_size=len(idx2class), + pretrained_model=pretrained_model, + cached_mode=cached + ).to(device) + + x = [] + y = [] + with open(dataset_fp) as f: + csv_reader = csv.reader(f, delimiter="\t") + for i, row in enumerate(tqdm(csv_reader, ascii=True)): + if row: + label = row[0] + text = row[1] + + try: + seq = discriminator.tokenizer.encode(text) + if (len(seq) < max_length_seq): + seq = torch.tensor( + [50256] + seq, + device=device, + dtype=torch.long + ) + + else: + print( + "Line {} is longer than maximum length {}".format( + i, max_length_seq + )) + continue + + x.append(seq) + y.append(class2idx[label]) + + except: + print("Error tokenizing line {}, skipping it".format(i)) + pass + + full_dataset = Dataset(x, y) + train_size = int(0.9 * len(full_dataset)) + test_size = len(full_dataset) - train_size + train_dataset, test_dataset = torch.utils.data.random_split( + full_dataset, + [train_size, test_size] + ) + + discriminator_meta = { + "class_size": len(idx2class), + "embed_size": discriminator.embed_size, + "pretrained_model": pretrained_model, + "class_vocab": class2idx, + "default_class": 0, + } + + end = time.time() + print("Preprocessed {} data points".format( + len(train_dataset) + len(test_dataset)) + ) + print("Data preprocessing took: {:.3f}s".format(end - start)) + + if cached: + print("Building representation cache...") + + start = time.time() + + train_loader = get_cached_data_loader( + train_dataset, batch_size, discriminator, shuffle=True + ) + + test_loader = get_cached_data_loader( + test_dataset, batch_size, discriminator + ) + + end = time.time() + print("Building representation cache took: {:.3f}s".format(end - start)) + + else: + train_loader = torch.utils.data.DataLoader(dataset=train_dataset, + batch_size=batch_size, + shuffle=True, + collate_fn=collate_fn) + test_loader = torch.utils.data.DataLoader(dataset=test_dataset, + batch_size=batch_size, + collate_fn=collate_fn) + + if save_model: + with open("{}_classifier_head_meta.json".format(dataset), + "w") as meta_file: + json.dump(discriminator_meta, meta_file) + + optimizer = optim.Adam(discriminator.parameters(), lr=0.0001) + + for epoch in range(epochs): + start = time.time() + print("\nEpoch", epoch + 1) + + train_epoch( + discriminator=discriminator, + data_loader=train_loader, + optimizer=optimizer, + epoch=epoch, + log_interval=log_interval + ) + evaluate_performance( + data_loader=test_loader, + discriminator=discriminator + ) + + end = time.time() + print("Epoch took: {:.3f}s".format(end - start)) + + print("\nExample prediction") + predict(example_sentence, discriminator, idx2class, cached) + + if save_model: + # torch.save(discriminator.state_dict(), + # "{}_discriminator_{}.pt".format( + # args.dataset, epoch + 1 + # )) + torch.save(discriminator.get_classifier().state_dict(), + "{}_classifier_head_epoch_{}.pt".format(dataset, + epoch + 1)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Train a discriminator on top of GPT-2 representations") + parser.add_argument("--dataset", type=str, default="SST", + choices=("SST", "clickbait", "toxic", "generic"), + help="dataset to train the discriminator on." + "In case of generic, the dataset is expected" + "to be a TSBV file with structure: class \\t text") + parser.add_argument("--dataset_fp", type=str, default="", + help="File path of the dataset to use. " + "Needed only in case of generic datadset") + parser.add_argument("--pretrained_model", type=str, default="gpt2-medium", + help="Pretrained model to use as encoder") + parser.add_argument("--epochs", type=int, default=10, metavar="N", + help="Number of training epochs") + parser.add_argument("--batch_size", type=int, default=64, metavar="N", + help="input batch size for training (default: 64)") + parser.add_argument("--log_interval", type=int, default=10, metavar="N", + help="how many batches to wait before logging training status") + parser.add_argument("--save_model", action="store_true", + help="whether to save the model") + parser.add_argument("--cached", action="store_true", + help="whether to cache the input representations") + parser.add_argument("--no_cuda", action="store_true", + help="use to turn off cuda") + args = parser.parse_args() + + train_discriminator(**(vars(args))) diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000000000000000000000000000000000000..8e42f71557c9bfe65479231f2f974b465055a7e2 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /NGINX_/ ) ? name : \"\" } }" < /dev/null )) + +envsubst "$defined_envs" < nginx.conf > /etc/nginx/nginx.conf + +nginx && node server/dist/server.js diff --git a/front/.vscode/settings.json b/front/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..9782a2e0a0a900c39bbc95df32124ab3a061e3e6 --- /dev/null +++ b/front/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + // Configure glob patterns for excluding files and folders in searches. Inherits all glob patterns from the files.exclude setting. + "search.exclude": { + "dist": true, + "build": true, + } +} \ No newline at end of file diff --git a/front/assets/Icon-info.svg b/front/assets/Icon-info.svg new file mode 100644 index 0000000000000000000000000000000000000000..a86f626c09567451359d16b2dd4eb6675e2dead0 --- /dev/null +++ b/front/assets/Icon-info.svg @@ -0,0 +1,9 @@ + + + + Icon-info + Created with Sketch. + + + + \ No newline at end of file diff --git a/front/assets/Salesforce_logo.svg b/front/assets/Salesforce_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..7090a08c1fc2b6ceca76ef29d9941e90c859967e --- /dev/null +++ b/front/assets/Salesforce_logo.svg @@ -0,0 +1,83 @@ + + + diff --git a/front/assets/Uber_logo.svg b/front/assets/Uber_logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..22f9bd9a40252748afdd47d5955acd8ee42a978b --- /dev/null +++ b/front/assets/Uber_logo.svg @@ -0,0 +1,11 @@ + + + + Uber_logo + Created with Sketch. + + + + \ No newline at end of file diff --git a/front/assets/cross-collab.svg b/front/assets/cross-collab.svg new file mode 100644 index 0000000000000000000000000000000000000000..e04b2c08a9088dcb2b449c158de638d25c8c4f7a --- /dev/null +++ b/front/assets/cross-collab.svg @@ -0,0 +1,14 @@ + + + + cross-collab + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/front/assets/github-buttons.js b/front/assets/github-buttons.js new file mode 100644 index 0000000000000000000000000000000000000000..621b865173e9ffb9cbffa21833ce13d1becc6250 --- /dev/null +++ b/front/assets/github-buttons.js @@ -0,0 +1,9 @@ +/*! + * github-buttons v2.2.10 + * (c) 2019 なつき + * @license BSD-2-Clause + */ +/** + * Julien: just modified to add a `transform: scale(1.5);` on the .widget + */ +!function(){"use strict";var e=window.document,t=e.location,o=window.encodeURIComponent,r=window.decodeURIComponent,n=window.Math,a=window.HTMLElement,i=window.XMLHttpRequest,l="https://unpkg.com/github-buttons@2.2.10/dist/buttons.html",c=i&&i.prototype&&"withCredentials"in i.prototype,d=c&&a&&a.prototype.attachShadow&&!a.prototype.attachShadow.prototype,s=function(e,t,o){e.addEventListener?e.addEventListener(t,o):e.attachEvent("on"+t,o)},u=function(e,t,o){e.removeEventListener?e.removeEventListener(t,o):e.detachEvent("on"+t,o)},h=function(e,t,o){var r=function(n){return u(e,t,r),o(n)};s(e,t,r)},f=function(e,t,o){var r=function(n){if(t.test(e.readyState))return u(e,"readystatechange",r),o(n)};s(e,"readystatechange",r)},p=function(e){return function(t,o,r){var n=e.createElement(t);if(o)for(var a in o){var i=o[a];null!=i&&(null!=n[a]?n[a]=i:n.setAttribute(a,i))}if(r)for(var l=0,c=r.length;l'},eye:{width:16,height:16,path:''},star:{width:14,height:16,path:''},"repo-forked":{width:10,height:16,path:''},"issue-opened":{width:14,height:16,path:''},"cloud-download":{width:16,height:16,path:''}},w={},x=function(e,t,o){var r=p(e.ownerDocument),n=e.appendChild(r("style",{type:"text/css"}));n.styleSheet?n.styleSheet.cssText=m:n.appendChild(e.ownerDocument.createTextNode(m));var a,l,d=r("a",{className:"btn",href:t.href,target:"_blank",innerHTML:(a=t["data-icon"],l=/^large$/i.test(t["data-size"])?16:14,a=(""+a).toLowerCase().replace(/^octicon-/,""),{}.hasOwnProperty.call(v,a)||(a="mark-github"),'"),"aria-label":t["aria-label"]||void 0},[" ",r("span",{},[t["data-text"]||""])]);/\.github\.com$/.test("."+d.hostname)?/^https?:\/\/((gist\.)?github\.com\/[^\/?#]+\/[^\/?#]+\/archive\/|github\.com\/[^\/?#]+\/[^\/?#]+\/releases\/download\/|codeload\.github\.com\/)/.test(d.href)&&(d.target="_top"):(d.href="#",d.target="_self");var u,h,g,x,y=e.appendChild(r("div",{className:"widget"+(/^large$/i.test(t["data-size"])?" lg":"")},[d]));/^(true|1)$/i.test(t["data-show-count"])&&"github.com"===d.hostname&&(u=d.pathname.replace(/^(?!\/)/,"/").match(/^\/([^\/?#]+)(?:\/([^\/?#]+)(?:\/(?:(subscription)|(fork)|(issues)|([^\/?#]+)))?)?(?:[\/?#]|$)/))&&!u[6]?(u[2]?(h="/repos/"+u[1]+"/"+u[2],u[3]?(x="subscribers_count",g="watchers"):u[4]?(x="forks_count",g="network"):u[5]?(x="open_issues_count",g="issues"):(x="stargazers_count",g="stargazers")):(h="/users/"+u[1],g=x="followers"),function(e,t){var o=w[e]||(w[e]=[]);if(!(o.push(t)>1)){var r=b(function(){for(delete w[e];t=o.shift();)t.apply(null,arguments)});if(c){var n=new i;s(n,"abort",r),s(n,"error",r),s(n,"load",function(){var e;try{e=JSON.parse(n.responseText)}catch(e){return void r(e)}r(200!==n.status,e)}),n.open("GET",e),n.send()}else{var a=this||window;a._=function(e){a._=null,r(200!==e.meta.status,e.data)};var l=p(a.document)("script",{async:!0,src:e+(/\?/.test(e)?"&":"?")+"callback=_"}),d=function(){a._&&a._({meta:{}})};s(l,"load",d),s(l,"error",d),l.readyState&&f(l,/de|m/,d),a.document.getElementsByTagName("head")[0].appendChild(l)}}}.call(this,"https://api.github.com"+h,function(e,t){if(!e){var n=t[x];y.appendChild(r("a",{className:"social-count",href:t.html_url+"/"+g,target:"_blank","aria-label":n+" "+x.replace(/_count$/,"").replace("_"," ").slice(0,n<2?-1:void 0)+" on GitHub"},[r("b"),r("i"),r("span",{},[(""+n).replace(/\B(?=(\d{3})+(?!\d))/g,",")])]))}o&&o(y)})):o&&o(y)},y=window.devicePixelRatio||1,C=function(e){return(y>1?n.ceil(n.round(e*y)/y*2)/2:n.ceil(e))||0},F=function(e,t){e.style.width=t[0]+"px",e.style.height=t[1]+"px"},k=function(t,r){if(null!=t&&null!=r)if(t.getAttribute&&(t=function(e){for(var t={href:e.href,title:e.title,"aria-label":e.getAttribute("aria-label")},o=["icon","text","size","show-count"],r=0,n=o.length;r + + icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/icon-back.svg b/front/assets/icon-back.svg new file mode 100644 index 0000000000000000000000000000000000000000..1b0c954a42d35874fed9fb17308d3fe6855d104c --- /dev/null +++ b/front/assets/icon-back.svg @@ -0,0 +1,15 @@ + + + + icon-back + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/icon-publish.svg b/front/assets/icon-publish.svg new file mode 100644 index 0000000000000000000000000000000000000000..f0fd6f17cf2b0dfccdfca0ba0ed86467b1bd7633 --- /dev/null +++ b/front/assets/icon-publish.svg @@ -0,0 +1,16 @@ + + + + icon-publish + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/iconmonstr-download-14.svg b/front/assets/iconmonstr-download-14.svg new file mode 100644 index 0000000000000000000000000000000000000000..73a703d8d56e36012b27b05a43f3e39bf4ccd919 --- /dev/null +++ b/front/assets/iconmonstr-download-14.svg @@ -0,0 +1,13 @@ + + + + iconmonstr-download-14 + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/front/assets/iconmonstr-media-control-55.svg b/front/assets/iconmonstr-media-control-55.svg new file mode 100644 index 0000000000000000000000000000000000000000..a92b1af29fefd29f0193392b2482841ea57ee371 --- /dev/null +++ b/front/assets/iconmonstr-media-control-55.svg @@ -0,0 +1,13 @@ + + + + iconmonstr-media-control-55 + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/front/assets/iconmonstr-share-11-purple.svg b/front/assets/iconmonstr-share-11-purple.svg new file mode 100644 index 0000000000000000000000000000000000000000..c236ac3bcbb6ca90db6804bf66c3bc6ad26e5546 --- /dev/null +++ b/front/assets/iconmonstr-share-11-purple.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/iconmonstr-share-11.svg b/front/assets/iconmonstr-share-11.svg new file mode 100644 index 0000000000000000000000000000000000000000..4cf14f112fb4039d03d7f65316d5bf10c4162d34 --- /dev/null +++ b/front/assets/iconmonstr-share-11.svg @@ -0,0 +1,13 @@ + + + + iconmonstr-share-11 + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/front/assets/oval.svg b/front/assets/oval.svg new file mode 100644 index 0000000000000000000000000000000000000000..e135704dc27079d0ae60cb6ecad5edccbd9f1391 --- /dev/null +++ b/front/assets/oval.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/tail-spin.svg b/front/assets/tail-spin.svg new file mode 100644 index 0000000000000000000000000000000000000000..ee8bf9a5a571bb7f0c7c5cae4ff7b97ee12e3a84 --- /dev/null +++ b/front/assets/tail-spin.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/front/assets/thumbnail-large-distilgpt2.png b/front/assets/thumbnail-large-distilgpt2.png new file mode 100644 index 0000000000000000000000000000000000000000..9dddd3564d5f1b1404bfb8700e0edfefd297ad8d Binary files /dev/null and b/front/assets/thumbnail-large-distilgpt2.png differ diff --git a/front/assets/thumbnail-large-pplm.png b/front/assets/thumbnail-large-pplm.png new file mode 100644 index 0000000000000000000000000000000000000000..1109dfbfc200b72a40b5a76cb54e440c99b2efbc Binary files /dev/null and b/front/assets/thumbnail-large-pplm.png differ diff --git a/front/assets/thumbnail-large.png b/front/assets/thumbnail-large.png new file mode 100644 index 0000000000000000000000000000000000000000..af8f74d46659a8b5dabb79d8cc0949d76f174381 Binary files /dev/null and b/front/assets/thumbnail-large.png differ diff --git a/front/assets/unicorn-tweaked.svg b/front/assets/unicorn-tweaked.svg new file mode 100644 index 0000000000000000000000000000000000000000..5efcac3af410a770772fb28630a879a75aa30d03 --- /dev/null +++ b/front/assets/unicorn-tweaked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/front/favicon.ico b/front/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..81d91478583b2e6fdf85e5b7c1e4ac581fe058e5 Binary files /dev/null and b/front/favicon.ico differ diff --git a/front/js-src/Api.ts b/front/js-src/Api.ts new file mode 100644 index 0000000000000000000000000000000000000000..36401b35b6ee41325bce8ddbf8f749e56eec42d9 --- /dev/null +++ b/front/js-src/Api.ts @@ -0,0 +1,153 @@ +import { c } from './lib/Log'; + + +interface AutocompleteOutput { + sentences: { + value: string; + time: number; + }[]; + time: number; +} + +export class Api { + + private static ENDPOINT = + // `http://coconut-proxy.huggingface.test` + // `http://coconuthf.eastus.cloudapp.azure.com:6006` + // "http://localhost:6006" + `https://transformer.huggingface.co` + ; + static shared = new Api(); + + private path(p: string): string { + return `${Api.ENDPOINT}/${p}`; + } + + private async postAutocomplete( + params: { + context: string; + model_size?: string; /// 'small' | 'medium', + top_p?: number; /// float between 0 and 1 + temperature?: number; /// float between 0 and 100 + step_size?: number; + kl_scale?: number; + gm_scale?: number; + num_iterations?: number; + gen_length?: number; + max_time?: number; /// <- if we want to limit the response time. (in sec) + bow_or_discrim?: string; + use_sampling?: boolean; + } + ): Promise { + + const path = this.path(`autocomplete/${params.model_size || ""}`); + + const response = await fetch(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(params), + }); + return await response.json() as AutocompleteOutput; + } + + /** + * Demo-specific helpers + */ + async postWithSettings( + params: { + context: string; + } + ): Promise { + /// Retrieve all settings params then launch the request. + const model_size = + document.querySelector('.decoder-settings .setting.model_size .js-val')!.textContent + || undefined; + + const parseSliderVal = (sel: string): number | undefined => { + const x = document.querySelector(sel); + if (x && x.textContent) { + return Number(x.textContent); + } + return undefined; + }; + + const top_p = parseSliderVal('.decoder-settings .setting.top_p .js-val'); + const temperature = parseSliderVal('.decoder-settings .setting.temperature .js-val'); + const step_size = parseSliderVal('.decoder-settings .setting.step_size .js-val'); + const kl_scale = parseSliderVal('.decoder-settings .setting.kl_scale .js-val'); + const gm_scale = parseSliderVal('.decoder-settings .setting.gm_scale .js-val'); + const num_iterations = parseSliderVal('.decoder-settings .setting.num_iterations .js-val'); + const gen_length = parseSliderVal('.decoder-settings .setting.gen_length .js-val'); + const max_time = parseSliderVal('.decoder-settings .setting.max_time .js-val'); + + const bow_or_discrim = ( + document.querySelector('.decoder-settings input[name=bow_or_discrim]:checked') || {} + ).value; + const use_sampling = ( + document.querySelector('.decoder-settings input[name=use_sampling]') || {} + ).checked; + + return this.postAutocomplete({ + ...params, + model_size, + top_p, + temperature, + step_size, + kl_scale, + gm_scale, + num_iterations, + gen_length, + max_time, + bow_or_discrim, + use_sampling, + }); + } + + /** + * Edit AJAX endpoint + * + * Contrary to the autocomplete endpoint, + * this is on server, + * not on backend. + */ + async postEdit(body: any): Promise { + const doc = (window).doc as { [index: string]: string }; + if (!doc || !doc.longId) { + throw new Error(`invalid doc`); + } + + const path = `/edit/${doc.model}/${doc.longId}/${doc.shortId}`; + + const response = await fetch(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + return response.ok; + } + + /** + * Duplicate AJAX endpoint + * + * Contrary to the autocomplete endpoint, + * this is on server, + * not on backend. + */ + async postDuplicate(): Promise { + const doc = (window).doc as { [index: string]: string }; + if (!doc || !doc.shortId) { + throw new Error(`invalid doc`); + } + + const path = `/duplicate/${doc.shortId}`; + const response = await fetch(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }); + const url = await response.text(); + c.log('[new url]', url); + + return url; + } +} + diff --git a/front/js-src/Mention.ts b/front/js-src/Mention.ts new file mode 100644 index 0000000000000000000000000000000000000000..72db4af8385cbfc57f9f09e3d321edce5bc7e746 --- /dev/null +++ b/front/js-src/Mention.ts @@ -0,0 +1,441 @@ + +interface Datum { + id: string; + value: string; +} + + +export class Mention { + static Keys = { + TAB: 9, + ENTER: 13, + ESCAPE: 27, + UP: 38, + DOWN: 40, + }; + static numberIsNaN = (x: any) => x !== x; + private isOpen = false; + /** + * index of currently selected item. + */ + private itemIndex = 0; + private mentionCharPos: number | undefined = undefined; + private cursorPos: number | undefined = undefined; + private values = [] as Datum[]; + private suspendMouseEnter = false; + private options = { + source: (searchTerm: string, renderList: Function, mentionChar: string) => {}, + renderItem: (item: Datum, searchTerm: string) => { + return `${item.value}`; + }, + onSelect: (item: DOMStringMap, insertItem: (item: DOMStringMap) => void) => { + insertItem(item); + }, + mentionDenotationChars: ['@'], + showDenotationChar: true, + allowedChars: /^[a-zA-Z0-9_]*$/, + minChars: 0, + maxChars: 31, + offsetTop: 2, + offsetLeft: 0, + /** + * Whether or not the denotation character(s) should be isolated. For example, to avoid mentioning in an email. + */ + isolateCharacter: false, + fixMentionsToQuill: false, + defaultMenuOrientation: 'bottom', + dataAttributes: ['id', 'value', 'denotationChar', 'link', 'target'], + linkTarget: '_blank', + onOpen: () => true, + onClose: () => true, + // Style options + listItemClass: 'ql-mention-list-item', + mentionContainerClass: 'ql-mention-list-container', + mentionListClass: 'ql-mention-list', + }; + /// HTML elements + private mentionContainer = document.createElement('div'); + private mentionList = document.createElement('ul'); + + + constructor( + private quill: Quill, + ) { + this.mentionContainer.className = this.options.mentionContainerClass; + this.mentionContainer.style.cssText = 'display: none; position: absolute;'; + this.mentionContainer.onmousemove = this.onContainerMouseMove.bind(this); + + if (this.options.fixMentionsToQuill) { + this.mentionContainer.style.width = 'auto'; + } + + this.mentionList.className = this.options.mentionListClass; + this.mentionContainer.appendChild(this.mentionList); + + this.quill.container.appendChild(this.mentionContainer); + + quill.on('text-change', this.onTextChange.bind(this)); + quill.on('selection-change', this.onSelectionChange.bind(this)); + + quill.keyboard.addBinding({ + key: Mention.Keys.ENTER, + }, this.selectHandler.bind(this)); + quill.keyboard.bindings[Mention.Keys.ENTER].unshift( + quill.keyboard.bindings[Mention.Keys.ENTER].pop() + ); + /// ^^ place it at beginning of bindings. + + quill.keyboard.addBinding({ + key: Mention.Keys.ESCAPE, + }, this.escapeHandler.bind(this)); + + quill.keyboard.addBinding({ + key: Mention.Keys.UP, + }, this.upHandler.bind(this)); + + quill.keyboard.addBinding({ + key: Mention.Keys.DOWN, + }, this.downHandler.bind(this)); + + document.addEventListener("keypress", e => { + /// Quick’n’dirty hack. + if (! this.quill.hasFocus()) { + return ; + } + setTimeout(() => { + this.setCursorPos(); + this.quill.removeFormat(this.cursorPos! - 1, 1, 'silent'); + }, 0); + }); + } + + selectHandler() { + if (this.isOpen) { + this.selectItem(); + return false; + } + return true; + } + + escapeHandler() { + if (this.isOpen) { + this.hideMentionList(); + return false; + } + return true; + } + + upHandler() { + if (this.isOpen) { + this.prevItem(); + return false; + } + return true; + } + + downHandler() { + if (this.isOpen) { + this.nextItem(); + return false; + } + return true; + } + + showMentionList() { + this.mentionContainer.style.visibility = 'hidden'; + this.mentionContainer.style.display = ''; + this.setMentionContainerPosition(); + this.setIsOpen(true); + } + + hideMentionList() { + this.mentionContainer.style.display = 'none'; + this.setIsOpen(false); + } + + + private highlightItem(scrollItemInView = true) { + const childNodes = Array.from(this.mentionList.childNodes) as HTMLLIElement[]; + for (const node of childNodes) { + node.classList.remove('selected'); + } + childNodes[this.itemIndex].classList.add('selected'); + + if (scrollItemInView) { + const itemHeight = childNodes[this.itemIndex].offsetHeight; + const itemPos = this.itemIndex * itemHeight; + const containerTop = this.mentionContainer.scrollTop; + const containerBottom = containerTop + this.mentionContainer.offsetHeight; + + if (itemPos < containerTop) { + // Scroll up if the item is above the top of the container + this.mentionContainer.scrollTop = itemPos; + } else if (itemPos > (containerBottom - itemHeight)) { + // scroll down if any part of the element is below the bottom of the container + this.mentionContainer.scrollTop += (itemPos - containerBottom) + itemHeight; + } + } + } + + private getItemData(): DOMStringMap { + const node = this.mentionList.childNodes[this.itemIndex] as HTMLElement; + const { link } = node.dataset; + const itemTarget = node.dataset.target; + if (link !== undefined) { + node.dataset.value = `${node.dataset.value}`; + } + return node.dataset; + } + + onContainerMouseMove() { + this.suspendMouseEnter = false; + } + + selectItem() { + const data = this.getItemData(); + this.options.onSelect(data, (asyncData) => { + this.insertItem(asyncData); + }); + this.hideMentionList(); + } + + insertItem(data: DOMStringMap) { + const render = data; + if (render === null) { + return ; + } + if (!this.options.showDenotationChar) { + render.denotationChar = ''; + } + if (this.cursorPos === undefined) { + throw new Error(`Invalid this.cursorPos`); + } + if (!render.value) { + throw new Error(`Didn't receive value from server.`); + } + + this.quill.insertText(this.cursorPos, render.value, 'bold', Quill.sources.USER); + this.quill.setSelection(this.cursorPos + render.value.length, 0); + this.setCursorPos(); + this.hideMentionList(); + } + + onItemMouseEnter(e: MouseEvent) { + if (this.suspendMouseEnter) { + return ; + } + const index = Number( + (e.target as HTMLLIElement).dataset.index + ); + if (! Mention.numberIsNaN(index) && index !== this.itemIndex) { + this.itemIndex = index; + this.highlightItem(false); + } + } + + onItemClick(e: MouseEvent) { + e.stopImmediatePropagation(); + e.preventDefault(); + this.itemIndex = Number( + (e.currentTarget as HTMLElement).dataset.index + ); + this.highlightItem(); + this.selectItem(); + } + + private attachDataValues(element: HTMLLIElement, data: Datum): HTMLLIElement { + for (const [key, value] of Object.entries(data)) { + if (this.options.dataAttributes.includes(key)) { + element.dataset[key] = value; + } else { + delete element.dataset[key]; + } + } + return element; + } + + renderList(mentionChar: string, data: Datum[], searchTerm: string = "") { + if (data.length > 0) { + this.values = data; + this.mentionList.innerHTML = ''; + + for (const [i, datum] of data.entries()) { + const li = document.createElement('li'); + li.className = this.options.listItemClass; + li.dataset.index = `${i}`; + // li.innerHTML = this.options.renderItem(datum, searchTerm); + li.innerText = datum.value.replace(/\n/g, "↵"); + /// ^^ + li.onmouseenter = this.onItemMouseEnter.bind(this); + li.dataset.denotationChar = mentionChar; + li.onclick = this.onItemClick.bind(this); + this.mentionList.appendChild( + this.attachDataValues(li, datum) + ); + } + this.itemIndex = 0; + this.highlightItem(); + this.showMentionList(); + } else { + this.hideMentionList(); + } + } + + nextItem() { + this.itemIndex = (this.itemIndex + 1) % this.values.length; + this.suspendMouseEnter = true; + this.highlightItem(); + } + + prevItem() { + this.itemIndex = ((this.itemIndex + this.values.length) - 1) % this.values.length; + this.suspendMouseEnter = true; + this.highlightItem(); + } + + private hasValidChars(s: string) { + return this.options.allowedChars.test(s); + } + + private containerBottomIsNotVisible(topPos: number, containerPos: ClientRect | DOMRect) { + const mentionContainerBottom = topPos + this.mentionContainer.offsetHeight + containerPos.top; + return mentionContainerBottom > window.pageYOffset + window.innerHeight; + } + + private containerRightIsNotVisible(leftPos: number, containerPos: ClientRect | DOMRect) { + if (this.options.fixMentionsToQuill) { + return false; + } + const rightPos = leftPos + this.mentionContainer.offsetWidth + containerPos.left; + const browserWidth = window.pageXOffset + document.documentElement.clientWidth; + return rightPos > browserWidth; + } + + private setIsOpen(isOpen: boolean) { + if (this.isOpen !== isOpen) { + if (isOpen) { + this.options.onOpen(); + } else { + this.options.onClose(); + } + this.isOpen = isOpen; + } + } + + private setMentionContainerPosition() { + const containerPos = this.quill.container.getBoundingClientRect(); + /// vv Here we always trigger from the cursor. + if (this.cursorPos === undefined) { + throw new Error(`Invalid this.cursorPos`); + } + const mentionCharPos = this.quill.getBounds(this.cursorPos); + const containerHeight = this.mentionContainer.offsetHeight; + + let topPos = this.options.offsetTop; + let leftPos = this.options.offsetLeft; + + // handle horizontal positioning + if (this.options.fixMentionsToQuill) { + const rightPos = 0; + this.mentionContainer.style.right = `${rightPos}px`; + } else { + leftPos += mentionCharPos.left; + } + + if (this.containerRightIsNotVisible(leftPos, containerPos)) { + const containerWidth = this.mentionContainer.offsetWidth + this.options.offsetLeft; + const quillWidth = containerPos.width; + leftPos = quillWidth - containerWidth; + } + + // handle vertical positioning + if (this.options.defaultMenuOrientation === 'top') { + // Attempt to align the mention container with the top of the quill editor + if (this.options.fixMentionsToQuill) { + topPos = -1 * (containerHeight + this.options.offsetTop); + } else { + topPos = mentionCharPos.top - (containerHeight + this.options.offsetTop); + } + + // default to bottom if the top is not visible + if (topPos + containerPos.top <= 0) { + let overMentionCharPos = this.options.offsetTop; + + if (this.options.fixMentionsToQuill) { + overMentionCharPos += containerPos.height; + } else { + overMentionCharPos += mentionCharPos.bottom; + } + + topPos = overMentionCharPos; + } + } else { + // Attempt to align the mention container with the bottom of the quill editor + if (this.options.fixMentionsToQuill) { + topPos += containerPos.height; + } else { + topPos += mentionCharPos.bottom; + } + + // default to the top if the bottom is not visible + if (this.containerBottomIsNotVisible(topPos, containerPos)) { + let overMentionCharPos = this.options.offsetTop * -1; + + if (!this.options.fixMentionsToQuill) { + overMentionCharPos += mentionCharPos.top; + } + + topPos = overMentionCharPos - containerHeight; + } + } + + this.mentionContainer.style.top = `${topPos}px`; + this.mentionContainer.style.left = `${leftPos}px`; + this.mentionContainer.style.visibility = 'visible'; + } + + + /** + * HF Helpers for manual trigger + */ + setCursorPos() { + const range = this.quill.getSelection(); + if (range) { + this.cursorPos = range.index; + } else { + this.quill.setSelection(this.quill.getLength(), 0); + /// ^^ place cursor at the end of input by default. + this.cursorPos = this.quill.getLength(); + } + } + getCursorPos(): number { + return this.cursorPos!; + } + trigger(values: string[]) { + this.renderList("", values.map(x => { + return { id: x, value: x }; + }), ""); + } + + onSomethingChange() { + /// We trigger manually so here we can _probably_ just always close. + this.hideMentionList(); + } + + onTextChange(delta: Delta, oldDelta: Delta, source: Sources) { + if (source === 'user') { + this.onSomethingChange(); + } + } + + onSelectionChange(range: RangeStatic) { + if (range && range.length === 0) { + this.onSomethingChange(); + } else { + this.hideMentionList(); + } + } +} + + +Quill.register('modules/mention', Mention); diff --git a/front/js-src/controller.ts b/front/js-src/controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..be853019cf901c2c1a72cb2d59e73c7e41575191 --- /dev/null +++ b/front/js-src/controller.ts @@ -0,0 +1,319 @@ +import { Api } from './Api'; +import { Mention } from './Mention'; +import { c } from './lib/Log'; +import { Utils } from './lib/Utils'; +import { VanillaTilt } from './vanilla-tilt'; +import { ShareScreenshotModal, SavePublishModal } from './modals'; + +/// We experimented with a couple of different build systems +/// to integrate Quill (for instance module-then-postprocessing +/// like in `web3d`) but none worked really well so we just +/// hotlink the js and basically copy/paste the @types/quill +/// declaration here. +/// Update: we now use rollup (for html2canvas), but quill is +/// still a pain so it's still not in the same bundle. + +const DEBUG = false; +/// ^^ when debugging the quill integration, add the quill.snow.css to layout.hbs +/// +/// +/// We tried doing it programmatically here but it's a bit slow. +if (DEBUG) { + document.head.insertAdjacentHTML( + 'beforeend', + `` + ); + /// ^^ add css to debug. Do it as early as possible. +} + +enum Page { + app, landing, model +} +const App = { + page: + (document.body.classList.contains('app')) ? Page.app + : (document.body.classList.contains('landing')) ? Page.landing + : Page.model + , + editable: document.body.dataset.editable === 'true', + header: { + shuffleBtn: document.querySelector('header .js-shuffle') as HTMLAnchorElement, + triggerBtn: document.querySelector('header .js-trigger') as HTMLAnchorElement, + mainInfoBtn: document.querySelector('header .title .info') as HTMLImageElement, + shareBtn: document.querySelector('header .js-share'), + saveBtn: document.querySelector('header .js-save'), + duplicateBtn: document.querySelector('header .js-duplicate'), + }, + shareScreenBtn: document.querySelector('.page-container .js-share') as HTMLAnchorElement, + loaderEditor: document.querySelector('.page-container .js-loader') as HTMLImageElement, + sliders: Array.from( + document.querySelectorAll('.decoder-settings input.slider') + ) as HTMLInputElement[], + INITIAL_CONTENT: {} as Delta, + /** + * Helper function to more cleanly route different page types. + */ + onLoad: (p: Page, callback: () => void) => { + if (p === App.page) { + document.addEventListener('DOMContentLoaded', () => { + callback(); + }); + } + }, +}; + +const PROMPTS = [ + `Before boarding your rocket to Mars, remember to pack these items`, + `In a shocking finding, scientist discovered a herd of unicorns living in a remote, previously unexplored valley, in the Andes Mountains. Even more surprising to the researchers was the fact that the unicorns spoke perfect English.`, + `Legolas and Gimli advanced on the orcs, raising their weapons with a harrowing war cry.`, + `Today, scientists confirmed the worst possible outcome: the massive asteroid will collide with Earth`, + ` + Thor: The Tesseract belongs on Asgard, no human is a match for it. + Tony turns to leave, but Steve stops him. + Steve: You're not going alone! + Tony: You gonna stop me? + `.replace(/\t/g, "").trim().concat("\n"), +]; + + + + +App.onLoad(Page.app, () => { + const modalScreenshot = new ShareScreenshotModal; + + const opts: QuillOptionsStatic = DEBUG + ? { + theme: 'snow', + modules: { + mention: {}, + }, + } + : { + theme: undefined, + // formats: [], + modules: { + toolbar: [], + mention: {}, + }, + } + ; + if (! App.editable) { + opts.readOnly = true; + } + const quill = new Quill('div.editor', opts); + const mention = quill.getModule('mention') as Mention; + (window).quill = quill; + const QUILL_C = (window).QUILL_C; + if (QUILL_C) { + quill.setContents(QUILL_C); + } + + + + quill.container.appendChild(App.loaderEditor); + quill.container.appendChild(App.shareScreenBtn); + + // + // div.editor .ql-container <-- quill.container + // +--------------------------------+ + // | div.ql-editor contenteditable | <-- quill.root + // | +----------------------------+ | + // | | | | + // | | | | + // | +----------------------------+ | + // +--------------------------------+ + // + + quill.keyboard.addBinding({ key: Mention.Keys.TAB }, () => { + triggerAutocomplete(); + }); + quill.keyboard.bindings[Mention.Keys.TAB].unshift( + quill.keyboard.bindings[Mention.Keys.TAB].pop() + ); + /// ^^ important. + /// ^^ place it at beginning of bindings. + + + const triggerAutocomplete = async () => { + /// vv position loader + mention.setCursorPos(); + const cursorBbox = quill.getBounds(mention.getCursorPos()); + App.loaderEditor.style.top = `${cursorBbox.top - 4}px`; + App.loaderEditor.style.left = `${cursorBbox.left + 4}px`; + App.loaderEditor.classList.remove('hide'); + + /// vv Launch api request. + const text = quill.getText(0, mention.getCursorPos()); + // ^^ That is so much simpler that what we used to do + // when we were embbedding objects like in `quill-mention`. + c.debug( + `%c[About to launch autocomplete for]`, + `color: green;`, + text, + ); + const o = await Api.shared.postWithSettings({ context: text }); + App.loaderEditor.classList.add('hide'); + + /// vv Trigger mention module. + for (const x of o.sentences) { + c.log(x.value); + } + mention.trigger( + o.sentences.map(x => x.value) + ); + }; + + + App.header.duplicateBtn?.addEventListener('click', async (e) => { + e.preventDefault(); + const url = await Api.shared.postDuplicate(); + window.location.href = url; + }); + + + if (! App.editable) { + return ; + } + /** + * vv Below is only in editable mode. + */ + + const modalSave = new SavePublishModal(quill); + + App.header.shuffleBtn.addEventListener('click', (e) => { + e.preventDefault(); + quill.setText( + Utils.randomItem(PROMPTS) + ); + quill.setSelection(quill.getLength(), 0); + /// ^^ github.com/quilljs/quill/issues/2635 + triggerAutocomplete(); + }); + App.header.triggerBtn.addEventListener('click', (e) => { + e.preventDefault(); + triggerAutocomplete(); + }); + App.header.shareBtn?.addEventListener('click', async (e) => { + e.preventDefault(); + const text = `Write With Transformer via @huggingface`; + window.open(`https://twitter.com/share?url=${ encodeURIComponent(window.location.href) }&text=${ encodeURIComponent(text) }`); + }); + App.header.saveBtn?.addEventListener('click', (e) => { + e.preventDefault(); + mention.hideMentionList(); + modalSave.show(); + }); + + App.shareScreenBtn.addEventListener('click', async (e) => { + e.preventDefault(); + mention.hideMentionList(); + modalScreenshot.show(); + }); + quill.on('text-change', () => { + App.shareScreenBtn.classList.remove('hide'); /// <- we use a fadeout effect. + const hasTextFromAI = quill.getContents() + .ops + .some(op => op.attributes && op.attributes.bold === true) + ; + App.shareScreenBtn.classList.toggle('fadeout', ! hasTextFromAI); + }); + document.addEventListener('click', (e) => { + /// Handle clicks on links inside the editor. + if (! ( + e.target instanceof HTMLAnchorElement + && e.target.closest('div.ql-editor') !== null + )) { + return ; + } + /// Ok, let's do this. + e.preventDefault(); + e.stopPropagation(); + const href = e.target.getAttribute('href'); /// <- caution, get the original string. + c.debug(`[click]`, href); + if (href === '#js-shuffle') { + App.header.shuffleBtn.click(); + } else { + window.open(e.target.href); + } + }); + document.addEventListener("scroll", e => { + const trigger = document.getElementsByClassName("js-trigger")[0] as HTMLAnchorElement; + if (scrollY > 100) { + trigger.style.position = "fixed"; + trigger.style.top = "10px"; + trigger.style.border = "1px solid blue"; + trigger.style.backgroundColor = "white"; + trigger.style.borderRadius = "100px"; + trigger.style.padding = "5px"; + trigger.style.zIndex = "1"; + trigger.style.left = "50%"; + trigger.style.transform = "translateX(-50%)"; + } else { + trigger.style.position = "relative"; + trigger.style.top = "auto"; + trigger.style.border = "none"; + trigger.style.backgroundColor = "white"; + trigger.style.borderRadius = "0"; + trigger.style.padding = "0"; + trigger.style.zIndex = "1"; + trigger.style.left = "auto" + } + }); + + /** + * Settings + */ + const handleSliderChange = (slider: HTMLInputElement) => { + const div = slider.parentNode as HTMLDivElement; + const spanVal = div.querySelector('.js-val') as HTMLSpanElement; + const value = Number.isInteger(slider.valueAsNumber) + ? slider.valueAsNumber + : Number(slider.valueAsNumber.toFixed(2)) + ; + const valueKey = `value-${value}`; + if (slider.dataset[valueKey]) { + spanVal.innerText = slider.dataset[valueKey]!; + } else { + spanVal.innerText = value.toString(); + } + const min = Number(slider.getAttribute('min')); + const max = Number(slider.getAttribute('max')); + if (value < min + (max - min) / 3) { + spanVal.className = "js-val green"; + } else if (value < min + 2 * (max - min) / 3) { + spanVal.className = "js-val orange"; + } else { + spanVal.className = "js-val red"; + } + const isInverted = slider.classList.contains('js-inverted'); + if (isInverted) { + if (spanVal.classList.contains('green')) { + spanVal.classList.remove('green'); + spanVal.classList.add('red'); + } else if (spanVal.classList.contains('red')) { + spanVal.classList.remove('red'); + spanVal.classList.add('green'); + } + } + }; + for (const slider of App.sliders) { + handleSliderChange(slider); + slider.addEventListener('input', () => { + handleSliderChange(slider); + }); + } +}); + + + +App.onLoad(Page.landing, () => { + /** + * VanillaTilt + */ + VanillaTilt.init(document.querySelectorAll("[data-tilt]"), { + glare: true, + scale: 1.06, + 'max-glare': 0.3, + speed: 400, + }); +}); diff --git a/front/js-src/lib/Log.ts b/front/js-src/lib/Log.ts new file mode 100644 index 0000000000000000000000000000000000000000..056a2ace08862bf70365b878ca3941028ae07cf0 --- /dev/null +++ b/front/js-src/lib/Log.ts @@ -0,0 +1 @@ +export const c = console; diff --git a/front/js-src/lib/Utils.ts b/front/js-src/lib/Utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..6da705301195b190d2dc0264fa0f3182588779de --- /dev/null +++ b/front/js-src/lib/Utils.ts @@ -0,0 +1,76 @@ + +export class Utils { + private static escapeMap = { + /// From underscore.js + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + + /** + * Escape a message's content for insertion into html. + */ + static escape(s: string): string { + let x = s; + for (const [k, v] of Object.entries(this.escapeMap)) { + x = x.replace(new RegExp(k, 'g'), v); + } + return x.replace(/\n/g, '
'); + } + + /** + * Opposite of escape. + */ + static unescape(s: string): string { + let x = s.replace(/
/g, '\n'); + for (const [k, v] of Object.entries(this.escapeMap)) { + x = x.replace(new RegExp(v, 'g'), k); + } + return x; + } + + /** + * "Real" modulo (always >= 0), not remainder. + */ + static mod(a: number, n: number): number { + return ((a % n) + n) % n; + } + + /** + * Noop object with arbitrary number of nested attributes that are also noop. + */ + static deepNoop() { + const noop = new Proxy(() => {}, { + get: () => noop + }); + return noop; + } + + /** + * Capitalize + */ + static capitalize(s: string): string { + return s.charAt(0).toUpperCase() + s.slice(1); + } + + /** + * Returns a promise that will resolve after the specified time + * @param ms Number of ms to wait + */ + static delay(ms: number): Promise { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), ms); + }); + } + + /** + * Random element from array + */ + static randomItem(arr: T[]): T { + return arr[Math.floor(Math.random()*arr.length)]; + } +} + diff --git a/front/js-src/modals.ts b/front/js-src/modals.ts new file mode 100644 index 0000000000000000000000000000000000000000..5acee1cf79ca553bb8b8975237fe0a72cb7db16b --- /dev/null +++ b/front/js-src/modals.ts @@ -0,0 +1,134 @@ +import { Utils } from './lib/Utils'; +import html2canvas from 'html2canvas'; +import { c } from './lib/Log'; +import { Api } from './Api'; + +abstract class Modal { + protected div: HTMLDivElement; + protected doneBtn: HTMLAnchorElement | null; + protected loader: HTMLImageElement; + constructor(className: string) { + this.div = document.querySelector(`div.modal.${className}`) as HTMLDivElement; + this.doneBtn = this.div.querySelector('.js-close'); + this.loader = this.div.querySelector('.js-loader') as HTMLImageElement; + + this.doneBtn?.addEventListener('click', (e) => { + e.preventDefault(); + this.hide(); + }); + this.div.addEventListener('click', (e) => { + if (e.target === this.div) { + c.debug(`modal:background.click`); + this.hide(); + } + }); + } + /** + * Hooks: Implement those to perform the actual work done on show and hide. + */ + abstract performBeforeShow(): Promise; + abstract performShow(): Promise; + abstract performHide(): Promise; + async show() { + await this.performBeforeShow(); + this.div.classList.add('fadeout'); + this.div.classList.remove('hide'); + await Utils.delay(100); + this.div.classList.remove('fadeout'); + await this.performShow(); + this.loader.classList.add('hide'); + } + async hide() { + this.div.classList.add('fadeout'); + await Utils.delay(200); + this.div.classList.add('hide'); + this.div.classList.remove('fadeout'); + await this.performHide(); + } +} + +export class ShareScreenshotModal extends Modal { + private imResult = this.div.querySelector('.js-result') as HTMLImageElement; + + constructor() { + super(`share-screenshot`); + } + async performBeforeShow() { + this.loader.classList.remove('hide'); + } + async performShow() { + await Utils.delay(800); /// <- for good ux + const el = document.querySelector('div.page-inner') as HTMLDivElement; + const canvas = await html2canvas(el, { + logging: false, /// <- inoperant in our version of html2canvas. + onclone: (doc) => { + const clonedEl = doc.querySelector('div.page-inner') as HTMLDivElement; + clonedEl.classList.add('html2canvas'); + const watermark = doc.querySelector('div.watermark') as HTMLDivElement; + watermark.style.visibility = `visible`; + } + }); + this.imResult.src = canvas.toDataURL(); + } + async performHide() { + this.imResult.src = ""; + } +} + +export class SavePublishModal extends Modal { + private saveBtn = this.div.querySelector('.js-save') as HTMLAnchorElement; + private form = this.div.querySelector('form') as HTMLFormElement; + constructor( + private quill: Quill + ) { + super(`save-publish`); + + /// vv Url fields auto-select. + const urlInputs = Array.from( + this.div.querySelectorAll('.doc-url') + ) as HTMLInputElement[]; + for (const x of urlInputs) { + x.addEventListener('focus', () => { + x.select(); + }); + } + + this.saveBtn.addEventListener('click', (e) => { + e.preventDefault(); + if (! this.form.reportValidity()) { + /// Form is invalid. + return ; + } + this.save(); + }); + this.form.addEventListener('submit', (e) => { + e.preventDefault(); + this.saveBtn.click(); + }); + } + async performBeforeShow() {} + async performShow() {} + async performHide() {} + async save() { + this.loader.classList.remove('hide'); + + const inputTitle = this.div.querySelector('.doc-title') as HTMLInputElement; + const title = inputTitle.value; + const contents = this.quill.getContents(); + c.log(JSON.stringify({ title, contents })); + + const success = await Api.shared.postEdit({ title, contents }); + await Utils.delay(800); /// <- for good ux + + if (success) { + this.loader.classList.add('hide'); + this.hide(); + /// For now we always redirect to the edit url here: + /// vv + const inputEditUrl = this.div.querySelector('.doc-edit-url') as HTMLInputElement; + window.location.href = inputEditUrl.value; + } else { + window.alert(`did not manage to save`); + } + } +} diff --git a/front/js-src/quill.d.ts b/front/js-src/quill.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..78ec080f76416b9f836ea308d44e0094aabbf908 --- /dev/null +++ b/front/js-src/quill.d.ts @@ -0,0 +1,181 @@ + +// import { Blot } from "../node_modules/parchment/dist/src/blot/abstract/blot"; +interface Blot {} +interface Delta { + ops: DeltaOperation[]; +} + +/** + * A stricter type definition would be: + * + * type DeltaOperation ({ insert: any } | { delete: number } | { retain: number }) & OptionalAttributes; + * + * But this would break a lot of existing code as it would require manual discrimination of the union types. + */ +type DeltaOperation = { insert?: any, delete?: number, retain?: number } & OptionalAttributes; +type Sources = "api" | "user" | "silent"; + +interface Key { + key: string | number; + shortKey?: boolean; +} + +interface StringMap { + [key: string]: any; +} + +interface OptionalAttributes { + attributes?: StringMap; +} + +type TextChangeHandler = (delta: Delta, oldContents: Delta, source: Sources) => any; +type SelectionChangeHandler = (range: RangeStatic, oldRange: RangeStatic, source: Sources) => any; +type EditorChangeHandler = ((name: "text-change", delta: Delta, oldContents: Delta, source: Sources) => any) + | ((name: "selection-change", range: RangeStatic, oldRange: RangeStatic, source: Sources) => any); + +interface KeyboardStatic { + addBinding(key: Key, callback: (range: RangeStatic, context: any) => void): void; + addBinding(key: Key, context: any, callback: (range: RangeStatic, context: any) => void): void; + bindings: { [index: number]: any[] }; +} + +interface ClipboardStatic { + convert(html?: string): Delta; + addMatcher(selectorOrNodeType: string|number, callback: (node: any, delta: Delta) => Delta): void; + dangerouslyPasteHTML(html: string, source?: Sources): void; + dangerouslyPasteHTML(index: number, html: string, source?: Sources): void; +} + +interface QuillOptionsStatic { + debug?: string | boolean; + modules?: StringMap; + placeholder?: string; + readOnly?: boolean; + theme?: string; + formats?: string[]; + bounds?: HTMLElement | string; + scrollingContainer?: HTMLElement | string; + strict?: boolean; +} + +interface BoundsStatic { + bottom: number; + left: number; + right: number; + top: number; + height: number; + width: number; +} + +declare interface RangeStatic { + index: number; + length: number; +} + +declare class RangeStatic implements RangeStatic { + constructor(); + index: number; + length: number; +} + +interface EventEmitter { + on(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + on(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + on(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; + once(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + once(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + once(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; + off(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + off(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + off(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; +} + +declare class Quill { + /** + * @private Internal API + */ + root: HTMLDivElement; + container: HTMLElement; /// <- used by quill-mention + clipboard: ClipboardStatic; + scroll: Blot; + keyboard: KeyboardStatic; + constructor(container: string | Element, options?: QuillOptionsStatic); + deleteText(index: number, length: number, source?: Sources): Delta; + disable(): void; + enable(enabled?: boolean): void; + getContents(index?: number, length?: number): Delta; + getLength(): number; + getText(index?: number, length?: number): string; + insertEmbed(index: number, type: string, value: any, source?: Sources): Delta; + insertText(index: number, text: string, source?: Sources): Delta; + insertText(index: number, text: string, format: string, value: any, source?: Sources): Delta; + insertText(index: number, text: string, formats: StringMap, source?: Sources): Delta; + /** + * @deprecated Remove in 2.0. Use clipboard.dangerouslyPasteHTML(index: number, html: string, source: Sources) + */ + pasteHTML(index: number, html: string, source?: Sources): string; + /** + * @deprecated Remove in 2.0. Use clipboard.dangerouslyPasteHTML(html: string, source: Sources): void; + */ + pasteHTML(html: string, source?: Sources): string; + setContents(delta: Delta, source?: Sources): Delta; + setText(text: string, source?: Sources): Delta; + update(source?: Sources): void; + updateContents(delta: Delta, source?: Sources): Delta; + + format(name: string, value: any, source?: Sources): Delta; + formatLine(index: number, length: number, source?: Sources): Delta; + formatLine(index: number, length: number, format: string, value: any, source?: Sources): Delta; + formatLine(index: number, length: number, formats: StringMap, source?: Sources): Delta; + formatText(index: number, length: number, source?: Sources): Delta; + formatText(index: number, length: number, format: string, value: any, source?: Sources): Delta; + formatText(index: number, length: number, formats: StringMap, source?: Sources): Delta; + formatText(range: RangeStatic, format: string, value: any, source?: Sources): Delta; + formatText(range: RangeStatic, formats: StringMap, source?: Sources): Delta; + getFormat(range?: RangeStatic): StringMap; + getFormat(index: number, length?: number): StringMap; + removeFormat(index: number, length: number, source?: Sources): Delta; + + blur(): void; + focus(): void; + getBounds(index: number, length?: number): BoundsStatic; + getSelection(focus: true): RangeStatic; + getSelection(focus?: false): RangeStatic | null; + hasFocus(): boolean; + setSelection(index: number, length: number, source?: Sources): void; + setSelection(range: RangeStatic, source?: Sources): void; + + // static methods: debug, import, register, find + static debug(level: string|boolean): void; + static import(path: string): any; + static register(path: string, def: any, suppressWarning?: boolean): void; + static register(defs: StringMap, suppressWarning?: boolean): void; + static find(domNode: Node, bubble?: boolean): Quill | any; + + addContainer(classNameOrDomNode: string|Node, refNode?: Node): any; + getModule(name: string): any; + + // Blot interface is not exported on Parchment + getIndex(blot: any): number; + getLeaf(index: number): any; + getLine(index: number): [any, number]; + getLines(index?: number, length?: number): any[]; + getLines(range: RangeStatic): any[]; + + // EventEmitter methods + on(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + on(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + on(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; + once(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + once(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + once(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; + off(eventName: "text-change", handler: TextChangeHandler): EventEmitter; + off(eventName: "selection-change", handler: SelectionChangeHandler): EventEmitter; + off(eventName: "editor-change", handler: EditorChangeHandler): EventEmitter; + + static sources: { + API: 'api', + SILENT: 'silent', + USER: 'user', + }; +} diff --git a/front/js-src/vanilla-tilt.ts b/front/js-src/vanilla-tilt.ts new file mode 100644 index 0000000000000000000000000000000000000000..3165dc69e3049ba093b2ce9396ad76a64ce82b3b --- /dev/null +++ b/front/js-src/vanilla-tilt.ts @@ -0,0 +1,371 @@ +export namespace VanillaTilt { + /** + * Options which configures the tilting + */ + export interface TiltOptions { + /** + * Reverse the tilt direction + */ + reverse?: boolean; + /** + * Max tilt rotation (degrees) + */ + max?: number; + /** + * Transform perspective, the lower the more extreme the tilt gets. + */ + perspective?: number; + /** + * 2 = 200%, 1.5 = 150%, etc.. + */ + scale?: number; + /** + * Speed of the enter/exit transition + */ + speed?: number; + /** + * Set a transition on enter/exit. + */ + transition?: boolean; + /** + * What axis should be disabled. Can be X or Y. + */ + axis?: null | "x" | "y"; + /** + * If the tilt effect has to be reset on exit. + */ + reset?: boolean; + /** + * Easing on enter/exit. + */ + easing?: string; + /** + * Added (@julien-c) + */ + glare?: boolean; + 'max-glare'?: number; + } + + export interface TiltValues { + /** + * The current tilt on the X axis + */ + tiltX: number; + /** + * The current tilt on the Y axis + */ + tiltY: number; + /** + * The current percentage on the X axis + */ + percentageX: number; + /** + * The current percentage on the Y axis + */ + percentageY: number; + } + + export interface HTMLVanillaTiltElement extends HTMLElement { + vanillaTilt: VanillaTilt + } +} + + +export class VanillaTilt { + width: number | null; + height: number | null; + left: number | null; + top: number | null; + element: VanillaTilt.HTMLVanillaTiltElement; + settings: VanillaTilt.TiltOptions; + reverse : -1 | 1; + glare: boolean; + glarePrerender: boolean; + transitionTimeout: number | null; + updateCall: number | null; + glareElementWrapper: HTMLElement; + glareElement: HTMLElement; + updateBind: () => void; + resetBind: () => void; + onMouseEnterBind: (e: Event) => void; + onMouseMoveBind: (e: Event) => void; + onMouseLeaveBind: (e: Event) => void; + event: MouseEvent; + + constructor(element, settings: VanillaTilt.TiltOptions = {}) { + if (!(element instanceof Node)) { + throw ("Can't initialize VanillaTilt because " + element + " is not a Node."); + } + + this.width = null; + this.height = null; + this.left = null; + this.top = null; + this.transitionTimeout = null; + this.updateCall = null; + + this.updateBind = this.update.bind(this); + this.resetBind = this.reset.bind(this); + + this.element = element as VanillaTilt.HTMLVanillaTiltElement; + this.settings = this.extendSettings(settings); + + this.reverse = this.settings.reverse ? -1 : 1; + + this.glare = this.isSettingTrue(this.settings.glare); + this.glarePrerender = this.isSettingTrue(this.settings["glare-prerender"]); + + if (this.glare) { + this.prepareGlare(); + } + + this.addEventListeners(); + } + + isSettingTrue(setting) { + return setting === "" || setting === true || setting === 1; + } + + addEventListeners() { + this.onMouseEnterBind = this.onMouseEnter.bind(this); + this.onMouseMoveBind = this.onMouseMove.bind(this); + this.onMouseLeaveBind = this.onMouseLeave.bind(this); + this.onWindowResizeBind = this.onWindowResizeBind.bind(this); + + this.element.addEventListener("mouseenter", this.onMouseEnterBind); + this.element.addEventListener("mousemove", this.onMouseMoveBind); + this.element.addEventListener("mouseleave", this.onMouseLeaveBind); + if (this.glare) { + window.addEventListener("resize", this.onWindowResizeBind); + } + } + + + onMouseEnter(event) { + this.updateElementPosition(); + (this.element.style).willChange = "transform"; + this.setTransition(); + } + + onMouseMove(event) { + if (this.updateCall !== null) { + cancelAnimationFrame(this.updateCall); + } + + this.event = event; + this.updateCall = requestAnimationFrame(this.updateBind); + } + + onMouseLeave(event) { + this.setTransition(); + + if (this.settings.reset) { + requestAnimationFrame(this.resetBind); + } + } + + reset() { + this.event = { + pageX: this.left! + this.width! / 2, + pageY: this.top! + this.height! / 2 + } as MouseEvent; + + this.element.style.transform = "perspective(" + this.settings.perspective + "px) " + + "rotateX(0deg) " + + "rotateY(0deg) " + + "scale3d(1, 1, 1)" + ; + + if (this.glare) { + this.glareElement.style.transform = 'rotate(180deg) translate(-50%, -50%)'; + this.glareElement.style.opacity = '0'; + } + } + + getValues() { + let x = (this.event.clientX - this.left!) / this.width!; + let y = (this.event.clientY - this.top!) / this.height!; + + x = Math.min(Math.max(x, 0), 1); + y = Math.min(Math.max(y, 0), 1); + + let tiltX = (this.reverse * (this.settings.max! / 2 - x * this.settings.max!)).toFixed(2); + let tiltY = (this.reverse * (y * this.settings.max! - this.settings.max! / 2)).toFixed(2); + let angle = Math.atan2(this.event.clientX - (this.left! + this.width! / 2), -(this.event.clientY - (this.top! + this.height! / 2))) * (180 / Math.PI); + + return { + tiltX: tiltX, + tiltY: tiltY, + percentageX: x * 100, + percentageY: y * 100, + angle: angle + }; + } + + updateElementPosition() { + let rect = this.element.getBoundingClientRect(); + + this.width = this.element.offsetWidth; + this.height = this.element.offsetHeight; + this.left = rect.left; + this.top = rect.top; + } + + update() { + const values = this.getValues(); + + this.element.style.transform = [ + "perspective(" + this.settings.perspective + "px) ", + "rotateX(" + (this.settings.axis === "x" ? 0 : values.tiltY) + "deg) ", + "rotateY(" + (this.settings.axis === "y" ? 0 : values.tiltX) + "deg) ", + "scale3d(" + this.settings.scale + ", " + this.settings.scale + ", " + this.settings.scale + ")", + ].join(" "); + + if (this.glare) { + this.glareElement.style.transform = `rotate(${values.angle}deg) translate(-50%, -50%)`; + this.glareElement.style.opacity = `${values.percentageY * this.settings["max-glare"]! / 100}`; + } + + this.element.dispatchEvent(new CustomEvent("tiltChange", { + "detail": values + })); + + this.updateCall = null; + } + + /** + * Appends the glare element (if glarePrerender equals false) + * and sets the default style + */ + prepareGlare() { + // If option pre-render is enabled we assume all html/css is present for an optimal glare effect. + if (!this.glarePrerender) { + // Create glare element + const jsTiltGlare = document.createElement("div"); + jsTiltGlare.classList.add("js-tilt-glare"); + + const jsTiltGlareInner = document.createElement("div"); + jsTiltGlareInner.classList.add("js-tilt-glare-inner"); + + jsTiltGlare.appendChild(jsTiltGlareInner); + this.element.appendChild(jsTiltGlare); + } + + this.glareElementWrapper = this.element.querySelector(".js-tilt-glare") as HTMLElement; + this.glareElement = this.element.querySelector(".js-tilt-glare-inner") as HTMLElement; + + if (this.glarePrerender) { + return ; + } + + Object.assign(this.glareElementWrapper.style, { + "position": "absolute", + "top": "0", + "left": "0", + "width": "100%", + "height": "100%", + "overflow": "hidden", + 'pointer-events': 'none', + }); + + Object.assign(this.glareElement.style, { + 'position': 'absolute', + 'top': '50%', + 'left': '50%', + 'pointer-events': 'none', + 'background-image': `linear-gradient(0deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%)`, + 'width': `${this.element.offsetWidth * 2}px`, + 'height': `${this.element.offsetWidth * 2}px`, + 'transform': 'rotate(180deg) translate(-50%, -50%)', + 'transform-origin': '0% 0%', + 'opacity': '0', + }); + } + + updateGlareSize() { + Object.assign(this.glareElement.style, { + 'width': `${this.element.offsetWidth * 2}`, + 'height': `${this.element.offsetWidth * 2}`, + }); + } + + onWindowResizeBind() { + this.updateGlareSize(); + } + + setTransition() { + if (this.transitionTimeout) { + clearTimeout(this.transitionTimeout); + } + // this.element.style.transition = `${this.settings.speed}ms ${this.settings.easing}`; + /// From openai: + this.element.style.transition = `transform .4s cubic-bezier(0,0,.2,1)`; + if (this.glare) { + this.glareElement.style.transition = `opacity ${this.settings.speed}ms ${this.settings.easing}`; + } + + this.transitionTimeout = setTimeout(() => { + this.element.style.transition = ""; + if (this.glare) { + this.glareElement.style.transition = ""; + } + }, this.settings.speed); + + } + + extendSettings(settings) { + let defaultSettings = { + reverse: false, + max: 35, + perspective: 1000, + easing: "cubic-bezier(.03,.98,.52,.99)", + scale: "1", + speed: "300", + transition: true, + axis: null, + glare: false, + "max-glare": 1, + "glare-prerender": false, + reset: true, + }; + + let newSettings = {}; + for (var property in defaultSettings) { + if (property in settings) { + newSettings[property] = settings[property]; + } else if (this.element.hasAttribute("data-tilt-" + property)) { + let attribute = this.element.getAttribute("data-tilt-" + property); + try { + newSettings[property] = JSON.parse(attribute); + } catch (e) { + newSettings[property] = attribute; + } + } else { + newSettings[property] = defaultSettings[property]; + } + } + + return newSettings; + } + + static init(elements, settings: VanillaTilt.TiltOptions = {}) { + if (elements instanceof Node) { + elements = [elements]; + } + + if (elements instanceof NodeList) { + elements = [].slice.call(elements); + } + + if (!(elements instanceof Array)) { + return ; + } + + elements.forEach((element) => { + if (!("vanillaTilt" in element)) { + element.vanillaTilt = new VanillaTilt(element, settings); + } + }); + } +} + diff --git a/front/less/mixins/bfc.less b/front/less/mixins/bfc.less new file mode 100644 index 0000000000000000000000000000000000000000..88d850e016c62a4724413f1c1e83ec8c75fe1d4a --- /dev/null +++ b/front/less/mixins/bfc.less @@ -0,0 +1,3 @@ +.bfc { + overflow: hidden; +} \ No newline at end of file diff --git a/front/less/mixins/clearfix.less b/front/less/mixins/clearfix.less new file mode 100644 index 0000000000000000000000000000000000000000..103bc86cb5931b693d9d9a28f44e584dc7884aa1 --- /dev/null +++ b/front/less/mixins/clearfix.less @@ -0,0 +1,10 @@ +.clearfix { + &:before, + &:after { + content: " "; + display: table; + } + &:after { + clear: both; + } +} \ No newline at end of file diff --git a/front/less/mixins/user-select.less b/front/less/mixins/user-select.less new file mode 100644 index 0000000000000000000000000000000000000000..229473e001ff556842c0839edd638932c14fe26b --- /dev/null +++ b/front/less/mixins/user-select.less @@ -0,0 +1,6 @@ +.user-select(@select) { + -webkit-user-select: @select; + -moz-user-select: @select; + -ms-user-select: @select; // IE10+ + user-select: @select; +} diff --git a/front/less/quill.less b/front/less/quill.less new file mode 100644 index 0000000000000000000000000000000000000000..41f68f218af3173fde1739037f70420c220ba561 --- /dev/null +++ b/front/less/quill.less @@ -0,0 +1,36 @@ +/*! + * Quill Editor v1.3.6 + * https://quilljs.com/ + * Copyright (c) 2014, Jason Chen + * Copyright (c) 2013, salesforce.com + */ +.ql-container { + box-sizing: border-box; + height: 100%; + margin: 0px; + position: relative; +} +.ql-clipboard { + left: -100000px; + height: 1px; + overflow-y: hidden; + position: absolute; + top: 50%; +} +.ql-clipboard p { + margin: 0; + padding: 0; +} +.ql-editor { + box-sizing: border-box; + line-height: 1.42; + height: 100%; + outline: none; + overflow-y: auto; + tab-size: 4; + -moz-tab-size: 4; + text-align: left; + white-space: pre-wrap; + word-wrap: break-word; +} + diff --git a/front/less/style-end.less b/front/less/style-end.less new file mode 100644 index 0000000000000000000000000000000000000000..cb4d4a6030913a7c5b6991ced531538910fca7aa --- /dev/null +++ b/front/less/style-end.less @@ -0,0 +1,34 @@ + +@import "variables.less"; + +code, pre, div.pre { + background-color: rgba(0,0,0,0.04); + border-radius: 3px; + padding: 2px 6px; + font-family: @fontMonospace; + font-size: 12px; +} + +div.pre { + span.value { + white-space: pre; + color: #840a6e; + &.string { color: #000; } + &.boolean { color: #0086b3; } + &.number { color: #40a070; } + } +} + +kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #444d56; + vertical-align: text-bottom; + background-color: #fafbfc; + border: solid 1px #c6cbd1; + border-bottom-color: #959da5; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #959da5; +} diff --git a/front/less/style-mention.less b/front/less/style-mention.less new file mode 100644 index 0000000000000000000000000000000000000000..7bce13bf9e30710b6f132e8ef6b131b35f78c551 --- /dev/null +++ b/front/less/style-mention.less @@ -0,0 +1,65 @@ +.ql-mention-list-container { + width: 520px; + @media (max-width: 1100px) { + width: 320px; + } + border: 1px solid #F0F0F0; + border-radius: 4px; + background-color: #FFFFFF; + box-shadow: 0 2px 12px 0 rgba(30, 30, 30, 0.08); + z-index: 9001; + + @media only screen and (max-device-width: 640px) { + position: fixed !important; + left: 0 !important; + top: auto !important; + bottom: 0; + width: 100%; + overflow-y: scroll; + } +} + +.ql-mention-list { + list-style: none; + margin: 0; + padding: 0; + overflow: hidden; +} + +.ql-mention-list-item { + cursor: pointer; + height: 44px; + line-height: 44px; + padding: 0 20px; + vertical-align: middle; + &.selected { + background-color: #D3E1EB; + text-decoration: none; + } + //// vv Essential + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + /// Julien(tweaks) + // font-size: 16px; + + @media only screen and (max-device-width: 640px) { + overflow-x: scroll; + text-overflow: clip; + } +} + + + +/// Julien (AI spans): +.page-inner strong { + font-weight: 400; + border-radius: 6px; + background-color: #D3E1EB; + padding: 3px 1px; +} +.page-inner.html2canvas strong { + font-weight: 600; + background-color: initial; + padding: initial; +} diff --git a/front/less/style-pages.less b/front/less/style-pages.less new file mode 100644 index 0000000000000000000000000000000000000000..c3947adb02da63932e64587ba385d26931fe243d --- /dev/null +++ b/front/less/style-pages.less @@ -0,0 +1,215 @@ + + +/************ + * body.landing + ************/ + +body.landing { + header { + max-width: 800px; + margin: auto; + margin-top: 30px; + margin-bottom: 20px; + text-align: center; + img.logo { + width: 178px; + } + .title { + color: #222; + font-weight: 600; + font-size: 50px; + margin-bottom: 10px; + } + .tagline { + color: #9F9F9F; + font-weight: 600; + font-size: 26px; + margin-bottom: 10px; + } + } + + .container { + margin-left: auto; + margin-right: auto; + max-width: 560px; + } + + div.section-models { + margin-bottom: 50px; + .description { + a { + color: inherit; + } + } + .github-repo { + text-align: center; + margin-top: 20px; + margin-bottom: 40px; + } + div.title-section { + color: #222; + font-weight: 600; + font-size: 34px; + margin-bottom: 10px; + text-align: center; + } + .quote { + color: #9F9F9F; + font-weight: 600; + font-size: 24px; + margin-bottom: 10px; + border-left: 4px solid #ec8cff; + padding: 2px 14px; + max-width: 380px; + margin: auto; + margin-top: 48px; + } + } + + div.section-footer { + .clearfix(); + background-color: #f7fbfb; + padding-top: 80px; + padding-bottom: 90px; + .title { + font-size: 14px; + letter-spacing: 0.3px; + text-transform: uppercase; + text-align: center; + } + ul.documents { + list-style-type: none; + padding: 0; + a { + color: #696969; + font-size: 14px; + } + } + } + + div.model { + border-radius: 5px; + box-shadow: 0 10px 25px 0 rgba(50,94,128,.2); + padding: 18px 26px; + border: 1px solid #e6e6e6; + background-image: linear-gradient(136deg, #F8F8F8 29%, #ffffff 74%, #fafafa 100%); + margin-top: 20px; + margin-bottom: 72px; + a { + color: inherit; + } + .model-title { + font-size: 20px; + font-weight: 600; + &::first-letter { + font-size: 30px; + vertical-align: middle; + } + } + .model-details { + color: #666; + font-size: 15px; + } + .model-bottom { + .clearfix(); + } + a.btn { + float: right; + margin-left: 10px; + border-radius: 6px; + padding: 5px 10px; + color: white; + background-color: grey; + &.btn-primary { + background-color: #40bf0e; + } + &.btn-details { + background-color: #c388ef; + } + position: relative; + &:active { + top: 1px; + } + &:hover { + opacity: 0.7; + } + } + } +} + + +/************ + * body.model + ************/ + +body.model { + header { + padding-top: 80px; + text-align: center; + img.logo-collab { + height: 100px; + &.logo-uber { + height: 50px; + vertical-align: 20px; + } + } + img.cross { + height: 50px; + margin: 0 32px; + vertical-align: 20px; + } + } + .container { + margin-left: auto; + margin-right: auto; + max-width: 560px; + padding-top: 100px; + padding-bottom: 200px; + } + a.back { + font-size: 15px; + img { + margin-right: 1px; + width: 12px; + } + } + .model-title { + color: #222; + font-weight: 600; + font-size: 36px; + &::first-letter { + font-size: 42px; + vertical-align: middle; + } + } + .github-repo { + margin-top: 6px; + margin-bottom: 20px; + transform: scale(0.7) translateX(-85px); + } + .model-bottom { + .clearfix(); + margin-top: 52px; + text-align: center; + } + a.btn { + border-radius: 8px; + padding: 10px 32px; + font-size: 20px; + color: white; + background-color: grey; + &.btn-primary { + background-color: #40bf0e; + } + &.btn-details { + background-color: #c388ef; + } + position: relative; + &:active { + top: 1px; + } + &:hover { + opacity: 0.7; + } + } +} diff --git a/front/less/style.less b/front/less/style.less new file mode 100644 index 0000000000000000000000000000000000000000..5f98450b1547a08499ad75ff4a75ac50bf5558f0 --- /dev/null +++ b/front/less/style.less @@ -0,0 +1,414 @@ +@import (less) "../node_modules/normalize.css/normalize.css"; + +@import "mixins/bfc.less"; +@import "mixins/clearfix.less"; +@import "mixins/user-select.less"; + +@import "variables.less"; + +@blueText: #3B48F6; +@purple: #aa1dc1; +@pageWidth: 740px; + + +.clearfix { + .clearfix(); +} + +.hide { display: none !important; } + +/************ + * Common styles + ************/ + +body { + font-family: @fontSans; + font-size: 16px; + line-height: 1.4; + color: #333; + -webkit-font-smoothing: antialiased; +} +input { + color: #333; +} +a { + color: @blueText; + text-decoration: none; +} +button.input-button { + color: @blueText; + outline: none; + background: transparent; + border: none; + height: 32px; + cursor: pointer; + .user-select(none); +} + +/************ + * body.app + ************/ + +body.app { + background-color: #f8f9fa; + + /** + * Header + */ + + header { + background-color: white; + div.header { + .clearfix(); + padding: 10px 30px; + img.logo { + height: 50px; + float: left; + margin-right: 26px; + } + /** + * or: + */ + a.cross-collab { + float: left; + margin-right: 24px; + img.logo-collab { + height: 50px; + &.logo-uber { + height: 25px; + vertical-align: 10px; + } + } + img.cross { + height: 20px; + margin: 0 8px; + vertical-align: 12px; + } + } + + .header-inner { + float: left; + .title { + color: #222; + font-weight: 600; + font-size: 21px; + margin-bottom: 4px; + .info { + margin-left: 8px; + cursor: pointer; + opacity: 0.8; + &:hover { + opacity: 1; + } + } + code { + margin-left: 4px; + vertical-align: 3px; + } + } + .toolbar { + margin-bottom: 2px; + font-size: 14px; + color: #6D6D6D; + a { + img { + vertical-align: bottom; + margin-right: 6px; + } + &:hover { + opacity: 0.8; + } + } + .toolbar-el { + display: inline-block; + margin-right: 34px; + } + kbd { + margin-left: 2px; + } + } + &[data-page-editable="false"] { + margin-top: 4px; + } + } + .rightbar { + float: right; + margin-top: 14px; + font-size: 14px; + a { + img { + vertical-align: sub; + margin-left: 8px; + } + &:hover { + opacity: 0.8; + } + } + span.doc-status { + font-size: 14px; + color: #6D6D6D; + } + a.btn { + margin-left: 10px; + border-radius: 6px; + padding: 5px 10px; + color: white; + box-shadow: 0 5px 17px 0 rgba(64,64,64,0.4); + background-color: grey; + &.btn-primary { + background-color: #40bf0e; + } + position: relative; + &:active { + top: 1px; + } + } + } + } + div.ruler { + height: 5px; + background-color: #e8eaeda3; + border-top: 1px solid #dddddd61; + border-bottom: 1px solid #dddddd30; + } + } +} + + +/** + * page container + */ + +.page-wrapper { + padding-top: 50px; + padding-bottom: 80px; + @media (max-width: 1100px) { + padding-top: initial; + } + .page-container { + box-shadow: 0 1px 3px 1px rgba(60, 60, 60, 0.13); + box-sizing: border-box; + margin-left: auto; + margin-right: auto; + max-width: @pageWidth; + background-color: white; + padding: 56px 50px; + min-height: 700px; + .page-inner { + padding: 20px 30px; + /// div.editor is here. + .watermark { + visibility: hidden; + text-align: right; + margin-top: 20px; + span.sep { + margin-left: 3px; + margin-right: 3px; + } + span.website { + color: #aaa; + } + } + } + } +} + +/** + * Editor + */ + +div.editor { + /// This is a `position: relative` because of quill and .ql-container + /// ^^ + font-weight: 400; // also experimented w/ 600; + a { + text-decoration: underline; + color: inherit; + } + img.js-loader { + width: 20px; + position: absolute; + } + a.share-screenshot { + text-decoration: none; + font-size: 13px; + border-radius: 1000px; + border: 1px solid @purple; + color: @purple; + position: absolute; + top: 20px; + right: -230px; + padding: 1px 10px; + img { + vertical-align: text-top; + margin-left: 4px; + width: 16px; + } + transition: opacity 0.3s; + &.fadeout { + opacity: 0; + } + &:hover { + opacity: 0.8; + } + @media (max-width: 1100px) { + top: initial; + right: initial; + left: 0; + bottom: -40px; + } + } +} +@import "quill.less"; +@import "style-mention.less"; + +/** + * Settings and modals + */ + +div.decoder-settings { + @media (max-width: 1100px) { + display: none; + } + background-color: #ececec; + position: fixed; + bottom: 0; + width: 140px; + padding: 2px 6px; + .title { + font-size: 11px; + img { + margin-left: 4px; + vertical-align: middle; + opacity: 0.5; + &:hover { + opacity: 0.8; + } + } + } + .setting { + .desc { + font-size: 11px; + font-weight: bold; + } + .js-val { + margin-left: 3px; + &.green { color: rgb(109, 144, 6); } + &.orange { color: #FF7E00; } + &.red { color: red; } + } + } + input.slider { + width: 100%; + } + label.radio { + font-size: 12px; + input[type=radio] { + margin-right: 2px; + } + &.block { + display: block; + } + } +} + +div.modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 10000; + background-color: rgba(32, 23, 49, 0.36); + transition: opacity .3s; + &.fadeout { + opacity: 0; + } + .modal-inner { + width: 500px; + padding: 24px 24px; + box-sizing: border-box; + margin: 36px auto; + max-height: 80%; + overflow-y: auto; + @media (max-width: 500px) { + width: 100%; + } + background-color: white; + box-shadow: 0 11px 15px -7px rgba(0,0,0,.2), 0 24px 38px 3px rgba(0,0,0,.14), 0 9px 46px 8px rgba(0,0,0,.12); + border-radius: 2px; + .modal-content { + font-size: 15px; + p { + margin-top: 0; + } + } + img.js-loader.big { + width: 40px; + display: block; + margin: 60px auto; + } + div.buttons { + border-top: 1px solid #c0b5c57d; + padding-top: 4px; + a { + float: right; + } + } + } +} +div.modal.share-screenshot { + img.js-result { + width: 100%; + margin: 16px auto; + } +} +div.modal.save-publish { + .modal-title { + color: #222; + font-weight: 600; + font-size: 18px; + margin-bottom: 6px; + } + input.doc-url { + margin: 10px auto; + width: 100%; + font-size: 11px; + border-radius: 4px; + border: 1px solid #e0e0e0; + padding: 6px 6px; + box-sizing: border-box; + outline: none; + font-family: monospace; + background-color: #f7f7f7; + } + div.descr-doc-title { + font-weight: 600; + margin-bottom: 6px; + } + input.doc-title { + margin: 10px auto; + width: 100%; + border-radius: 4px; + border: 1px solid #e0e0e0; + padding: 6px 6px; + box-sizing: border-box; + outline: none; + font-weight: 600; + font-size: 16px; + } +} + + +/** + * Other pages + */ + @import "style-pages.less"; + +/** + * Imports + */ +@import "style-end.less"; +/** + * The End. + */ diff --git a/front/less/variables.less b/front/less/variables.less new file mode 100644 index 0000000000000000000000000000000000000000..c6da06c606d8c31d2467ca89303c4bf6bf1fc708 --- /dev/null +++ b/front/less/variables.less @@ -0,0 +1,3 @@ + +@fontSans: 'Source Sans Pro', sans-serif; +@fontMonospace: monospace; diff --git a/front/package-lock.json b/front/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..eaf3f047d76d3b56de280283109d397c0bb40bec --- /dev/null +++ b/front/package-lock.json @@ -0,0 +1,415 @@ +{ + "name": "foobar-front", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/code-frame": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", + "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.0.0" + } + }, + "@babel/highlight": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", + "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true + }, + "@types/node": { + "version": "12.0.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.7.tgz", + "integrity": "sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A==", + "dev": true + }, + "@types/resolve": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", + "integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "acorn": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "base64-arraybuffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.2.0.tgz", + "integrity": "sha512-7emyCsu1/xiBXgQZrscw/8KPRT44I4Yq9Pe6EGs3aPRTsWuggML1/1DTuZUuIaJPIm1FTDUVXl4x/yW8s0kQDQ==" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz", + "integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "css-line-break": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-1.1.1.tgz", + "integrity": "sha512-1feNVaM4Fyzdj4mKPIQNL2n70MmuYzAXZ1aytlROFX1JsOo070OsugwGjj7nl6jnDJWHDM8zRZswkmeYVWZJQA==", + "requires": { + "base64-arraybuffer": "^0.2.0" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "html2canvas": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.0.0-rc.3.tgz", + "integrity": "sha512-nWRk34IO3QopcDYpiPAbRW6VoI10H7uxEhcSFjox0JB6wZOMd6Mak+NqHPLljSFFEOvBjPafyRgcHnuWcFpWvg==", + "requires": { + "css-line-break": "1.1.1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "jest-worker": { + "version": "24.6.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", + "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", + "dev": true, + "requires": { + "merge-stream": "^1.0.1", + "supports-color": "^6.1.0" + }, + "dependencies": { + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, + "normalize.css": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize.css/-/normalize.css-8.0.1.tgz", + "integrity": "sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==" + }, + "parchment": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz", + "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true + }, + "quill": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.6.tgz", + "integrity": "sha512-K0mvhimWZN6s+9OQ249CH2IEPZ9JmkFuCQeHAOQax3EZ2nDJ3wfGh59mnlQaZV2i7u8eFarx6wAtvQKgShojug==", + "requires": { + "clone": "^2.1.1", + "deep-equal": "^1.0.1", + "eventemitter3": "^2.0.3", + "extend": "^3.0.1", + "parchment": "^1.1.4", + "quill-delta": "^3.6.2" + } + }, + "quill-delta": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz", + "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==", + "requires": { + "deep-equal": "^1.0.1", + "extend": "^3.0.2", + "fast-diff": "1.1.2" + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "resolve": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", + "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "rollup": { + "version": "1.14.6", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.14.6.tgz", + "integrity": "sha512-A8f54Fms9PAG2VBLBg/XOBgN6tRQKgroltD86f+gF5+6eRmAAlFBJAFV0K7MHl2aHOCcrATWO1B8SAjVD2Ehsw==", + "dev": true, + "requires": { + "@types/estree": "0.0.39", + "@types/node": "^12.0.7", + "acorn": "^6.1.1" + } + }, + "rollup-plugin-node-resolve": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.0.1.tgz", + "integrity": "sha512-9s3dTu44SKQZM/Pwll42GpqXgT+WdvO0Ga01lF8cwZqJGqRUATtD+GrP3uIzZdpnbPonEJbVasfFt80VGPQqKw==", + "dev": true, + "requires": { + "@types/resolve": "0.0.8", + "builtin-modules": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.11.0", + "rollup-pluginutils": "^2.8.0" + } + }, + "rollup-plugin-terser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-5.0.0.tgz", + "integrity": "sha512-W+jJ4opYnlmNyVW0vtRufs+EGf68BIJ7bnOazgz8mgz8pA9lUyrEifAhPs5y9M16wFeAyBGaRjKip4dnFBtXaw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "jest-worker": "^24.6.0", + "serialize-javascript": "^1.7.0", + "terser": "^4.0.0" + } + }, + "rollup-pluginutils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz", + "integrity": "sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg==", + "dev": true, + "requires": { + "estree-walker": "^0.6.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "serialize-javascript": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.7.0.tgz", + "integrity": "sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", + "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "terser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.0.0.tgz", + "integrity": "sha512-dOapGTU0hETFl1tCo4t56FN+2jffoKyER9qBGoUFyZ6y7WLoKT0bF+lAYi6B6YsILcGF3q1C2FBh8QcKSCgkgA==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.10" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + } + } +} diff --git a/front/package.json b/front/package.json new file mode 100644 index 0000000000000000000000000000000000000000..01129187f3831693e100017cb09ad8c61c5071c2 --- /dev/null +++ b/front/package.json @@ -0,0 +1,26 @@ +{ + "name": "foobar-front", + "version": "1.0.0", + "description": "", + "main": "index.js", + "dependencies": { + "html2canvas": "^1.0.0-rc.3", + "normalize.css": "^8.0.1", + "quill": "^1.3.6" + }, + "devDependencies": { + "rollup": "^1.14.6", + "rollup-plugin-node-resolve": "^5.0.1", + "rollup-plugin-terser": "^5.0.0", + "typescript": "4.0.3" + }, + "scripts": { + "prebuild": "rm -rf dist/* && rm -rf build/* && mkdir dist && mkdir build", + "build:dev": "rollup -c", + "build:prod": "PRODUCTION=1 rollup -c", + "grunt:watch": "cd ../grunt && grunt watch", + "watch": "rollup -c -w" + }, + "author": "", + "license": "ISC" +} diff --git a/front/robots.txt b/front/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..c2a49f4fb82f19ffa9089fcf8630e5a2870d3feb --- /dev/null +++ b/front/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Allow: / diff --git a/front/rollup.config.js b/front/rollup.config.js new file mode 100644 index 0000000000000000000000000000000000000000..cd62d313370f2ee84c3216e1b0aaac008221c03b --- /dev/null +++ b/front/rollup.config.js @@ -0,0 +1,40 @@ +import resolve from 'rollup-plugin-node-resolve'; +import { terser } from 'rollup-plugin-terser'; +import { promisify } from 'util'; +import { exec } from 'child_process'; +const __exec = promisify(exec); + +const PRODUCTION = !!process.env.PRODUCTION; + +const OUTFILE_DEV = `build/bundle.js`; +const OUTFILE_PROD = `build/b${ Date.now() }.min.js`; + +(async () => { + if (! PRODUCTION) { + return ; + } + const outDev = OUTFILE_DEV .replace(/\//g, '\\/'); + const outProd = OUTFILE_PROD.replace(/\//g, '\\/'); + const sed = process.platform === 'darwin' + ? `sed -i ''` + : `sed -i''` + ; + console.log( + await __exec(`${sed} "s/${ outDev }/${ outProd }/g" views/layout.hbs`) + ); +})(); + +export default { + input: `dist/controller.js`, + output: { + file: PRODUCTION + ? OUTFILE_PROD + : OUTFILE_DEV + , + format: `iife`, + }, + plugins: [ + resolve(), + PRODUCTION ? terser() : undefined, + ] +} diff --git a/front/tsconfig.json b/front/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..ee256995cbd9179ac6d86ae27d9eeee18ebda0a2 --- /dev/null +++ b/front/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "esnext", + "moduleResolution": "node", + "outDir": "dist", + "strictNullChecks": true, + "strictBindCallApply": true, + "lib": ["dom", "es6", "es2016", "es2017", "es2018", "esnext"], + } +} \ No newline at end of file diff --git a/front/views/index.hbs b/front/views/index.hbs new file mode 100644 index 0000000000000000000000000000000000000000..a64c48a5d1dcd1306f2a808fd57903896239fcc8 --- /dev/null +++ b/front/views/index.hbs @@ -0,0 +1,376 @@ +
+
+ {{#if doc.hasCustomLogo}} + {{#eq doc.model "ctrl"}} + + + + + + {{/eq}} + {{#eq doc.model "pplm"}} + + + + + + {{/eq}} + {{else}} + + + + {{/if}} +
+
+ {{#if doc.title}} + {{doc.title}} + {{else}} + Write With Transformer + {{/if}} + {{#if doc.modelFamily}} + + {{#if doc.title}} + written with {{doc.modelFamily}} + {{else}} + {{doc.modelFamily}} + {{/if}} + + {{/if}} + + + +
+ {{#if editable}} +
+ +
+ Trigger autocomplete +   or tab +
+
+ Select suggestion and enter +
+
+ Cancel suggestion esc +
+
+ {{/if}} +
+
+ {{#if legacy}} + Share + {{else}} + {{#if editable}} + Save & Publish + {{else}} + Read-only document + Duplicate & edit + {{/if}} + {{/if}} +
+
+
+
+ +
+
+ + +
+
+ {{#if doc.getHTML}} + {{{ doc.getHTML }}} + {{else}} + {{#eq doc.model "pplm"}} +

+ PPLM builds on top of other large transformer-based generative models (like GPT-2), where it enables + finer-grained control of attributes of the generated language (e.g. gradually switching topic 🐱 or sentiment 😃). +

+

+ ⚠️ 🐍 We had to turn off the PPLM machine as it was costly to host – try it locally using + transformers, + or contact us if you really need it as a hosted service. 🐍 ⚠️ + {{!-- Replace this text and hit tab to trigger generations. Have fun! --}} +

+ {{else}} +

+ See how a modern neural network auto-completes your text 🤗 +

+

+ {{!-- In February, OpenAI unveiled a language model called GPT-2 – for Generative Pre-Trained Transformer – that generates coherent paragraphs of text one word at a time. --}} + This site, built by the Hugging Face team, lets you write a whole document directly from your browser, + and you can trigger the Transformer anywhere using the Tab key. It's like having a smart machine that completes your thoughts 😀 +

+

+ Get started by typing a custom snippet, + check out the repository, + or try one of the examples. Have fun! +

+ {{/eq}} + {{/if}} +
+
+ Written by Transformer · transformer.huggingface.co 🦄 +
+
+
+
+ +{{#if editable}} +
+
+ Model & decoder settings + + + +
+ {{#eq doc.model "pplm"}} +
+
+ Bag-of-words +
+ + + + + + + + + +
+ Discriminators +
+
+ + +
+
+ + +
+ {{!--
+ + +
--}} +
+ {{/eq}} + +
+ {{#not doc.model "pplm"}} +
+ Model size +
+ {{/not}} + {{#eq doc.model "pplm"}} + pplm + {{/eq}} + + {{#eq doc.model "distil-gpt2"}} + + {{/eq}} + {{#eq doc.model "arxiv-nlp"}} + + {{/eq}} + {{#eq doc.model "gpt2-large"}} + + {{/eq}} + {{#eq doc.model "xlnet"}} + + {{/eq}} + {{#eq doc.model "gpt"}} + + {{/eq}} +
+ {{#not doc.model "pplm"}} +
+
+ Top-p +
+ +
+
+
+ Temperature +
+ +
+ {{/not}} + {{#eq doc.model "pplm"}} +
+
+ Step size +
+ +
+
+
+ KL-scale +
+ +
+
+
+ GM-scale +
+ +
+
+
+ Num iterations (impacts gen. time) +
+ +
+
+
+ Gen. length (impacts gen. time) +
+ +
+
+
+ Use sampling +
+ +
+ {{/eq}} + + {{#not doc.model "pplm"}} +
+
+ Max time +
+ +
+ {{/not}} +
+{{/if}} + + + +{{#if editable}} + +{{/if}} + + diff --git a/front/views/landing.hbs b/front/views/landing.hbs new file mode 100644 index 0000000000000000000000000000000000000000..5bc75a9a4a3a6f050cdbb2974ff5a63e5e1cebd1 --- /dev/null +++ b/front/views/landing.hbs @@ -0,0 +1,120 @@ + +
+ +
+ Write With Transformer +
+
+ Get a modern neural network to
auto-complete your thoughts. +
+
+ +
+
+
+ This web app, built by the Hugging Face team, is the official demo of the + 🤗/transformers + repository's text generation capabilities. +
+ + +
Models
+ +
+
🦄 GPT-2
+
+ The almighty king of text generation, GPT-2 comes in four available sizes, only three of which have been publicly made available. Feared for its fake news generation capabilities, + it currently stands as the most syntactically coherent model. A direct successor to the original GPT, it reinforces the already established pre-training/fine-tuning killer duo. + From the paper: Language Models are Unsupervised Multitask Learners by Alec Radford, Jeffrey Wu, Rewon Child, David Luan, Dario Amodei and Ilya Sutskever. +
+ +
+ +
+
💯 XLNet
+
+ Overcoming the unidirectional limit while maintaining an independent masking algorithm based on permutation, XLNet improves upon the state-of-the-art autoregressive model that is TransformerXL. Using a bidirectional context while keeping its autoregressive approach, this model outperforms BERT on 20 tasks while keeping an impressive generative coherence. + From the paper: XLNet: Generalized Autoregressive Pretraining for Language Understanding, by Zhilin Yang, Zihang Dai, Yiming Yang, Jaime Carbonell, Ruslan Salakhutdinov and Quoc V. Le. +
+ +
+ +
+
☠️ GPT
+
+ Released by OpenAI, this seminal architecture has shown that large gains on several NLP tasks can be achieved by generative pre-training a language model + on unlabeled text before fine-tuning it on a downstream task. + From the paper: Improving Language Understanding by Generative Pre-Training, by Alec Radford, Karthik Naraimhan, Tim Salimans and Ilya Sutskever. +
+ +
+ + + + + + + + + +
+ Do you want to contribute or suggest a new model checkpoint? Open an issue on + 🤗/transformers 🔥. +
+
+ “It is to writing what calculators are to calculus.” +
+
+
+ + diff --git a/front/views/layout.hbs b/front/views/layout.hbs new file mode 100644 index 0000000000000000000000000000000000000000..42dbbf777d0d98ed3fd12b2681322f8fe8009ade --- /dev/null +++ b/front/views/layout.hbs @@ -0,0 +1,72 @@ + + + + + + {{#if meta.title}} + {{meta.title}} + {{else if doc.title}} + {{doc.title}} + {{else}} + Write With Transformer + {{/if}} + + + + + {{#if meta.title}} + + {{else}} + + {{/if}} + + + {{#if meta.thumbnail}} + + {{else}} + + {{/if}} + + + + + + + {{{ body }}} + + + + + + + + + + + + \ No newline at end of file diff --git a/front/views/model.hbs b/front/views/model.hbs new file mode 100644 index 0000000000000000000000000000000000000000..ac458d60af6d84372ccf2d72698755d0eb43fc6c --- /dev/null +++ b/front/views/model.hbs @@ -0,0 +1,81 @@ + +{{#eq model "ctrl"}} +
+ + + +
+{{/eq}} +{{#eq model "pplm"}} +
+ + + +
+{{/eq}} + +
+ + + See all models and checkpoints + +
+ {{#eq model "arxiv-nlp"}} + 🤓 ArXiv NLP model checkpoint + {{/eq}} + {{#eq model "distil-gpt2"}} + 🐎 DistilGPT-2 model checkpoint + {{/eq}} + {{#eq model "ctrl"}} + ☁️ Salesforce Research CTRL + {{/eq}} + {{#eq model "pplm"}} + 🚕 Uber AI Plug and Play Language Model (PPLM) + {{/eq}} +
+ +
+ {{#eq model "distil-gpt2"}} +

The student of the now ubiquitous GPT-2 does not come short of its teacher’s expectations. + Obtained by distillation, DistilGPT-2 weighs 37% less, and is twice as fast as its OpenAI counterpart, while keeping the same generative power. + Runs smoothly on an iPhone 7. The dawn of lightweight generative transformers? 🤯

+ +

From the paper: DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter by Victor Sanh, Lysandre Debut, Julien Chaumond and Thomas Wolf. + The same method was applied to distill GPT-2, and a Medium blogpost describes the process in detail.

+ {{/eq}} + + {{#eq model "arxiv-nlp"}} +

Built on the OpenAI GPT-2 model, the Hugging Face team has fine-tuned the small version of the model on a tiny dataset (60MB of text) of Arxiv papers. + The targeted subject is Natural Language Processing, resulting in a very Linguistics/Deep Learning oriented generation.

+ +

All articles were downloaded from Cornell University’s arxiv.org website using arXiv Bulk Data Access.

+ {{/eq}} + + {{#eq model "ctrl"}} +

CTRL transcends the pre-training/fine-tuning approach by taking advantage of a whopping 1.6 billion parameters 🤯.

+ +

Controllable Generation: this model generates some text directly tuned to several subreddits (fitness, personal finance, running and many more), Wikipedia articles or product reviews. Take advantage of its control codes and use it for question answering, translation or styled text generation. Kindly implemented by the Salesforce team in 🤗/transformers.

+ +

From the paper CTRL: A Conditional Transformer Language Model for Controllable Generation by Nitish Shirish Keskar*, Bryan McCann*, Lav R. Varshney, Caiming Xiong and Richard Socher.

+ {{/eq}} + + {{#eq model "pplm"}} +

PPLM builds on top of other large transformer-based generative models (like GPT-2), where it enables finer-grained control of attributes of the generated language (e.g. gradually switching topic 🐱 or sentiment 😃).

+ +

This controlled language generation method consists of plugging in simple bag-of-words or one-layer classifiers as attribute controllers, and making updates in the activation space, without changing any model parameters. + Kindly implemented by the Uber AI team in 🤗/transformers.

+ +

From the paper Plug and Play Language Model: A simple baseline for controlled language generation by + Sumanth Dathathri, Andrea Madotto, Janice Lan, Jane Hung, Eric Frank, Piero Molino, Jason Yosinski, and Rosanne Liu.

+ {{/eq}} +
+ +
diff --git a/grunt/Gruntfile.js b/grunt/Gruntfile.js new file mode 100644 index 0000000000000000000000000000000000000000..bb36983eda2cf246c328cdad88c4b2e40d9ee0f5 --- /dev/null +++ b/grunt/Gruntfile.js @@ -0,0 +1,31 @@ +const __path = require('path'); +const CWD = __path.normalize(`${__dirname}/../front`); + +module.exports = function(grunt) { + + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.registerTask('default', ['less']); + + grunt.initConfig({ + less: { + options: { + compress: false, + }, + dist: { + src: [ + `${CWD}/less/style.less`, + ], + dest: `${CWD}/build/style.css` + } + }, + watch: { + options: { + livereload: true, + cwd: CWD, + }, + files: ["views/*", "less/*", "build/**/*.js"], + tasks: 'default' + }, + }); +}; diff --git a/grunt/package-lock.json b/grunt/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..5d0db8fc740332fb7a9852d17700b8e0ea9f70ad --- /dev/null +++ b/grunt/package-lock.json @@ -0,0 +1,1389 @@ +{ + "name": "foobar-grunt", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "optional": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + } + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "optional": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "optional": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true + }, + "async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", + "requires": { + "lodash": "^4.17.11" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", + "requires": { + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=" + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "coffeescript": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-1.10.0.tgz", + "integrity": "sha1-56qDAZF+9iGzXYo580jc3R234z4=" + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "optional": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "dateformat": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", + "requires": { + "get-stdin": "^4.0.1", + "meow": "^3.3.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/error/-/error-7.0.2.tgz", + "integrity": "sha1-pfdf/02ZJhJt2sDqXcOOaJFTywI=", + "requires": { + "string-template": "~0.2.1", + "xtend": "~4.0.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=" + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "optional": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "optional": true + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", + "optional": true + }, + "faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "findup-sync": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", + "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", + "requires": { + "glob": "~5.0.0" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "requires": { + "globule": "^1.0.0" + } + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "getobject": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-0.1.0.tgz", + "integrity": "sha1-BHpEl4n6Fg0Bj1SG7ZEyC27HiFw=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "grunt": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.0.4.tgz", + "integrity": "sha512-PYsMOrOC+MsdGEkFVwMaMyc6Ob7pKmq+deg1Sjr+vvMWp35sztfwKE7qoN51V+UEtHsyNuMcGdgMLFkBHvMxHQ==", + "requires": { + "coffeescript": "~1.10.0", + "dateformat": "~1.0.12", + "eventemitter2": "~0.4.13", + "exit": "~0.1.1", + "findup-sync": "~0.3.0", + "glob": "~7.0.0", + "grunt-cli": "~1.2.0", + "grunt-known-options": "~1.1.0", + "grunt-legacy-log": "~2.0.0", + "grunt-legacy-util": "~1.1.1", + "iconv-lite": "~0.4.13", + "js-yaml": "~3.13.0", + "minimatch": "~3.0.2", + "mkdirp": "~0.5.1", + "nopt": "~3.0.6", + "path-is-absolute": "~1.0.0", + "rimraf": "~2.6.2" + }, + "dependencies": { + "glob": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.0.6.tgz", + "integrity": "sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo=", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "grunt-cli": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.2.0.tgz", + "integrity": "sha1-VisRnrsGndtGSs4oRVAb6Xs1tqg=", + "requires": { + "findup-sync": "~0.3.0", + "grunt-known-options": "~1.1.0", + "nopt": "~3.0.6", + "resolve": "~1.1.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "grunt-contrib-less": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-less/-/grunt-contrib-less-2.0.0.tgz", + "integrity": "sha512-nsaODoEMjVn61OuqPaFeFQpb4Qd/EbfxQDeYnh2oONXm8L5Gnuchtv59kl0V3hjiFdOkZlPILDc3ZrkoZI0PNw==", + "requires": { + "async": "^2.0.0", + "chalk": "^1.0.0", + "less": "^3.0.4", + "lodash": "^4.17.10" + } + }, + "grunt-contrib-watch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", + "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", + "requires": { + "async": "^2.6.0", + "gaze": "^1.1.0", + "lodash": "^4.17.10", + "tiny-lr": "^1.1.1" + } + }, + "grunt-known-options": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-1.1.1.tgz", + "integrity": "sha512-cHwsLqoighpu7TuYj5RonnEuxGVFnztcUqTqp5rXFGYL4OuPFofwC4Ycg7n9fYwvK6F5WbYgeVOwph9Crs2fsQ==" + }, + "grunt-legacy-log": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-2.0.0.tgz", + "integrity": "sha512-1m3+5QvDYfR1ltr8hjiaiNjddxGdQWcH0rw1iKKiQnF0+xtgTazirSTGu68RchPyh1OBng1bBUjLmX8q9NpoCw==", + "requires": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.5" + } + }, + "grunt-legacy-log-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.0.1.tgz", + "integrity": "sha512-o7uHyO/J+i2tXG8r2bZNlVk20vlIFJ9IEYyHMCQGfWYru8Jv3wTqKZzvV30YW9rWEjq0eP3cflQ1qWojIe9VFA==", + "requires": { + "chalk": "~2.4.1", + "lodash": "~4.17.10" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "grunt-legacy-util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-1.1.1.tgz", + "integrity": "sha512-9zyA29w/fBe6BIfjGENndwoe1Uy31BIXxTH3s8mga0Z5Bz2Sp4UCjkeyv2tI449ymkx3x26B+46FV4fXEddl5A==", + "requires": { + "async": "~1.5.2", + "exit": "~0.1.1", + "getobject": "~0.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.10", + "underscore.string": "~3.3.4", + "which": "~1.3.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "optional": true, + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=" + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + }, + "http-parser-js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.0.tgz", + "integrity": "sha512-cZdEF7r4gfRIq7ezX9J0T+kQmJNOub71dWbgAXVHDct80TKP4MCETtZQ31xyv38UwgzkWPYF/Xc0ge55dW9Z9w==" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "optional": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "optional": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "optional": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "less": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", + "integrity": "sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w==", + "requires": { + "clone": "^2.1.2", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "mime": "^1.4.1", + "mkdirp": "^0.5.0", + "promise": "^7.1.1", + "request": "^2.83.0", + "source-map": "~0.6.0" + } + }, + "livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==" + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "lodash": { + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "optional": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "optional": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "optional": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "optional": true, + "requires": { + "asap": "~2.0.3" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "optional": true + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==", + "optional": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "optional": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", + "requires": { + "bytes": "1", + "string_decoder": "0.10" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "optional": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "resolve": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", + "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true + }, + "safe-json-parse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz", + "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==" + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "tiny-lr": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", + "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", + "requires": { + "body": "^5.1.0", + "debug": "^3.1.0", + "faye-websocket": "~0.10.0", + "livereload-js": "^2.3.0", + "object-assign": "^4.1.0", + "qs": "^6.4.0" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "optional": true, + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "optional": true + } + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "underscore.string": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", + "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", + "requires": { + "sprintf-js": "^1.0.3", + "util-deprecate": "^1.0.2" + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "optional": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "optional": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "websocket-driver": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", + "integrity": "sha1-DK+dLXVdk67gSdS90NP+LMoqJOs=", + "requires": { + "http-parser-js": ">=0.4.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", + "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==" + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/grunt/package.json b/grunt/package.json new file mode 100644 index 0000000000000000000000000000000000000000..6b1ea24cca0e8fa3ed30b0fc577ad77567fc83b4 --- /dev/null +++ b/grunt/package.json @@ -0,0 +1,16 @@ +{ + "name": "foobar-grunt", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "grunt": "^1.0.4", + "grunt-contrib-less": "^2.0.0", + "grunt-contrib-watch": "^1.1.0" + } +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000000000000000000000000000000000000..e51710b0b167fbee5c860248b211fbc412d7491e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,76 @@ +worker_processes auto; +pid /tmp/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + include /etc/nginx/mime.types; + default_type application/octet-stream; + access_log /tmp/access.log; + error_log /tmp/error.log; + gzip on; + client_max_body_size 0; + + server { + listen 8080; + server_name _; + + location / { + proxy_pass http://localhost:3210; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_http_version 1.1; + } + + location /autocomplete { + proxy_pass $NGINX_NEURALGENV2_URL; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_http_version 1.1; + add_header X-JeanClaude True always; + add_header Access-Control-Allow-Headers Content-Type always; # <- circumvent cors for Firefox + # mirror /mirror_autocomplete; + } + + location ~ ^/autocomplete/(gpt2\/xl) { + # was turned off. + proxy_pass $NGINX_AUTOCOMPLETE_URL; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_http_version 1.1; + add_header X-Jacqueline True always; + add_header Access-Control-Allow-Headers Content-Type always; # <- circumvent cors for Firefox + } + + location ~ ^/autocomplete/(ctrl|pplm) { + # was turned off. + proxy_pass $NGINX_CTRL_PPLM_URL; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_http_version 1.1; + add_header X-Jacinthe True always; + add_header Access-Control-Allow-Headers Content-Type always; # <- circumvent cors for Firefox + } + } +} diff --git a/server/lib/Extensions.ts b/server/lib/Extensions.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d8540c46d57682ad37d3cd986a1656cfb06d6da --- /dev/null +++ b/server/lib/Extensions.ts @@ -0,0 +1,43 @@ + +declare global { + interface Array { + randomItem: () => T; + randomIndex: () => number; + last: () => T; + } + + interface ReadonlyArray { + randomItem: () => T; + randomIndex: () => number; + last: () => T; + } + + interface String { + capitalize: () => string; + } +} + +const extendArray = () => { + Array.prototype['randomItem'] = function() { + return this[Math.floor(Math.random()*this.length)]; + } + + Array.prototype['randomIndex'] = function() { + return Math.floor(Math.random()*this.length); + } + + Array.prototype['last'] = function() { + return this[this.length - 1]; + } +} + +const extendString = () => { + String.prototype['capitalize'] = function() { + return this.charAt(0).toUpperCase() + this.slice(1); + } +} + +export const install = () => { + extendArray(); + extendString(); +} \ No newline at end of file diff --git a/server/lib/Hbs.ts b/server/lib/Hbs.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ebc0e6ca4d336155e0a4cb61e0f3f5190372324 --- /dev/null +++ b/server/lib/Hbs.ts @@ -0,0 +1,20 @@ +import * as HbsModule from 'hbs'; + +export const hbs = HbsModule.create(); + +hbs.registerHelper('urlenc', (str) => { + return encodeURIComponent(str); +}); + +hbs.registerHelper('stringify', (o) => { + return JSON.stringify(o); +}); + +hbs.registerHelper('eq', function(arg1, arg2, options) { + return (arg1 === arg2) ? options.fn(this) : options.inverse(this); +}); + +hbs.registerHelper('not', function(arg1, arg2, options) { + return (arg1 !== arg2) ? options.fn(this) : options.inverse(this); +}); + diff --git a/server/lib/Log.ts b/server/lib/Log.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fcb654ba837b8d55fd25e1206c53501f08e875d --- /dev/null +++ b/server/lib/Log.ts @@ -0,0 +1,53 @@ +import * as colors from 'colors'; +import * as util from 'util'; + + + +export const c = { + __log: (args: any[], opts: { + color?: colors.Color, + colors?: boolean, + } = {}) => { + const inspectOpts = (opts.colors !== undefined) + ? { depth: 20, colors: opts.colors } + : { depth: 20, colors: true } + ; + const s = args.map(o => { + if (o instanceof Error) { + // return colors.red(`${o.name}: ${o.message}\n${o.stack}`); + return (o.stack || `${o.name}: ${o.message}`) + .split('\n') + .map(x => colors.red(x)) + .join('\n') + ; + } else if (typeof o === 'string') { + return o; + } else { + return util.inspect(o, inspectOpts); + } + }).join(' '); + console.log(opts.color ? opts.color(s) : s); + }, + log: (...args) => { + c.__log(args); + }, + debug: (...args) => { + c.__log(args, { color: colors.gray, colors: false }); + }, + success: (...args) => { + c.__log(args, { color: colors.green }); + }, + error: (...args) => { + c.__log(args, { color: colors.red }); + }, + info: (...args) => { + c.__log(args, { color: colors.cyan }); + }, + introspect: (...args) => { + c.__log(args.map(a => [ + a, + typeof a, + a.constructor.name, + ])); + }, +} diff --git a/server/lib/RootDirFinder.ts b/server/lib/RootDirFinder.ts new file mode 100644 index 0000000000000000000000000000000000000000..26eb3100951c645c72bc8e5e8fe7637055914208 --- /dev/null +++ b/server/lib/RootDirFinder.ts @@ -0,0 +1,8 @@ + +const rootDirFinder = function(): string { + let i = __dirname.split('/').findIndex((dir) => dir === 'transformer-autocomplete'); + return __dirname.split('/').slice(0, i+1).join('/'); +}; +const __rootDir: string = rootDirFinder(); + +export default __rootDir; diff --git a/server/lib/Utils.ts b/server/lib/Utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..dd3810b6f4ad016b0d2670b0858dd3731e655810 --- /dev/null +++ b/server/lib/Utils.ts @@ -0,0 +1,72 @@ +import * as util from 'util'; +import * as child_process from 'child_process'; +import { ObjectID } from 'mongodb'; +const __exec = util.promisify(child_process.exec); + + +export namespace Utils { + /** + * Return a random integer between min and max (upper bound is exclusive). + */ + export function randomInt(maxOrMin: number, max?: number): number { + return (max) + ? maxOrMin + Math.floor(Math.random() * (max - maxOrMin)) + : Math.floor(Math.random() * maxOrMin); + } + + /** + * Random element from array (usually not needed b/c we extend Array in Extensions.ts). + */ + export function randomItem(arr: T[]): T { + return arr[Math.floor(Math.random()*arr.length)]; + } + + /** + * Return copy of object, only keeping whitelisted properties. + */ + export function pick(o: T, props: K[]): Pick { + // inspired by stackoverflow.com/questions/25553910/one-liner-to-take-some-properties-from-object-in-es-6 + // Warning: this adds {p: undefined} for props not in the o object. + return Object.assign({}, ...props.map(prop => ({[prop]: o[prop]}))); + } + /** + * One param: create list of integers from 0 (inclusive) to n (exclusive) + * Two params: create list of integers from a (inclusive) to b (exclusive) + */ + export function range(n: number, b?: number): number[] { + return (b) + ? Array(b - n).fill(0).map((_, i) => n + i) + : Array(n).fill(0).map((_, i) => i); + } + + /** + * Gets the value at path of object, or undefined. + */ + export function get(o: any, path: string | string[]) { + const properties = Array.isArray(path) ? path : path.split('.'); + let x = o; + for (const p of properties) { + x = x[p]; + if (x === undefined) { + return undefined; + } + } + return x; + } + + /** + * Asynchronously filter on the given array + */ + export async function filter(array: T[], __filter: (element: T, index: number, array: T[]) => Promise, thisArg?: any): Promise { + const keeps = await Promise.all(array.map(__filter)); + return array.filter((e, i) => keeps[i], thisArg); + } + + export function randomStr(length: number): string { + const chars = range(97, 123).map(x => String.fromCharCode(x)); + return Array(length).fill(0) + .map(x => randomInt(2) ? randomItem(chars) : randomItem(chars).toUpperCase()) + .join("") + ; + } +} diff --git a/server/lib/config.ts b/server/lib/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a7c0dc3471ce0da3f4683b694757c0e403feab5 --- /dev/null +++ b/server/lib/config.ts @@ -0,0 +1,59 @@ +import * as os from 'os'; + +const HOSTNAME = os.hostname().split('.')[0]; + +type EnvironmentType = "development" | "production" | "staging"; +export class Environment { + static development: EnvironmentType = "development"; + static production: EnvironmentType = "production"; + static staging: EnvironmentType = "staging"; + + static current(): EnvironmentType { + switch (process.env.NODE_ENV) { + case Environment.development: + return Environment.development; + case Environment.production: + return Environment.production; + case Environment.staging: + return Environment.staging; + default: + return ["banana", "katia"].includes(HOSTNAME) + ? Environment.production + : Environment.development + } + } +} + +export class Configuration { + static prepareProperty(prop: {[index: string]: T}): T { + if (typeof prop === 'object' && Object.keys(prop).includes(Environment.current())) { + return prop[Environment.current()]; + } + // Fallback on development config + return prop[Environment.development]; + } +} + +export const config = { + environment: Environment.current(), + hostname: HOSTNAME, // (katia).huggingface.co || (banana).huggingface.co || ... + + appPort: Configuration.prepareProperty({ + [Environment.production]: 3210, + [Environment.development]: 3210, + [Environment.staging]: 3210, + }), + + transformerAutocompleteUrl: Configuration.prepareProperty({ + [Environment.production]: "https://transformer.huggingface.co", + [Environment.development]: "http://localhost:3210", + [Environment.staging]: "https://transformer-staging.huggingface.co", + }), + + mongoUrl: process.env.NODE_MONGODB_URL ?? "mongodb://localhost:27017", + mongoDbName: Configuration.prepareProperty({ + [Environment.production]: "transformer-autocomplete", + [Environment.development]: "transformer-autocomplete", + [Environment.staging]: "transformer-autocomplete-staging", + }), +} diff --git a/server/lib/obj.d.ts b/server/lib/obj.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..6694da54405dccd020d42661d46166767c03ceab --- /dev/null +++ b/server/lib/obj.d.ts @@ -0,0 +1,8 @@ +/** + * Hf helper type for a dictionary-like object with arbitrary keys. + */ +declare interface Obj { + [key: string]: T +} + +type Extend = T & Obj; diff --git a/server/models/Database.ts b/server/models/Database.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a8e50fef57d04585705085815582d43cedc13bd --- /dev/null +++ b/server/models/Database.ts @@ -0,0 +1,94 @@ +import { MongoClient, Db, Collection } from 'mongodb'; +import { config } from '../lib/config'; + + + +/** + * Database wrapper with collection access directly on the db object. + */ +export class Database { + private MONGO_URI: string; + private MONGO_DBNAME: string; + constructor(opts: { MONGO_URI?: string, MONGO_DBNAME?: string } = {}) { + this.MONGO_URI = opts.MONGO_URI || config.mongoUrl; + this.MONGO_DBNAME = opts.MONGO_DBNAME || config.mongoDbName; + } + private __client: MongoClient | null = null; + private __db: Db | null = null; + protected static __collectionNames = [ + 'docs', + ]; + docs: Collection; + private __promiseConnect: Promise | null = null; + + + get isReady(): boolean { + return this.__db !== null; + } + + + private attach() { + for (const c of Database.__collectionNames) { + this[c] = this.__db!.collection(c); + } + } + + collection(name: string): Collection { + return this.__db!.collection(name); + } + + command(command: Object): Promise { + return this.__db!.command(command); + } + + listShards(): Promise { + return this.__db!.admin().command({ listShards: 1 }); + } + + database(dbName?: string): Db | null { + if (!dbName) { + return this.__db; + } + return (this.__client) + ? this.__client.db(dbName) + : null + ; + } + + connect(): Promise { + if (!this.__promiseConnect) { + this.__promiseConnect = MongoClient.connect(this.MONGO_URI, { + useNewUrlParser: true, + }).then((client) => { + this.__client = client; + this.__db = this.__client.db(this.MONGO_DBNAME); + this.attach(); + return true; + }).catch((err) => { + console.error("Connection error", err); + process.exit(1); + return false; + }); + } + + return this.__promiseConnect; + } + + onConnect(handler: () => void) { + this.connect().then(handler); + } + + async close() { + if (this.__client) { + await this.__client.close(); + } + } +} + + + + +const db = new Database(); + +export default db; + diff --git a/server/models/Doc.ts b/server/models/Doc.ts new file mode 100644 index 0000000000000000000000000000000000000000..e09a2558c8dbd5c5b8ff179a44ecc3b055eae767 --- /dev/null +++ b/server/models/Doc.ts @@ -0,0 +1,146 @@ +import { ObjectID } from 'mongodb'; +import { MongoObject } from './MongoObject'; +import { Utils } from '../lib/Utils'; +import { config } from '../lib/config'; +import db from './Database'; + + +export enum ModelId { + 'distil-gpt2' = 'distil-gpt2', + 'arxiv-nlp' = 'arxiv-nlp', + 'gpt2-large' = 'gpt2-large', + 'gpt2-xl' = 'gpt2-xl', + 'xlnet' = 'xlnet', + 'gpt' = 'gpt', + 'ctrl' = 'ctrl', + 'pplm' = 'pplm', +} +export const ALL_MODELS = Object.values(ModelId) as string[]; + +namespace Doc { + interface InsertDeltaOperation { + insert: string; + attributes?: { + link?: string; + bold?: true; + }; + } + export interface Contents { + ops: InsertDeltaOperation[]; + } +} +class Doc extends MongoObject { + protected static __type = Doc; + protected static __collectionName: string = 'docs'; + protected static __idField: string = 'shortId'; + + _id: ObjectID; + shortId: string; + longId?: string; + model: ModelId; + contents: Doc.Contents; + /** + * Optional reference to another doc's `shortId`. + */ + clonedFrom?: string; + title?: string; + /** + * Whether to display it on the front page. + */ + featured?: boolean; + + static fromObject(o: Partial): Doc { + return Object.assign(new Doc(), o); + } + + /** + * Initialize a new doc with random ids. + */ + static seed(model: ModelId): Doc { + return this.fromObject({ + shortId: Utils.randomStr(10), + longId: Utils.randomStr(24), + model + }); + } + + /** + * Insert a new doc duplicated from an existing one. + */ + async duplicate(): Promise { + const newdoc = Doc.seed(this.model); + newdoc.contents = this.contents; + newdoc.title = this.title; + newdoc.clonedFrom = this.shortId; + await db.docs.insertOne(newdoc); + return newdoc; + } + + /** + * This is displayed next to the doc title in a `` tag. + */ + get modelFamily(): string { + if ([ + 'arxiv-nlp', + 'gpt2-large', + 'gpt2-xl', + ].includes(this.model)) { + return 'gpt2'; + } + if (this.model === 'ctrl') { + return `salesforce/ctrl`; + } + if (this.model === 'pplm') { + return `uber/pplm`; + } + return this.model; + } + get hasCustomLogo(): boolean { + return ['ctrl', 'pplm'].includes(this.model); + } + get modelInfoLink(): string { + if (this.model === 'pplm') { + return `https://github.com/huggingface/transformers/tree/master/examples/pplm`; + } + return `https://github.com/huggingface/transformers`; + } + get shareUrl(): string { + return `${config.transformerAutocompleteUrl}/share/${this.shortId}`; + } + get editUrl(): string { + return `${config.transformerAutocompleteUrl}/doc/${this.model}/${this.longId}/edit`; + } + + /** + * Construct a (safe) HTML representation of the quill delta content format. + */ + get getHTML(): string | undefined { + if (! this.contents) { + return ; + } + return this.contents.ops.map(op => { + const escaped = op.insert?.toString().replace(/${escaped}`; + } + return escaped; + }).join(""); + } + + /** + * Construct a (safe) JSON representation of the doc.contents. + */ + get contentsJson(): string | undefined { + if (! this.contents) { + return ; + } + return JSON.stringify(this.contents).replace(/this.constructor).__wlistJsonAttrs); + } + + toJson(): string { + return JSON.stringify(this.toJsonRepr()); + } + + + /// Find family of methods + + static async findOne(id: string | ObjectID | mongodb.FilterQuery, options?: mongodb.FindOneOptions): Promise { + const q = (typeof id === 'string' || id instanceof ObjectID) + ? { [this.__idField]: id } + : id; + + const o = await db.collection(this.__collectionName).findOne(q, options); + if (o) { + return ObjectFactory.create(this.__type, o); + } + return null; + } + + static async findOneAndUpdate(filter: mongodb.FilterQuery, update: Object, options?: mongodb.FindOneAndReplaceOption): Promise { + const o = await db.collection(this.__collectionName).findOneAndUpdate(filter, update, options); + if (o && o.value) { + return ObjectFactory.create(this.__type, o.value); + } + return null; + } + + static find(query: mongodb.FilterQuery = {}, options?: mongodb.FindOneOptions): HfCursor { + const cursor = db.collection(this.__collectionName).find(query, options); + return HfCursor.cast(cursor, this.__type); + } +} + + + +export class HfCursor extends Cursor { + protected __type; + + static cast(cursor: Cursor, type: any): HfCursor { + // “The use of __proto__ is controversial, and has been discouraged.” + // see stackoverflow.com/a/32186367 + // see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto + (cursor).__proto__ = HfCursor.prototype; + (cursor).__type = type; + return cursor as HfCursor; + } + + toArray(): Promise { + return super.toArray().then((objs) => { + return objs.map((o) => { + return ObjectFactory.create(this.__type, o); + }); + }); + } + + forEach(__iterator: mongodb.IteratorCallback, __callback: mongodb.EndCallback = () => {}) { + super.forEach((o) => { + const newObject = ObjectFactory.create(this.__type, o); + __iterator(newObject); + }, __callback); + } + + + + + on(event: string, listener: (...args) => void): this { + if (event === 'data') { + super.on('data', (o) => { + const newObject = ObjectFactory.create(this.__type, o); + listener(newObject); + }); + } + else { + super.on(event, listener); + } + return this; + } + + once(event: string, listener: (...args) => void): this { + if (event === 'data') { + super.once('data', (o) => { + const newObject = ObjectFactory.create(this.__type, o); + listener(newObject); + }); + } + else { + super.once(event, listener); + } + return this; + } + + // + // Below: cursor methods are only here to make Typescript + // know that they return the HfCursor object itself. + // (We have checked that the mongo driver does the right thing underneath) + // + + limit(value: number): HfCursor { + return super.limit(value) as HfCursor; + } + + skip(value: number): HfCursor { + return super.skip(value) as HfCursor; + } + + sort(keyOrList: string | Object[] | Object, direction?: number): HfCursor { + return super.sort(keyOrList, direction) as HfCursor; + } + + stream(options?: { transform?: Function }): HfCursor { + return super.stream(options) as HfCursor; + } +} diff --git a/server/package-lock.json b/server/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..d15504409d3e4a99acbd35d4c5721c4bf43f6e9d --- /dev/null +++ b/server/package-lock.json @@ -0,0 +1,629 @@ +{ + "name": "foobar-server", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bson": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.0.tgz", + "integrity": "sha512-pq/rqJwJWkbS10crsG5bgnrisL8pML79KlMKQMoQwLUjlPAkrUHMvHJ3oGwE7WHR61Lv/nadMwXVAD2b+fpD8Q==", + "requires": { + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/events": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", + "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==" + }, + "@types/express": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.2.tgz", + "integrity": "sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz", + "integrity": "sha512-Xnub7w57uvcBqFdIGoRg1KhNOeEj0vB6ykUM7uFWyxvbdE89GFyqgmUcanAriMr4YOxNFZBAWkfcWIb4WBPt3g==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/hbs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/hbs/-/hbs-4.0.1.tgz", + "integrity": "sha512-kbgeYPLGOG8LQhqNlAvMm7vMz6Iu3IaXDEufpkEYT/viTko1ZlIrj+b6lvo4cAIDRkh6eg+JdKVZkriDGTisEw==", + "requires": { + "handlebars": "^4.1.0" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/mongodb": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.2.tgz", + "integrity": "sha512-gdaK55B+qLI2QgeE8xXP0gJcVKSYKN20CY2CwvkMzS/LIIEIiQgQM7sIbjoRk1KOGldYjGf8PctXvXqej0qyqg==", + "requires": { + "@types/bson": "*", + "@types/events": "*", + "@types/node": "*" + } + }, + "@types/node": { + "version": "12.12.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.14.tgz", + "integrity": "sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bson": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.1.tgz", + "integrity": "sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg==" + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "colors": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.3.tgz", + "integrity": "sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==" + }, + "commander": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", + "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "optional": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "foreachasync": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/foreachasync/-/foreachasync-3.0.0.tgz", + "integrity": "sha1-VQKYfchxS+M5IJfzLgBxyd7gfPY=" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "handlebars": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz", + "integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + }, + "hbs": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/hbs/-/hbs-4.0.5.tgz", + "integrity": "sha512-lzyXY0HalmVqHyhty05cLoPqxY51gNnCI6cMlXiPbrIKvB3BTJh3waQ9de86x25jX9QhU/tkAojsOO4KB6vPfg==", + "requires": { + "handlebars": "4.3.3", + "walk": "2.3.14" + }, + "dependencies": { + "handlebars": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.3.tgz", + "integrity": "sha512-VupOxR91xcGojfINrzMqrvlyYbBs39sXIrWa7YdaQWeBudOlvKEGvCczMfJPgnuwHE/zyH1M6J+IUP6cgDVyxg==", + "requires": { + "neo-async": "^2.6.0", + "optimist": "^0.6.1", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4" + } + } + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + }, + "mongodb": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.10.tgz", + "integrity": "sha512-Uml42GeFxhTGQVml1XQ4cD0o/rp7J2ROy0fdYUcVitoE7vFqEhKH4TYVqRDpQr/bXtCJVxJdNQC1ntRxNREkPQ==", + "requires": { + "mongodb-core": "3.1.9", + "safe-buffer": "^5.1.2" + } + }, + "mongodb-core": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.9.tgz", + "integrity": "sha512-MJpciDABXMchrZphh3vMcqu8hkNf/Mi+Gk6btOimVg1XMxLXh87j6FAvRm+KmwD1A9fpu3qRQYcbQe4egj23og==", + "requires": { + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "neo-async": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", + "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "uglify-js": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", + "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "optional": true, + "requires": { + "commander": "~2.20.0", + "source-map": "~0.6.1" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "walk": { + "version": "2.3.14", + "resolved": "https://registry.npmjs.org/walk/-/walk-2.3.14.tgz", + "integrity": "sha512-5skcWAUmySj6hkBdH6B6+3ddMjVQYH5Qy9QGbPmN8kVmLteXk+yVXg+yfk1nbX30EYakahLrr8iPcCxJQSCBeg==", + "requires": { + "foreachasync": "^3.0.0" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000000000000000000000000000000000000..289beaf09d103f0a131bca9af729a093b16bc5b3 --- /dev/null +++ b/server/package.json @@ -0,0 +1,25 @@ +{ + "name": "foobar-server", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/express": "^4.17.2", + "@types/hbs": "^4.0.1", + "@types/mongodb": "^3.1.2", + "@types/node": "^12.12.14", + "body-parser": "^1.19.0", + "colors": "^1.3.3", + "express": "^4.17.1", + "hbs": "^4.0.5", + "mongodb": "^3.1.10" + }, + "devDependencies": { + "typescript": "^4.0.3" + } +} diff --git a/server/server.ts b/server/server.ts new file mode 100644 index 0000000000000000000000000000000000000000..135886d14d8473ea180509124a067fac8ee96b12 --- /dev/null +++ b/server/server.ts @@ -0,0 +1,178 @@ +require('./lib/Extensions').install(); +import * as express from 'express'; +import * as bodyParser from 'body-parser'; +import __rootDir from './lib/RootDirFinder'; +import { c } from './lib/Log'; +import db from './models/Database'; +import Doc, { ALL_MODELS, ModelId } from './models/Doc'; +import { config } from './lib/config'; +import { hbs } from './lib/Hbs'; +const app = express(); +const PORT = process.env.PORT || config.appPort; +const __frontDir = __rootDir+`/front`; + +// Express setup +app.set('trust proxy', 'loopback'); +app.disable('x-powered-by'); +app.use(bodyParser.json()); +app.set('views', `${__frontDir}/views`); +app.set('view engine', 'hbs'); +app.engine('hbs', hbs.__express); + +const staticHandler = express.static(__frontDir); +const staticHandlerNoHtml: express.RequestHandler = function(req, res, next) { + if (req.path === '/' || req.path.endsWith('.html') || req.path.endsWith('.hbs')) { + return next(); + } else { + return staticHandler(req, res, next); + } +}; +app.get('/favicon.ico', (req, res) => res.sendFile(`${__frontDir}/favicon.ico`)); +app.get('/robots.txt', (req, res) => res.sendFile(`${__frontDir}/robots.txt`)); +app.use('/front', staticHandlerNoHtml); +/// ^^ in production, those aren't used b/c we serve static files from nginx. + +/** + * Routes(html) + */ + +app.get('/', async function(req, res) { + const docs = await db.docs + .find({ featured: true }) + .sort({ _id: -1 }) + .limit(10) + .project({ shortId: true, title: true }) + .toArray() + ; + /// ^^ cache this query at some point for performance. + + res.render('landing', { + body_classes: `landing`, + docs + }); +}); + +app.get('/model/:model', function(req, res) { + const model = req.params.model; + if (! ['arxiv-nlp', 'distil-gpt2', 'ctrl', 'pplm'].includes(model)) { + return res.sendStatus(404); + } + res.render('model', { + body_classes: `model`, + model, + meta: { + /// ^^ do NOT call this key `layout`. + title: (model === 'distil-gpt2') ? `🐎 DistilGPT-2 model checkpoint` + : (model === 'ctrl') ? `☁️ Salesforce Research CTRL` + : (model === 'pplm') ? `🚕 Uber AI Plug and Play Language Model` + : `🤓 ArXiv NLP model checkpoint`, + thumbnail: (model === 'distil-gpt2') ? 'thumbnail-large-distilgpt2' + : (model === 'pplm') ? `thumbnail-large-pplm` + : undefined, + path: req.path, + }, + }); +}); + + +app.get('/doc/:model', function(req, res) { + const model = req.params.model; + if (! ALL_MODELS.includes(model)) { + return res.sendStatus(404); + } + + const doc = Doc.seed(model as ModelId); + /// ^^ new document. It doesn't exist in the db yet, + /// will only be stored if user presses save. + + res.render('index', { + body_classes: `app`, + editable: true, + doc, + }); +}); + +app.get('/doc/:model/:id/edit', async function(req, res) { + const model = req.params.model; + if (! ALL_MODELS.includes(model)) { + return res.sendStatus(404); + } + + const doc = await Doc.findOne({ + longId: req.params.id, + }); + if (!doc) { + return res.sendStatus(404); + } + /// Existing document, accessed through its private edit url. + + res.render('index', { + body_classes: `app`, + editable: true, + doc, + }); +}); + +app.get('/share/:shortId', async function(req, res) { + const doc = await Doc.findOne(req.params.shortId); + if (!doc) { + return res.sendStatus(404); + } + /// Existing document, accessed through its public share url. + /// CAUTION: Make sure we don't expose the edit url! + delete doc.longId; + + res.render('index', { + body_classes: `app`, + editable: false, + doc, + }); +}); + + +/** + * Routes(ajax) + */ + +app.post('/edit/:model/:longId/:shortId', async function(req, res) { + const query = { + shortId: req.params.shortId, + longId: req.params.longId, + model: req.params.model, + }; + c.debug(`––`); + c.log(`Attempting to save doc`, query); + + const result = await db.docs.updateOne( + query, + { $set: req.body }, + { upsert: true } + ); + c.log(result.result); + + res.sendStatus(200); +}); + +app.post('/duplicate/:shortId', async function(req, res) { + const doc = await Doc.findOne(req.params.shortId); + if (!doc) { + return res.sendStatus(404); + } + + c.debug(`––`); + c.log(`Duplicating doc`, doc.shortId); + + const newdoc = await doc.duplicate(); + res.send(newdoc.editUrl); +}); + + +// Start engine. + +(async () => { + await db.connect(); + app.listen(PORT, () => { + c.debug(`Running on http://localhost:${PORT}`); + }); +})(); + diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..97608337c1e13844aaf5feba9407fe4f5978c4f6 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2018", + "module": "commonjs", + "outDir": "dist/", + "sourceMap": true, + "strictNullChecks": true, + "strictBindCallApply": true, + "lib": ["es6", "es2016", "es2017", "es2018", "esnext"], + } +} \ No newline at end of file