[
  {
    "path": ".dockerignore",
    "content": "ui/node_modules"
  },
  {
    "path": ".gitignore",
    "content": "### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore\n\n# -*- mode: gitignore; -*-\n*~\n\\#*\\#\n/.emacs.desktop\n/.emacs.desktop.lock\n*.elc\nauto-save-list\ntramp\n.\\#*\n\n# Org-mode\n.org-id-locations\n*_archive\n\n# flymake-mode\n*_flymake.*\n\n# eshell files\n/eshell/history\n/eshell/lastdir\n\n# elpa packages\n/elpa/\n\n# reftex files\n*.rel\n\n# AUCTeX auto folder\n/auto/\n\n# cask packages\n.cask/\ndist/\n\n# Flycheck\nflycheck_*.el\n\n# server auth directory\n/server/\n\n# projectiles files\n.projectile\n\n# directory configuration\n.dir-locals.el\n\n# network security\n/network-security.data\n\n\n\n### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Python.gitignore\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n/data/data/\n\n.DS_Store\n*/.DS_Store\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3.7\n\nRUN curl -sL https://deb.nodesource.com/setup_12.x | bash -\n\nRUN apt-get update && apt-get install -y\\\n  build-essential libpq-dev nodejs nginx-extras\n\nENV NODE_PATH /usr/lib/node_modules\n\nRUN pip install uwsgi\n\nWORKDIR /usr/src/app\nCOPY requirements.txt ./\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY ui/package.json ./ui/\n\nWORKDIR /usr/src/app/ui\nRUN npm install\n\n\nCOPY . /usr/src/app\n\nENV NODE_ENV production\nRUN npm run build\nWORKDIR /usr/src/app\nCOPY nginx.conf /etc/nginx/\nCOPY nginx-server.conf /etc/nginx/sites-enabled/default\nEXPOSE 80\nCMD './start.sh'\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Senghoo Kim\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Word AI\n\n一款使用自然语言处理辅助背单词的程序。\n\n通过导入TMX翻译语料库，进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。\n\n*PS: 因为版权原因，不提供词典文件和TMX语料文件。需要自行下载导入。业余项目，分享出来供学习和交流，提供有限的技术支持，如有疑问欢迎Issue。*\n\n## 功能特色\n\n* 根据中文填写英文句子中的单词。\n* 根据语料库自动生成题目，*拒绝重复*\n* 根据艾宾浩斯记忆曲线重复出现需要复习的单词。\n* 对于错误单词自动弹出词典，进行学习。\n* 支持自定义词库。\n* B/S架构，兼容移动端和PC端。\n\n\n## 个人对背单词的一点看法\n\n1. 背单词不能只针对独立的单词，需要放到语境中学习。\n2. 单词需要自己完整的拼写出来，不能是只进行选择题。不然实际应用中容易出现拼写错误的情况。\n3. 单词记忆时要关注时态、语态。\n4. 学习的内容不能重复，也就是每次学习的上下文要发生改变，不能是固定的句子，学习固定的单词。这种情况下容易脱离了当前上下文，还是想不起单词。\n\n## Examples\n\n### 单词学习\n\n如遇到不会的单词，可以长按空格查看答案。\n\n![练习](doc/word.gif?raw=true)\n\n对于错误单词自动弹出词典以供学习。\n\n![错误单词](doc/error.gif?raw=true)\n\n### 自定义单词列表\n\n文本框输入单词列表。一行一个，自动提示词典查询结果，以检查是否添加正确。\n\n![单词列表](doc/newlist.gif?raw=true)\n\n### 学习统计\n\n提供简单的统计分析功能。\n\n![单词列表](doc/analysis.gif?raw=true)\n\n## 管理命令\n\n因为是简单自用为目的设计的软件，没有单独做管理页面。系统管理通过命令行进行。\n\n添加用户\n\n```\n# 普通用户\npython manage.py useradd <用户名> <口令>\n# 管理员\npython manage.py useradd <用户名> <口令> admin\n```\n\n\n导入字典\n\n```\npython manage.py sync_dict\n```\n\n导入例句\n\n```\npython manage.py sync_sentence\n```\n\n分析词典\n\n```\npython manage.py wordlist # 标注 单词星级\npython manage.py dict_parse # 拆分中英文解释\n```\n"
  },
  {
    "path": "app.py",
    "content": "from wordai.api import app\nfrom config import api_env, flask_config\n\napp.config.update(flask_config())\n\ndef run_dev():\n    app.run(**api_env())\n"
  },
  {
    "path": "commands/__init__.py",
    "content": "import string\nfrom data import load_dict, load_sentence\nfrom data.dict import Description\nfrom wordai.models import Word, Sentence, User, WordList, DictDescInfo\n\ndef dict_to_mongo():\n    items = load_dict()\n    for word, item in items.items():\n        if Word.objects(word=word).count() == 0:\n            dbitem = Word(**item)\n            dbitem.save()\n\n\ndef sentence_to_mongo(*typ):\n    items = load_sentence(*typ)\n    print(items)\n    for k, v in items.items():\n        _sentence_to_mongo(k, v)\n\ndef _sentence_to_mongo(typ, items):\n    import nltk\n    from nltk.corpus import wordnet\n\n    def wordnet_pos(tag):\n        if tag.startswith('J'):\n            return wordnet.ADJ\n        elif tag.startswith('V'):\n            return wordnet.VERB\n        elif tag.startswith('N'):\n            return wordnet.NOUN\n        elif tag.startswith('R'):\n            return wordnet.ADV\n        else:\n            return wordnet.NOUN\n\n    # nltk.download('punkt')\n    nltk.download('averaged_perceptron_tagger')\n    nltk.download('stopwords')\n    nltk.download('wordnet')\n    nltk.download('punkt')\n    stop_words = set(nltk.corpus.stopwords.words('english'))\n    stemmer = nltk.stem.WordNetLemmatizer()\n    sentences = []\n    for trans in items:\n        eng, chn = trans.getsource(), trans.gettarget()\n        tokens = nltk.word_tokenize(eng)\n        pos_tag = [pos[1] for pos in nltk.pos_tag(tokens)]\n        roots = [stemmer.lemmatize(word, wordnet_pos(pos_tag[idx])) for idx, word in enumerate(tokens)]\n        cleanword = [token for token in roots if token.isalpha() and token not in stop_words and len(token) >= 3]\n        # remove duplicates\n        clean_word = list(dict.fromkeys(cleanword))\n        if len(clean_word) > 0:\n            score = Word.search_words(*clean_word).sum('star') / len(clean_word)\n        else:\n            score = -1\n        sentence = Sentence(eng=eng, chn=chn, words=tokens, pos_tag=pos_tag, roots=roots, score=score, typ=typ)\n        sentences.append(sentence)\n        if len(sentences) > 50:\n            Sentence.objects.insert(sentences)\n            sentences = []\n\n\ndef new_user(username, passwd, role='user', *args):\n    u = User(username=username, password=passwd, role=role)\n    u.save()\n\ndef dict_star_word_list():\n    for star in range(1, 6, 1):\n        words = [word.word for word in Word.objects(star=star).only('word')]\n        wl = WordList(name=\"word star {}\".format(star), words=words)\n        wl.save()\n\ndef dict_parse():\n    for word in Word.objects:\n        for des in word.descriptions:\n            res = Description(des.description)\n            des.seq = res.seq\n            des.cn = res.cn\n            des.en = res.en\n            des.speech = res.speech\n            des.infos = [DictDescInfo(**info) for info in res.info]\n        word.save()\n\ndef run(method, *args):\n    if method == \"sync_dict\":\n        dict_to_mongo()\n    elif method == \"sync_sentence\":\n        sentence_to_mongo(*args)\n    elif method == \"useradd\":\n        new_user(*args)\n    elif method == \"wordlist\":\n        dict_star_word_list(*args)\n    elif method == \"dict_parse\":\n        dict_parse(*args)\n    else:\n        print(\"unknown command {0}\".format(method))\n"
  },
  {
    "path": "config/__init__.py",
    "content": "import os\n\ndef c(name, default=None):\n    return os.environ.get(name.upper(), default)\n\ndef mongo_config():\n    return {\n        'host': c('mongo_host', \"127.0.0.1\"),\n        'port': int(c('mongo_port', \"27017\")),\n        'db': c('mongo_db', \"wordai\"),\n        'username': c('mongo_username', \"root\"),\n        'password': c('mongo_password', \"\"),\n    }\n\ndef api_env():\n    return {\n        'host': c('host', '127.0.0.1'),\n        'port': int(c('port', 8000)),\n        'debug': c('debug', \"true\") == \"true\"\n    }\n\ndef flask_config():\n    return {\n        'JWT_SECRET_KEY': c('secret', '__SOME_SECRET_KEYS_HERE__')\n    }\n"
  },
  {
    "path": "config/wsgi.ini",
    "content": "[uwsgi]\nmodule = app:app\nmaster = true\nprocesses = 3\n\nsocket = /var/run/wordai.sock\nlogto = /var/log/wordai.log\nchmod-socket = 660\nvacuum = true"
  },
  {
    "path": "data/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport os\nimport json\nfrom itertools import chain\nfrom translate.storage.tmx import tmxfile\n\nDATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), \"data\")\n\ndef data_file(filename):\n    return os.path.join(DATA_DIR, filename)\n\ndef load_dict():\n    with open(data_file(\"dict.json\"), \"r\") as f:\n        return json.load(f)\n\n\ndef load_sentence(*typ):\n    files = {\n        'talks': ['日常口语_20190906111009_1.tmx', '日常口语_20190906111009_2.tmx', '日常口语_20190906111009_3.tmx'],\n        'dictexams': ['词典例句汇集1.tmx', '词典例句汇集3.tmx', '词典例句汇集5.tmx', '词典例句汇集7.tmx',\n                      '词典例句汇集2.tmx', '词典例句汇集4.tmx', '词典例句汇集6.tmx', '词典例句汇集8.tmx']\n    }\n    iters = {}\n    for t, fs in files.items():\n        if len(typ) == 0 or t in typ:\n            type_iterns = []\n            for fname in fs:\n                with open(data_file(fname), 'rb') as fin:\n                    tmx = tmxfile(fin, 'en', 'cn')\n                    type_iterns.append(tmx.unit_iter())\n            iters[t] = chain(*type_iterns)\n    return iters\n"
  },
  {
    "path": "data/dict.py",
    "content": "# -*- coding: utf-8 -*-\nimport string\nimport re\n\nclass Description(object):\n    def __init__(self, des):\n        self.des = des\n        self.idx = 0\n        self.length = len(des)\n        self.info = []\n        if not self.read_seq():\n            self.seq = 0\n            self.en = self.des[self.idx:]\n            self.cn = self.des[self.idx:]\n            self.speech = 'OTHER'\n            return\n\n        if not self.read_word_speech():\n            self.en = self.des[self.idx:]\n            self.cn = self.des[self.idx:]\n            self.speech = 'OTHER'\n            return\n        if not self.read_cn_and_en():\n            self.en = self.des[self.idx:]\n            self.cn = self.des[self.idx:]\n            return\n        self.read_info()\n\n    def read_seq(self):\n        res = ''\n        while(self.idx < self.length):\n            if self.des[self.idx] <= '9' and self.des[self.idx] >= '0':\n                res += self.des[self.idx]\n                self.idx += 1\n            else:\n                break\n        if self.des[self.idx] == '.':\n            self.idx += 1\n        if len(res) > 0:\n            self.seq = int(res.strip())\n            return True\n        return False\n\n    def read_word_speech(self):\n        res = ''\n        while(self.idx < self.length):\n            if self.des[self.idx] in string.ascii_uppercase+\"- ;\":\n                res += self.des[self.idx]\n                self.idx += 1\n            else:\n                break\n        res = res.strip()\n        if self.des[self.idx] == '\\t':\n            self.idx += 1\n        if len(res) > 0:\n            self.speech = res\n            return True\n        return False\n\n\n    def read_cn_and_en(self):\n        end = self.des.find('【', self.idx)\n        if end == -1:\n            end = len(self.des)\n        idx = end\n        while(idx >= self.idx):\n            idx -= 1\n            if self.des[idx] not in string.printable:\n                break\n        if idx == self.idx:\n            self.cn = ''\n            self.en = self.des[idx:end]\n        elif idx == end-1:\n            self.cn = self.des[idx:end]\n            self.en = self.des[idx:end]\n        else:\n            self.cn = self.des[self.idx:idx+1]\n            self.en = self.des[idx+1:end]\n        self.idx = end\n        return True\n\n    def read_info(self):\n        text = self.des[self.idx:]\n        res = re.findall(r'【([^【】]+)】：([^【】]+)', text, re.M)\n        for item in res:\n            self.info.append({\n                'name': item[0],\n                'value': item[1],\n            })\n"
  },
  {
    "path": "jsondict/anki.py",
    "content": "import sys\nimport csv\nfrom search import search_string, description\n\nres = []\n\ndef cloze_word(sentense, word, level=\"1\"):\n    return sentense.lower().replace(word, \"{{c\"+level+\"::\"+word+\"}}\")\n\n\nwith open(sys.argv[1], 'r') as f:\n    contents = f.read()\n    items = contents.split(\"\\n\\n\")\n    for item in items:\n        lines = item.split(\"\\n\")\n        word = lines[0]\n        exp = lines[1]\n        eg = lines[2:]\n        dct = search_string(word)\n        exp2 = cloze_word(description(word), word, \"1\")\n        for x in [{\"sentence\": cloze_word(x, word, \"1\"),\"exp2\": exp2, \"exp\": exp, \"dict\": dct  } for x in eg]:\n            res.append(x)\n\n\nwith open('{0}.csv'.format(sys.argv[1]), 'w') as csv_file:\n    fieldnames = ['sentence','exp2', 'exp', 'dict']\n    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)\n    for row in res:\n        print(row['sentence'])\n    writer.writerows(res)\n"
  },
  {
    "path": "jsondict/search.py",
    "content": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\nimport sys\nimport json\n\n\nwith open('dict.json', 'r') as f:\n    dictionary = json.load(f)\n\ndef search(word):\n    try:\n        return dictionary[word]\n    except:\n        return None\n\ndef format(item):\n    res = \"{word}\\tStar:{star}\\n\".format(**item)\n    for desc in item['descriptions']:\n        res += \"\\t{description}\\n\".format(**desc)\n        for ex in desc['examples']:\n            res += \"\\t\\tEN:\\t{en}\\n\\t\\tCN:\\t{cn}\\n\".format(**ex)\n        res +=\"\\n\"\n    return res\n\ndef description(word):\n    item = search(word)\n    if item is None:\n        return \"\"\n    return \"\\n\".join([x['description'] for x in item['descriptions']])\n\ndef search_string(word):\n    res = search(word)\n    if res is None:\n        return \"\"\n    return format(res)\n\nif __name__ == '__main__':\n    res = search(sys.argv[1])\n    if res is not None:\n        print(format(res))\n"
  },
  {
    "path": "manage.py",
    "content": "import os\nimport sys\n\nROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, ROOT_DIR)\n\nimport commands\nimport app\n\n\ndef command(method, *args):\n    if method == 'task':\n        commands.run(*args)\n    elif method == 'dev_server':\n        app.run_dev()\n    else:\n        print(\"unknown command {0}\".format(method))\n\n\ndef usage():\n    print(\"{0} task\".format( sys.argv[0]))\n\n\nif __name__ == '__main__':\n    if len(sys.argv) > 1:\n        command(*sys.argv[1:])\n    else:\n        usage()\n"
  },
  {
    "path": "nginx-server.conf",
    "content": "server {\n    listen 80;\n    server_name 0.0.0.0;\n\n    location /api/ {\n        include uwsgi_params;\n        uwsgi_pass unix:/var/run/wordai.sock;\n    }\n    location / {\n        root /usr/src/app/ui/dist/;\n    }\n}"
  },
  {
    "path": "nginx.conf",
    "content": "user root;\nworker_processes auto;\npid /run/nginx.pid;\ninclude /etc/nginx/modules-enabled/*.conf;\n\nevents {\n\tworker_connections 768;\n}\n\nhttp {\n\n\n\tsendfile on;\n\ttcp_nopush on;\n\ttcp_nodelay on;\n\tkeepalive_timeout 65;\n\ttypes_hash_max_size 2048;\n\n\tinclude /etc/nginx/mime.types;\n\tdefault_type application/octet-stream;\n\n\n\taccess_log /var/log/nginx/access.log;\n\terror_log /var/log/nginx/error.log;\n\n\tgzip on;\n\n\tinclude /etc/nginx/conf.d/*.conf;\n\tinclude /etc/nginx/sites-enabled/*;\n}\n\n\n"
  },
  {
    "path": "requirements.txt",
    "content": "mongoengine\nmongoengine-goodjson\nlxml\ntranslate-toolkit\nnltk\nflask\nflask-restful\nflask-jwt-extended\nflask-cors\nbcrypt\njsonschema\n"
  },
  {
    "path": "start.sh",
    "content": "#!/bin/bash\n\nservice nginx start\nuwsgi --ini config/wsgi.ini \n\n"
  },
  {
    "path": "ui/.babelrc",
    "content": "{\n  \"presets\": [\n    [\"env\", {\n      \"modules\": false,\n      \"targets\": {\n        \"browsers\": [\"> 1%\", \"last 2 versions\", \"not ie <= 8\"]\n      }\n    }],\n    \"stage-2\"\n  ],\n  \"plugins\": [\"transform-vue-jsx\", \"transform-runtime\"]\n}\n"
  },
  {
    "path": "ui/.editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": "ui/.eslintignore",
    "content": "/build/\n/config/\n/dist/\n/*.js\n"
  },
  {
    "path": "ui/.eslintrc.js",
    "content": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n  root: true,\n  parserOptions: {\n    parser: 'babel-eslint'\n  },\n  env: {\n    browser: true,\n  },\n  extends: [\n    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention\n    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.\n    'plugin:vue/essential', \n    // https://github.com/standard/standard/blob/master/docs/RULES-en.md\n    'standard'\n  ],\n  // required to lint *.vue files\n  plugins: [\n    'vue'\n  ],\n  // add your custom rules here\n  rules: {\n    // allow async-await\n    'generator-star-spacing': 'off',\n    // allow debugger during development\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'\n  }\n}\n"
  },
  {
    "path": "ui/.gitignore",
    "content": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n"
  },
  {
    "path": "ui/.postcssrc.js",
    "content": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    \"postcss-import\": {},\n    \"postcss-url\": {},\n    // to edit target browsers: use \"browserslist\" field in package.json\n    \"autoprefixer\": {}\n  }\n}\n"
  },
  {
    "path": "ui/README.md",
    "content": "# wordai\n\n> Word AI\n\n## Build Setup\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at localhost:8080\nnpm run dev\n\n# build for production with minification\nnpm run build\n\n# build for production and view the bundle analyzer report\nnpm run build --report\n```\n\nFor a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).\n"
  },
  {
    "path": "ui/build/build.js",
    "content": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = require('rimraf')\nconst path = require('path')\nconst chalk = require('chalk')\nconst webpack = require('webpack')\nconst config = require('../config')\nconst webpackConfig = require('./webpack.prod.conf')\n\nconst spinner = ora('building for production...')\nspinner.start()\n\nrm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {\n  if (err) throw err\n  webpack(webpackConfig, (err, stats) => {\n    spinner.stop()\n    if (err) throw err\n    process.stdout.write(stats.toString({\n      colors: true,\n      modules: false,\n      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.\n      chunks: false,\n      chunkModules: false\n    }) + '\\n\\n')\n\n    if (stats.hasErrors()) {\n      console.log(chalk.red('  Build failed with errors.\\n'))\n      process.exit(1)\n    }\n\n    console.log(chalk.cyan('  Build complete.\\n'))\n    console.log(chalk.yellow(\n      '  Tip: built files are meant to be served over an HTTP server.\\n' +\n      '  Opening index.html over file:// won\\'t work.\\n'\n    ))\n  })\n})\n"
  },
  {
    "path": "ui/build/check-versions.js",
    "content": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.json')\nconst shell = require('shelljs')\n\nfunction exec (cmd) {\n  return require('child_process').execSync(cmd).toString().trim()\n}\n\nconst versionRequirements = [\n  {\n    name: 'node',\n    currentVersion: semver.clean(process.version),\n    versionRequirement: packageConfig.engines.node\n  }\n]\n\nif (shell.which('npm')) {\n  versionRequirements.push({\n    name: 'npm',\n    currentVersion: exec('npm --version'),\n    versionRequirement: packageConfig.engines.npm\n  })\n}\n\nmodule.exports = function () {\n  const warnings = []\n\n  for (let i = 0; i < versionRequirements.length; i++) {\n    const mod = versionRequirements[i]\n\n    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {\n      warnings.push(mod.name + ': ' +\n        chalk.red(mod.currentVersion) + ' should be ' +\n        chalk.green(mod.versionRequirement)\n      )\n    }\n  }\n\n  if (warnings.length) {\n    console.log('')\n    console.log(chalk.yellow('To use this template, you must update following to modules:'))\n    console.log()\n\n    for (let i = 0; i < warnings.length; i++) {\n      const warning = warnings[i]\n      console.log('  ' + warning)\n    }\n\n    console.log()\n    process.exit(1)\n  }\n}\n"
  },
  {
    "path": "ui/build/utils.js",
    "content": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst ExtractTextPlugin = require('extract-text-webpack-plugin')\nconst packageConfig = require('../package.json')\n\nexports.assetsPath = function (_path) {\n  const assetsSubDirectory = process.env.NODE_ENV === 'production'\n    ? config.build.assetsSubDirectory\n    : config.dev.assetsSubDirectory\n\n  return path.posix.join(assetsSubDirectory, _path)\n}\n\nexports.cssLoaders = function (options) {\n  options = options || {}\n\n  const cssLoader = {\n    loader: 'css-loader',\n    options: {\n      sourceMap: options.sourceMap\n    }\n  }\n\n  const postcssLoader = {\n    loader: 'postcss-loader',\n    options: {\n      sourceMap: options.sourceMap\n    }\n  }\n\n  // generate loader string to be used with extract text plugin\n  function generateLoaders (loader, loaderOptions) {\n    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]\n\n    if (loader) {\n      loaders.push({\n        loader: loader + '-loader',\n        options: Object.assign({}, loaderOptions, {\n          sourceMap: options.sourceMap\n        })\n      })\n    }\n\n    // Extract CSS when that option is specified\n    // (which is the case during production build)\n    if (options.extract) {\n      return ExtractTextPlugin.extract({\n        use: loaders,\n        fallback: 'vue-style-loader'\n      })\n    } else {\n      return ['vue-style-loader'].concat(loaders)\n    }\n  }\n\n  // https://vue-loader.vuejs.org/en/configurations/extract-css.html\n  return {\n    css: generateLoaders(),\n    postcss: generateLoaders(),\n    less: generateLoaders('less'),\n    sass: generateLoaders('sass', { indentedSyntax: true }),\n    scss: generateLoaders('sass'),\n    stylus: generateLoaders('stylus'),\n    styl: generateLoaders('stylus')\n  }\n}\n\n// Generate loaders for standalone style files (outside of .vue)\nexports.styleLoaders = function (options) {\n  const output = []\n  const loaders = exports.cssLoaders(options)\n\n  for (const extension in loaders) {\n    const loader = loaders[extension]\n    output.push({\n      test: new RegExp('\\\\.' + extension + '$'),\n      use: loader\n    })\n  }\n\n  return output\n}\n\nexports.createNotifierCallback = () => {\n  const notifier = require('node-notifier')\n\n  return (severity, errors) => {\n    if (severity !== 'error') return\n\n    const error = errors[0]\n    const filename = error.file && error.file.split('!').pop()\n\n    notifier.notify({\n      title: packageConfig.name,\n      message: severity + ': ' + error.name,\n      subtitle: filename || '',\n      icon: path.join(__dirname, 'logo.png')\n    })\n  }\n}\n"
  },
  {
    "path": "ui/build/vue-loader.conf.js",
    "content": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_ENV === 'production'\nconst sourceMapEnabled = isProduction\n  ? config.build.productionSourceMap\n  : config.dev.cssSourceMap\n\nmodule.exports = {\n  loaders: utils.cssLoaders({\n    sourceMap: sourceMapEnabled,\n    extract: isProduction\n  }),\n  cssSourceMap: sourceMapEnabled,\n  cacheBusting: config.dev.cacheBusting,\n  transformToRequire: {\n    video: ['src', 'poster'],\n    source: 'src',\n    img: 'src',\n    image: 'xlink:href'\n  }\n}\n"
  },
  {
    "path": "ui/build/webpack.base.conf.js",
    "content": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vueLoaderConfig = require('./vue-loader.conf')\n\nfunction resolve (dir) {\n  return path.join(__dirname, '..', dir)\n}\n\nconst createLintingRule = () => ({\n  test: /\\.(js|vue)$/,\n  loader: 'eslint-loader',\n  enforce: 'pre',\n  include: [resolve('src'), resolve('test')],\n  options: {\n    formatter: require('eslint-friendly-formatter'),\n    emitWarning: !config.dev.showEslintErrorsInOverlay\n  }\n})\n\nmodule.exports = {\n  context: path.resolve(__dirname, '../'),\n  entry: {\n    app: './src/main.js'\n  },\n  output: {\n    path: config.build.assetsRoot,\n    filename: '[name].js',\n    publicPath: process.env.NODE_ENV === 'production'\n      ? config.build.assetsPublicPath\n      : config.dev.assetsPublicPath\n  },\n  resolve: {\n    extensions: ['.js', '.vue', '.json'],\n    alias: {\n      'vue$': 'vue/dist/vue.esm.js',\n      '@': resolve('src'),\n    }\n  },\n  module: {\n    rules: [\n      ...(config.dev.useEslint ? [createLintingRule()] : []),\n      {\n        test: /\\.vue$/,\n        loader: 'vue-loader',\n        options: vueLoaderConfig\n      },\n      {\n        test: /\\.s[a|c]ss$/,\n        loader: 'style!css!sass'\n      },\n      {\n        test: /\\.js$/,\n        loader: 'babel-loader',\n        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]\n      },\n      {\n        test: /\\.(png|jpe?g|gif|svg)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: utils.assetsPath('img/[name].[hash:7].[ext]')\n        }\n      },\n      {\n        test: /\\.(mp4|webm|ogg|mp3|wav|flac|aac)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: utils.assetsPath('media/[name].[hash:7].[ext]')\n        }\n      },\n      {\n        test: /\\.(woff2?|eot|ttf|otf)(\\?.*)?$/,\n        loader: 'url-loader',\n        options: {\n          limit: 10000,\n          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')\n        }\n      }\n    ]\n  },\n  node: {\n    // prevent webpack from injecting useless setImmediate polyfill because Vue\n    // source contains it (although only uses it if it's native).\n    setImmediate: false,\n    // prevent webpack from injecting mocks to Node native modules\n    // that does not make sense for the client\n    dgram: 'empty',\n    fs: 'empty',\n    net: 'empty',\n    tls: 'empty',\n    child_process: 'empty'\n  }\n}\n"
  },
  {
    "path": "ui/build/webpack.dev.conf.js",
    "content": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\nconst merge = require('webpack-merge')\nconst path = require('path')\nconst baseWebpackConfig = require('./webpack.base.conf')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')\nconst portfinder = require('portfinder')\n\nconst HOST = process.env.HOST\nconst PORT = process.env.PORT && Number(process.env.PORT)\n\nconst devWebpackConfig = merge(baseWebpackConfig, {\n  module: {\n    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })\n  },\n  // cheap-module-eval-source-map is faster for development\n  devtool: config.dev.devtool,\n\n  // these devServer options should be customized in /config/index.js\n  devServer: {\n    clientLogLevel: 'warning',\n    historyApiFallback: {\n      rewrites: [\n        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },\n      ],\n    },\n    hot: true,\n    contentBase: false, // since we use CopyWebpackPlugin.\n    compress: true,\n    host: HOST || config.dev.host,\n    port: PORT || config.dev.port,\n    open: config.dev.autoOpenBrowser,\n    overlay: config.dev.errorOverlay\n      ? { warnings: false, errors: true }\n      : false,\n    publicPath: config.dev.assetsPublicPath,\n    proxy: config.dev.proxyTable,\n    quiet: true, // necessary for FriendlyErrorsPlugin\n    watchOptions: {\n      poll: config.dev.poll,\n    }\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': require('../config/dev.env')\n    }),\n    new webpack.HotModuleReplacementPlugin(),\n    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.\n    new webpack.NoEmitOnErrorsPlugin(),\n    // https://github.com/ampedandwired/html-webpack-plugin\n    new HtmlWebpackPlugin({\n      filename: 'index.html',\n      template: 'index.html',\n      inject: true\n    }),\n    // copy custom static assets\n    new CopyWebpackPlugin([\n      {\n        from: path.resolve(__dirname, '../static'),\n        to: config.dev.assetsSubDirectory,\n        ignore: ['.*']\n      }\n    ])\n  ]\n})\n\nmodule.exports = new Promise((resolve, reject) => {\n  portfinder.basePort = process.env.PORT || config.dev.port\n  portfinder.getPort((err, port) => {\n    if (err) {\n      reject(err)\n    } else {\n      // publish the new Port, necessary for e2e tests\n      process.env.PORT = port\n      // add port to devServer config\n      devWebpackConfig.devServer.port = port\n\n      // Add FriendlyErrorsPlugin\n      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({\n        compilationSuccessInfo: {\n          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],\n        },\n        onErrors: config.dev.notifyOnErrors\n        ? utils.createNotifierCallback()\n        : undefined\n      }))\n\n      resolve(devWebpackConfig)\n    }\n  })\n})\n"
  },
  {
    "path": "ui/build/webpack.prod.conf.js",
    "content": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\nconst merge = require('webpack-merge')\nconst baseWebpackConfig = require('./webpack.base.conf')\nconst CopyWebpackPlugin = require('copy-webpack-plugin')\nconst HtmlWebpackPlugin = require('html-webpack-plugin')\nconst ExtractTextPlugin = require('extract-text-webpack-plugin')\nconst OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')\nconst UglifyJsPlugin = require('uglifyjs-webpack-plugin')\n\nconst env = require('../config/prod.env')\n\nconst webpackConfig = merge(baseWebpackConfig, {\n  module: {\n    rules: utils.styleLoaders({\n      sourceMap: config.build.productionSourceMap,\n      extract: true,\n      usePostCSS: true\n    })\n  },\n  devtool: config.build.productionSourceMap ? config.build.devtool : false,\n  output: {\n    path: config.build.assetsRoot,\n    filename: utils.assetsPath('js/[name].[chunkhash].js'),\n    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')\n  },\n  plugins: [\n    // http://vuejs.github.io/vue-loader/en/workflow/production.html\n    new webpack.DefinePlugin({\n      'process.env': env\n    }),\n    new UglifyJsPlugin({\n      uglifyOptions: {\n        compress: {\n          warnings: false\n        }\n      },\n      sourceMap: config.build.productionSourceMap,\n      parallel: true\n    }),\n    // extract css into its own file\n    new ExtractTextPlugin({\n      filename: utils.assetsPath('css/[name].[contenthash].css'),\n      // Setting the following option to `false` will not extract CSS from codesplit chunks.\n      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.\n      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, \n      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110\n      allChunks: true,\n    }),\n    // Compress extracted CSS. We are using this plugin so that possible\n    // duplicated CSS from different components can be deduped.\n    new OptimizeCSSPlugin({\n      cssProcessorOptions: config.build.productionSourceMap\n        ? { safe: true, map: { inline: false } }\n        : { safe: true }\n    }),\n    // generate dist index.html with correct asset hash for caching.\n    // you can customize output by editing /index.html\n    // see https://github.com/ampedandwired/html-webpack-plugin\n    new HtmlWebpackPlugin({\n      filename: config.build.index,\n      template: 'index.html',\n      inject: true,\n      minify: {\n        removeComments: true,\n        collapseWhitespace: true,\n        removeAttributeQuotes: true\n        // more options:\n        // https://github.com/kangax/html-minifier#options-quick-reference\n      },\n      // necessary to consistently work with multiple chunks via CommonsChunkPlugin\n      chunksSortMode: 'dependency'\n    }),\n    // keep module.id stable when vendor modules does not change\n    new webpack.HashedModuleIdsPlugin(),\n    // enable scope hoisting\n    new webpack.optimize.ModuleConcatenationPlugin(),\n    // split vendor js into its own file\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'vendor',\n      minChunks (module) {\n        // any required modules inside node_modules are extracted to vendor\n        return (\n          module.resource &&\n          /\\.js$/.test(module.resource) &&\n          module.resource.indexOf(\n            path.join(__dirname, '../node_modules')\n          ) === 0\n        )\n      }\n    }),\n    // extract webpack runtime and module manifest to its own file in order to\n    // prevent vendor hash from being updated whenever app bundle is updated\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'manifest',\n      minChunks: Infinity\n    }),\n    // This instance extracts shared chunks from code splitted chunks and bundles them\n    // in a separate chunk, similar to the vendor chunk\n    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk\n    new webpack.optimize.CommonsChunkPlugin({\n      name: 'app',\n      async: 'vendor-async',\n      children: true,\n      minChunks: 3\n    }),\n\n    // copy custom static assets\n    new CopyWebpackPlugin([\n      {\n        from: path.resolve(__dirname, '../static'),\n        to: config.build.assetsSubDirectory,\n        ignore: ['.*']\n      }\n    ])\n  ]\n})\n\nif (config.build.productionGzip) {\n  const CompressionWebpackPlugin = require('compression-webpack-plugin')\n\n  webpackConfig.plugins.push(\n    new CompressionWebpackPlugin({\n      asset: '[path].gz[query]',\n      algorithm: 'gzip',\n      test: new RegExp(\n        '\\\\.(' +\n        config.build.productionGzipExtensions.join('|') +\n        ')$'\n      ),\n      threshold: 10240,\n      minRatio: 0.8\n    })\n  )\n}\n\nif (config.build.bundleAnalyzerReport) {\n  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin\n  webpackConfig.plugins.push(new BundleAnalyzerPlugin())\n}\n\nmodule.exports = webpackConfig\n"
  },
  {
    "path": "ui/config/dev.env.js",
    "content": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n  NODE_ENV: '\"development\"',\n  BASE_API: '\"http://localhost:8000/api\"',\n})\n"
  },
  {
    "path": "ui/config/index.js",
    "content": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path = require('path')\n\nmodule.exports = {\n  dev: {\n\n    // Paths\n    assetsSubDirectory: 'static',\n    assetsPublicPath: '/',\n    proxyTable: {},\n\n    // Various Dev Server settings\n    host: 'localhost', // can be overwritten by process.env.HOST\n    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined\n    autoOpenBrowser: false,\n    errorOverlay: true,\n    notifyOnErrors: true,\n    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-\n\n    // Use Eslint Loader?\n    // If true, your code will be linted during bundling and\n    // linting errors and warnings will be shown in the console.\n    useEslint: true,\n    // If true, eslint errors and warnings will also be shown in the error overlay\n    // in the browser.\n    showEslintErrorsInOverlay: false,\n\n    /**\n     * Source Maps\n     */\n\n    // https://webpack.js.org/configuration/devtool/#development\n    devtool: 'cheap-module-eval-source-map',\n\n    // If you have problems debugging vue-files in devtools,\n    // set this to false - it *may* help\n    // https://vue-loader.vuejs.org/en/options.html#cachebusting\n    cacheBusting: true,\n\n    cssSourceMap: true\n  },\n\n  build: {\n    // Template for index.html\n    index: path.resolve(__dirname, '../dist/index.html'),\n\n    // Paths\n    assetsRoot: path.resolve(__dirname, '../dist'),\n    assetsSubDirectory: 'static',\n    assetsPublicPath: '/',\n\n    /**\n     * Source Maps\n     */\n\n    productionSourceMap: true,\n    // https://webpack.js.org/configuration/devtool/#production\n    devtool: '#source-map',\n\n    // Gzip off by default as many popular static hosts such as\n    // Surge or Netlify already gzip all static assets for you.\n    // Before setting to `true`, make sure to:\n    // npm install --save-dev compression-webpack-plugin\n    productionGzip: false,\n    productionGzipExtensions: ['js', 'css'],\n\n    // Run the build command with an extra argument to\n    // View the bundle analyzer report after build finishes:\n    // `npm run build --report`\n    // Set to `true` or `false` to always turn it on or off\n    bundleAnalyzerReport: process.env.npm_config_report\n  }\n}\n"
  },
  {
    "path": "ui/config/prod.env.js",
    "content": "'use strict'\nmodule.exports = {\n  NODE_ENV: '\"production\"',\n  BASE_API: '\"/api\"',\n}\n"
  },
  {
    "path": "ui/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1,minimum-scale=1, user-scalable=no\">\n    <title>Word-AI</title>\n  </head>\n  <style>\n    body {\n      margin:0;\n    }\n  </style>\n  <body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "ui/package.json",
    "content": "{\n  \"name\": \"wordai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Word AI\",\n  \"author\": \"Senghoo Kim <shkdmb@gmail.com>\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"webpack-dev-server --inline --progress --config build/webpack.dev.conf.js\",\n    \"start\": \"npm run dev\",\n    \"lint\": \"eslint --ext .js,.vue src\",\n    \"build\": \"node build/build.js\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^0.19.0\",\n    \"element-ui\": \"^2.12.0\",\n    \"font-awesome\": \"^4.7.0\",\n    \"js-cookie\": \"^2.2.1\",\n    \"moment-timezone\": \"^0.5.26\",\n    \"vue\": \"^2.5.2\",\n    \"vue-axios\": \"^2.1.4\",\n    \"vue-cli\": \"^2.9.6\",\n    \"vue-moment\": \"^4.0.0\",\n    \"vue-router\": \"^3.0.1\",\n    \"vue-underscore\": \"^0.1.4\",\n    \"vuex\": \"^3.1.1\"\n  },\n  \"devDependencies\": {\n    \"autoprefixer\": \"^7.1.2\",\n    \"babel-core\": \"^6.22.1\",\n    \"babel-eslint\": \"^8.2.1\",\n    \"babel-helper-vue-jsx-merge-props\": \"^2.0.3\",\n    \"babel-loader\": \"^7.1.1\",\n    \"babel-plugin-syntax-jsx\": \"^6.18.0\",\n    \"babel-plugin-transform-runtime\": \"^6.22.0\",\n    \"babel-plugin-transform-vue-jsx\": \"^3.5.0\",\n    \"babel-preset-env\": \"^1.3.2\",\n    \"babel-preset-stage-2\": \"^6.22.0\",\n    \"chalk\": \"^2.0.1\",\n    \"copy-webpack-plugin\": \"^4.0.1\",\n    \"css-loader\": \"^0.28.11\",\n    \"echarts\": \"^4.2.1\",\n    \"eslint\": \"^4.15.0\",\n    \"eslint-config-standard\": \"^10.2.1\",\n    \"eslint-friendly-formatter\": \"^3.0.0\",\n    \"eslint-loader\": \"^1.7.1\",\n    \"eslint-plugin-import\": \"^2.7.0\",\n    \"eslint-plugin-node\": \"^5.2.0\",\n    \"eslint-plugin-promise\": \"^3.4.0\",\n    \"eslint-plugin-standard\": \"^3.0.1\",\n    \"eslint-plugin-vue\": \"^4.0.0\",\n    \"extract-text-webpack-plugin\": \"^3.0.0\",\n    \"file-loader\": \"^1.1.4\",\n    \"friendly-errors-webpack-plugin\": \"^1.6.1\",\n    \"html-webpack-plugin\": \"^2.30.1\",\n    \"node-notifier\": \"^5.1.2\",\n    \"node-sass\": \"^4.12.0\",\n    \"optimize-css-assets-webpack-plugin\": \"^3.2.0\",\n    \"ora\": \"^1.2.0\",\n    \"portfinder\": \"^1.0.13\",\n    \"postcss-import\": \"^11.0.0\",\n    \"postcss-loader\": \"^2.0.8\",\n    \"postcss-url\": \"^7.2.1\",\n    \"rimraf\": \"^2.6.0\",\n    \"sass-loader\": \"^7.3.1\",\n    \"semver\": \"^5.3.0\",\n    \"shelljs\": \"^0.7.8\",\n    \"style-loader\": \"^1.0.0\",\n    \"uglifyjs-webpack-plugin\": \"^1.1.1\",\n    \"url-loader\": \"^0.5.8\",\n    \"vue-loader\": \"^13.3.0\",\n    \"vue-style-loader\": \"^3.0.1\",\n    \"vue-template-compiler\": \"^2.5.2\",\n    \"webpack\": \"^3.6.0\",\n    \"webpack-bundle-analyzer\": \"^3.3.2\",\n    \"webpack-dev-server\": \"^3.1.11\",\n    \"webpack-merge\": \"^4.1.0\"\n  },\n  \"engines\": {\n    \"node\": \">= 6.0.0\",\n    \"npm\": \">= 3.0.0\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not ie <= 8\"\n  ]\n}\n"
  },
  {
    "path": "ui/src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <router-view/>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'App'\n}\n</script>\n\n\n<style lang=\"scss\">\n#app {\n  direction: ltr;\n  color: #1a3b71;\n  background-color: #DFE6E5;\n  background-color: #DFE6E5;\n  flex-direction: column;\n  overflow: hidden;\n  height: 100vh;\n}\n\n</style>\n"
  },
  {
    "path": "ui/src/components/Card.vue",
    "content": "<template>\n  <div class=\"card\" v-if=\"!alldone\">\n    <div class=\"cn\" >\n      <span>{{quiz.cn}}</span>\n    </div>\n    <div class=\"quiz\">\n      <span class=\"en\">{{quiz.cloze}}</span>\n    </div>\n    <el-form label-width=\"50px\" class=\"answer\">\n      <el-form-item v-for=\"(ans, idx) in answers\" label=\"答案\">\n        <el-input v-model=\"answers[idx]\"\n                  @keydown.enter.native=\"submitAnswer\"\n                  @keydown.tab.native=\"(event)=>{tabEvent(event, idx)}\"\n                  @keydown.space.native=\"(event)=>{spaceDownEvent(event, idx)}\"\n                  @keyup.space.native=\"(event)=>{spaceUpEvent(event, idx)}\"\n                  autocomplete=\"off\"\n                  autocorrect=\"off\"\n                  autocapitalize=\"off\"\n                  spellcheck=\"false\"\n                  :ref=\"'answer-'+idx\" >\n          <div slot=\"suffix\">\n            <el-tooltip v-model=showAns[idx] :content=\"quiz.answers[idx]\" effect=\"light\" placement=\"right\">\n              <span> 查看答案</span>\n            </el-tooltip>\n          </div>\n        </el-input>\n      </el-form-item>\n    </el-form>\n  </div>\n  <div v-else>\n    <el-row>\n      <span>您已经完成所有字卡。</span>\n    </el-row>\n  </div>\n</template>\n\n<script>\nimport Dictionary from './Dictionary'\nexport default {\n  name: 'Main',\n  components: {\n    Dictionary\n  },\n  mounted (){\n    this.load_quiz()\n  },\n  data () {\n    return {\n      showAns:{},\n      showDict: false,\n      quiz: {},\n      answers: [],\n      alldone: false\n    }\n  },\n  methods: {\n    load_quiz (){\n      this.showDict = false\n      this.axios({\n\t      \"method\": \"GET\",\n\t      \"url\": \"/learn/word\"\n      }).then(res => {\n        this.quiz = res.data\n        this.cleanAnswer()\n        this.$nextTick(() => {\n          this.focusAnswer(0)\n        })\n      }, err=>{\n        if (err.response.status == 404) {\n          this.alldone = true\n        }\n      })\n    },\n    cleanAnswer(){\n      this.answers = []\n      for (var i = 0; i < this.quiz.check.length; i++) {\n        this.answers.push('')\n      }\n    },\n    focusAnswer(idx){\n      this.$refs[\"answer-\"+idx][0].focus()\n    },\n    submitAnswer() {\n      this.axios({\n\t      'method': 'POST',\n\t      'url': '/learn/word',\n\t      'data': {\n\t\t      'check': this.quiz.check,\n\t\t      'id': this.quiz.id,\n\t\t      'sid': this.quiz.sid,\n\t\t      'answers': this.answers\n        }\n      }).then(res => {\n        if (res.data.result) {\n          this.load_quiz()\n          this.$emit('correct', this.quiz.word)\n        } else {\n          this.cleanAnswer()\n          this.$emit('wrong', this.quiz.word)\n        }\n      })\n    },\n    tabEvent(event, idx){\n      if (this.answers.length > idx+1){\n        this.focusAnswer(idx+1)\n    }else{\n\n      this.focusAnswer(0)\n    }\n      event.preventDefault()\n    },\n    spaceDownEvent(event, idx){\n      this.showAns[idx] = true\n      event.preventDefault()\n    },\n    spaceUpEvent(event, idx){\n      this.showAns[idx] = false\n      event.preventDefault()\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  .cn{\n  padding:20px 20px 0px 20px;\n  }\n  .quiz{\n  padding:5px 20px 20px 20px;\n  font-size: 1.5em;\n  }\n  .answer{\n  padding: 0px 20px 20px 20px;\n  }\n</style>\n"
  },
  {
    "path": "ui/src/components/Dictionary.vue",
    "content": "<template>\n<div class=\"main\">\n  <div class=\"dict-head\">\n    <span>{{word}}</span>\n  </div>\n  <el-collapse v-model=\"activeName\" accordion>\n    <el-collapse-item v-for=\"desc in define\" :name=\"desc.seq\">\n      <template slot=\"title\">\n        <el-tag size=\"mini\">{{desc.speech}}</el-tag>\n        <span class=\"en\">{{desc.cn}}</span>\n      </template>\n      <div>{{desc.en}}</div>\n      <div v-for=\"eg in desc.examples\">\n        <el-divider content-position=\"left\">例句</el-divider>\n        <div>{{eg.en}}</div>\n        <div>{{eg.cn}}</div>\n      </div>\n    </el-collapse-item>\n  </el-collapse>\n</div>\n</template>\n\n<script>\nexport default {\n  name: 'Main',\n  mounted (){\n    this.load_dict()\n  },\n  computed: {\n  },\n  data () {\n    return {\n      start: 0,\n      define: [],\n      activeName: '1'\n    }\n  },\n  props: {\n    word: ''\n  },\n  watch: {\n    word () {\n      this.load_dict()\n    }\n  },\n  methods: {\n    load_dict (){\n      this.axios({\n        \"method\": \"GET\",\n        \"url\": \"/dictionary/\"+this.word\n      }).then(res => {\n        this.star = res.data.star\n        this.define = res.data.descriptions\n      }, err=>{\n      })\n    }\n  }\n}\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n\n<style rel=\"stylesheet/scss\" lang=\"scss\" scoped>\n.dict-head{\n  text-align: left;\n  font-size: 2em;\n  background: #C1D5D6;\n  padding: 5px;\n}\n.answer{\n  margin: 30px;\n}\n.card {\n  margin:100px auto;\n  width:500px;\n  background-color: white;\n  min-height: 250px;\n  border-radius: .5rem;\n  box-shadow: 0 3px 0.5rem #d9d9d9;\n  .quiz {\n    margin: 10px 30px;\n  }\n  .en {\n    font-size: 2rem;\n  }\n}\n.main{\n}\n.el-row {\n  margin-bottom: 20px;\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n.en{\n  line-height: 13px;\n  padding: 5px 10px;\n  align-items: flex-start;\n  overflow: hidden;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/Login.vue",
    "content": "<template>\n<div class=\"login\">\n<el-row>\n  <el-col :span=\"24\">\n    <el-input id=\"name\"  v-model=\"name\" placeholder=\"请输入帐号\">\n      <template slot=\"prepend\">帐号</template>\n    </el-input>\n  </el-col>\n</el-row>\n<el-row>\n  <el-col :span=\"24\">\n    <el-input id=\"password\" v-model=\"password\" type=\"password\" placeholder=\"请输入密码\" @keyup.enter.native=\"login\">\n      <template slot=\"prepend\">密码</template>\n    </el-input>\n  </el-col>\n</el-row>\n<el-row>\n  <el-col :span=\"24\">\n    <el-button id=\"login\" v-on:click=\"login\" style=\"width:100%\" type=\"primary\">登录</el-button>\n  </el-col>\n</el-row>\n</div>\n</template>\n<script>\nimport { mapGetters, mapActions } from 'vuex'\nexport default {\n  data() {\n    return {\n      name: '',\n      password: '',\n      passwordErr: false\n    }\n  },\n  computed: {\n    ...mapGetters([\n      'username',\n    ])\n  },\n  methods: {\n    ...mapActions([\n      `fetchJWT`\n    ]),\n    login() {\n      this.fetchJWT({\n        // #Security...\n        username: this.name,\n        password: this.password\n      }).then(()=>{\n        this.$router.push({ path: '/' })\n      }, (err)=>{\n        console.log(err)\n        this.$message({\n          message: '用户名密码错误',\n          type: 'warning'\n        });\n      })\n    }\n  },\n\n  mounted() {\n  }\n}\n</script>\n<style rel=\"stylesheet/scss\" lang=\"scss\" scoped>\n.login {\n  margin:20% auto;\n  width:300px;\n}\n.el-row {\n  margin-bottom: 20px;\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/Main.vue",
    "content": "<template>\n<div class=\"main\">\n  <div class=\"content\">\n  <el-menu  class=\"right-menu\" :collapse=\"true\" active-text-color=\"#303133\">\n    <el-submenu index=\"1\">\n      <template slot=\"title\">\n        <i class=\"el-icon-notebook-2\"></i>\n        <!-- <i class=\"el-icon-list-alt\"></i> -->\n        <span slot=\"title\">单词表</span>\n      </template>\n      <el-menu-item-group>\n        <span slot=\"title\">单词表</span>\n        <el-menu-item index=\"1-1\" @click=\"openLearned\">已学单词</el-menu-item>\n        <el-menu-item index=\"1-2\" @click=\"openToLearn\">未学单词</el-menu-item>\n      </el-menu-item-group>\n    </el-submenu>\n    <el-menu-item index=\"2\" @click=\"openStatistic\">\n      <i class=\"el-icon-s-marketing\"></i>\n      <span slot=\"title\">统计数据</span>\n    </el-menu-item>\n    <el-menu-item index=\"3\" >\n      <i class=\"el-icon-reading\" @click=\"openWordlistList\"></i>\n      <span slot=\"title\">单词本</span>\n    </el-menu-item>\n    <el-menu-item index=\"4\" @click=\"settingVisible = true\">\n      <i class=\"el-icon-setting\"></i>\n      <span slot=\"title\">设置</span>\n    </el-menu-item>\n  </el-menu>\n  <el-row>\n    <el-col :sm=\"{span: 18, offset: 3}\" :md=\"{span: 12, offset: 6}\" class=\"card\" >\n      <card ref=\"card\" v-on:correct=\"onCorrect\" v-on:wrong=\"onWrong\"></card>\n    </el-col>\n  </el-row>\n  </div>\n\n  <el-dialog title=\"单词本\" :visible.sync=\"wordlistListVisible\" width=\"90%\" class=\"wordlist-list\" custom-class=\"wordlist-list-2\">\n    <wordlist-list ref=\"wordlistList\"> </wordlist-list>\n  </el-dialog>\n  <el-dialog title=\"设置\" :visible.sync=\"settingVisible\">\n    <wordlist-setting ref=\"wordlistSetting\"> </wordlist-setting>\n    <div slot=\"footer\" class=\"dialog-footer\">\n      <el-button @click=\"settingVisible = false\">取 消</el-button>\n      <el-button type=\"primary\" @click=\"settingSubmit\">确 定</el-button>\n    </div>\n  </el-dialog>\n  <el-dialog\n    title=\"统计信息\"\n    :visible.sync=\"statisticVisible\"\n    width=\"80%\"\n    center>\n    <statistic ref=\"statistics\"></statistic>\n    <span slot=\"footer\" class=\"dialog-footer\">\n      <el-button @click=\"statisticVisible = false\">关 闭</el-button>\n    </span>\n  </el-dialog>\n  <el-drawer class=\"right-drawer\"\n             title=\"单词表 \"\n             custom-class=\"right-drawer\"\n             :visible.sync=\"showWordlist\"\n             size=\"250\"\n             direction=\"rtl\">\n    <wordlist ref=\"wordlist\" class=\"wordlist\"></wordlist>\n  </el-drawer>\n  <el-drawer class=\"bottom-drawer\"\n             title=\"词典\"\n             custom-class=\"dict-drawer\"\n             :visible.sync=\"showDict\"\n             :modal=\"false\"\n             direction=\"btt\">\n      <dictionary :word=\"word\"></dictionary>\n  </el-drawer>\n</div>\n</template>\n\n<script>\nimport 'element-ui/lib/theme-chalk/display.css';\nimport Dictionary from './Dictionary'\nimport Card from './Card'\nimport wordlist from './wordlist'\nimport WordlistSetting from './wordlist/Setting'\nimport WordlistList from './wordlist/List'\nimport statistic from './statistic'\nexport default {\n  name: 'Main',\n  components: {\n    Dictionary,\n    Card,\n    wordlist,\n    WordlistSetting,\n    WordlistList,\n    statistic\n  },\n  mounted (){},\n  data () {\n    return {\n      showDict: false,\n      word:\"\",\n      showWordlist: false,\n      settingVisible: false,\n      statisticVisible: false,\n      wordlistListVisible: false\n    }\n  },\n  methods: {\n    openWordlistList(){\n      this.wordlistListVisible = true\n    },\n    openStatistic(){\n      this.statisticVisible = true\n      this.$refs.statistics.load()\n    },\n    openLearned(){\n      this.showWordlist = true\n\n      this.$nextTick(() => {\n        this.$refs.wordlist.openLearned()\n      })\n    },\n    openToLearn(){\n      this.showWordlist = true\n      this.$nextTick(() => {\n        this.$refs.wordlist.openToLearn()\n      })\n    },\n    onWrong(word) {\n      this.word = word\n      this.showDict = true\n      this.$message({\n        message: '回答错误',\n        type: 'warning'\n      })\n    },\n    onCorrect() {\n      this.word = \"\"\n      this.showDict = false\n      this.$message({\n        message: '回答正确，继续努力!!',\n        type: 'success'\n      })\n    },\n    settingSubmit() {\n      var self = this\n      this.$refs.wordlistSetting.submit().then(()=>{\n        self.$refs.card.load_quiz()\n        self.settingVisible = false\n        self.$message({\n          message: '设置成功',\n          type: 'success'\n        })\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n  .dict-drawer{\n  overflow-y: scroll;\n  #el-drawer__title{\n  padding: 5px 10px;\n    margin: 0;\n  }\n  }\n</style>\n<style lang=\"scss\" scoped>\n  .content{\n  margin: 5px;\n  }\n\n  .right-menu{\n  position: fixed;\n  bottom: 20px;\n  right: 5px;\n  }\n\n  .content{\n  padding-top: 20px;\n  }\n  @media(min-width: 768px){\n  .content{\n  padding-top: 20vh;\n  }\n\n  .right-menu{\n  position: absolute;\n  z-index:100;\n  bottom: initial;\n  right: initial;\n  }\n  }\n  .main{\n  height: 100%;\n  }\n  .top-padding{\n  height: 20%;\n  }\n  .card {\n  background-color: white;\n  min-height: 250px;\n  border-radius: .5rem;\n  box-shadow: 0 3px 0.5rem #d9d9d9;\n  }\n  .dict {\n  width: 800px;\n  margin: 0 auto;\n  overflow: hidden;\n  }\n  .right-drawer{\n  max-height: 100%;\n  .right-drawer-body{\n  max-height: 100%;\n  min-width:300px;\n  }\n  }\n  .wordlist{\n  height: 500px;\n}\n  </style>\n"
  },
  {
    "path": "ui/src/components/statistic/DayChart.vue",
    "content": "<template>\n  <div>\n    <div id=\"day_chart_echart\" style=\"height: 400px; width: 100%;\"></div>\n  </div>\n</template>\n<script>\nvar echarts = require('echarts');\nexport default {\n  mounted () {\n    this.load_data();\n  },\n  data () {\n    return {\n      // now: new Date(),\n      data: {\n        exercise: {},\n        review: {}\n      }\n    }\n  },\n  computed:{\n    xaxis() {\n      var res = []\n      return this.$_.sortBy(\n        this.$_.uniq(this.$_.union(\n          this.$_.keys(this.data.exercise),\n          this.$_.keys(this.data.review))),\n        a=>{a}\n      )\n      return res\n    },\n    yaxis(){\n      return [\n        {name: '已学', type: 'line', data: this.$_.map(this.xaxis, date=>{\n          return this.data.exercise[date] ? this.data.exercise[date] : 0\n        })},\n        {name: '需要复习', type: 'line', data: this.$_.map(this.xaxis, date=>{\n          return this.data.review[date] ? this.data.review[date] : 0\n        })}\n      ]\n    }\n  },\n  methods: {\n    load_data(){\n      this.axios({\n\t      \"method\": \"GET\",\n\t      \"url\": \"/statistic/learn\",\n      }).then(res=>{\n        this.data = res.data\n        this.init_echarts()\n      })\n    },\n    init_echarts () {\n      // 基于准备好的dom，初始化echarts实例\n      var echart_etl_stat = echarts.init(document.getElementById('day_chart_echart'));\n      // 设置option\n      var echart_etl_stat_option = {\n          title: {\n              text: '学习数量'\n          },\n          tooltip: {\n              trigger: 'axis'\n          },\n          legend: {\n              data:['已学', '需要复习'  ]\n          },\n          grid: {\n              left: '3%',\n              right: '4%',\n              bottom: '3%',\n              containLabel: true\n          },\n          xAxis: {\n              type: 'category',\n              boundaryGap: false,\n              data: this.xaxis\n          },\n          yAxis: {\n              type: 'value'\n          },\n          series: this.yaxis\n      };\n      // 绘制图表\n      echart_etl_stat.setOption(echart_etl_stat_option);\n    },\n    needReview(act){\n      var now = this.$moment()\n      var review = this.$moment(act.review)\n      return review.isBefore(now)\n    },\n    tp (act){\n      if (this.needReview(act)){\n        return 'danger'\n      } else {\n        return 'success'\n      }\n    },\n    ts (act) {\n      var prefix='预计复习: '\n      if (this.needReview(act)){\n        prefix = '记忆过期：'\n      }\n      return prefix + this.$moment(act.review).tz(\"Asia/Shanghai\").fromNow()\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  ul{\n  max-height: 500px;\n  }\n</style>\n"
  },
  {
    "path": "ui/src/components/statistic/index.vue",
    "content": "<template>\n<div>\n  <el-row>\n    <el-col :span=\"24\">\n      <day-chart ref=\"dayChart\"></day-chart>\n    </el-col>\n  </el-row>\n</div>\n</template>\n<script>\nimport DayChart from './DayChart'\n\nexport default {\n  components: {\n    DayChart,\n  },\n  data() {\n    return {\n    };\n  },\n  methods: {\n    load(){\n      this.$refs.dayChart.load()\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/Form.vue",
    "content": "<template>\n<el-form :model=\"form\" label-width=\"80px\">\n  <el-form-item label=\"名称\">\n    <el-input v-model=\"form.name\"></el-input>\n  </el-form-item>\n  <el-form-item label=\"描述\">\n    <el-input v-model=\"form.description\"></el-input>\n  </el-form-item>\n  <label class=\"wl-label\" >单词列表:</label>\n  <div class=\"wl-border\">\n    <div class=\"wl-container\">\n      <div class=\"wl-backdrop\" ref=\"backdrop\">\n        <div class=\"wl-highlights\" v-html=\"highlight\"></div>\n      </div>\n      <textarea ref=\"textInput\" class=\"wl-input\" v-model=\"wordText\"\n                autocomplete=\"off\" autocorrect=\"off\" autocapitalize=\"off\" spellcheck=\"false\"\n                @keydown.enter=\"load_define\"\n                @blur=\"load_define\"\n                ></textarea>\n    </div>\n  </div>\n  <br/>\n  <el-form-item>\n    <el-button type=\"primary\" @click=\"submit\">提交</el-button>\n    <el-button @click=\"cancel\">取消</el-button>\n  </el-form-item>\n</el-form>\n</template>\n<script>\nexport default {\n  mounted (){\n    this.load_word()\n    this.$refs.textInput.addEventListener('scroll', () => {\n      this.$refs.backdrop.scrollTop = this.$refs.textInput.scrollTop\n    }, false)\n  },\n  data () {\n    return {\n      wordText: '',\n      form: {\n        name: '',\n        description: '',\n      },\n      checkRes: {\n        defines: {},\n        not_dict: [],\n        not_sentence: []\n      }\n    }\n  },\n  props: {\n    value: ''\n  },\n\n  watch: {\n    value () {\n      this.load_word()\n    }\n  },\n  computed: {\n    words() {\n      return this.$_.uniq(this.$_.reject(\n        this.$_.map(\n          this.wordText.split(\"\\n\"),\n          w=>{\n            return w.trim()\n          }\n        ), word=>{\n          return !word || word === ''\n        }))\n    },\n    highlight(){\n      var texts = this.wordText\n          .replace(/&/g, \"&amp;\")\n          .replace(/</g, \"&lt;\")\n          .replace(/>/g, \"&gt;\")\n          .replace(/\"/g, \"&quot;\")\n          .replace(/'/g, \"&#039;\").split(\"\\n\")\n      return this.$_.map(texts, l=>{\n        var w = l.trim()\n        var define = this.checkRes.defines[w]\n        if (define){\n          return '<div class=\"wl-line-div\"><span class=\"highlight-word\">' + l +'</span> <span class=\"desc\">'+define.descriptions[0].description+'</span></div>'\n        } else if (w ==='') {\n          return l\n        } else {\n          return '<div class=\"wl-line-div\"><span class=\"highlight-word-ne\">' + l +'</span> <span class=\"desc\">未找到定义或例句</span></div>'\n        }\n      }).join('')\n    }\n  },\n  methods: {\n    cancel() {\n      this.$emit('cancel')\n    },\n    load_word() {\n      this.clean()\n      if (this.value){\n        this.axios({\n\t        'method': 'GET',\n\t        'url': '/wordlist/'+this.value,\n        }).then(res=>{\n          this.form.name = res.data.name\n          this.form.description = res.data.description\n          this.wordText = res.data.words.join('\\n')\n          this.load_define()\n        }, err=>{\n          this.$message({\n            message: '您无权访问此单词表',\n            type: 'warning'\n          })\n          this.cancel()\n        })\n      }\n    },\n    clean(){\n      this.form.name =''\n      this.form.description = ''\n      this.wordText = ''\n      this.checkRes =  {\n        defines: {},\n        not_dict: [],\n        not_sentence: []\n      }\n    },\n    submit() {\n      if (!this.value){\n        this.axios({\n          'method': 'POST',\n          'url': '/wordlist',\n          'data': {\n            'name': this.form.name,\n            'description': this.form.description,\n            'words': this.words\n          }\n        }).then(res=>{\n          this.$emit('submit')\n        })\n      } else {\n        this.axios({\n          'method': 'PUT',\n\t        'url': '/wordlist/'+this.value,\n          'data': {\n            'name': this.form.name,\n            'description': this.form.description,\n            'words': this.words\n          }\n        }).then(res=>{\n          this.$emit('submit')\n        })\n      }\n    },\n    load_define(){\n      this.axios({\n        'method': 'PUT',\n        'url': '/wordlist',\n        'data': this.words\n      }).then(res=>{\n        this.checkRes = res.data\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n.wl-container, .wl-backdrop, .wl-input {\n  width: 100%;\n  height: 180px;\n}\n.wl-highlights, .wl-input {\n  font-family: Consolas,Liberation Mono,Courier,monospace;\n  letter-spacing: 1px;\n  font-size: 14px; word-wrap: break-word;\n}\n.wl-border{\n  border: 1px solid;\n}\n.wl-container {\n  display: block;\n  margin: 0 auto;\n  transform: translateZ(0);\n  -webkit-text-size-adjust: none;\n  padding:1px;\n\n}\n.wl-backdrop {\n  position: absolute;\n  z-index: 1;\n  background-color: #fff;\n  overflow: auto;\n  pointer-events: none;\n  transition: transform 1s;\n}\n\n.wl-highlights {\n  white-space: pre-wrap;\n  word-wrap: break-word;\n  color: transparent;\n  /* color: red; */\n}\n.wl-input {\n  display: block;\n  position: absolute;\n  z-index: 2;\n  margin: 0;\n  border: 2px solid #74637f;\n  border-radius: 0;\n  color: #444;\n  background-color: transparent;\n  overflow: auto;\n  resize: none;\n  transition: transform 1s;\n  padding:0;\n  border-style: none;\n}\n.wl-line-div{\n  overflow: visible;\n  white-space: nowrap;\n  height:16px;\n}\n.highlight-word{\n  display: inline-block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n.highlight-word-ne{\n  display: inline-block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  border-radius: 3px;\n  color: transparent;\n  border-bottom:1px solid red;\n}\n.desc{\n  display: inline-block;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  overflow-y: vi;\n  white-space: nowrap;\n  color: gray;\n  font-size: 12px;\n\n}\n\n\nmark {\n  color: transparent;\n  background-color: #d4e9ab; /* or whatever */\n}\n\ntextarea {\n  margin: 0;\n  border-radius: 0;\n}\ntextarea:focus, button:focus {\n  outline: none;\n  box-shadow: 0 0 0 2px #c6aada;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/Learned.vue",
    "content": "<template>\n<el-timeline>\n    <el-timeline-item\n      v-for=\"(activity, index) in learned\"\n      :key=\"activity.wordname\"\n      :type=\"tp(activity)\"\n      size=\"large\"\n      color=\"color\"\n      :timestamp=\"ts(activity)\">\n      {{activity.wordname}}\n    </el-timeline-item>\n  </el-timeline>\n</template>\n<script>\nexport default {\n  mounted () {\n    this.load()\n  },\n  data () {\n    return {\n      // now: new Date(),\n      learned: []\n    }\n  },\n  methods: {\n    load () {\n      this.axios({\n        'method': 'GET',\n        'url': '/wordlist/learned'\n      }).then(res=>{\n        this.learned = res.data\n      })\n    },\n    needReview(act){\n      var now = this.$moment()\n      var review = this.$moment(act.review)\n      return review.isBefore(now)\n    },\n    tp (act){\n      if (this.needReview(act)){\n        return 'danger'\n      } else {\n        return 'success'\n      }\n    },\n    ts (act) {\n      var prefix='预计复习: '\n      if (this.needReview(act)){\n        prefix = '记忆过期：'\n      }\n      return prefix + this.$moment(act.review).tz(\"Asia/Shanghai\").fromNow()\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  ul{\n  max-height: 500px;\n  }\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/List.vue",
    "content": "<template>\n  <div>\n<div class=\"wordlist\" v-if=\"show=='list'\">\n  <el-button type=\"primary\" icon=\"el-icon-edit\" size=\"mini\" @click=\"create\">新建</el-button>\n  <el-table\n    :data=\"lists\"\n    style=\"width: 100%\">\n    <el-table-column\n      label=\"名称\"\n      prop=\"name\">\n    </el-table-column>\n    <el-table-column\n      label=\"单词数\"\n      prop=\"words\"\n      :formatter=\"formatWordCount\">\n    </el-table-column>\n    <el-table-column\n      prop=\"user\"\n      label=\"类型\"\n      width=\"100\">\n      <template slot-scope=\"scope\">\n        <el-tag\n          :type=\"scope.row.user ?  'success' : 'primary'\"\n          disable-transitions>{{scope.row.user ?  '用户' : '系统'}}</el-tag>\n      </template>\n    </el-table-column>\n    <el-table-column\n      align=\"right\">\n      <template slot-scope=\"scope\">\n        <el-button\n          v-if=\"scope.row.user\"\n          type=\"text\"\n          size=\"mini\"\n          @click=\"handleEdit(scope.$index, scope.row)\">编辑</el-button>\n        <el-button\n          v-if=\"scope.row.user\"\n          type=\"text danger\"\n          size=\"mini\"\n          @click=\"handleDelete(scope.$index, scope.row)\">Delete</el-button>\n      </template>\n    </el-table-column>\n  </el-table>\n</div>\n<div class=\"wordlist\" v-if=\"show=='form'\">\n  <wordlist-form\n    v-model=\"formId\"\n    @cancel=\"formCancel\"\n    @submit=\"formSubmit\"\n    >\n  </wordlist-form>\n</div>\n</div>\n</template>\n<script>\nimport WordlistForm from './Form'\nexport default {\n  components: {\n    WordlistForm\n  },\n  mounted (){\n    this.load_list()\n  },\n  data () {\n    return {\n      formId: null,\n      show: 'list',\n      lists: [],\n      selected: {}\n    }\n  },\n  methods: {\n    formCancel(){\n      this.formId = null\n      this.show = 'list'\n      this.load_list()\n    },\n    formSubmit(){\n      this.formId = null\n      this.show = 'list'\n      this.load_list()\n    },\n    handleEdit(idx, row){\n      this.formId = row.id\n      this.show = 'form'\n    },\n    handleDelete(idx, row){\n      this.axios({\n\t      'method': 'DELETE',\n\t      'url': '/wordlist/'+row.id,\n      }).then(res=>{\n        this.$message({\n          message: '删除成功',\n          type: 'success'\n        })\n        this.load_list()\n      }, err=>{\n        this.$message({\n          message: '您无权访问此单词表',\n          type: 'warning'\n        })\n        this.load_list()\n      })\n    },\n    create(){\n      this.selected = {}\n      this.show='form'\n    },\n    load_list(){\n      return this.axios({\n        'method': 'GET',\n        'url': '/wordlist'\n      }).then(res=>{\n        this.lists = res.data\n      })\n    },\n    formatWordCount(row){\n      return row.words.length\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\nul{\n  max-height: 500px;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/Setting.vue",
    "content": "<template>\n<el-form :model=\"form\">\n  <el-form-item label=\"单词本\" :label-width=\"'70px'\">\n    <el-select v-model=\"form.wordlist\" placeholder=\"请选择活动区域\">\n      <el-option v-for=\"item in lists\" :label=\"item.name\" :value=\"item.id\"></el-option>\n    </el-select>\n  </el-form-item>\n</el-form>\n</template>\n<script>\nexport default {\n  mounted (){\n    this.load_current()\n    this.load_list()\n  },\n  data () {\n    return {\n      form: {\n        wordlist: '',\n      },\n      lists: []\n    }\n  },\n  methods: {\n    load_current(){\n      return this.axios({\n\t      'method': 'GET',\n\t      'url': '/user/wordlist',\n      }).then(res=>{\n        this.form.wordlist = res.data.wordlist\n      })\n    },\n    load_list(){\n      return this.axios({\n\t      'method': 'GET',\n\t      'url': '/wordlist'\n      }).then(res=>{\n        this.lists = res.data\n      })\n    },\n    submit(){\n      return this.axios({\n\t      'method': 'POST',\n\t      'url': 'http://127.0.0.1:8000/api/user/wordlist',\n\t      'data': this.form\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\nul{\n  max-height: 500px;\n}\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/ToLearn.vue",
    "content": "<template>\n  <div class=\"list-container\">\n    <ul class=\"infinite-list\" v-infinite-scroll=\"load\" style=\"overflow:auto\">\n      <li v-for=\"i in to_learn\" class=\"infinite-list-item\">\n        <span class=\"word\">{{i}}</span>\n      </li>\n    </ul>\n  </div>\n</template>\n<script>\nexport default {\n  data () {\n    return {\n      now: new Date(),\n      to_learn: []\n    }\n  },\n  methods: {\n    load () {\n      this.axios({\n        'method': 'GET',\n        'url': '/wordlist/to_learn'\n      }).then(res=>{\n        this.to_learn = res.data\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  ul{\n  max-height: 500px;\n  }\n</style>\n"
  },
  {
    "path": "ui/src/components/wordlist/index.vue",
    "content": "<template>\n  <el-tabs v-model=\"activeName\"  class=\"tabs\">\n    <el-tab-pane label=\"已学单词\" name=\"learned\">\n      <learned ref=\"learned\" class=\"words\"></learned>\n    </el-tab-pane>\n    <el-tab-pane label=\"未学单词\" name=\"tolearn\">\n      <to-learn ref=\"toLearn\" class=\"words\"></to-learn>\n    </el-tab-pane>\n  </el-tabs>\n</template>\n<script>\nimport Learned from './Learned'\nimport ToLearn from './ToLearn'\n\nexport default {\n  components: {\n    Learned,\n    ToLearn\n  },\n  data() {\n    return {\n      activeName: 'learned'\n    };\n  },\n  methods: {\n    openLearned(){\n      this.$refs.learned.load()\n      this.$refs.toLearn.load()\n      this.activeName = 'learned'\n    },\n    openToLearn(){\n      this.$refs.learned.load()\n      this.$refs.toLearn.load()\n      this.activeName = 'tolearn'\n    },\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  .tabs {\n  margin: 0 20px;\n}\n</style>\n"
  },
  {
    "path": "ui/src/main.js",
    "content": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base.conf with an alias.\nimport Vue from 'vue'\nimport VueAxios from 'vue-axios'\nimport axios from 'axios'\nimport underscore from 'vue-underscore'\n\n\nimport ElementUI from 'element-ui'\nimport 'element-ui/lib/theme-chalk/index.css'\n\nimport App from './App'\nimport router from './router'\nimport store from './store'\nimport request from './request'\nimport '@/permission'\n\n\n\nVue.use(ElementUI)\nVue.use(VueAxios, request)\nVue.use(underscore)\n\nimport moment from 'moment-timezone'\nmoment.tz.setDefault(\"UTC\")\nrequire('moment/locale/zh-cn')\nVue.use(require('vue-moment'), {\n  moment\n})\n\nVue.config.productionTip = false\n\n\n/* eslint-disable no-new */\nnew Vue({\n  el: '#app',\n  router,\n  store,\n  components: { App },\n  template: '<App/>'\n})\n"
  },
  {
    "path": "ui/src/permission.js",
    "content": "import router from './router'\nimport store from './store'\nimport {getToken} from './store'\n\nimport { Message } from 'element-ui'\n\nconst whiteList = ['/login']\n\nrouter.beforeEach((to, from, next) => {\n  if (getToken()) {\n    if (to.path === '/login') {\n      next({ path: '/' })\n    } else {\n      store.dispatch('getUserInfo').then(res => {\n        next()\n      }).catch((err) => {\n        store.dispatch('logOut').then(() => {\n          Message.error(err || 'Verification failed, please login again')\n          next({ path: '/' })\n        })\n      })\n    }\n  } else {\n    if (whiteList.indexOf(to.path) !== -1) {\n      next()\n    } else {\n      next('/login')\n    }\n  }\n})\n"
  },
  {
    "path": "ui/src/request.js",
    "content": "import axios from 'axios'\nimport {MessageBox} from 'element-ui'\nimport store from './store'\nimport { getToken } from './store'\n\nconst service = axios.create({\n  baseURL: process.env.BASE_API\n})\n\nvar lastRefresh = (new Date()).getTime()\nservice.interceptors.request.use(config => {\n  if (getToken()) {\n    if (!config.headers['Authorization']){\n      config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改\n    }\n    var now = (new Date()).getTime()\n    // debugger;\n    if (now - lastRefresh > 1 * 60 * 1000 && config.url !== '/token/refresh' && config.url !== '/login') {\n      store.dispatch('refreshJWT').then(() => {\n        lastRefresh = now\n      })\n    }\n  }\n  return config\n}, error => {\n  // Do something with request error\n  console.log(error) // for debug\n  Promise.reject(error)\n})\n\nservice.interceptors.response.use(\n\n  response => {\n    return response\n  },\n  error => {\n    const req = error.request\n    const res = error.response\n    const url = new URL(req.responseURL)\n    if (res.status === 401 && url.pathname !== '/login') {\n      MessageBox.confirm('你已被登出', '确定登出', {\n        confirmButtonText: '重新登录',\n        cancelButtonText: '取消',\n        type: 'warning'\n      }).then(() => {\n        store.dispatch('logOut').then(() => {\n          location.reload()// 为了重新实例化vue-router对象 避免bug\n        })\n      })\n    }\n    return Promise.reject(error)\n  }\n)\n\nexport default service\n"
  },
  {
    "path": "ui/src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\nexport default new Router({\n  routes: [\n    {\n      path: '/',\n      name: 'HelloWorld',\n      component: () => import('@/components/Main')\n    },\n    {\n      path: '/login',\n      name: 'Login',\n      component: () => import('@/components/Login')\n    }\n  ]\n})\n"
  },
  {
    "path": "ui/src/store.js",
    "content": "import Vue from 'vue'\nimport vuex from 'vuex'\nimport service from './request'\n//import Cookies from 'js-cookie'\n\n// Vue.use(service)\nVue.use(vuex)\n\nconst TokenKey = 'accessToken'\nconst refreshKey = 'refreshToken'\n\nexport function getToken () {\n  return sessionStorage.getItem(TokenKey)\n}\nexport function getRefreshToken () {\n  return sessionStorage.getItem(refreshKey)\n}\n\nexport function setToken (token) {\n  return sessionStorage.setItem(TokenKey, token)\n}\nexport function setRefreshTokens (token) {\n  return sessionStorage.setItem(refreshKey, token)\n}\n\nexport function removeToken () {\n  sessionStorage.removeItem(refreshKey)\n  return sessionStorage.removeItem(TokenKey)\n}\n\nexport default new vuex.Store({\n  state: {\n    accessToken: '',\n    refreshToken: '',\n    wordlist: ''\n  },\n  getters: {\n    accessToken: state => state.accessToken,\n    refreshToken: state => state.refreshToken,\n    username: (state, getters) => state.accessToken ? JSON.parse(atob(getters.accessToken.split('.')[1])) : null,\n  },\n  mutations: {\n    setTokens (state, access) {\n      // When this updates, the getters and anything bound to them updates as well.\n      state.accessToken = access\n    },\n    setRefreshTokens (state, refresh) {\n      // When this updates, the getters and anything bound to them updates as well.\n      state.refreshToken = refresh\n    },\n    setWordList (state, wordlist) {\n      state.wordList = wordlist\n    }\n  },\n  actions: {\n    fetchJWT ({ commit }, { username, password }) {\n      return service({\n        'method': 'POST',\n        'url': '/login',\n        'headers': {\n          'Content-Type': 'application/json; charset=utf-8'\n        },\n        'data': {\n          'username': username,\n          'password': password\n        }\n      }).then(res => {\n        commit('setTokens', res.data.access_token)\n        commit('setRefreshTokens', res.data.refresh_token)\n        setToken(res.data.access_token)\n        setRefreshTokens(res.data.refresh_token)\n      } )\n    },\n    refreshJWT ({ commit, state}) {\n      return service({\n        'method': 'POST',\n        'url': '/token/refresh',\n        'headers': {\n          'Content-Type': 'application/json; charset=utf-8',\n          'Authorization': 'Bearer ' + getRefreshToken()\n        }\n      }).then(res => {\n        commit('setTokens', res.data.access_token)\n        // commit('setRefreshTokens', res.data.refresh_token)\n        setToken(res.data.access_token, res.data.refresh_token)\n      })\n    },\n    getUserInfo ({commit, state}) {\n      return service({\n        'method': 'GET',\n        'url': '/user/wordlist',\n        'headers': {\n          'Content-Type': 'application/json; charset=utf-8',\n          'Authorization': 'Bearer ' + getToken()\n        }\n      }).then(res => {\n        commit('setWordList', res.wordlist)\n      })\n    },\n    logOut ({commit, state}) {\n      return new Promise((resolve, reject) => {\n        commit('setTokens', '', '')\n        removeToken()\n        resolve()\n      })\n    }\n  }\n})\n"
  },
  {
    "path": "ui/static/.gitkeep",
    "content": ""
  },
  {
    "path": "wordai/__init__.py",
    "content": ""
  },
  {
    "path": "wordai/api/__init__.py",
    "content": "from flask import Flask, jsonify\nfrom flask_cors import CORS\n\nfrom flask_jwt_extended import (JWTManager)\n\napp = Flask(__name__)\nCORS(app)\n\njwt = JWTManager(app)\nfrom wordai.models import User\n\n\n\n@app.route('/')\ndef index():\n    return jsonify({'message': 'Hello, World!'})\n\nfrom wordai.api.apis import blueprint as api\n\napp.register_blueprint(api, url_prefix='/api')\n\n"
  },
  {
    "path": "wordai/api/apis.py",
    "content": "import hashlib\nimport json\nfrom datetime import datetime, timedelta\n\nimport wordai.models as models\nfrom flask import Blueprint\nfrom flask_jwt_extended import (JWTManager, create_access_token,\n                                create_refresh_token, get_jwt_identity,\n                                get_raw_jwt, jwt_refresh_token_required,\n                                jwt_required)\nfrom flask_restful import Api, Resource, abort, reqparse, request\nfrom jsonschema import validate\n\nblueprint = Blueprint('profile', __name__,\n                    template_folder='templates',\n                    static_folder='static')\n\napi = Api(blueprint)\n\nclass api_register(object):\n    def __init__(self, path):\n        self.path = path\n\n    def __call__(self, cls):\n        api.add_resource(cls, self.path)\n        return cls\n\ndef admin_required(f):\n    def __inner__(self, *args, **kwargs):\n        identify = get_jwt_identity()\n        user = models.User.find_by_username(identify)\n        if user and user.role == 'admin':\n                return f(self, user, *args, **kwargs)\n        return {\n            'message': 'Not found',\n        }, 404\n    return jwt_required(__inner__)\n\ndef user_required(f):\n    def __inner__(self, *args, **kwargs):\n        identify = get_jwt_identity()\n        user = models.User.find_by_username(identify)\n        if user and user.role in ['admin', 'user']  :\n                return f(self, user, *args, **kwargs)\n        return {\n            'message': 'Not found',\n        }, 404\n    return jwt_required(__inner__)\n\nuser_parser = reqparse.RequestParser()\nuser_parser.add_argument('username', help='This username cannot be blank', required=True)\nuser_parser.add_argument('password', help='This password cannot be blank', required=True)\n\n@api_register(\"/registration\")\nclass UserRegistration(Resource):\n    def post(self):\n        return {'message': 'User registration'}\n\n\n@api_register(\"/login\")\nclass UserLogin(Resource):\n    def post(self):\n        data = user_parser.parse_args()\n        current_user = models.User.check_user(data['username'], data['password'])\n        if not current_user:\n            abort(401)\n            return {\n                'message': 'User {} doesn\\'t exist'.format(data['username']),\n            }\n\n        access_token = create_access_token(identity=data['username'])\n        refresh_token = create_refresh_token(identity=data['username'])\n        return {\n            'message': 'Logged in as {}'.format(current_user.username),\n            'role': current_user.role,\n            'access_token': access_token,\n            'refresh_token': refresh_token\n        }\n\n@api_register(\"/token/refresh\")\nclass TokenRefresh(Resource):\n    @jwt_refresh_token_required\n    def post(self):\n        current_user = get_jwt_identity()\n        if current_user:\n            access_token = create_access_token(identity=current_user)\n            return {\n                'access_token': access_token}\n        abort(401)\n        return {'message': 'invalid refresh token'}\n\n\n@api_register(\"/wordlist\")\nclass WordListList(Resource):\n    @user_required\n    def get(self, user):\n        return [json.loads(x.to_json()) for x in user.wordlists()]\n\n    @user_required\n    def put(self, user):\n        schema = {\n            \"type\": \"array\",\n            \"items\": {\"type\": \"string\"},\n            \"uniqueItems\": True\n        }\n        try:\n            body = request.json\n            validate(instance=body, schema=schema)\n            wordok, not_has, wnot_has = models.WordList.check_word(*body)\n            defines = models.Word.search_words(*wordok)\n            return {\n                \"defines\": {w['word']: w for w in json.loads(defines.to_json())},\n                \"not_dict\": wnot_has,\n                \"not_sentence\": not_has,\n            }\n        except Exception as err:\n            return {\n                \"message\": \"invalid request body\",\n                \"error\": str(err)\n            }, 422\n    @user_required\n    def post(self, user):\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"description\": {\"type\": \"string\"},\n                \"words\": {\n                    \"type\": \"array\",\n                    \"items\": {\"type\": \"string\"},\n                    \"uniqueItems\": True\n                }\n            }\n        }\n        try:\n            body = request.json\n            validate(instance=body, schema=schema)\n            wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])\n            body['words'] = list(wordok)\n            wordlist = models.WordList(**body)\n            wordlist.user = user\n            wordlist.save()\n            return {\n                \"message\": \"ok\",\n                \"has\": list(wordok),\n                \"not_dict\": wnot_has,\n                \"not_sentence\": not_has,\n            }\n        except Exception as err:\n            return {\n                \"message\": \"invalid request body\",\n                \"error\": str(err)\n            }, 422\n\n@api_register(\"/wordlist/<string:lid>\")\nclass WordListItem(Resource):\n    @user_required\n    def get(self, user, lid):\n        print(lid)\n        return json.loads(user.wordlists().filter(id=lid).first().to_json())\n\n    @user_required\n    def put(self, user, lid):\n        wordlist = models.WordList.objects(user=user, id=lid).first()\n        if not wordlist:\n            return {\n                \"message\": \"wordlist not exists\",\n            }, 404\n        schema = {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\"type\": \"string\"},\n                \"description\": {\"type\": \"string\"},\n                \"words\": {\n                    \"type\": \"array\",\n                    \"items\": {\"type\": \"string\"},\n                    \"uniqueItems\": True\n                }\n            }\n        }\n        try:\n            body = request.json\n            validate(instance=body, schema=schema)\n            wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])\n            wordlist.words = wordok\n            wordlist.name = body['name']\n            wordlist.description = body['description']\n            wordlist.user = user\n            wordlist.save()\n            return {\n                \"message\": \"ok\",\n                \"has\": list(wordok),\n                \"not_dict\": wnot_has,\n                \"not_sentence\": not_has,\n            }\n        except Exception as err:\n            return {\n                \"message\": \"invalid request body\",\n                \"error\": str(err)\n            }, 422\n\n    @user_required\n    def delete(self, user, lid):\n        wordlist = models.WordList.objects(user=user, id=lid).first()\n        if not wordlist:\n            return {\n                \"message\": \"wordlist not exists\",\n            }, 404\n        wordlist.delete()\n\n@api_register(\"/user/wordlist\")\nclass UserWordList(Resource):\n    @user_required\n    def get(self, user):\n        if not user.wordlist:\n            return {\n                \"message\", \"wordlist not set\"\n            }, 404\n        data = json.loads(user.wordlist.to_json())\n        return {\n            \"message\": \"ok\",\n            \"wordlist\": data['id'],\n            \"wordlist_name\": data['name']\n        }\n    @user_required\n    def post(self, user):\n        parser = reqparse.RequestParser()\n        parser.add_argument('wordlist', help='This wordlist cannot be blank', required=True)\n        wordlist_id = parser.parse_args()\n        wordlist = models.WordList.objects(id=wordlist_id['wordlist']).first()\n        user.wordlist = wordlist\n        user.save()\n        return {\n            \"message\": \"ok\",\n            \"wordlist\": wordlist.name\n        }\n\n\n@api_register(\"/learn/word\")\nclass LearnNext(Resource):\n    @user_required\n    def get(self, user):\n        ex = user.next_exercise()\n        if ex:\n            sentence_id = json.loads(ex.sentence.to_json())['id']\n            word_id = json.loads(ex.word.to_json())['id']\n            return {\n                \"id\": word_id,\n                \"word\": ex.word.word,\n                \"message\": \"ok\",\n                \"cloze\": ex.cloze,\n                \"cn\": ex.sentence.chn,\n                \"sid\": sentence_id,\n                \"answers\": [a for a in ex.answers],\n                \"check\": [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in ex.answers]\n            }\n        else:\n            return {\n                \"message\": \"no word need exercise\"\n            }, 404\n    @user_required\n    def post(self, user):\n        parser = reqparse.RequestParser()\n        parser.add_argument('id', help='This answers cannot be blank', required=True)\n        parser.add_argument('sid', help='This answers cannot be blank', required=True)\n        parser.add_argument('answers', help='This answers cannot be blank', required=True,action='append')\n        parser.add_argument('check', help='This answer_check cannot be blank', required=True, action='append')\n        data = parser.parse_args()\n        word_id = data['id']\n        word = models.Word.objects(id=word_id).first()\n        if not word:\n            return {\n                \"message\": \"word not exist\"\n            }, 404\n        sentence_id = data['sid']\n        answers = data['answers']\n        check = data['check']\n        check_res = [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in answers]\n        result = check == check_res\n        slog = models.SentenceLog(sentence=sentence_id, result=result, time=datetime.utcnow())\n        models.ExerciseLog.objects(user=user, word=word).update_one(\n            push__sentences=slog, wordname=word.word,\n            upsert=True)\n        log = models.ExerciseLog.objects(user=user, word=word).first()\n        log.calucate_review()\n        log.save()\n        return {\n            \"message\": \"ok\",\n            \"result\": result,\n        }\n\n\n@api_register(\"/dictionary/<string:word>\")\nclass Dictionary(Resource):\n    @user_required\n    def get(self, user, word):\n        define = models.Word.objects(word=word).first()\n        if define:\n            return json.loads(define.to_json())\n        else:\n            return {\"message\": \"not found\"}, 404\n\n\n@api_register(\"/wordlist/learned\")\nclass WordlistLearned(Resource):\n    @user_required\n    def get(self, user):\n        words = user.wordlist.user_learned(user).only(\"wordname\", \"review\")\n        return json.loads(words.to_json())\n\n\n@api_register(\"/wordlist/to_learn\")\nclass WordlistToLearn(Resource):\n    @user_required\n    def get(self, user):\n        words = user.wordlist.user_to_learn(user)\n        return words\n\n@api_register(\"/statistic/learn\")\nclass StatisticLearn(Resource):\n    @user_required\n    def get(self, user):\n        return {\n            'exercise': models.ExerciseLog.exercise_count(\n                user,\n                datetime.now()-timedelta(days=7),\n                datetime.now()+timedelta(days=7)\n            ),\n            'review': models.ExerciseLog.review_count(\n                user,\n                datetime.now()-timedelta(days=7),\n                datetime.now()+timedelta(days=7)\n            )\n        }\n"
  },
  {
    "path": "wordai/models/__init__.py",
    "content": "from mongoengine import connect\n\nfrom config import mongo_config\n\nconnect(**mongo_config())\n\nfrom .models import *\n"
  },
  {
    "path": "wordai/models/models.py",
    "content": "\nfrom datetime import datetime, timedelta\n\nimport bcrypt\nfrom mongoengine import (BooleanField, DateTimeField, EmbeddedDocumentField,\n                         FloatField, IntField, ListField, ReferenceField,\n                         SortedListField, StringField)\nfrom mongoengine_goodjson import Document, EmbeddedDocument\n\n\nclass DictExample(EmbeddedDocument):\n    en = StringField(required=True)\n    cn = StringField(required=True)\n\n\nclass DictDescInfo(EmbeddedDocument):\n    name = StringField(required=True)\n    value = StringField(required=True)\n\nclass DictDescription(EmbeddedDocument):\n    description = StringField(required=True)\n    seq = IntField(required=True)\n    cn = StringField(required=True)\n    en = StringField(required=True)\n    speech = StringField(required=True)\n    infos = ListField(EmbeddedDocumentField(DictDescInfo))\n    examples = ListField(EmbeddedDocumentField(DictExample))\n\n\nclass Word(Document):\n    word = StringField(required=True)\n    star = IntField(max_value=5)\n    descriptions = ListField(EmbeddedDocumentField(DictDescription))\n    meta = {\n        'indexes': [\n            ('word', '-star'),\n        ]\n    }\n    @classmethod\n    def search_words(cls, *words):\n        return Word.objects(word__in=words)\n\n    @classmethod\n    def has(cls, *words):\n        has = []\n        not_has = []\n        for word in words:\n            if Word.objects(word=word).count() > 0:\n                has.append(word)\n            else:\n                not_has.append(word)\n        return has, not_has\n\n\n    @classmethod\n    def search_word(cls, word):\n        return Word.objects(word=word).first()\n\nclass Sentence(Document):\n    eng = StringField(required=True)\n    chn = StringField(required=True)\n    score = FloatField()\n    words = ListField(StringField())\n    pos_tag = ListField(StringField())\n    roots = ListField(StringField())\n    typ = StringField(required=True)\n    meta = {\n        'indexes': [\n            ('roots', '-score'),\n            ('typ', 'roots', '-score')\n        ]\n    }\n\n    @classmethod\n    def has(cls, *words):\n        has = []\n        not_has = []\n        for word in words:\n            if cls.objects(roots=word).count() > 0:\n                has.append(word)\n            else:\n                not_has.append(word)\n        return has, not_has\n\n    @classmethod\n    def search_by_root(cls, word):\n        return cls.objects(roots=word, typ='dictexams').order_by('-score')\n\n    def cloze(self, word):\n        answers = []\n        cloz = self.eng\n        for idx, root in enumerate(self.roots):\n            if root == word:\n                answers.append(self.words[idx])\n                cloz = cloz.replace(self.words[idx], '[___]')\n        return cloz, answers\n\n    \nclass User(Document):\n    username = StringField(required=True, unique=True)\n    encrypted_password = StringField(required=True)\n    salt = StringField(required=True, default=lambda: bcrypt.gensalt().decode())\n    role = StringField(required=True, default='user')\n    wordlist = ReferenceField('WordList', required=True)\n    meta = {\n        'indexes': [\n            '#username'\n        ]\n    }\n    def __init__(self, *args, **kwargs):\n        passwd = kwargs.get(\"password\")\n        if passwd:\n            del kwargs['password']\n        Document.__init__(self, *args, **kwargs)\n        if passwd:\n            self.password = passwd\n\n    @classmethod\n    def find_by_username(cls, username):\n        return cls.objects(username=username).first()\n\n    @classmethod\n    def check_user(cls, username, passwd):\n        user = cls.objects(username=username).first()\n        if user and user.check_password(passwd):\n            return user\n        return None\n\n    @property\n    def password(self):\n        return \"\"\n\n    @password.setter\n    def password(self, passwd):\n        passwd = passwd.encode()\n        salt = bcrypt.gensalt()\n        self.encrypted_password = bcrypt.hashpw(passwd, salt).decode()\n\n    def check_password(self, passwd):\n        passwd = passwd.encode()\n        if (not self.encrypted_password) or (not self.salt):\n            return False\n        # hashed = bcrypt.hashpw(passwd, self.salt)\n        return bcrypt.checkpw(passwd, self.encrypted_password.encode())\n\n    def wordlist_exercise_log(self):\n        list_words = self.wordlist.words\n        return ExerciseLog.objects(user=self, wordname__in=list_words)\n\n    def word_exercise_log(self, word):\n        return ExerciseLog.objects(user=self, wordname=word).first()\n\n    def new_words(self):\n        list_words = self.wordlist.words\n        learned = [l.wordname for l in self.wordlist_exercise_log().only('wordname')]\n        return list(set(list_words) - set(learned))\n\n    def due_words(self):\n        return [l.wordname for l in self.wordlist_exercise_log().filter(review__lt=datetime.utcnow()).only('wordname')]\n\n    def next_word(self):\n        words = self.due_words()\n        if words :\n            return words[0]\n        words = self.new_words()\n        if words :\n            return words[0]\n        return None\n\n    def next_exercise(self):\n        word = self.next_word()\n        if not word:\n            return None\n        word_item = Word.search_word(word)\n        if not word_item:\n            return None\n        now = datetime.utcnow()\n        exlog = self.word_exercise_log(word)\n        sentence_log = exlog.sentences if exlog else []\n        log = {l.sentence.id: score_w(now - l.time, l.result) for l in sentence_log}\n        sentences = Sentence.search_by_root(word).limit(100)\n        top = None\n        top_score = 0\n        for s in sentences:\n            score = s.score * log.get(s.id, 1)\n            if score > top_score:\n                top = s\n                top_score = score\n        if not top:\n            return None\n        class cloze:\n            def __init__(self, word, quiz, answers, sentence):\n                self.word = word\n                self.cloze = quiz\n                self.answers = answers\n                self.sentence = sentence\n        quiz, answer = top.cloze(word)\n        return cloze(word_item, quiz, answer, top)\n    def wordlists(self):\n        return WordList.objects(user__in=[None, self])\n\n\nclass WordList(Document):\n    name = StringField(required=True)\n    description = StringField(required=True)\n    words = ListField(StringField())\n    user = ReferenceField(User)\n    meta = {\n        'indexes': [\n            ('user', 'name'),\n        ]\n    }\n\n    @classmethod\n    def check_word(self, *words):\n        has, not_has = Sentence.has(*words)\n        whas, wnot_has = Word.has(*words)\n        has = set(has)\n        whas = set(whas)\n        ok = list(has.intersection(whas))\n        return ok, not_has, wnot_has\n\n\n    def user_learned(self, user):\n        return ExerciseLog.objects(user=user, wordname__in=list(self.words))\n\n    def user_to_learn(self, user):\n        learned = set([e.wordname for e in ExerciseLog.objects(user=user).only('wordname')])\n        words = set(self.words)\n        return list(words - learned)\n\n\ndef score_w(delta, result):\n    if delta < timedelta(minutes=20):\n        w = (1-.58)\n    elif delta < timedelta(hours=1):\n        w = (1-.44)\n    elif delta < timedelta(hours=9):\n        w = (1-.36)\n    elif delta < timedelta(days=1):\n        w = (1-.33)\n    elif delta < timedelta(days=2):\n        w = (1-.28)\n    elif delta < timedelta(days=6):\n        w = (1-.25)\n    elif delta < timedelta(days=31):\n        w = (1-.21)\n    elif delta < timedelta(days=60):\n        w = (1-.10)\n    else:\n        w = .99\n    return w if result else w * 1.5\n\nclass SentenceLog(EmbeddedDocument):\n    sentence = ReferenceField(Sentence, required=True)\n    result = BooleanField(required=True)\n    time = DateTimeField(required=True)\nebbinghaus = {\n    0: timedelta(minutes=1),\n    1: timedelta(minutes=5),\n    2: timedelta(minutes=30),\n    3: timedelta(hours=12),\n    4: timedelta(days=1),\n    5: timedelta(days=2),\n    6: timedelta(days=4),\n    7: timedelta(days=7),\n    8: timedelta(days=15),\n}\n\n\nclass ExerciseLog(Document):\n    user = ReferenceField(User, required=True)\n    wordname = StringField(required=True)\n    word = ReferenceField(Word, required=True)\n    review = DateTimeField(required=True)\n    sentences = SortedListField(\n        EmbeddedDocumentField(SentenceLog),\n        ordering=\"time\", reverse=True)\n\n    def calucate_review(self):\n        count = 0\n        for s in reversed(self.sentences):\n            if not s.result:\n                break\n            count += 1\n        delta = ebbinghaus.get(count, timedelta(days=15))\n        self.review = datetime.utcnow() + delta\n\n    @classmethod\n    def review_count(cls, user, start, end):\n        data =  ExerciseLog.objects(user=user, review__gt=start, review__lt=end).\\\n            aggregate(\n                {'$group' : {'_id' : {'$dateToString' : {\n                    'date': \"$review\",\n                    'format': \"%Y/%m/%d\",\n                } }, 'count' : { '$sum' : 1 }}},\n                {'$sort': {'_id': 1}}\n            )\n        return {i['_id']:i['count'] for i in list(data)}\n\n    @classmethod\n    def exercise_count(cls, user, start, end):\n        data = ExerciseLog.objects(user=user).\\\n            aggregate(\n                {\"$match\": {\"sentences.time\": {\"$gte\": start, \"$lte\": end}}},\n                {\"$project\":{'_id':0, 'sentences.time': 1, 'sentences.result':1}},\n                {\"$unwind\":\"$sentences\"},\n                {\"$match\": {\"sentences.time\": {\"$gte\": start, \"$lte\": end}}},\n                {'$project': {'day': '$sentences.time',\n                              'result': '$sentences.result'}},\n                {'$group': { '_id': {'$dateToString' : {\n                    'date': \"$day\",\n                    'format': \"%Y/%m/%d\",\n                } }, 'count': { '$sum': 1 } } },\n                {'$sort': {'_id': 1}}\n            )\n        return {i['_id']:i['count'] for i in list(data)}\n"
  }
]