Full Code of senghoo/wordai for AI

master 2210cc558409 cached
61 files
92.3 KB
26.0k tokens
115 symbols
1 requests
Download .txt
Repository: senghoo/wordai
Branch: master
Commit: 2210cc558409
Files: 61
Total size: 92.3 KB

Directory structure:
gitextract_fuu3vrhz/

├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands/
│   └── __init__.py
├── config/
│   ├── __init__.py
│   └── wsgi.ini
├── data/
│   ├── __init__.py
│   └── dict.py
├── jsondict/
│   ├── anki.py
│   └── search.py
├── manage.py
├── nginx-server.conf
├── nginx.conf
├── requirements.txt
├── start.sh
├── ui/
│   ├── .babelrc
│   ├── .editorconfig
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── .postcssrc.js
│   ├── README.md
│   ├── build/
│   │   ├── build.js
│   │   ├── check-versions.js
│   │   ├── utils.js
│   │   ├── vue-loader.conf.js
│   │   ├── webpack.base.conf.js
│   │   ├── webpack.dev.conf.js
│   │   └── webpack.prod.conf.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   └── prod.env.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   ├── Card.vue
│   │   │   ├── Dictionary.vue
│   │   │   ├── Login.vue
│   │   │   ├── Main.vue
│   │   │   ├── statistic/
│   │   │   │   ├── DayChart.vue
│   │   │   │   └── index.vue
│   │   │   └── wordlist/
│   │   │       ├── Form.vue
│   │   │       ├── Learned.vue
│   │   │       ├── List.vue
│   │   │       ├── Setting.vue
│   │   │       ├── ToLearn.vue
│   │   │       └── index.vue
│   │   ├── main.js
│   │   ├── permission.js
│   │   ├── request.js
│   │   ├── router/
│   │   │   └── index.js
│   │   └── store.js
│   └── static/
│       └── .gitkeep
└── wordai/
    ├── __init__.py
    ├── api/
    │   ├── __init__.py
    │   └── apis.py
    └── models/
        ├── __init__.py
        └── models.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
ui/node_modules

================================================
FILE: .gitignore
================================================
### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore

# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*

# Org-mode
.org-id-locations
*_archive

# flymake-mode
*_flymake.*

# eshell files
/eshell/history
/eshell/lastdir

# elpa packages
/elpa/

# reftex files
*.rel

# AUCTeX auto folder
/auto/

# cask packages
.cask/
dist/

# Flycheck
flycheck_*.el

# server auth directory
/server/

# projectiles files
.projectile

# directory configuration
.dir-locals.el

# network security
/network-security.data



### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Python.gitignore

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# celery beat schedule file
celerybeat-schedule

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/
/data/data/

.DS_Store
*/.DS_Store


================================================
FILE: Dockerfile
================================================
FROM python:3.7

RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -

RUN apt-get update && apt-get install -y\
  build-essential libpq-dev nodejs nginx-extras

ENV NODE_PATH /usr/lib/node_modules

RUN pip install uwsgi

WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY ui/package.json ./ui/

WORKDIR /usr/src/app/ui
RUN npm install


COPY . /usr/src/app

ENV NODE_ENV production
RUN npm run build
WORKDIR /usr/src/app
COPY nginx.conf /etc/nginx/
COPY nginx-server.conf /etc/nginx/sites-enabled/default
EXPOSE 80
CMD './start.sh'


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Senghoo Kim

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Word AI

一款使用自然语言处理辅助背单词的程序。

通过导入TMX翻译语料库,进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。

*PS: 因为版权原因,不提供词典文件和TMX语料文件。需要自行下载导入。业余项目,分享出来供学习和交流,提供有限的技术支持,如有疑问欢迎Issue。*

## 功能特色

* 根据中文填写英文句子中的单词。
* 根据语料库自动生成题目,*拒绝重复*
* 根据艾宾浩斯记忆曲线重复出现需要复习的单词。
* 对于错误单词自动弹出词典,进行学习。
* 支持自定义词库。
* B/S架构,兼容移动端和PC端。


## 个人对背单词的一点看法

1. 背单词不能只针对独立的单词,需要放到语境中学习。
2. 单词需要自己完整的拼写出来,不能是只进行选择题。不然实际应用中容易出现拼写错误的情况。
3. 单词记忆时要关注时态、语态。
4. 学习的内容不能重复,也就是每次学习的上下文要发生改变,不能是固定的句子,学习固定的单词。这种情况下容易脱离了当前上下文,还是想不起单词。

## Examples

### 单词学习

如遇到不会的单词,可以长按空格查看答案。

![练习](doc/word.gif?raw=true)

对于错误单词自动弹出词典以供学习。

![错误单词](doc/error.gif?raw=true)

### 自定义单词列表

文本框输入单词列表。一行一个,自动提示词典查询结果,以检查是否添加正确。

![单词列表](doc/newlist.gif?raw=true)

### 学习统计

提供简单的统计分析功能。

![单词列表](doc/analysis.gif?raw=true)

## 管理命令

因为是简单自用为目的设计的软件,没有单独做管理页面。系统管理通过命令行进行。

添加用户

```
# 普通用户
python manage.py useradd <用户名> <口令>
# 管理员
python manage.py useradd <用户名> <口令> admin
```


导入字典

```
python manage.py sync_dict
```

导入例句

```
python manage.py sync_sentence
```

分析词典

```
python manage.py wordlist # 标注 单词星级
python manage.py dict_parse # 拆分中英文解释
```


================================================
FILE: app.py
================================================
from wordai.api import app
from config import api_env, flask_config

app.config.update(flask_config())

def run_dev():
    app.run(**api_env())


================================================
FILE: commands/__init__.py
================================================
import string
from data import load_dict, load_sentence
from data.dict import Description
from wordai.models import Word, Sentence, User, WordList, DictDescInfo

def dict_to_mongo():
    items = load_dict()
    for word, item in items.items():
        if Word.objects(word=word).count() == 0:
            dbitem = Word(**item)
            dbitem.save()


def sentence_to_mongo(*typ):
    items = load_sentence(*typ)
    print(items)
    for k, v in items.items():
        _sentence_to_mongo(k, v)

def _sentence_to_mongo(typ, items):
    import nltk
    from nltk.corpus import wordnet

    def wordnet_pos(tag):
        if tag.startswith('J'):
            return wordnet.ADJ
        elif tag.startswith('V'):
            return wordnet.VERB
        elif tag.startswith('N'):
            return wordnet.NOUN
        elif tag.startswith('R'):
            return wordnet.ADV
        else:
            return wordnet.NOUN

    # nltk.download('punkt')
    nltk.download('averaged_perceptron_tagger')
    nltk.download('stopwords')
    nltk.download('wordnet')
    nltk.download('punkt')
    stop_words = set(nltk.corpus.stopwords.words('english'))
    stemmer = nltk.stem.WordNetLemmatizer()
    sentences = []
    for trans in items:
        eng, chn = trans.getsource(), trans.gettarget()
        tokens = nltk.word_tokenize(eng)
        pos_tag = [pos[1] for pos in nltk.pos_tag(tokens)]
        roots = [stemmer.lemmatize(word, wordnet_pos(pos_tag[idx])) for idx, word in enumerate(tokens)]
        cleanword = [token for token in roots if token.isalpha() and token not in stop_words and len(token) >= 3]
        # remove duplicates
        clean_word = list(dict.fromkeys(cleanword))
        if len(clean_word) > 0:
            score = Word.search_words(*clean_word).sum('star') / len(clean_word)
        else:
            score = -1
        sentence = Sentence(eng=eng, chn=chn, words=tokens, pos_tag=pos_tag, roots=roots, score=score, typ=typ)
        sentences.append(sentence)
        if len(sentences) > 50:
            Sentence.objects.insert(sentences)
            sentences = []


def new_user(username, passwd, role='user', *args):
    u = User(username=username, password=passwd, role=role)
    u.save()

def dict_star_word_list():
    for star in range(1, 6, 1):
        words = [word.word for word in Word.objects(star=star).only('word')]
        wl = WordList(name="word star {}".format(star), words=words)
        wl.save()

def dict_parse():
    for word in Word.objects:
        for des in word.descriptions:
            res = Description(des.description)
            des.seq = res.seq
            des.cn = res.cn
            des.en = res.en
            des.speech = res.speech
            des.infos = [DictDescInfo(**info) for info in res.info]
        word.save()

def run(method, *args):
    if method == "sync_dict":
        dict_to_mongo()
    elif method == "sync_sentence":
        sentence_to_mongo(*args)
    elif method == "useradd":
        new_user(*args)
    elif method == "wordlist":
        dict_star_word_list(*args)
    elif method == "dict_parse":
        dict_parse(*args)
    else:
        print("unknown command {0}".format(method))


================================================
FILE: config/__init__.py
================================================
import os

def c(name, default=None):
    return os.environ.get(name.upper(), default)

def mongo_config():
    return {
        'host': c('mongo_host', "127.0.0.1"),
        'port': int(c('mongo_port', "27017")),
        'db': c('mongo_db', "wordai"),
        'username': c('mongo_username', "root"),
        'password': c('mongo_password', ""),
    }

def api_env():
    return {
        'host': c('host', '127.0.0.1'),
        'port': int(c('port', 8000)),
        'debug': c('debug', "true") == "true"
    }

def flask_config():
    return {
        'JWT_SECRET_KEY': c('secret', '__SOME_SECRET_KEYS_HERE__')
    }


================================================
FILE: config/wsgi.ini
================================================
[uwsgi]
module = app:app
master = true
processes = 3

socket = /var/run/wordai.sock
logto = /var/log/wordai.log
chmod-socket = 660
vacuum = true

================================================
FILE: data/__init__.py
================================================
# -*- coding: utf-8 -*-

import os
import json
from itertools import chain
from translate.storage.tmx import tmxfile

DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")

def data_file(filename):
    return os.path.join(DATA_DIR, filename)

def load_dict():
    with open(data_file("dict.json"), "r") as f:
        return json.load(f)


def load_sentence(*typ):
    files = {
        'talks': ['日常口语_20190906111009_1.tmx', '日常口语_20190906111009_2.tmx', '日常口语_20190906111009_3.tmx'],
        'dictexams': ['词典例句汇集1.tmx', '词典例句汇集3.tmx', '词典例句汇集5.tmx', '词典例句汇集7.tmx',
                      '词典例句汇集2.tmx', '词典例句汇集4.tmx', '词典例句汇集6.tmx', '词典例句汇集8.tmx']
    }
    iters = {}
    for t, fs in files.items():
        if len(typ) == 0 or t in typ:
            type_iterns = []
            for fname in fs:
                with open(data_file(fname), 'rb') as fin:
                    tmx = tmxfile(fin, 'en', 'cn')
                    type_iterns.append(tmx.unit_iter())
            iters[t] = chain(*type_iterns)
    return iters


================================================
FILE: data/dict.py
================================================
# -*- coding: utf-8 -*-
import string
import re

class Description(object):
    def __init__(self, des):
        self.des = des
        self.idx = 0
        self.length = len(des)
        self.info = []
        if not self.read_seq():
            self.seq = 0
            self.en = self.des[self.idx:]
            self.cn = self.des[self.idx:]
            self.speech = 'OTHER'
            return

        if not self.read_word_speech():
            self.en = self.des[self.idx:]
            self.cn = self.des[self.idx:]
            self.speech = 'OTHER'
            return
        if not self.read_cn_and_en():
            self.en = self.des[self.idx:]
            self.cn = self.des[self.idx:]
            return
        self.read_info()

    def read_seq(self):
        res = ''
        while(self.idx < self.length):
            if self.des[self.idx] <= '9' and self.des[self.idx] >= '0':
                res += self.des[self.idx]
                self.idx += 1
            else:
                break
        if self.des[self.idx] == '.':
            self.idx += 1
        if len(res) > 0:
            self.seq = int(res.strip())
            return True
        return False

    def read_word_speech(self):
        res = ''
        while(self.idx < self.length):
            if self.des[self.idx] in string.ascii_uppercase+"- ;":
                res += self.des[self.idx]
                self.idx += 1
            else:
                break
        res = res.strip()
        if self.des[self.idx] == '\t':
            self.idx += 1
        if len(res) > 0:
            self.speech = res
            return True
        return False


    def read_cn_and_en(self):
        end = self.des.find('【', self.idx)
        if end == -1:
            end = len(self.des)
        idx = end
        while(idx >= self.idx):
            idx -= 1
            if self.des[idx] not in string.printable:
                break
        if idx == self.idx:
            self.cn = ''
            self.en = self.des[idx:end]
        elif idx == end-1:
            self.cn = self.des[idx:end]
            self.en = self.des[idx:end]
        else:
            self.cn = self.des[self.idx:idx+1]
            self.en = self.des[idx+1:end]
        self.idx = end
        return True

    def read_info(self):
        text = self.des[self.idx:]
        res = re.findall(r'【([^【】]+)】:([^【】]+)', text, re.M)
        for item in res:
            self.info.append({
                'name': item[0],
                'value': item[1],
            })


================================================
FILE: jsondict/anki.py
================================================
import sys
import csv
from search import search_string, description

res = []

def cloze_word(sentense, word, level="1"):
    return sentense.lower().replace(word, "{{c"+level+"::"+word+"}}")


with open(sys.argv[1], 'r') as f:
    contents = f.read()
    items = contents.split("\n\n")
    for item in items:
        lines = item.split("\n")
        word = lines[0]
        exp = lines[1]
        eg = lines[2:]
        dct = search_string(word)
        exp2 = cloze_word(description(word), word, "1")
        for x in [{"sentence": cloze_word(x, word, "1"),"exp2": exp2, "exp": exp, "dict": dct  } for x in eg]:
            res.append(x)


with open('{0}.csv'.format(sys.argv[1]), 'w') as csv_file:
    fieldnames = ['sentence','exp2', 'exp', 'dict']
    writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
    for row in res:
        print(row['sentence'])
    writer.writerows(res)


================================================
FILE: jsondict/search.py
================================================
#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys
import json


with open('dict.json', 'r') as f:
    dictionary = json.load(f)

def search(word):
    try:
        return dictionary[word]
    except:
        return None

def format(item):
    res = "{word}\tStar:{star}\n".format(**item)
    for desc in item['descriptions']:
        res += "\t{description}\n".format(**desc)
        for ex in desc['examples']:
            res += "\t\tEN:\t{en}\n\t\tCN:\t{cn}\n".format(**ex)
        res +="\n"
    return res

def description(word):
    item = search(word)
    if item is None:
        return ""
    return "\n".join([x['description'] for x in item['descriptions']])

def search_string(word):
    res = search(word)
    if res is None:
        return ""
    return format(res)

if __name__ == '__main__':
    res = search(sys.argv[1])
    if res is not None:
        print(format(res))


================================================
FILE: manage.py
================================================
import os
import sys

ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT_DIR)

import commands
import app


def command(method, *args):
    if method == 'task':
        commands.run(*args)
    elif method == 'dev_server':
        app.run_dev()
    else:
        print("unknown command {0}".format(method))


def usage():
    print("{0} task".format( sys.argv[0]))


if __name__ == '__main__':
    if len(sys.argv) > 1:
        command(*sys.argv[1:])
    else:
        usage()


================================================
FILE: nginx-server.conf
================================================
server {
    listen 80;
    server_name 0.0.0.0;

    location /api/ {
        include uwsgi_params;
        uwsgi_pass unix:/var/run/wordai.sock;
    }
    location / {
        root /usr/src/app/ui/dist/;
    }
}

================================================
FILE: nginx.conf
================================================
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 768;
}

http {


	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;

	include /etc/nginx/mime.types;
	default_type application/octet-stream;


	access_log /var/log/nginx/access.log;
	error_log /var/log/nginx/error.log;

	gzip on;

	include /etc/nginx/conf.d/*.conf;
	include /etc/nginx/sites-enabled/*;
}




================================================
FILE: requirements.txt
================================================
mongoengine
mongoengine-goodjson
lxml
translate-toolkit
nltk
flask
flask-restful
flask-jwt-extended
flask-cors
bcrypt
jsonschema


================================================
FILE: start.sh
================================================
#!/bin/bash

service nginx start
uwsgi --ini config/wsgi.ini 



================================================
FILE: ui/.babelrc
================================================
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"]
}


================================================
FILE: ui/.editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: ui/.eslintignore
================================================
/build/
/config/
/dist/
/*.js


================================================
FILE: ui/.eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parserOptions: {
    parser: 'babel-eslint'
  },
  env: {
    browser: true,
  },
  extends: [
    // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
    // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
    'plugin:vue/essential', 
    // https://github.com/standard/standard/blob/master/docs/RULES-en.md
    'standard'
  ],
  // required to lint *.vue files
  plugins: [
    'vue'
  ],
  // add your custom rules here
  rules: {
    // allow async-await
    'generator-star-spacing': 'off',
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
  }
}


================================================
FILE: ui/.gitignore
================================================
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln


================================================
FILE: ui/.postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-url": {},
    // to edit target browsers: use "browserslist" field in package.json
    "autoprefixer": {}
  }
}


================================================
FILE: ui/README.md
================================================
# wordai

> Word AI

## Build Setup

``` bash
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm run dev

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report
```

For 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).


================================================
FILE: ui/build/build.js
================================================
'use strict'
require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})


================================================
FILE: ui/build/check-versions.js
================================================
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')

function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

const versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  }
]

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log('  ' + warning)
    }

    console.log()
    process.exit(1)
  }
}


================================================
FILE: ui/build/utils.js
================================================
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')

exports.assetsPath = function (_path) {
  const assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory

  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}

  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  const output = []
  const loaders = exports.cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

exports.createNotifierCallback = () => {
  const notifier = require('node-notifier')

  return (severity, errors) => {
    if (severity !== 'error') return

    const error = errors[0]
    const filename = error.file && error.file.split('!').pop()

    notifier.notify({
      title: packageConfig.name,
      message: severity + ': ' + error.name,
      subtitle: filename || '',
      icon: path.join(__dirname, 'logo.png')
    })
  }
}


================================================
FILE: ui/build/vue-loader.conf.js
================================================
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
  ? config.build.productionSourceMap
  : config.dev.cssSourceMap

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: sourceMapEnabled,
    extract: isProduction
  }),
  cssSourceMap: sourceMapEnabled,
  cacheBusting: config.dev.cacheBusting,
  transformToRequire: {
    video: ['src', 'poster'],
    source: 'src',
    img: 'src',
    image: 'xlink:href'
  }
}


================================================
FILE: ui/build/webpack.base.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

const createLintingRule = () => ({
  test: /\.(js|vue)$/,
  loader: 'eslint-loader',
  enforce: 'pre',
  include: [resolve('src'), resolve('test')],
  options: {
    formatter: require('eslint-friendly-formatter'),
    emitWarning: !config.dev.showEslintErrorsInOverlay
  }
})

module.exports = {
  context: path.resolve(__dirname, '../'),
  entry: {
    app: './src/main.js'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      'vue$': 'vue/dist/vue.esm.js',
      '@': resolve('src'),
    }
  },
  module: {
    rules: [
      ...(config.dev.useEslint ? [createLintingRule()] : []),
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      {
        test: /\.s[a|c]ss$/,
        loader: 'style!css!sass'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('media/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      }
    ]
  },
  node: {
    // prevent webpack from injecting useless setImmediate polyfill because Vue
    // source contains it (although only uses it if it's native).
    setImmediate: false,
    // prevent webpack from injecting mocks to Node native modules
    // that does not make sense for the client
    dgram: 'empty',
    fs: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty'
  }
}


================================================
FILE: ui/build/webpack.dev.conf.js
================================================
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')

const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: config.dev.devtool,

  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay
      ? { warnings: false, errors: true }
      : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': require('../config/dev.env')
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})


================================================
FILE: ui/build/webpack.prod.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

const env = require('../config/prod.env')

const webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true,
      usePostCSS: true
    })
  },
  devtool: config.build.productionSourceMap ? config.build.devtool : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new UglifyJsPlugin({
      uglifyOptions: {
        compress: {
          warnings: false
        }
      },
      sourceMap: config.build.productionSourceMap,
      parallel: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: config.build.productionSourceMap
        ? { safe: true, map: { inline: false } }
        : { safe: true }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    // keep module.id stable when vendor modules does not change
    new webpack.HashedModuleIdsPlugin(),
    // enable scope hoisting
    new webpack.optimize.ModuleConcatenationPlugin(),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks (module) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      minChunks: Infinity
    }),
    // This instance extracts shared chunks from code splitted chunks and bundles them
    // in a separate chunk, similar to the vendor chunk
    // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: 'app',
      async: 'vendor-async',
      children: true,
      minChunks: 3
    }),

    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ])
  ]
})

if (config.build.productionGzip) {
  const CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig


================================================
FILE: ui/config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  BASE_API: '"http://localhost:8000/api"',
})


================================================
FILE: ui/config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.

const path = require('path')

module.exports = {
  dev: {

    // Paths
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},

    // Various Dev Server settings
    host: 'localhost', // can be overwritten by process.env.HOST
    port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
    autoOpenBrowser: false,
    errorOverlay: true,
    notifyOnErrors: true,
    poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-

    // Use Eslint Loader?
    // If true, your code will be linted during bundling and
    // linting errors and warnings will be shown in the console.
    useEslint: true,
    // If true, eslint errors and warnings will also be shown in the error overlay
    // in the browser.
    showEslintErrorsInOverlay: false,

    /**
     * Source Maps
     */

    // https://webpack.js.org/configuration/devtool/#development
    devtool: 'cheap-module-eval-source-map',

    // If you have problems debugging vue-files in devtools,
    // set this to false - it *may* help
    // https://vue-loader.vuejs.org/en/options.html#cachebusting
    cacheBusting: true,

    cssSourceMap: true
  },

  build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',

    /**
     * Source Maps
     */

    productionSourceMap: true,
    // https://webpack.js.org/configuration/devtool/#production
    devtool: '#source-map',

    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],

    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  }
}


================================================
FILE: ui/config/prod.env.js
================================================
'use strict'
module.exports = {
  NODE_ENV: '"production"',
  BASE_API: '"/api"',
}


================================================
FILE: ui/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1,minimum-scale=1, user-scalable=no">
    <title>Word-AI</title>
  </head>
  <style>
    body {
      margin:0;
    }
  </style>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: ui/package.json
================================================
{
  "name": "wordai",
  "version": "1.0.0",
  "description": "Word AI",
  "author": "Senghoo Kim <shkdmb@gmail.com>",
  "private": true,
  "scripts": {
    "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "lint": "eslint --ext .js,.vue src",
    "build": "node build/build.js"
  },
  "dependencies": {
    "axios": "^0.19.0",
    "element-ui": "^2.12.0",
    "font-awesome": "^4.7.0",
    "js-cookie": "^2.2.1",
    "moment-timezone": "^0.5.26",
    "vue": "^2.5.2",
    "vue-axios": "^2.1.4",
    "vue-cli": "^2.9.6",
    "vue-moment": "^4.0.0",
    "vue-router": "^3.0.1",
    "vue-underscore": "^0.1.4",
    "vuex": "^3.1.1"
  },
  "devDependencies": {
    "autoprefixer": "^7.1.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^8.2.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "chalk": "^2.0.1",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.11",
    "echarts": "^4.2.1",
    "eslint": "^4.15.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-friendly-formatter": "^3.0.0",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.2.0",
    "eslint-plugin-promise": "^3.4.0",
    "eslint-plugin-standard": "^3.0.1",
    "eslint-plugin-vue": "^4.0.0",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^2.30.1",
    "node-notifier": "^5.1.2",
    "node-sass": "^4.12.0",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "postcss-url": "^7.2.1",
    "rimraf": "^2.6.0",
    "sass-loader": "^7.3.1",
    "semver": "^5.3.0",
    "shelljs": "^0.7.8",
    "style-loader": "^1.0.0",
    "uglifyjs-webpack-plugin": "^1.1.1",
    "url-loader": "^0.5.8",
    "vue-loader": "^13.3.0",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.5.2",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^3.3.2",
    "webpack-dev-server": "^3.1.11",
    "webpack-merge": "^4.1.0"
  },
  "engines": {
    "node": ">= 6.0.0",
    "npm": ">= 3.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 8"
  ]
}


================================================
FILE: ui/src/App.vue
================================================
<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>


<style lang="scss">
#app {
  direction: ltr;
  color: #1a3b71;
  background-color: #DFE6E5;
  background-color: #DFE6E5;
  flex-direction: column;
  overflow: hidden;
  height: 100vh;
}

</style>


================================================
FILE: ui/src/components/Card.vue
================================================
<template>
  <div class="card" v-if="!alldone">
    <div class="cn" >
      <span>{{quiz.cn}}</span>
    </div>
    <div class="quiz">
      <span class="en">{{quiz.cloze}}</span>
    </div>
    <el-form label-width="50px" class="answer">
      <el-form-item v-for="(ans, idx) in answers" label="答案">
        <el-input v-model="answers[idx]"
                  @keydown.enter.native="submitAnswer"
                  @keydown.tab.native="(event)=>{tabEvent(event, idx)}"
                  @keydown.space.native="(event)=>{spaceDownEvent(event, idx)}"
                  @keyup.space.native="(event)=>{spaceUpEvent(event, idx)}"
                  autocomplete="off"
                  autocorrect="off"
                  autocapitalize="off"
                  spellcheck="false"
                  :ref="'answer-'+idx" >
          <div slot="suffix">
            <el-tooltip v-model=showAns[idx] :content="quiz.answers[idx]" effect="light" placement="right">
              <span> 查看答案</span>
            </el-tooltip>
          </div>
        </el-input>
      </el-form-item>
    </el-form>
  </div>
  <div v-else>
    <el-row>
      <span>您已经完成所有字卡。</span>
    </el-row>
  </div>
</template>

<script>
import Dictionary from './Dictionary'
export default {
  name: 'Main',
  components: {
    Dictionary
  },
  mounted (){
    this.load_quiz()
  },
  data () {
    return {
      showAns:{},
      showDict: false,
      quiz: {},
      answers: [],
      alldone: false
    }
  },
  methods: {
    load_quiz (){
      this.showDict = false
      this.axios({
	      "method": "GET",
	      "url": "/learn/word"
      }).then(res => {
        this.quiz = res.data
        this.cleanAnswer()
        this.$nextTick(() => {
          this.focusAnswer(0)
        })
      }, err=>{
        if (err.response.status == 404) {
          this.alldone = true
        }
      })
    },
    cleanAnswer(){
      this.answers = []
      for (var i = 0; i < this.quiz.check.length; i++) {
        this.answers.push('')
      }
    },
    focusAnswer(idx){
      this.$refs["answer-"+idx][0].focus()
    },
    submitAnswer() {
      this.axios({
	      'method': 'POST',
	      'url': '/learn/word',
	      'data': {
		      'check': this.quiz.check,
		      'id': this.quiz.id,
		      'sid': this.quiz.sid,
		      'answers': this.answers
        }
      }).then(res => {
        if (res.data.result) {
          this.load_quiz()
          this.$emit('correct', this.quiz.word)
        } else {
          this.cleanAnswer()
          this.$emit('wrong', this.quiz.word)
        }
      })
    },
    tabEvent(event, idx){
      if (this.answers.length > idx+1){
        this.focusAnswer(idx+1)
    }else{

      this.focusAnswer(0)
    }
      event.preventDefault()
    },
    spaceDownEvent(event, idx){
      this.showAns[idx] = true
      event.preventDefault()
    },
    spaceUpEvent(event, idx){
      this.showAns[idx] = false
      event.preventDefault()
    }
  }
}
</script>

<style lang="scss" scoped>
  .cn{
  padding:20px 20px 0px 20px;
  }
  .quiz{
  padding:5px 20px 20px 20px;
  font-size: 1.5em;
  }
  .answer{
  padding: 0px 20px 20px 20px;
  }
</style>


================================================
FILE: ui/src/components/Dictionary.vue
================================================
<template>
<div class="main">
  <div class="dict-head">
    <span>{{word}}</span>
  </div>
  <el-collapse v-model="activeName" accordion>
    <el-collapse-item v-for="desc in define" :name="desc.seq">
      <template slot="title">
        <el-tag size="mini">{{desc.speech}}</el-tag>
        <span class="en">{{desc.cn}}</span>
      </template>
      <div>{{desc.en}}</div>
      <div v-for="eg in desc.examples">
        <el-divider content-position="left">例句</el-divider>
        <div>{{eg.en}}</div>
        <div>{{eg.cn}}</div>
      </div>
    </el-collapse-item>
  </el-collapse>
</div>
</template>

<script>
export default {
  name: 'Main',
  mounted (){
    this.load_dict()
  },
  computed: {
  },
  data () {
    return {
      start: 0,
      define: [],
      activeName: '1'
    }
  },
  props: {
    word: ''
  },
  watch: {
    word () {
      this.load_dict()
    }
  },
  methods: {
    load_dict (){
      this.axios({
        "method": "GET",
        "url": "/dictionary/"+this.word
      }).then(res => {
        this.star = res.data.star
        this.define = res.data.descriptions
      }, err=>{
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->

<style rel="stylesheet/scss" lang="scss" scoped>
.dict-head{
  text-align: left;
  font-size: 2em;
  background: #C1D5D6;
  padding: 5px;
}
.answer{
  margin: 30px;
}
.card {
  margin:100px auto;
  width:500px;
  background-color: white;
  min-height: 250px;
  border-radius: .5rem;
  box-shadow: 0 3px 0.5rem #d9d9d9;
  .quiz {
    margin: 10px 30px;
  }
  .en {
    font-size: 2rem;
  }
}
.main{
}
.el-row {
  margin-bottom: 20px;
  &:last-child {
    margin-bottom: 0;
  }
}
.en{
  line-height: 13px;
  padding: 5px 10px;
  align-items: flex-start;
  overflow: hidden;
}
</style>


================================================
FILE: ui/src/components/Login.vue
================================================
<template>
<div class="login">
<el-row>
  <el-col :span="24">
    <el-input id="name"  v-model="name" placeholder="请输入帐号">
      <template slot="prepend">帐号</template>
    </el-input>
  </el-col>
</el-row>
<el-row>
  <el-col :span="24">
    <el-input id="password" v-model="password" type="password" placeholder="请输入密码" @keyup.enter.native="login">
      <template slot="prepend">密码</template>
    </el-input>
  </el-col>
</el-row>
<el-row>
  <el-col :span="24">
    <el-button id="login" v-on:click="login" style="width:100%" type="primary">登录</el-button>
  </el-col>
</el-row>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
  data() {
    return {
      name: '',
      password: '',
      passwordErr: false
    }
  },
  computed: {
    ...mapGetters([
      'username',
    ])
  },
  methods: {
    ...mapActions([
      `fetchJWT`
    ]),
    login() {
      this.fetchJWT({
        // #Security...
        username: this.name,
        password: this.password
      }).then(()=>{
        this.$router.push({ path: '/' })
      }, (err)=>{
        console.log(err)
        this.$message({
          message: '用户名密码错误',
          type: 'warning'
        });
      })
    }
  },

  mounted() {
  }
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.login {
  margin:20% auto;
  width:300px;
}
.el-row {
  margin-bottom: 20px;
  &:last-child {
    margin-bottom: 0;
  }
}
</style>


================================================
FILE: ui/src/components/Main.vue
================================================
<template>
<div class="main">
  <div class="content">
  <el-menu  class="right-menu" :collapse="true" active-text-color="#303133">
    <el-submenu index="1">
      <template slot="title">
        <i class="el-icon-notebook-2"></i>
        <!-- <i class="el-icon-list-alt"></i> -->
        <span slot="title">单词表</span>
      </template>
      <el-menu-item-group>
        <span slot="title">单词表</span>
        <el-menu-item index="1-1" @click="openLearned">已学单词</el-menu-item>
        <el-menu-item index="1-2" @click="openToLearn">未学单词</el-menu-item>
      </el-menu-item-group>
    </el-submenu>
    <el-menu-item index="2" @click="openStatistic">
      <i class="el-icon-s-marketing"></i>
      <span slot="title">统计数据</span>
    </el-menu-item>
    <el-menu-item index="3" >
      <i class="el-icon-reading" @click="openWordlistList"></i>
      <span slot="title">单词本</span>
    </el-menu-item>
    <el-menu-item index="4" @click="settingVisible = true">
      <i class="el-icon-setting"></i>
      <span slot="title">设置</span>
    </el-menu-item>
  </el-menu>
  <el-row>
    <el-col :sm="{span: 18, offset: 3}" :md="{span: 12, offset: 6}" class="card" >
      <card ref="card" v-on:correct="onCorrect" v-on:wrong="onWrong"></card>
    </el-col>
  </el-row>
  </div>

  <el-dialog title="单词本" :visible.sync="wordlistListVisible" width="90%" class="wordlist-list" custom-class="wordlist-list-2">
    <wordlist-list ref="wordlistList"> </wordlist-list>
  </el-dialog>
  <el-dialog title="设置" :visible.sync="settingVisible">
    <wordlist-setting ref="wordlistSetting"> </wordlist-setting>
    <div slot="footer" class="dialog-footer">
      <el-button @click="settingVisible = false">取 消</el-button>
      <el-button type="primary" @click="settingSubmit">确 定</el-button>
    </div>
  </el-dialog>
  <el-dialog
    title="统计信息"
    :visible.sync="statisticVisible"
    width="80%"
    center>
    <statistic ref="statistics"></statistic>
    <span slot="footer" class="dialog-footer">
      <el-button @click="statisticVisible = false">关 闭</el-button>
    </span>
  </el-dialog>
  <el-drawer class="right-drawer"
             title="单词表 "
             custom-class="right-drawer"
             :visible.sync="showWordlist"
             size="250"
             direction="rtl">
    <wordlist ref="wordlist" class="wordlist"></wordlist>
  </el-drawer>
  <el-drawer class="bottom-drawer"
             title="词典"
             custom-class="dict-drawer"
             :visible.sync="showDict"
             :modal="false"
             direction="btt">
      <dictionary :word="word"></dictionary>
  </el-drawer>
</div>
</template>

<script>
import 'element-ui/lib/theme-chalk/display.css';
import Dictionary from './Dictionary'
import Card from './Card'
import wordlist from './wordlist'
import WordlistSetting from './wordlist/Setting'
import WordlistList from './wordlist/List'
import statistic from './statistic'
export default {
  name: 'Main',
  components: {
    Dictionary,
    Card,
    wordlist,
    WordlistSetting,
    WordlistList,
    statistic
  },
  mounted (){},
  data () {
    return {
      showDict: false,
      word:"",
      showWordlist: false,
      settingVisible: false,
      statisticVisible: false,
      wordlistListVisible: false
    }
  },
  methods: {
    openWordlistList(){
      this.wordlistListVisible = true
    },
    openStatistic(){
      this.statisticVisible = true
      this.$refs.statistics.load()
    },
    openLearned(){
      this.showWordlist = true

      this.$nextTick(() => {
        this.$refs.wordlist.openLearned()
      })
    },
    openToLearn(){
      this.showWordlist = true
      this.$nextTick(() => {
        this.$refs.wordlist.openToLearn()
      })
    },
    onWrong(word) {
      this.word = word
      this.showDict = true
      this.$message({
        message: '回答错误',
        type: 'warning'
      })
    },
    onCorrect() {
      this.word = ""
      this.showDict = false
      this.$message({
        message: '回答正确,继续努力!!',
        type: 'success'
      })
    },
    settingSubmit() {
      var self = this
      this.$refs.wordlistSetting.submit().then(()=>{
        self.$refs.card.load_quiz()
        self.settingVisible = false
        self.$message({
          message: '设置成功',
          type: 'success'
        })
      })
    }
  }
}
</script>

<style lang="scss">
  .dict-drawer{
  overflow-y: scroll;
  #el-drawer__title{
  padding: 5px 10px;
    margin: 0;
  }
  }
</style>
<style lang="scss" scoped>
  .content{
  margin: 5px;
  }

  .right-menu{
  position: fixed;
  bottom: 20px;
  right: 5px;
  }

  .content{
  padding-top: 20px;
  }
  @media(min-width: 768px){
  .content{
  padding-top: 20vh;
  }

  .right-menu{
  position: absolute;
  z-index:100;
  bottom: initial;
  right: initial;
  }
  }
  .main{
  height: 100%;
  }
  .top-padding{
  height: 20%;
  }
  .card {
  background-color: white;
  min-height: 250px;
  border-radius: .5rem;
  box-shadow: 0 3px 0.5rem #d9d9d9;
  }
  .dict {
  width: 800px;
  margin: 0 auto;
  overflow: hidden;
  }
  .right-drawer{
  max-height: 100%;
  .right-drawer-body{
  max-height: 100%;
  min-width:300px;
  }
  }
  .wordlist{
  height: 500px;
}
  </style>


================================================
FILE: ui/src/components/statistic/DayChart.vue
================================================
<template>
  <div>
    <div id="day_chart_echart" style="height: 400px; width: 100%;"></div>
  </div>
</template>
<script>
var echarts = require('echarts');
export default {
  mounted () {
    this.load_data();
  },
  data () {
    return {
      // now: new Date(),
      data: {
        exercise: {},
        review: {}
      }
    }
  },
  computed:{
    xaxis() {
      var res = []
      return this.$_.sortBy(
        this.$_.uniq(this.$_.union(
          this.$_.keys(this.data.exercise),
          this.$_.keys(this.data.review))),
        a=>{a}
      )
      return res
    },
    yaxis(){
      return [
        {name: '已学', type: 'line', data: this.$_.map(this.xaxis, date=>{
          return this.data.exercise[date] ? this.data.exercise[date] : 0
        })},
        {name: '需要复习', type: 'line', data: this.$_.map(this.xaxis, date=>{
          return this.data.review[date] ? this.data.review[date] : 0
        })}
      ]
    }
  },
  methods: {
    load_data(){
      this.axios({
	      "method": "GET",
	      "url": "/statistic/learn",
      }).then(res=>{
        this.data = res.data
        this.init_echarts()
      })
    },
    init_echarts () {
      // 基于准备好的dom,初始化echarts实例
      var echart_etl_stat = echarts.init(document.getElementById('day_chart_echart'));
      // 设置option
      var echart_etl_stat_option = {
          title: {
              text: '学习数量'
          },
          tooltip: {
              trigger: 'axis'
          },
          legend: {
              data:['已学', '需要复习'  ]
          },
          grid: {
              left: '3%',
              right: '4%',
              bottom: '3%',
              containLabel: true
          },
          xAxis: {
              type: 'category',
              boundaryGap: false,
              data: this.xaxis
          },
          yAxis: {
              type: 'value'
          },
          series: this.yaxis
      };
      // 绘制图表
      echart_etl_stat.setOption(echart_etl_stat_option);
    },
    needReview(act){
      var now = this.$moment()
      var review = this.$moment(act.review)
      return review.isBefore(now)
    },
    tp (act){
      if (this.needReview(act)){
        return 'danger'
      } else {
        return 'success'
      }
    },
    ts (act) {
      var prefix='预计复习: '
      if (this.needReview(act)){
        prefix = '记忆过期:'
      }
      return prefix + this.$moment(act.review).tz("Asia/Shanghai").fromNow()
    }
  }
}
</script>

<style lang="scss" scoped>
  ul{
  max-height: 500px;
  }
</style>


================================================
FILE: ui/src/components/statistic/index.vue
================================================
<template>
<div>
  <el-row>
    <el-col :span="24">
      <day-chart ref="dayChart"></day-chart>
    </el-col>
  </el-row>
</div>
</template>
<script>
import DayChart from './DayChart'

export default {
  components: {
    DayChart,
  },
  data() {
    return {
    };
  },
  methods: {
    load(){
      this.$refs.dayChart.load()
    }
  }
}
</script>

<style lang="scss" scoped>
</style>


================================================
FILE: ui/src/components/wordlist/Form.vue
================================================
<template>
<el-form :model="form" label-width="80px">
  <el-form-item label="名称">
    <el-input v-model="form.name"></el-input>
  </el-form-item>
  <el-form-item label="描述">
    <el-input v-model="form.description"></el-input>
  </el-form-item>
  <label class="wl-label" >单词列表:</label>
  <div class="wl-border">
    <div class="wl-container">
      <div class="wl-backdrop" ref="backdrop">
        <div class="wl-highlights" v-html="highlight"></div>
      </div>
      <textarea ref="textInput" class="wl-input" v-model="wordText"
                autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
                @keydown.enter="load_define"
                @blur="load_define"
                ></textarea>
    </div>
  </div>
  <br/>
  <el-form-item>
    <el-button type="primary" @click="submit">提交</el-button>
    <el-button @click="cancel">取消</el-button>
  </el-form-item>
</el-form>
</template>
<script>
export default {
  mounted (){
    this.load_word()
    this.$refs.textInput.addEventListener('scroll', () => {
      this.$refs.backdrop.scrollTop = this.$refs.textInput.scrollTop
    }, false)
  },
  data () {
    return {
      wordText: '',
      form: {
        name: '',
        description: '',
      },
      checkRes: {
        defines: {},
        not_dict: [],
        not_sentence: []
      }
    }
  },
  props: {
    value: ''
  },

  watch: {
    value () {
      this.load_word()
    }
  },
  computed: {
    words() {
      return this.$_.uniq(this.$_.reject(
        this.$_.map(
          this.wordText.split("\n"),
          w=>{
            return w.trim()
          }
        ), word=>{
          return !word || word === ''
        }))
    },
    highlight(){
      var texts = this.wordText
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#039;").split("\n")
      return this.$_.map(texts, l=>{
        var w = l.trim()
        var define = this.checkRes.defines[w]
        if (define){
          return '<div class="wl-line-div"><span class="highlight-word">' + l +'</span> <span class="desc">'+define.descriptions[0].description+'</span></div>'
        } else if (w ==='') {
          return l
        } else {
          return '<div class="wl-line-div"><span class="highlight-word-ne">' + l +'</span> <span class="desc">未找到定义或例句</span></div>'
        }
      }).join('')
    }
  },
  methods: {
    cancel() {
      this.$emit('cancel')
    },
    load_word() {
      this.clean()
      if (this.value){
        this.axios({
	        'method': 'GET',
	        'url': '/wordlist/'+this.value,
        }).then(res=>{
          this.form.name = res.data.name
          this.form.description = res.data.description
          this.wordText = res.data.words.join('\n')
          this.load_define()
        }, err=>{
          this.$message({
            message: '您无权访问此单词表',
            type: 'warning'
          })
          this.cancel()
        })
      }
    },
    clean(){
      this.form.name =''
      this.form.description = ''
      this.wordText = ''
      this.checkRes =  {
        defines: {},
        not_dict: [],
        not_sentence: []
      }
    },
    submit() {
      if (!this.value){
        this.axios({
          'method': 'POST',
          'url': '/wordlist',
          'data': {
            'name': this.form.name,
            'description': this.form.description,
            'words': this.words
          }
        }).then(res=>{
          this.$emit('submit')
        })
      } else {
        this.axios({
          'method': 'PUT',
	        'url': '/wordlist/'+this.value,
          'data': {
            'name': this.form.name,
            'description': this.form.description,
            'words': this.words
          }
        }).then(res=>{
          this.$emit('submit')
        })
      }
    },
    load_define(){
      this.axios({
        'method': 'PUT',
        'url': '/wordlist',
        'data': this.words
      }).then(res=>{
        this.checkRes = res.data
      })
    }
  }
}
</script>

<style lang="scss">
.wl-container, .wl-backdrop, .wl-input {
  width: 100%;
  height: 180px;
}
.wl-highlights, .wl-input {
  font-family: Consolas,Liberation Mono,Courier,monospace;
  letter-spacing: 1px;
  font-size: 14px; word-wrap: break-word;
}
.wl-border{
  border: 1px solid;
}
.wl-container {
  display: block;
  margin: 0 auto;
  transform: translateZ(0);
  -webkit-text-size-adjust: none;
  padding:1px;

}
.wl-backdrop {
  position: absolute;
  z-index: 1;
  background-color: #fff;
  overflow: auto;
  pointer-events: none;
  transition: transform 1s;
}

.wl-highlights {
  white-space: pre-wrap;
  word-wrap: break-word;
  color: transparent;
  /* color: red; */
}
.wl-input {
  display: block;
  position: absolute;
  z-index: 2;
  margin: 0;
  border: 2px solid #74637f;
  border-radius: 0;
  color: #444;
  background-color: transparent;
  overflow: auto;
  resize: none;
  transition: transform 1s;
  padding:0;
  border-style: none;
}
.wl-line-div{
  overflow: visible;
  white-space: nowrap;
  height:16px;
}
.highlight-word{
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.highlight-word-ne{
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  border-radius: 3px;
  color: transparent;
  border-bottom:1px solid red;
}
.desc{
  display: inline-block;
  overflow: hidden;
  text-overflow: ellipsis;
  overflow-y: vi;
  white-space: nowrap;
  color: gray;
  font-size: 12px;

}


mark {
  color: transparent;
  background-color: #d4e9ab; /* or whatever */
}

textarea {
  margin: 0;
  border-radius: 0;
}
textarea:focus, button:focus {
  outline: none;
  box-shadow: 0 0 0 2px #c6aada;
}
</style>


================================================
FILE: ui/src/components/wordlist/Learned.vue
================================================
<template>
<el-timeline>
    <el-timeline-item
      v-for="(activity, index) in learned"
      :key="activity.wordname"
      :type="tp(activity)"
      size="large"
      color="color"
      :timestamp="ts(activity)">
      {{activity.wordname}}
    </el-timeline-item>
  </el-timeline>
</template>
<script>
export default {
  mounted () {
    this.load()
  },
  data () {
    return {
      // now: new Date(),
      learned: []
    }
  },
  methods: {
    load () {
      this.axios({
        'method': 'GET',
        'url': '/wordlist/learned'
      }).then(res=>{
        this.learned = res.data
      })
    },
    needReview(act){
      var now = this.$moment()
      var review = this.$moment(act.review)
      return review.isBefore(now)
    },
    tp (act){
      if (this.needReview(act)){
        return 'danger'
      } else {
        return 'success'
      }
    },
    ts (act) {
      var prefix='预计复习: '
      if (this.needReview(act)){
        prefix = '记忆过期:'
      }
      return prefix + this.$moment(act.review).tz("Asia/Shanghai").fromNow()
    }
  }
}
</script>

<style lang="scss" scoped>
  ul{
  max-height: 500px;
  }
</style>


================================================
FILE: ui/src/components/wordlist/List.vue
================================================
<template>
  <div>
<div class="wordlist" v-if="show=='list'">
  <el-button type="primary" icon="el-icon-edit" size="mini" @click="create">新建</el-button>
  <el-table
    :data="lists"
    style="width: 100%">
    <el-table-column
      label="名称"
      prop="name">
    </el-table-column>
    <el-table-column
      label="单词数"
      prop="words"
      :formatter="formatWordCount">
    </el-table-column>
    <el-table-column
      prop="user"
      label="类型"
      width="100">
      <template slot-scope="scope">
        <el-tag
          :type="scope.row.user ?  'success' : 'primary'"
          disable-transitions>{{scope.row.user ?  '用户' : '系统'}}</el-tag>
      </template>
    </el-table-column>
    <el-table-column
      align="right">
      <template slot-scope="scope">
        <el-button
          v-if="scope.row.user"
          type="text"
          size="mini"
          @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
        <el-button
          v-if="scope.row.user"
          type="text danger"
          size="mini"
          @click="handleDelete(scope.$index, scope.row)">Delete</el-button>
      </template>
    </el-table-column>
  </el-table>
</div>
<div class="wordlist" v-if="show=='form'">
  <wordlist-form
    v-model="formId"
    @cancel="formCancel"
    @submit="formSubmit"
    >
  </wordlist-form>
</div>
</div>
</template>
<script>
import WordlistForm from './Form'
export default {
  components: {
    WordlistForm
  },
  mounted (){
    this.load_list()
  },
  data () {
    return {
      formId: null,
      show: 'list',
      lists: [],
      selected: {}
    }
  },
  methods: {
    formCancel(){
      this.formId = null
      this.show = 'list'
      this.load_list()
    },
    formSubmit(){
      this.formId = null
      this.show = 'list'
      this.load_list()
    },
    handleEdit(idx, row){
      this.formId = row.id
      this.show = 'form'
    },
    handleDelete(idx, row){
      this.axios({
	      'method': 'DELETE',
	      'url': '/wordlist/'+row.id,
      }).then(res=>{
        this.$message({
          message: '删除成功',
          type: 'success'
        })
        this.load_list()
      }, err=>{
        this.$message({
          message: '您无权访问此单词表',
          type: 'warning'
        })
        this.load_list()
      })
    },
    create(){
      this.selected = {}
      this.show='form'
    },
    load_list(){
      return this.axios({
        'method': 'GET',
        'url': '/wordlist'
      }).then(res=>{
        this.lists = res.data
      })
    },
    formatWordCount(row){
      return row.words.length
    }
  }
}
</script>

<style lang="scss" scoped>
ul{
  max-height: 500px;
}
</style>


================================================
FILE: ui/src/components/wordlist/Setting.vue
================================================
<template>
<el-form :model="form">
  <el-form-item label="单词本" :label-width="'70px'">
    <el-select v-model="form.wordlist" placeholder="请选择活动区域">
      <el-option v-for="item in lists" :label="item.name" :value="item.id"></el-option>
    </el-select>
  </el-form-item>
</el-form>
</template>
<script>
export default {
  mounted (){
    this.load_current()
    this.load_list()
  },
  data () {
    return {
      form: {
        wordlist: '',
      },
      lists: []
    }
  },
  methods: {
    load_current(){
      return this.axios({
	      'method': 'GET',
	      'url': '/user/wordlist',
      }).then(res=>{
        this.form.wordlist = res.data.wordlist
      })
    },
    load_list(){
      return this.axios({
	      'method': 'GET',
	      'url': '/wordlist'
      }).then(res=>{
        this.lists = res.data
      })
    },
    submit(){
      return this.axios({
	      'method': 'POST',
	      'url': 'http://127.0.0.1:8000/api/user/wordlist',
	      'data': this.form
      })
    }
  }
}
</script>

<style lang="scss" scoped>
ul{
  max-height: 500px;
}
</style>


================================================
FILE: ui/src/components/wordlist/ToLearn.vue
================================================
<template>
  <div class="list-container">
    <ul class="infinite-list" v-infinite-scroll="load" style="overflow:auto">
      <li v-for="i in to_learn" class="infinite-list-item">
        <span class="word">{{i}}</span>
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data () {
    return {
      now: new Date(),
      to_learn: []
    }
  },
  methods: {
    load () {
      this.axios({
        'method': 'GET',
        'url': '/wordlist/to_learn'
      }).then(res=>{
        this.to_learn = res.data
      })
    }
  }
}
</script>

<style lang="scss" scoped>
  ul{
  max-height: 500px;
  }
</style>


================================================
FILE: ui/src/components/wordlist/index.vue
================================================
<template>
  <el-tabs v-model="activeName"  class="tabs">
    <el-tab-pane label="已学单词" name="learned">
      <learned ref="learned" class="words"></learned>
    </el-tab-pane>
    <el-tab-pane label="未学单词" name="tolearn">
      <to-learn ref="toLearn" class="words"></to-learn>
    </el-tab-pane>
  </el-tabs>
</template>
<script>
import Learned from './Learned'
import ToLearn from './ToLearn'

export default {
  components: {
    Learned,
    ToLearn
  },
  data() {
    return {
      activeName: 'learned'
    };
  },
  methods: {
    openLearned(){
      this.$refs.learned.load()
      this.$refs.toLearn.load()
      this.activeName = 'learned'
    },
    openToLearn(){
      this.$refs.learned.load()
      this.$refs.toLearn.load()
      this.activeName = 'tolearn'
    },
  }
}
</script>

<style lang="scss" scoped>
  .tabs {
  margin: 0 20px;
}
</style>


================================================
FILE: ui/src/main.js
================================================
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import VueAxios from 'vue-axios'
import axios from 'axios'
import underscore from 'vue-underscore'


import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

import App from './App'
import router from './router'
import store from './store'
import request from './request'
import '@/permission'



Vue.use(ElementUI)
Vue.use(VueAxios, request)
Vue.use(underscore)

import moment from 'moment-timezone'
moment.tz.setDefault("UTC")
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
  moment
})

Vue.config.productionTip = false


/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})


================================================
FILE: ui/src/permission.js
================================================
import router from './router'
import store from './store'
import {getToken} from './store'

import { Message } from 'element-ui'

const whiteList = ['/login']

router.beforeEach((to, from, next) => {
  if (getToken()) {
    if (to.path === '/login') {
      next({ path: '/' })
    } else {
      store.dispatch('getUserInfo').then(res => {
        next()
      }).catch((err) => {
        store.dispatch('logOut').then(() => {
          Message.error(err || 'Verification failed, please login again')
          next({ path: '/' })
        })
      })
    }
  } else {
    if (whiteList.indexOf(to.path) !== -1) {
      next()
    } else {
      next('/login')
    }
  }
})


================================================
FILE: ui/src/request.js
================================================
import axios from 'axios'
import {MessageBox} from 'element-ui'
import store from './store'
import { getToken } from './store'

const service = axios.create({
  baseURL: process.env.BASE_API
})

var lastRefresh = (new Date()).getTime()
service.interceptors.request.use(config => {
  if (getToken()) {
    if (!config.headers['Authorization']){
      config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    var now = (new Date()).getTime()
    // debugger;
    if (now - lastRefresh > 1 * 60 * 1000 && config.url !== '/token/refresh' && config.url !== '/login') {
      store.dispatch('refreshJWT').then(() => {
        lastRefresh = now
      })
    }
  }
  return config
}, error => {
  // Do something with request error
  console.log(error) // for debug
  Promise.reject(error)
})

service.interceptors.response.use(

  response => {
    return response
  },
  error => {
    const req = error.request
    const res = error.response
    const url = new URL(req.responseURL)
    if (res.status === 401 && url.pathname !== '/login') {
      MessageBox.confirm('你已被登出', '确定登出', {
        confirmButtonText: '重新登录',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        store.dispatch('logOut').then(() => {
          location.reload()// 为了重新实例化vue-router对象 避免bug
        })
      })
    }
    return Promise.reject(error)
  }
)

export default service


================================================
FILE: ui/src/router/index.js
================================================
import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: () => import('@/components/Main')
    },
    {
      path: '/login',
      name: 'Login',
      component: () => import('@/components/Login')
    }
  ]
})


================================================
FILE: ui/src/store.js
================================================
import Vue from 'vue'
import vuex from 'vuex'
import service from './request'
//import Cookies from 'js-cookie'

// Vue.use(service)
Vue.use(vuex)

const TokenKey = 'accessToken'
const refreshKey = 'refreshToken'

export function getToken () {
  return sessionStorage.getItem(TokenKey)
}
export function getRefreshToken () {
  return sessionStorage.getItem(refreshKey)
}

export function setToken (token) {
  return sessionStorage.setItem(TokenKey, token)
}
export function setRefreshTokens (token) {
  return sessionStorage.setItem(refreshKey, token)
}

export function removeToken () {
  sessionStorage.removeItem(refreshKey)
  return sessionStorage.removeItem(TokenKey)
}

export default new vuex.Store({
  state: {
    accessToken: '',
    refreshToken: '',
    wordlist: ''
  },
  getters: {
    accessToken: state => state.accessToken,
    refreshToken: state => state.refreshToken,
    username: (state, getters) => state.accessToken ? JSON.parse(atob(getters.accessToken.split('.')[1])) : null,
  },
  mutations: {
    setTokens (state, access) {
      // When this updates, the getters and anything bound to them updates as well.
      state.accessToken = access
    },
    setRefreshTokens (state, refresh) {
      // When this updates, the getters and anything bound to them updates as well.
      state.refreshToken = refresh
    },
    setWordList (state, wordlist) {
      state.wordList = wordlist
    }
  },
  actions: {
    fetchJWT ({ commit }, { username, password }) {
      return service({
        'method': 'POST',
        'url': '/login',
        'headers': {
          'Content-Type': 'application/json; charset=utf-8'
        },
        'data': {
          'username': username,
          'password': password
        }
      }).then(res => {
        commit('setTokens', res.data.access_token)
        commit('setRefreshTokens', res.data.refresh_token)
        setToken(res.data.access_token)
        setRefreshTokens(res.data.refresh_token)
      } )
    },
    refreshJWT ({ commit, state}) {
      return service({
        'method': 'POST',
        'url': '/token/refresh',
        'headers': {
          'Content-Type': 'application/json; charset=utf-8',
          'Authorization': 'Bearer ' + getRefreshToken()
        }
      }).then(res => {
        commit('setTokens', res.data.access_token)
        // commit('setRefreshTokens', res.data.refresh_token)
        setToken(res.data.access_token, res.data.refresh_token)
      })
    },
    getUserInfo ({commit, state}) {
      return service({
        'method': 'GET',
        'url': '/user/wordlist',
        'headers': {
          'Content-Type': 'application/json; charset=utf-8',
          'Authorization': 'Bearer ' + getToken()
        }
      }).then(res => {
        commit('setWordList', res.wordlist)
      })
    },
    logOut ({commit, state}) {
      return new Promise((resolve, reject) => {
        commit('setTokens', '', '')
        removeToken()
        resolve()
      })
    }
  }
})


================================================
FILE: ui/static/.gitkeep
================================================


================================================
FILE: wordai/__init__.py
================================================


================================================
FILE: wordai/api/__init__.py
================================================
from flask import Flask, jsonify
from flask_cors import CORS

from flask_jwt_extended import (JWTManager)

app = Flask(__name__)
CORS(app)

jwt = JWTManager(app)
from wordai.models import User



@app.route('/')
def index():
    return jsonify({'message': 'Hello, World!'})

from wordai.api.apis import blueprint as api

app.register_blueprint(api, url_prefix='/api')



================================================
FILE: wordai/api/apis.py
================================================
import hashlib
import json
from datetime import datetime, timedelta

import wordai.models as models
from flask import Blueprint
from flask_jwt_extended import (JWTManager, create_access_token,
                                create_refresh_token, get_jwt_identity,
                                get_raw_jwt, jwt_refresh_token_required,
                                jwt_required)
from flask_restful import Api, Resource, abort, reqparse, request
from jsonschema import validate

blueprint = Blueprint('profile', __name__,
                    template_folder='templates',
                    static_folder='static')

api = Api(blueprint)

class api_register(object):
    def __init__(self, path):
        self.path = path

    def __call__(self, cls):
        api.add_resource(cls, self.path)
        return cls

def admin_required(f):
    def __inner__(self, *args, **kwargs):
        identify = get_jwt_identity()
        user = models.User.find_by_username(identify)
        if user and user.role == 'admin':
                return f(self, user, *args, **kwargs)
        return {
            'message': 'Not found',
        }, 404
    return jwt_required(__inner__)

def user_required(f):
    def __inner__(self, *args, **kwargs):
        identify = get_jwt_identity()
        user = models.User.find_by_username(identify)
        if user and user.role in ['admin', 'user']  :
                return f(self, user, *args, **kwargs)
        return {
            'message': 'Not found',
        }, 404
    return jwt_required(__inner__)

user_parser = reqparse.RequestParser()
user_parser.add_argument('username', help='This username cannot be blank', required=True)
user_parser.add_argument('password', help='This password cannot be blank', required=True)

@api_register("/registration")
class UserRegistration(Resource):
    def post(self):
        return {'message': 'User registration'}


@api_register("/login")
class UserLogin(Resource):
    def post(self):
        data = user_parser.parse_args()
        current_user = models.User.check_user(data['username'], data['password'])
        if not current_user:
            abort(401)
            return {
                'message': 'User {} doesn\'t exist'.format(data['username']),
            }

        access_token = create_access_token(identity=data['username'])
        refresh_token = create_refresh_token(identity=data['username'])
        return {
            'message': 'Logged in as {}'.format(current_user.username),
            'role': current_user.role,
            'access_token': access_token,
            'refresh_token': refresh_token
        }

@api_register("/token/refresh")
class TokenRefresh(Resource):
    @jwt_refresh_token_required
    def post(self):
        current_user = get_jwt_identity()
        if current_user:
            access_token = create_access_token(identity=current_user)
            return {
                'access_token': access_token}
        abort(401)
        return {'message': 'invalid refresh token'}


@api_register("/wordlist")
class WordListList(Resource):
    @user_required
    def get(self, user):
        return [json.loads(x.to_json()) for x in user.wordlists()]

    @user_required
    def put(self, user):
        schema = {
            "type": "array",
            "items": {"type": "string"},
            "uniqueItems": True
        }
        try:
            body = request.json
            validate(instance=body, schema=schema)
            wordok, not_has, wnot_has = models.WordList.check_word(*body)
            defines = models.Word.search_words(*wordok)
            return {
                "defines": {w['word']: w for w in json.loads(defines.to_json())},
                "not_dict": wnot_has,
                "not_sentence": not_has,
            }
        except Exception as err:
            return {
                "message": "invalid request body",
                "error": str(err)
            }, 422
    @user_required
    def post(self, user):
        schema = {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "description": {"type": "string"},
                "words": {
                    "type": "array",
                    "items": {"type": "string"},
                    "uniqueItems": True
                }
            }
        }
        try:
            body = request.json
            validate(instance=body, schema=schema)
            wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])
            body['words'] = list(wordok)
            wordlist = models.WordList(**body)
            wordlist.user = user
            wordlist.save()
            return {
                "message": "ok",
                "has": list(wordok),
                "not_dict": wnot_has,
                "not_sentence": not_has,
            }
        except Exception as err:
            return {
                "message": "invalid request body",
                "error": str(err)
            }, 422

@api_register("/wordlist/<string:lid>")
class WordListItem(Resource):
    @user_required
    def get(self, user, lid):
        print(lid)
        return json.loads(user.wordlists().filter(id=lid).first().to_json())

    @user_required
    def put(self, user, lid):
        wordlist = models.WordList.objects(user=user, id=lid).first()
        if not wordlist:
            return {
                "message": "wordlist not exists",
            }, 404
        schema = {
            "type": "object",
            "properties": {
                "name": {"type": "string"},
                "description": {"type": "string"},
                "words": {
                    "type": "array",
                    "items": {"type": "string"},
                    "uniqueItems": True
                }
            }
        }
        try:
            body = request.json
            validate(instance=body, schema=schema)
            wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])
            wordlist.words = wordok
            wordlist.name = body['name']
            wordlist.description = body['description']
            wordlist.user = user
            wordlist.save()
            return {
                "message": "ok",
                "has": list(wordok),
                "not_dict": wnot_has,
                "not_sentence": not_has,
            }
        except Exception as err:
            return {
                "message": "invalid request body",
                "error": str(err)
            }, 422

    @user_required
    def delete(self, user, lid):
        wordlist = models.WordList.objects(user=user, id=lid).first()
        if not wordlist:
            return {
                "message": "wordlist not exists",
            }, 404
        wordlist.delete()

@api_register("/user/wordlist")
class UserWordList(Resource):
    @user_required
    def get(self, user):
        if not user.wordlist:
            return {
                "message", "wordlist not set"
            }, 404
        data = json.loads(user.wordlist.to_json())
        return {
            "message": "ok",
            "wordlist": data['id'],
            "wordlist_name": data['name']
        }
    @user_required
    def post(self, user):
        parser = reqparse.RequestParser()
        parser.add_argument('wordlist', help='This wordlist cannot be blank', required=True)
        wordlist_id = parser.parse_args()
        wordlist = models.WordList.objects(id=wordlist_id['wordlist']).first()
        user.wordlist = wordlist
        user.save()
        return {
            "message": "ok",
            "wordlist": wordlist.name
        }


@api_register("/learn/word")
class LearnNext(Resource):
    @user_required
    def get(self, user):
        ex = user.next_exercise()
        if ex:
            sentence_id = json.loads(ex.sentence.to_json())['id']
            word_id = json.loads(ex.word.to_json())['id']
            return {
                "id": word_id,
                "word": ex.word.word,
                "message": "ok",
                "cloze": ex.cloze,
                "cn": ex.sentence.chn,
                "sid": sentence_id,
                "answers": [a for a in ex.answers],
                "check": [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in ex.answers]
            }
        else:
            return {
                "message": "no word need exercise"
            }, 404
    @user_required
    def post(self, user):
        parser = reqparse.RequestParser()
        parser.add_argument('id', help='This answers cannot be blank', required=True)
        parser.add_argument('sid', help='This answers cannot be blank', required=True)
        parser.add_argument('answers', help='This answers cannot be blank', required=True,action='append')
        parser.add_argument('check', help='This answer_check cannot be blank', required=True, action='append')
        data = parser.parse_args()
        word_id = data['id']
        word = models.Word.objects(id=word_id).first()
        if not word:
            return {
                "message": "word not exist"
            }, 404
        sentence_id = data['sid']
        answers = data['answers']
        check = data['check']
        check_res = [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in answers]
        result = check == check_res
        slog = models.SentenceLog(sentence=sentence_id, result=result, time=datetime.utcnow())
        models.ExerciseLog.objects(user=user, word=word).update_one(
            push__sentences=slog, wordname=word.word,
            upsert=True)
        log = models.ExerciseLog.objects(user=user, word=word).first()
        log.calucate_review()
        log.save()
        return {
            "message": "ok",
            "result": result,
        }


@api_register("/dictionary/<string:word>")
class Dictionary(Resource):
    @user_required
    def get(self, user, word):
        define = models.Word.objects(word=word).first()
        if define:
            return json.loads(define.to_json())
        else:
            return {"message": "not found"}, 404


@api_register("/wordlist/learned")
class WordlistLearned(Resource):
    @user_required
    def get(self, user):
        words = user.wordlist.user_learned(user).only("wordname", "review")
        return json.loads(words.to_json())


@api_register("/wordlist/to_learn")
class WordlistToLearn(Resource):
    @user_required
    def get(self, user):
        words = user.wordlist.user_to_learn(user)
        return words

@api_register("/statistic/learn")
class StatisticLearn(Resource):
    @user_required
    def get(self, user):
        return {
            'exercise': models.ExerciseLog.exercise_count(
                user,
                datetime.now()-timedelta(days=7),
                datetime.now()+timedelta(days=7)
            ),
            'review': models.ExerciseLog.review_count(
                user,
                datetime.now()-timedelta(days=7),
                datetime.now()+timedelta(days=7)
            )
        }


================================================
FILE: wordai/models/__init__.py
================================================
from mongoengine import connect

from config import mongo_config

connect(**mongo_config())

from .models import *


================================================
FILE: wordai/models/models.py
================================================

from datetime import datetime, timedelta

import bcrypt
from mongoengine import (BooleanField, DateTimeField, EmbeddedDocumentField,
                         FloatField, IntField, ListField, ReferenceField,
                         SortedListField, StringField)
from mongoengine_goodjson import Document, EmbeddedDocument


class DictExample(EmbeddedDocument):
    en = StringField(required=True)
    cn = StringField(required=True)


class DictDescInfo(EmbeddedDocument):
    name = StringField(required=True)
    value = StringField(required=True)

class DictDescription(EmbeddedDocument):
    description = StringField(required=True)
    seq = IntField(required=True)
    cn = StringField(required=True)
    en = StringField(required=True)
    speech = StringField(required=True)
    infos = ListField(EmbeddedDocumentField(DictDescInfo))
    examples = ListField(EmbeddedDocumentField(DictExample))


class Word(Document):
    word = StringField(required=True)
    star = IntField(max_value=5)
    descriptions = ListField(EmbeddedDocumentField(DictDescription))
    meta = {
        'indexes': [
            ('word', '-star'),
        ]
    }
    @classmethod
    def search_words(cls, *words):
        return Word.objects(word__in=words)

    @classmethod
    def has(cls, *words):
        has = []
        not_has = []
        for word in words:
            if Word.objects(word=word).count() > 0:
                has.append(word)
            else:
                not_has.append(word)
        return has, not_has


    @classmethod
    def search_word(cls, word):
        return Word.objects(word=word).first()

class Sentence(Document):
    eng = StringField(required=True)
    chn = StringField(required=True)
    score = FloatField()
    words = ListField(StringField())
    pos_tag = ListField(StringField())
    roots = ListField(StringField())
    typ = StringField(required=True)
    meta = {
        'indexes': [
            ('roots', '-score'),
            ('typ', 'roots', '-score')
        ]
    }

    @classmethod
    def has(cls, *words):
        has = []
        not_has = []
        for word in words:
            if cls.objects(roots=word).count() > 0:
                has.append(word)
            else:
                not_has.append(word)
        return has, not_has

    @classmethod
    def search_by_root(cls, word):
        return cls.objects(roots=word, typ='dictexams').order_by('-score')

    def cloze(self, word):
        answers = []
        cloz = self.eng
        for idx, root in enumerate(self.roots):
            if root == word:
                answers.append(self.words[idx])
                cloz = cloz.replace(self.words[idx], '[___]')
        return cloz, answers

    
class User(Document):
    username = StringField(required=True, unique=True)
    encrypted_password = StringField(required=True)
    salt = StringField(required=True, default=lambda: bcrypt.gensalt().decode())
    role = StringField(required=True, default='user')
    wordlist = ReferenceField('WordList', required=True)
    meta = {
        'indexes': [
            '#username'
        ]
    }
    def __init__(self, *args, **kwargs):
        passwd = kwargs.get("password")
        if passwd:
            del kwargs['password']
        Document.__init__(self, *args, **kwargs)
        if passwd:
            self.password = passwd

    @classmethod
    def find_by_username(cls, username):
        return cls.objects(username=username).first()

    @classmethod
    def check_user(cls, username, passwd):
        user = cls.objects(username=username).first()
        if user and user.check_password(passwd):
            return user
        return None

    @property
    def password(self):
        return ""

    @password.setter
    def password(self, passwd):
        passwd = passwd.encode()
        salt = bcrypt.gensalt()
        self.encrypted_password = bcrypt.hashpw(passwd, salt).decode()

    def check_password(self, passwd):
        passwd = passwd.encode()
        if (not self.encrypted_password) or (not self.salt):
            return False
        # hashed = bcrypt.hashpw(passwd, self.salt)
        return bcrypt.checkpw(passwd, self.encrypted_password.encode())

    def wordlist_exercise_log(self):
        list_words = self.wordlist.words
        return ExerciseLog.objects(user=self, wordname__in=list_words)

    def word_exercise_log(self, word):
        return ExerciseLog.objects(user=self, wordname=word).first()

    def new_words(self):
        list_words = self.wordlist.words
        learned = [l.wordname for l in self.wordlist_exercise_log().only('wordname')]
        return list(set(list_words) - set(learned))

    def due_words(self):
        return [l.wordname for l in self.wordlist_exercise_log().filter(review__lt=datetime.utcnow()).only('wordname')]

    def next_word(self):
        words = self.due_words()
        if words :
            return words[0]
        words = self.new_words()
        if words :
            return words[0]
        return None

    def next_exercise(self):
        word = self.next_word()
        if not word:
            return None
        word_item = Word.search_word(word)
        if not word_item:
            return None
        now = datetime.utcnow()
        exlog = self.word_exercise_log(word)
        sentence_log = exlog.sentences if exlog else []
        log = {l.sentence.id: score_w(now - l.time, l.result) for l in sentence_log}
        sentences = Sentence.search_by_root(word).limit(100)
        top = None
        top_score = 0
        for s in sentences:
            score = s.score * log.get(s.id, 1)
            if score > top_score:
                top = s
                top_score = score
        if not top:
            return None
        class cloze:
            def __init__(self, word, quiz, answers, sentence):
                self.word = word
                self.cloze = quiz
                self.answers = answers
                self.sentence = sentence
        quiz, answer = top.cloze(word)
        return cloze(word_item, quiz, answer, top)
    def wordlists(self):
        return WordList.objects(user__in=[None, self])


class WordList(Document):
    name = StringField(required=True)
    description = StringField(required=True)
    words = ListField(StringField())
    user = ReferenceField(User)
    meta = {
        'indexes': [
            ('user', 'name'),
        ]
    }

    @classmethod
    def check_word(self, *words):
        has, not_has = Sentence.has(*words)
        whas, wnot_has = Word.has(*words)
        has = set(has)
        whas = set(whas)
        ok = list(has.intersection(whas))
        return ok, not_has, wnot_has


    def user_learned(self, user):
        return ExerciseLog.objects(user=user, wordname__in=list(self.words))

    def user_to_learn(self, user):
        learned = set([e.wordname for e in ExerciseLog.objects(user=user).only('wordname')])
        words = set(self.words)
        return list(words - learned)


def score_w(delta, result):
    if delta < timedelta(minutes=20):
        w = (1-.58)
    elif delta < timedelta(hours=1):
        w = (1-.44)
    elif delta < timedelta(hours=9):
        w = (1-.36)
    elif delta < timedelta(days=1):
        w = (1-.33)
    elif delta < timedelta(days=2):
        w = (1-.28)
    elif delta < timedelta(days=6):
        w = (1-.25)
    elif delta < timedelta(days=31):
        w = (1-.21)
    elif delta < timedelta(days=60):
        w = (1-.10)
    else:
        w = .99
    return w if result else w * 1.5

class SentenceLog(EmbeddedDocument):
    sentence = ReferenceField(Sentence, required=True)
    result = BooleanField(required=True)
    time = DateTimeField(required=True)
ebbinghaus = {
    0: timedelta(minutes=1),
    1: timedelta(minutes=5),
    2: timedelta(minutes=30),
    3: timedelta(hours=12),
    4: timedelta(days=1),
    5: timedelta(days=2),
    6: timedelta(days=4),
    7: timedelta(days=7),
    8: timedelta(days=15),
}


class ExerciseLog(Document):
    user = ReferenceField(User, required=True)
    wordname = StringField(required=True)
    word = ReferenceField(Word, required=True)
    review = DateTimeField(required=True)
    sentences = SortedListField(
        EmbeddedDocumentField(SentenceLog),
        ordering="time", reverse=True)

    def calucate_review(self):
        count = 0
        for s in reversed(self.sentences):
            if not s.result:
                break
            count += 1
        delta = ebbinghaus.get(count, timedelta(days=15))
        self.review = datetime.utcnow() + delta

    @classmethod
    def review_count(cls, user, start, end):
        data =  ExerciseLog.objects(user=user, review__gt=start, review__lt=end).\
            aggregate(
                {'$group' : {'_id' : {'$dateToString' : {
                    'date': "$review",
                    'format': "%Y/%m/%d",
                } }, 'count' : { '$sum' : 1 }}},
                {'$sort': {'_id': 1}}
            )
        return {i['_id']:i['count'] for i in list(data)}

    @classmethod
    def exercise_count(cls, user, start, end):
        data = ExerciseLog.objects(user=user).\
            aggregate(
                {"$match": {"sentences.time": {"$gte": start, "$lte": end}}},
                {"$project":{'_id':0, 'sentences.time': 1, 'sentences.result':1}},
                {"$unwind":"$sentences"},
                {"$match": {"sentences.time": {"$gte": start, "$lte": end}}},
                {'$project': {'day': '$sentences.time',
                              'result': '$sentences.result'}},
                {'$group': { '_id': {'$dateToString' : {
                    'date': "$day",
                    'format': "%Y/%m/%d",
                } }, 'count': { '$sum': 1 } } },
                {'$sort': {'_id': 1}}
            )
        return {i['_id']:i['count'] for i in list(data)}
Download .txt
gitextract_fuu3vrhz/

├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands/
│   └── __init__.py
├── config/
│   ├── __init__.py
│   └── wsgi.ini
├── data/
│   ├── __init__.py
│   └── dict.py
├── jsondict/
│   ├── anki.py
│   └── search.py
├── manage.py
├── nginx-server.conf
├── nginx.conf
├── requirements.txt
├── start.sh
├── ui/
│   ├── .babelrc
│   ├── .editorconfig
│   ├── .eslintignore
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── .postcssrc.js
│   ├── README.md
│   ├── build/
│   │   ├── build.js
│   │   ├── check-versions.js
│   │   ├── utils.js
│   │   ├── vue-loader.conf.js
│   │   ├── webpack.base.conf.js
│   │   ├── webpack.dev.conf.js
│   │   └── webpack.prod.conf.js
│   ├── config/
│   │   ├── dev.env.js
│   │   ├── index.js
│   │   └── prod.env.js
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── App.vue
│   │   ├── components/
│   │   │   ├── Card.vue
│   │   │   ├── Dictionary.vue
│   │   │   ├── Login.vue
│   │   │   ├── Main.vue
│   │   │   ├── statistic/
│   │   │   │   ├── DayChart.vue
│   │   │   │   └── index.vue
│   │   │   └── wordlist/
│   │   │       ├── Form.vue
│   │   │       ├── Learned.vue
│   │   │       ├── List.vue
│   │   │       ├── Setting.vue
│   │   │       ├── ToLearn.vue
│   │   │       └── index.vue
│   │   ├── main.js
│   │   ├── permission.js
│   │   ├── request.js
│   │   ├── router/
│   │   │   └── index.js
│   │   └── store.js
│   └── static/
│       └── .gitkeep
└── wordai/
    ├── __init__.py
    ├── api/
    │   ├── __init__.py
    │   └── apis.py
    └── models/
        ├── __init__.py
        └── models.py
Download .txt
SYMBOL INDEX (115 symbols across 17 files)

FILE: app.py
  function run_dev (line 6) | def run_dev():

FILE: commands/__init__.py
  function dict_to_mongo (line 6) | def dict_to_mongo():
  function sentence_to_mongo (line 14) | def sentence_to_mongo(*typ):
  function _sentence_to_mongo (line 20) | def _sentence_to_mongo(typ, items):
  function new_user (line 63) | def new_user(username, passwd, role='user', *args):
  function dict_star_word_list (line 67) | def dict_star_word_list():
  function dict_parse (line 73) | def dict_parse():
  function run (line 84) | def run(method, *args):

FILE: config/__init__.py
  function c (line 3) | def c(name, default=None):
  function mongo_config (line 6) | def mongo_config():
  function api_env (line 15) | def api_env():
  function flask_config (line 22) | def flask_config():

FILE: data/__init__.py
  function data_file (line 10) | def data_file(filename):
  function load_dict (line 13) | def load_dict():
  function load_sentence (line 18) | def load_sentence(*typ):

FILE: data/dict.py
  class Description (line 5) | class Description(object):
    method __init__ (line 6) | def __init__(self, des):
    method read_seq (line 29) | def read_seq(self):
    method read_word_speech (line 44) | def read_word_speech(self):
    method read_cn_and_en (line 61) | def read_cn_and_en(self):
    method read_info (line 82) | def read_info(self):

FILE: jsondict/anki.py
  function cloze_word (line 7) | def cloze_word(sentense, word, level="1"):

FILE: jsondict/search.py
  function search (line 11) | def search(word):
  function format (line 17) | def format(item):
  function description (line 26) | def description(word):
  function search_string (line 32) | def search_string(word):

FILE: manage.py
  function command (line 11) | def command(method, *args):
  function usage (line 20) | def usage():

FILE: ui/build/check-versions.js
  function exec (line 7) | function exec (cmd) {

FILE: ui/build/utils.js
  function generateLoaders (line 33) | function generateLoaders (loader, loaderOptions) {

FILE: ui/build/webpack.base.conf.js
  function resolve (line 7) | function resolve (dir) {

FILE: ui/build/webpack.dev.conf.js
  constant HOST (line 13) | const HOST = process.env.HOST
  constant PORT (line 14) | const PORT = process.env.PORT && Number(process.env.PORT)

FILE: ui/build/webpack.prod.conf.js
  method minChunks (line 84) | minChunks (module) {

FILE: ui/src/store.js
  function getToken (line 12) | function getToken () {
  function getRefreshToken (line 15) | function getRefreshToken () {
  function setToken (line 19) | function setToken (token) {
  function setRefreshTokens (line 22) | function setRefreshTokens (token) {
  function removeToken (line 26) | function removeToken () {
  method setTokens (line 43) | setTokens (state, access) {
  method setRefreshTokens (line 47) | setRefreshTokens (state, refresh) {
  method setWordList (line 51) | setWordList (state, wordlist) {
  method fetchJWT (line 56) | fetchJWT ({ commit }, { username, password }) {
  method refreshJWT (line 74) | refreshJWT ({ commit, state}) {
  method getUserInfo (line 88) | getUserInfo ({commit, state}) {
  method logOut (line 100) | logOut ({commit, state}) {

FILE: wordai/api/__init__.py
  function index (line 15) | def index():

FILE: wordai/api/apis.py
  class api_register (line 20) | class api_register(object):
    method __init__ (line 21) | def __init__(self, path):
    method __call__ (line 24) | def __call__(self, cls):
  function admin_required (line 28) | def admin_required(f):
  function user_required (line 39) | def user_required(f):
  class UserRegistration (line 55) | class UserRegistration(Resource):
    method post (line 56) | def post(self):
  class UserLogin (line 61) | class UserLogin(Resource):
    method post (line 62) | def post(self):
  class TokenRefresh (line 81) | class TokenRefresh(Resource):
    method post (line 83) | def post(self):
  class WordListList (line 94) | class WordListList(Resource):
    method get (line 96) | def get(self, user):
    method put (line 100) | def put(self, user):
    method post (line 122) | def post(self, user):
  class WordListItem (line 156) | class WordListItem(Resource):
    method get (line 158) | def get(self, user, lid):
    method put (line 163) | def put(self, user, lid):
    method delete (line 203) | def delete(self, user, lid):
  class UserWordList (line 212) | class UserWordList(Resource):
    method get (line 214) | def get(self, user):
    method post (line 226) | def post(self, user):
  class LearnNext (line 240) | class LearnNext(Resource):
    method get (line 242) | def get(self, user):
    method post (line 262) | def post(self, user):
  class Dictionary (line 294) | class Dictionary(Resource):
    method get (line 296) | def get(self, user, word):
  class WordlistLearned (line 305) | class WordlistLearned(Resource):
    method get (line 307) | def get(self, user):
  class WordlistToLearn (line 313) | class WordlistToLearn(Resource):
    method get (line 315) | def get(self, user):
  class StatisticLearn (line 320) | class StatisticLearn(Resource):
    method get (line 322) | def get(self, user):

FILE: wordai/models/models.py
  class DictExample (line 11) | class DictExample(EmbeddedDocument):
  class DictDescInfo (line 16) | class DictDescInfo(EmbeddedDocument):
  class DictDescription (line 20) | class DictDescription(EmbeddedDocument):
  class Word (line 30) | class Word(Document):
    method search_words (line 40) | def search_words(cls, *words):
    method has (line 44) | def has(cls, *words):
    method search_word (line 56) | def search_word(cls, word):
  class Sentence (line 59) | class Sentence(Document):
    method has (line 75) | def has(cls, *words):
    method search_by_root (line 86) | def search_by_root(cls, word):
    method cloze (line 89) | def cloze(self, word):
  class User (line 99) | class User(Document):
    method __init__ (line 110) | def __init__(self, *args, **kwargs):
    method find_by_username (line 119) | def find_by_username(cls, username):
    method check_user (line 123) | def check_user(cls, username, passwd):
    method password (line 130) | def password(self):
    method password (line 134) | def password(self, passwd):
    method check_password (line 139) | def check_password(self, passwd):
    method wordlist_exercise_log (line 146) | def wordlist_exercise_log(self):
    method word_exercise_log (line 150) | def word_exercise_log(self, word):
    method new_words (line 153) | def new_words(self):
    method due_words (line 158) | def due_words(self):
    method next_word (line 161) | def next_word(self):
    method next_exercise (line 170) | def next_exercise(self):
    method wordlists (line 199) | def wordlists(self):
  class WordList (line 203) | class WordList(Document):
    method check_word (line 215) | def check_word(self, *words):
    method user_learned (line 224) | def user_learned(self, user):
    method user_to_learn (line 227) | def user_to_learn(self, user):
  function score_w (line 233) | def score_w(delta, result):
  class SentenceLog (line 254) | class SentenceLog(EmbeddedDocument):
  class ExerciseLog (line 271) | class ExerciseLog(Document):
    method calucate_review (line 280) | def calucate_review(self):
    method review_count (line 290) | def review_count(cls, user, start, end):
    method exercise_count (line 302) | def exercise_count(cls, user, start, end):
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (104K chars).
[
  {
    "path": ".dockerignore",
    "chars": 15,
    "preview": "ui/node_modules"
  },
  {
    "path": ".gitignore",
    "chars": 2460,
    "preview": "### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore\n\n# -*- mode:"
  },
  {
    "path": "Dockerfile",
    "chars": 591,
    "preview": "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"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2019 Senghoo Kim\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 1082,
    "preview": "# Word AI\n\n一款使用自然语言处理辅助背单词的程序。\n\n通过导入TMX翻译语料库,进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。\n\n*PS: 因为版权原因,不提供词典文件和TMX语料文件。需"
  },
  {
    "path": "app.py",
    "chars": 144,
    "preview": "from wordai.api import app\nfrom config import api_env, flask_config\n\napp.config.update(flask_config())\n\ndef run_dev():\n "
  },
  {
    "path": "commands/__init__.py",
    "chars": 3173,
    "preview": "import string\nfrom data import load_dict, load_sentence\nfrom data.dict import Description\nfrom wordai.models import Word"
  },
  {
    "path": "config/__init__.py",
    "chars": 619,
    "preview": "import os\n\ndef c(name, default=None):\n    return os.environ.get(name.upper(), default)\n\ndef mongo_config():\n    return {"
  },
  {
    "path": "config/wsgi.ini",
    "chars": 144,
    "preview": "[uwsgi]\nmodule = app:app\nmaster = true\nprocesses = 3\n\nsocket = /var/run/wordai.sock\nlogto = /var/log/wordai.log\nchmod-so"
  },
  {
    "path": "data/__init__.py",
    "chars": 1044,
    "preview": "# -*- coding: utf-8 -*-\n\nimport os\nimport json\nfrom itertools import chain\nfrom translate.storage.tmx import tmxfile\n\nDA"
  },
  {
    "path": "data/dict.py",
    "chars": 2521,
    "preview": "# -*- coding: utf-8 -*-\nimport string\nimport re\n\nclass Description(object):\n    def __init__(self, des):\n        self.de"
  },
  {
    "path": "jsondict/anki.py",
    "chars": 891,
    "preview": "import sys\nimport csv\nfrom search import search_string, description\n\nres = []\n\ndef cloze_word(sentense, word, level=\"1\")"
  },
  {
    "path": "jsondict/search.py",
    "chars": 892,
    "preview": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\nimport sys\nimport json\n\n\nwith open('dict.json', 'r') as f:\n    dictionary = j"
  },
  {
    "path": "manage.py",
    "chars": 517,
    "preview": "import os\nimport sys\n\nROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, ROOT_DIR)\n\n"
  },
  {
    "path": "nginx-server.conf",
    "chars": 213,
    "preview": "server {\n    listen 80;\n    server_name 0.0.0.0;\n\n    location /api/ {\n        include uwsgi_params;\n        uwsgi_pass "
  },
  {
    "path": "nginx.conf",
    "chars": 480,
    "preview": "user root;\nworker_processes auto;\npid /run/nginx.pid;\ninclude /etc/nginx/modules-enabled/*.conf;\n\nevents {\n\tworker_conne"
  },
  {
    "path": "requirements.txt",
    "chars": 129,
    "preview": "mongoengine\nmongoengine-goodjson\nlxml\ntranslate-toolkit\nnltk\nflask\nflask-restful\nflask-jwt-extended\nflask-cors\nbcrypt\njs"
  },
  {
    "path": "start.sh",
    "chars": 63,
    "preview": "#!/bin/bash\n\nservice nginx start\nuwsgi --ini config/wsgi.ini \n\n"
  },
  {
    "path": "ui/.babelrc",
    "chars": 230,
    "preview": "{\n  \"presets\": [\n    [\"env\", {\n      \"modules\": false,\n      \"targets\": {\n        \"browsers\": [\"> 1%\", \"last 2 versions\""
  },
  {
    "path": "ui/.editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": "ui/.eslintignore",
    "chars": 30,
    "preview": "/build/\n/config/\n/dist/\n/*.js\n"
  },
  {
    "path": "ui/.eslintrc.js",
    "chars": 791,
    "preview": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n  root: true,\n  parserOptions: {\n    parser: 'babe"
  },
  {
    "path": "ui/.gitignore",
    "chars": 154,
    "preview": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vsc"
  },
  {
    "path": "ui/.postcssrc.js",
    "chars": 246,
    "preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    \"postcss-import\": {},"
  },
  {
    "path": "ui/README.md",
    "chars": 454,
    "preview": "# 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:8"
  },
  {
    "path": "ui/build/build.js",
    "chars": 1198,
    "preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
  },
  {
    "path": "ui/build/check-versions.js",
    "chars": 1290,
    "preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
  },
  {
    "path": "ui/build/utils.js",
    "chars": 2587,
    "preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst ExtractTextPlugin = require('extract"
  },
  {
    "path": "ui/build/vue-loader.conf.js",
    "chars": 553,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_"
  },
  {
    "path": "ui/build/webpack.base.conf.js",
    "chars": 2464,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vue"
  },
  {
    "path": "ui/build/webpack.dev.conf.js",
    "chars": 3004,
    "preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
  },
  {
    "path": "ui/build/webpack.prod.conf.js",
    "chars": 5055,
    "preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
  },
  {
    "path": "ui/config/dev.env.js",
    "chars": 200,
    "preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
  },
  {
    "path": "ui/config/index.js",
    "chars": 2291,
    "preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
  },
  {
    "path": "ui/config/prod.env.js",
    "chars": 84,
    "preview": "'use strict'\nmodule.exports = {\n  NODE_ENV: '\"production\"',\n  BASE_API: '\"/api\"',\n}\n"
  },
  {
    "path": "ui/index.html",
    "chars": 373,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initia"
  },
  {
    "path": "ui/package.json",
    "chars": 2548,
    "preview": "{\n  \"name\": \"wordai\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Word AI\",\n  \"author\": \"Senghoo Kim <shkdmb@gmail.com>\",\n  "
  },
  {
    "path": "ui/src/App.vue",
    "chars": 319,
    "preview": "<template>\n  <div id=\"app\">\n    <router-view/>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'App'\n}\n</script>"
  },
  {
    "path": "ui/src/components/Card.vue",
    "chars": 3159,
    "preview": "<template>\n  <div class=\"card\" v-if=\"!alldone\">\n    <div class=\"cn\" >\n      <span>{{quiz.cn}}</span>\n    </div>\n    <div"
  },
  {
    "path": "ui/src/components/Dictionary.vue",
    "chars": 1804,
    "preview": "<template>\n<div class=\"main\">\n  <div class=\"dict-head\">\n    <span>{{word}}</span>\n  </div>\n  <el-collapse v-model=\"activ"
  },
  {
    "path": "ui/src/components/Login.vue",
    "chars": 1437,
    "preview": "<template>\n<div class=\"login\">\n<el-row>\n  <el-col :span=\"24\">\n    <el-input id=\"name\"  v-model=\"name\" placeholder=\"请输入帐号"
  },
  {
    "path": "ui/src/components/Main.vue",
    "chars": 5191,
    "preview": "<template>\n<div class=\"main\">\n  <div class=\"content\">\n  <el-menu  class=\"right-menu\" :collapse=\"true\" active-text-color="
  },
  {
    "path": "ui/src/components/statistic/DayChart.vue",
    "chars": 2525,
    "preview": "<template>\n  <div>\n    <div id=\"day_chart_echart\" style=\"height: 400px; width: 100%;\"></div>\n  </div>\n</template>\n<scrip"
  },
  {
    "path": "ui/src/components/statistic/index.vue",
    "chars": 391,
    "preview": "<template>\n<div>\n  <el-row>\n    <el-col :span=\"24\">\n      <day-chart ref=\"dayChart\"></day-chart>\n    </el-col>\n  </el-ro"
  },
  {
    "path": "ui/src/components/wordlist/Form.vue",
    "chars": 5824,
    "preview": "<template>\n<el-form :model=\"form\" label-width=\"80px\">\n  <el-form-item label=\"名称\">\n    <el-input v-model=\"form.name\"></el"
  },
  {
    "path": "ui/src/components/wordlist/Learned.vue",
    "chars": 1155,
    "preview": "<template>\n<el-timeline>\n    <el-timeline-item\n      v-for=\"(activity, index) in learned\"\n      :key=\"activity.wordname\""
  },
  {
    "path": "ui/src/components/wordlist/List.vue",
    "chars": 2679,
    "preview": "<template>\n  <div>\n<div class=\"wordlist\" v-if=\"show=='list'\">\n  <el-button type=\"primary\" icon=\"el-icon-edit\" size=\"mini"
  },
  {
    "path": "ui/src/components/wordlist/Setting.vue",
    "chars": 1082,
    "preview": "<template>\n<el-form :model=\"form\">\n  <el-form-item label=\"单词本\" :label-width=\"'70px'\">\n    <el-select v-model=\"form.wordl"
  },
  {
    "path": "ui/src/components/wordlist/ToLearn.vue",
    "chars": 627,
    "preview": "<template>\n  <div class=\"list-container\">\n    <ul class=\"infinite-list\" v-infinite-scroll=\"load\" style=\"overflow:auto\">\n"
  },
  {
    "path": "ui/src/components/wordlist/index.vue",
    "chars": 868,
    "preview": "<template>\n  <el-tabs v-model=\"activeName\"  class=\"tabs\">\n    <el-tab-pane label=\"已学单词\" name=\"learned\">\n      <learned r"
  },
  {
    "path": "ui/src/main.js",
    "chars": 846,
    "preview": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base."
  },
  {
    "path": "ui/src/permission.js",
    "chars": 674,
    "preview": "import router from './router'\nimport store from './store'\nimport {getToken} from './store'\n\nimport { Message } from 'ele"
  },
  {
    "path": "ui/src/request.js",
    "chars": 1426,
    "preview": "import axios from 'axios'\nimport {MessageBox} from 'element-ui'\nimport store from './store'\nimport { getToken } from './"
  },
  {
    "path": "ui/src/router/index.js",
    "chars": 333,
    "preview": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\nexport default new Router({\n  routes: [\n    {\n  "
  },
  {
    "path": "ui/src/store.js",
    "chars": 2987,
    "preview": "import Vue from 'vue'\nimport vuex from 'vuex'\nimport service from './request'\n//import Cookies from 'js-cookie'\n\n// Vue."
  },
  {
    "path": "ui/static/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wordai/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "wordai/api/__init__.py",
    "chars": 369,
    "preview": "from flask import Flask, jsonify\nfrom flask_cors import CORS\n\nfrom flask_jwt_extended import (JWTManager)\n\napp = Flask(_"
  },
  {
    "path": "wordai/api/apis.py",
    "chars": 11089,
    "preview": "import hashlib\nimport json\nfrom datetime import datetime, timedelta\n\nimport wordai.models as models\nfrom flask import Bl"
  },
  {
    "path": "wordai/models/__init__.py",
    "chars": 115,
    "preview": "from mongoengine import connect\n\nfrom config import mongo_config\n\nconnect(**mongo_config())\n\nfrom .models import *\n"
  },
  {
    "path": "wordai/models/models.py",
    "chars": 9862,
    "preview": "\nfrom datetime import datetime, timedelta\n\nimport bcrypt\nfrom mongoengine import (BooleanField, DateTimeField, EmbeddedD"
  }
]

About this extraction

This page contains the full source code of the senghoo/wordai GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (92.3 KB), approximately 26.0k tokens, and a symbol index with 115 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!