├── LICENSE ├── README.md ├── deploy ├── crontab ├── fabric │ └── deploy.py ├── nginx-conf ├── nginx-default-conf ├── uwsgi-systemd └── vassals │ └── django.ini ├── dialog ├── __init__.py ├── admin.py ├── apps.py ├── chatbots.py ├── chatbottest.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── manage.py ├── project ├── __init__.py ├── environment.py ├── jinja2 │ ├── __init__.py │ └── environment.py ├── prodsettings.py ├── settings.py ├── urls.py └── wsgi.py ├── requirements.txt ├── static ├── image │ ├── close.png │ ├── loading.png │ └── tutorial │ │ ├── closeandrate.png │ │ └── upvotes.png ├── js │ └── cookie.js ├── perfectscrollbar │ ├── perfect-scrollbar.css │ └── perfect-scrollbar.min.js ├── reset.css ├── script.js ├── style.css ├── style.css.map └── style.less ├── templates ├── base.html └── index.html └── templatesadmin ├── chatratingchangelist.html ├── chatrecordchangelist.html └── studychange.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Craig Ferguson, Asma Ghandeharioun, Natasha Jaques, Noah Jones, Judy Hanwen Shen, Agata Lapedriza, Rosalind Picard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neural Chat Web 2 | 3 | ## Preparation - Key Concepts 4 | 5 | This is the Django server portion of the Neural Chat project and is accompanied by another [**repository including our open-sourced dialog models**](https://github.com/natashamjaques/neural_chat). The system is engineered such that the two repositories can be maintained separately. 6 | You can interact with our models here: **http://neural.chat**. 7 | 8 | The project is written in Django and contains setup and deployment scripts to configure Nginx, uWSGI, MySQL on a Unix environment. 9 | 10 | The project has a bunch of convenience functions that work with [Autoenv](https://github.com/kennethreitz/autoenv), and it is highly recommended that you install it when working with this project. 11 | 12 | These instructions assume you have a server on Google Cloud running Debian 9 or later. It is not required to use Google Cloud, but for the chatbots you will want a server with a GPU. 13 | 14 | ## Code Configuration 15 | 16 | First of all, all passwords have been stripped out of all the code and must be replaced. You can do a search for the string EDITME to find all locations, but a full list is presented here: 17 | 18 | * deploy/fabric/deploy.py `MYSQL_ROOT_PASSWORD` and `GITHUB_SSH_URL` 19 | * deploy/nginx-default-conf change `server_name` to your server's hostname 20 | * settings.py `SECRET_KEY` and `DATABASES` 21 | * .env `SERVER_HOSTNAME` 22 | * Make sure you update the Informed Consent text in templates/index.html in the `informedconsent` section! 23 | 24 | 25 | ## Local Installation (Mac OSX Instructions) 26 | 27 | * Install MySQL, and good luck. My recommendation is to install [homebrew](https://docs.brew.sh/Installation) run `brew install mysql-connector-c; brew unlink mysql-connector-c; brew install mysql`. 28 | * Check if you have Python 3.6 (`which python3.6`). If you don't have it, [download it](https://www.python.org/downloads/release/python-367/). 29 | * Create the logging directory the Django project will use. `sudo mkdir /var/log/django/`. Then make sure you can read/write in it `sudo chmod 777 /var/log/django/`. 30 | * Check out the project from Github. 31 | * `cd` into project the directory, and create a virtual environment. `virtualenv -p python3.6 env` 32 | * If you haven't already, install [Autoenv](https://github.com/kennethreitz/autoenv#install). 33 | * In a new terminal, `cd` into the directory again to make sure autoenv picks up the virtual environment. It'll ask if you want to run the .env script. Say Yes. 34 | * Run `pip install -r requirements.txt` 35 | * Run `fab -f deploy/fabric/deploy.py configure_local_db` to set up your database locally. 36 | * Run `python manage.py migrate` to set up all the Django model tables. 37 | 38 | ## Running the server locally 39 | 40 | * If you have autoenv, just `cd` into the project's directory and run `runserver` then access the server at http://localhost:8000/. If you don't, you must make sure you're in the proper python environment (`source env/bin/activate`) and use the command `python manage.py runserver 0.0.0.0:8000` which is the equivalent. 41 | 42 | ## Setting Up Automated Deployment Scripts for Google Cloud 43 | 44 | We provide deployment and server configuration scripts to make your lives much easier, but they assume you are running Debian 9. They involve connecting to your server via command line, which is a little tricky in Google Cloud. Here's how to configure your local machine to be able to do that. 45 | 46 | * Download the [Google Cloud SDK](https://cloud.google.com/sdk/docs/). 47 | * Unzip the file somewhere permanent, like /opt/. `cd` to the directory, then run `./google-cloud-sdk/install.sh` 48 | * Close your terminal tab and open a new one, to load the changes. 49 | * In the new tab run `gcloud auth login` and authorize it with the correct account. 50 | * Set the project with `gcloud config set project [[ PROJECT NAME ]]` 51 | * Run `gcloud compute config-ssh --ssh-key-file=~/.ssh/google` 52 | * Now convert the keyfile to RSA so Fabric can use it. `ssh-keygen -p -m PEM -f ~/.ssh/google` 53 | * Again, close the terminal and open a new one to read the changes. 54 | * Now you can SSH into the Virtual Instance with the command `ssh [[ SERVER HOSTNAME ]]`! 55 | 56 | ## Setting Up Production Server 57 | 58 | First, make sure you have the code checked in to your own Github project. The deploy scripts need a reference to that project so the server can download the code and install it. It will always download the code from the master branch, so make sure whatever's checked into master is ready to be sent to your server! 59 | 60 | Also, make sure you have a server running somewhere that you have SSH access for and have sudo privileges. You can't deploy code to a server without a server running! 61 | 62 | And finally.... Don't skip any of these steps! 63 | 64 | #### Deploying a Chatbots project 65 | 66 | As mentioned above, this project is intended to import code from a separate, pre-existing Chatbots project. 67 | Use our [**Neural Chat**](https://github.com/natashamjaques/neural_chat) open-source project for a range of neural dialog models. 68 | 69 | If you would like to create your own chatbot models, now is the time to make one. Here's how to create one yourself: 70 | 71 | * Create a new Github project. Copy dialog/chatbots.py to that project, and rename it web_chatbots.py. Edit it to remove all references to Django and settings. You can make any number of chatbots, so long as they implement the `Chatbot` class and use the `@registerbot()` decorator. They will all automatically be picked up by the Django project! 72 | * Create a virtual environment with Python 3.6. We recommend placing it in /opt/virtualenvironment/. Basically, run this command: `cd /opt/; virtualenv -p python3.6 virtualenvironment` 73 | * We recommend adding a copy of dialog/chatbottest.py to your Chatbots project to test and run your chatbots on the command line. 74 | * SSH into your server and check out your chatbots project. Activate your virtual environment, install all requirements, and run chatbottest.py to make sure it's working. 75 | 76 | #### Configuring the Django project to talk to the Chatbots project 77 | 78 | Now that you have your chatbots running on your server, it's time to update the code in this project to be able to locate that project and its virtual environment on your server. 79 | 80 | * Make sure `VIRTUAL_ENVIRONMENT` in deploy/fabric/deploy.py points to the virtual environment set up for your chatbots. 81 | * Make sure `virtualenv` in deploy/fabric/vassals/django.ini points to the same directory 82 | * Make sure `ExecStart` in deploy/fabric/uwsgi-systemd points to the same directory 83 | * Make sure the second `pythonpath` in deploy/fabric/vassals/django.ini points to the directory your chatbots project is in. 84 | * Be sure to check in and push all your configuration changes! 85 | 86 | #### Generating Github Deploy Keys 87 | 88 | You will need to make sure the scripts can access your Github project. So we must generate keys. These keys will be automatically ignored by git, so they must be manually sent between team members once generated. 89 | 90 | * In the project directory, run `fab deploy/fabric/deploy.py create_ssh_key`. You should see two new files in the deploy/fabric/keys directory. 91 | * Go to your Github project's page, and go to Settings -> Deploy Keys -> Add New. Paste the contents of the public key file (deploy/fabric/keys/github.pub) into the box, and give the key a name (it doesn't matter what you call it). 92 | * Now your server will be able to check out your code from Github! 93 | 94 | #### Running the server setup scripts 95 | 96 | * In the main directory of your project, run `fab -f deploy/fabric/deploy.py -H [[SERVER HOSTNAME]] setup_server`. During this process, you will be prompted by [Certbot](https://certbot.eff.org/) on configuration options. Generally, just choose the default options, or look up terms on the Certbot page. If everything was configured correctly, your server should now be running! 97 | * Run `fab -f deploy/fabric/deploy.py -H [[SERVER HOSTNAME]] create_superuser` to create an account in your Django Admin Console. You can now access your admin console by going to https://[[SERVER HOSTNAME]]/admin/ and logging in with the account you just created. 98 | 99 | ## Deploying Code Changes to the Server 100 | 101 | After the server has been successfully set up, it's simple to update it with any subsequent code changes. 102 | 103 | * Make sure you have all changes and files checked into your project's master branch! 104 | * With autoenv, simply `cd` into the project's base directory and run `deploy`. The server will check out the code from the master branch of your github project, run any necessary database migrations, and restart the server for you. 105 | 106 | ## Editing LESS Files 107 | 108 | We highly recommend using [PyCharm](https://www.jetbrains.com/pycharm/) for this project (especially the free educational edition). Located in the /static/ directory is a file, style.css. It is automatically generated from style.less by PyCharm every time style.less is edited. So we highly recommend using an IDE where you can continue editing the .less file and never manually editing the .css file. 109 | 110 | 111 | ## Setting up Studies and Exporting Your Data 112 | 113 | When you register a chatbot with the `@registerchatbot()` decorator, it will automatically be included on the index page of the site. But you can also set up specific studies to run with a subset of your bots with specific configurations! 114 | 115 | To set up a study, log in to your Django Admin Console (located at https://[[ SERVER HOSTNAME ]]/admin/). Then create a new Study object. When you create a new Study, you can give it a couple options. Basically, give it a human readable name and select which bots you want included. Additionally, you can choose to flag the study as being random. If you do, the participants will be shown one random and anonymized bot. Otherwise, the participants will get to choose which bot they want to chat with out of a list. You can also flag the study as being a Mechanical Turk study, in which case the participant will be asked for their Mechanical Turk ID and will be given a code at the end of their chat session they can paste into their Turk page, to help match which Turk Worker had which conversations. 116 | 117 | Once you've configured and saved up your studies, you can see a link to each one right there in the Admin Console. Just click the link to see what your participants will see! 118 | 119 | After people have used your chatbots, you can download their conversations and ratings by going to /admin/dialog/chatrating/ and /admin/dialog/chatrecord/ and clicking the "DOWNLOAD ALL AS CSV" link in the upper right corner. 120 | 121 | You can also download the data for a specific study by navigating to that study in the admin and clicking the "DOWNLOAD AS CSV" buttons in the upper right of the screen. 122 | 123 | ## Licence 124 | 125 | The MIT License 126 | 127 | Copyright (c) 2019 Craig Ferguson, Natasha Jaques, Asma Ghandeharioun, Judy Shen, Noah Jones, Agata Lapedriza, Rosalind Picard 128 | 129 | Permission is hereby granted, free of charge, to any person obtaining a copy 130 | of this software and associated documentation files (the "Software"), to deal 131 | in the Software without restriction, including without limitation the rights 132 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 133 | copies of the Software, and to permit persons to whom the Software is 134 | furnished to do so, subject to the following conditions: 135 | 136 | The above copyright notice and this permission notice shall be included in 137 | all copies or substantial portions of the Software. 138 | 139 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 140 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 141 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 142 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 143 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 144 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 145 | THE SOFTWARE. 146 | 147 | ## Reference 148 | If you use this code, please reference [the following paper](https://arxiv.org/abs/1906.09308): 149 | 150 | ``` 151 | @article{ghandeharioun2019approximating, 152 | title={Approximating Interactive Human Evaluation with Self-Play for Open-Domain Dialog Systems}, 153 | author={Ghandeharioun, Asma and Shen, Judy and Jaques, Natasha and Ferguson, Craig and Jones, Noah, and Lapedriza, Agata and Picard, Rosalind}, 154 | journal={arXiv preprint arXiv:1906.09308}, 155 | year={2019} 156 | } 157 | ``` -------------------------------------------------------------------------------- /deploy/crontab: -------------------------------------------------------------------------------- 1 | # This is just the example crontab that will run nightly backups! 2 | # Feel free to edit it as you see fit 3 | 4 | # 0 0,12 * * * python -c 'import random; import time; time.sleep(random.random() * 3600)' && /opt/certbot-auto renew 5 | -------------------------------------------------------------------------------- /deploy/fabric/deploy.py: -------------------------------------------------------------------------------- 1 | from fabric.api import run, cd, sudo, get, put, env, local, prefix 2 | from fabric.contrib.files import exists 3 | from contextlib import contextmanager 4 | 5 | from project import settings 6 | 7 | import os 8 | 9 | env.use_ssh_config = True 10 | 11 | FILE_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) 12 | 13 | MYSQL_ROOT_PASSWORD = "[[ EDITME MySQL root user password ]]" 14 | 15 | SETUP_DIRECTORY = "/opt/" 16 | CODE_HOME = os.path.join(SETUP_DIRECTORY, "django") 17 | 18 | DIRECTORIES_TO_CREATE = () 19 | 20 | GITHUB_SSH_URL = "[[ EDITME URL to your github fork of this project ]]" 21 | 22 | VIRTUAL_ENVIRONMENT = "/opt/virtualenvironment/" 23 | 24 | class FabricException(Exception): 25 | pass 26 | 27 | 28 | @contextmanager 29 | def virtualenv(): 30 | with prefix('source {0}bin/activate'.format(VIRTUAL_ENVIRONMENT)): 31 | yield 32 | 33 | def test(): 34 | run("ls") 35 | 36 | 37 | def deploy(): 38 | with cd(CODE_HOME): 39 | run("git reset --hard") 40 | run("git clean -f -d") 41 | run("git pull origin master") 42 | 43 | with virtualenv(): 44 | sudo('pip install -r requirements.txt') 45 | 46 | # run('cp /opt/passwords.py src') 47 | 48 | sudo("python3 manage.py migrate", user="www-data") 49 | 50 | sudo("python3 manage.py collectstatic --noinput", user="www-data") 51 | 52 | 53 | sudo('service uwsgi restart') 54 | 55 | 56 | def shell(): 57 | with cd(CODE_HOME): 58 | sudo("python manage.py shell", user="www-data") 59 | 60 | 61 | def setup_db(): 62 | with cd(SETUP_DIRECTORY): 63 | sudo('service mysql stop') 64 | sudo('nohup /usr/sbin/mysqld --skip-grant-tables --skip-networking > /dev/null 2>&1 &') 65 | sudo("mysql -uroot -e 'use mysql; update user set authentication_string=PASSWORD(\"{0}\") where User=\"root\"; flush privileges;'".format(MYSQL_ROOT_PASSWORD)) 66 | sudo("mysqladmin -u root -p{0} shutdown".format(MYSQL_ROOT_PASSWORD)) 67 | sudo('sudo service mysql start') 68 | 69 | for command in _get_configure_db_commands(MYSQL_ROOT_PASSWORD): 70 | sudo(command) 71 | 72 | 73 | def deploy_crontab(): 74 | put(os.path.join(FILE_DIRECTORY, '../crontab'), '/tmp/crontab') 75 | sudo('crontab < /tmp/crontab') 76 | 77 | 78 | def setup_server(): 79 | sudo('apt-get update') 80 | 81 | # get the programs we'll need 82 | sudo('apt-get -y install python3.6 python-setuptools git nginx mysql-server python3-mysqldb') 83 | sudo('apt-get -y install python3-pip python3-dev build-essential default-libmysqlclient-dev uwsgi-plugin-python3') 84 | sudo('apt-get -y install libffi-dev libssl-dev libxml2-dev libxslt1-dev') 85 | sudo('apt-get -y install yui-compressor') 86 | 87 | sudo('apt-get -y remove uwsgi') # not sure why it's even there on some servers 88 | 89 | with virtualenv(): 90 | run('pip3 install uwsgi') 91 | 92 | sudo('apt-get -y install uwsgi-plugin-python3') 93 | 94 | # get code 95 | sudo('mkdir -p ' + SETUP_DIRECTORY) 96 | sudo('chown {0}:www-data '.format(env.user) + SETUP_DIRECTORY) 97 | for d in DIRECTORIES_TO_CREATE: 98 | sudo('mkdir -p ' + d) 99 | sudo('chown {0}:www-data '.format(env.user) + d) 100 | sudo('chmod g+w ' + d) 101 | 102 | with cd(SETUP_DIRECTORY): 103 | 104 | run('git config --global user.name "Deploy"') 105 | sudo('mkdir -p /home/{0}/.ssh/'.format(env.user)) 106 | _put_key_file('github') 107 | 108 | setup_db() 109 | 110 | run("touch ~/.ssh/config") 111 | run( 112 | 'if ! cat ~/.ssh/config | grep "github"; then echo -e "Host github.mit.edu\n User git\n IdentityFile ~/.ssh/github" >> ~/.ssh/config; fi;') 113 | 114 | if exists('/opt/code'): 115 | run('rm -rf /opt/code') 116 | if not exists("/opt/code"): 117 | run('git init code') 118 | with cd('/opt/code'): 119 | run('git remote add -f origin {0}'.format(GITHUB_SSH_URL)) 120 | run('git pull origin master') 121 | if exists(CODE_HOME): 122 | run('rm ' + CODE_HOME) 123 | run('ln -s {0} {1}'.format("/opt/code/", CODE_HOME)) 124 | 125 | put('{0}/nginx-default-conf'.format(os.path.dirname(FILE_DIRECTORY)), '/etc/nginx/sites-enabled/default', 126 | use_sudo=True) 127 | put('{0}/nginx-conf'.format(os.path.dirname(FILE_DIRECTORY)), '/etc/nginx/nginx.conf', use_sudo=True) 128 | put('{0}/uwsgi-systemd'.format(os.path.dirname(FILE_DIRECTORY)), '/etc/systemd/system/uwsgi.service', 129 | use_sudo=True) 130 | 131 | sudo('[ -d `command -v systemctl` ] || systemctl daemon-reload') # run systemctl only if it exists 132 | sudo('systemctl enable uwsgi') 133 | 134 | sudo('apt-get install -y certbot python-certbot-nginx -t stretch-backports') 135 | sudo('certbot --nginx') 136 | 137 | 138 | sudo('mkdir -p /opt/staticfiles') 139 | sudo('chown www-data:www-data /opt/staticfiles') 140 | sudo('mkdir -p /opt/media') 141 | sudo('chown www-data:www-data /opt/media') 142 | 143 | sudo('mkdir -p /var/log/django/') 144 | sudo('chown www-data:www-data /var/log/django/') 145 | sudo('touch /var/log/django/django.log') 146 | sudo('chown www-data:www-data /var/log/django/django.log') 147 | sudo('chmod g+w /var/log/django/') 148 | sudo('touch /var/log/uwsgi.log') 149 | sudo('chmod 777 /var/log/uwsgi.log') 150 | sudo('service nginx restart') 151 | 152 | deploy_crontab() 153 | 154 | deploy() 155 | 156 | 157 | def create_superuser(): 158 | with cd(CODE_HOME): 159 | with virtualenv(): 160 | sudo("python manage.py createsuperuser", user="www-data") 161 | 162 | 163 | def tail_log(): 164 | with cd(CODE_HOME): 165 | sudo("tail -n100 /var/log/django/django.log", user="www-data") 166 | 167 | 168 | def copy_db(): 169 | run("mysqldump {1} -uroot -p{0} > ".format(MYSQL_ROOT_PASSWORD, 170 | settings.DATABASES['default']['NAME']) + "/tmp/dump.sql") 171 | get("/tmp/dump.sql", "/tmp/dump.sql") 172 | local("mysql {0} -uroot < /tmp/dump.sql".format(settings.DATABASES['default']['NAME'])) 173 | 174 | 175 | def _put_key_file(localkeyname): 176 | localpath = os.path.join(FILE_DIRECTORY, 'keys', localkeyname) 177 | put(localpath, '/home/{0}/.ssh/'.format(env.user)) 178 | sudo('chmod 600 /home/{0}/.ssh/{1}'.format(env.user, localkeyname)) 179 | 180 | 181 | def _get_configure_db_commands(root_password): 182 | # configure our db 183 | username = settings.DATABASES['default']['USER'] 184 | db = settings.DATABASES['default']['NAME'] 185 | password = settings.DATABASES['default']['PASSWORD'] 186 | 187 | if root_password: 188 | root_password = "-p" + root_password 189 | 190 | commands = ( 191 | "mysql -uroot {0} -e 'CREATE DATABASE IF NOT EXISTS {1}'".format(root_password, db), 192 | "mysql -uroot {0} -e 'CREATE USER IF NOT EXISTS \"{1}\"@\"localhost\" IDENTIFIED BY \"{2}\"'".format(root_password, username, password), 193 | "mysql -uroot {0} -e 'GRANT ALL PRIVILEGES ON {2}.* TO {3}@localhost'".format( 194 | root_password, password, db, username), 195 | "mysql -uroot {0} -e 'ALTER DATABASE {1} CHARACTER SET \"utf8\" COLLATE \"utf8_unicode_ci\"'".format( 196 | root_password, db), 197 | 198 | "mysql -uroot {0} -e 'FLUSH PRIVILEGES'".format(root_password), 199 | ) 200 | return commands 201 | 202 | 203 | def configure_local_db(root_password=""): 204 | for command in _get_configure_db_commands(root_password): 205 | local(command) 206 | 207 | print("MySQL configured successfully!") 208 | 209 | 210 | def create_ssh_key(keypath="github"): 211 | keyfiles_dir = "{0}/keys/".format(FILE_DIRECTORY) 212 | local("mkdir -p {0}".format(keyfiles_dir)) 213 | keypath = os.path.join(keyfiles_dir, keypath) 214 | if not os.path.isfile(keypath): 215 | local("ssh-keygen -b 2048 -t rsa -f {0} -q -N \"\"".format(keypath)) 216 | 217 | print("Created SSH Key at: " + keypath) 218 | 219 | return keypath + ".pub" 220 | -------------------------------------------------------------------------------- /deploy/nginx-conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes 4; 3 | pid /var/run/nginx.pid; 4 | 5 | events { 6 | worker_connections 768; 7 | # multi_accept on; 8 | } 9 | 10 | http { 11 | 12 | ## 13 | # Basic Settings 14 | ## 15 | 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | underscores_in_headers on; 22 | # server_tokens off; 23 | 24 | # server_names_hash_bucket_size 64; 25 | # server_name_in_redirect off; 26 | 27 | include /etc/nginx/mime.types; 28 | default_type application/octet-stream; 29 | 30 | ## 31 | # Logging Settings 32 | ## 33 | 34 | access_log /var/log/nginx/access.log; 35 | error_log /var/log/nginx/error.log; 36 | 37 | ## 38 | # Gzip Settings 39 | ## 40 | 41 | gzip on; 42 | gzip_disable "msie6"; 43 | 44 | gzip_vary on; 45 | gzip_proxied any; 46 | gzip_comp_level 6; 47 | gzip_buffers 16 8k; 48 | # gzip_http_version 1.1; 49 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 50 | 51 | ## 52 | # nginx-naxsi config 53 | ## 54 | # Uncomment it if you installed nginx-naxsi 55 | ## 56 | 57 | #include /etc/nginx/naxsi_core.rules; 58 | 59 | ## 60 | # nginx-passenger config 61 | ## 62 | # Uncomment it if you installed nginx-passenger 63 | ## 64 | 65 | #passenger_root /usr; 66 | #passenger_ruby /usr/bin/ruby; 67 | 68 | ## 69 | # Virtual Host Configs 70 | ## 71 | 72 | include /etc/nginx/conf.d/*.conf; 73 | include /etc/nginx/sites-enabled/*; 74 | } -------------------------------------------------------------------------------- /deploy/nginx-default-conf: -------------------------------------------------------------------------------- 1 | upstream django { 2 | server unix:///tmp/uwsgi.django.socket; # for a web port socket (we'll use this first) 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | charset utf-8; 9 | client_max_body_size 15M; 10 | server_name [[ EDITME SERVER NAME ]]; 11 | # return 301 https://$server_name$request_uri; 12 | 13 | location /static { 14 | expires 365d; 15 | alias /opt/staticfiles/; 16 | } 17 | 18 | location /media { 19 | expires 365d; 20 | alias /opt/media/; 21 | } 22 | 23 | location /favicon.ico { 24 | expires 365d; 25 | alias /opt/staticfiles/favicon.ico; 26 | } 27 | location /robots.txt { 28 | expires 365d; 29 | alias /opt/staticfiles/robots.txt; 30 | } 31 | 32 | location / { 33 | include uwsgi_params; 34 | uwsgi_pass django; 35 | uwsgi_param UWSGI_PYHOME /opt/django/; 36 | uwsgi_param UWSGI_CHIDIR /opt/django/; 37 | uwsgi_param UWSGI_SCRIPT project.wsgi; 38 | } 39 | 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /deploy/uwsgi-systemd: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=uWSGI Emperor service 3 | After=syslog.target 4 | 5 | [Service] 6 | User=www-data 7 | Group=www-data 8 | ExecStart=/opt/virtualenvironment/bin/uwsgi --enable-threads --lazy-apps --emperor /opt/django/deploy/vassals/ 9 | Restart=always 10 | KillSignal=SIGQUIT 11 | Type=simple 12 | StandardError=syslog 13 | NotifyAccess=all 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | Alias=uwsgi.service 18 | Alias=uwsgi.service 19 | -------------------------------------------------------------------------------- /deploy/vassals/django.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | base = /opt/django/ 3 | virtualenv = /opt/virtualenvironment/ 4 | 5 | chdir = %(base) 6 | pythonpath = /opt/django/ 7 | pythonpath = %(base) 8 | pythonpath = [[ EDITME PATH-TO-CHATBOTS-PROJECT ]] 9 | wsgi-file = %(base)/project/wsgi.py 10 | 11 | master = true 12 | processes = 1 13 | 14 | socket = /tmp/uwsgi.django.socket 15 | chmod-socket = 666 16 | vacuum = true 17 | umask = 002 18 | uid = www-data 19 | gid = www-data 20 | 21 | enable-threads = true 22 | single-interpreter = true 23 | lazy-apps = true 24 | 25 | daemonize=/var/log/uwsgi.log 26 | -------------------------------------------------------------------------------- /dialog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/dialog/__init__.py -------------------------------------------------------------------------------- /dialog/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import ChatRecord, ChatRating, Study 3 | from django.conf.urls import url 4 | from django.http.response import HttpResponse 5 | import csv, simplejson 6 | from django import forms 7 | from .chatbots import chatbots 8 | from django.contrib.admin.widgets import FilteredSelectMultiple 9 | from django.urls import reverse 10 | from django.utils.safestring import mark_safe 11 | from django.db.models import Count 12 | 13 | def register(model): 14 | def inner(admin_class): 15 | admin.site.register(model,admin_class) 16 | return admin_class 17 | 18 | return inner 19 | 20 | 21 | def _download_records_csv(request, study_key=None): 22 | all_records = ChatRecord.objects.order_by('study_key', 'chat_id', 'timestamp') 23 | 24 | study = None 25 | if study_key: 26 | study = Study.objects.get(key=study_key) 27 | all_records = all_records.filter(study_key=study.key) 28 | 29 | all_records = all_records.all() 30 | 31 | chat_ids = set([record.chat_id for record in all_records]) 32 | 33 | ratings = ChatRating.objects.filter(chat_id__in=chat_ids) 34 | ratings = {rating.chat_id: rating for rating in ratings} 35 | 36 | response = HttpResponse(content_type='text/csv') 37 | response['Content-Disposition'] = 'attachment; filename="chatrecords{0}.csv"'.format("_" + study.key if study else "") 38 | 39 | writer = csv.writer(response) 40 | writer.writerow(["ID", "Chat ID", "Datetime", "Chatbot ID", "Message", "Response", "Bot Input", "Study Key", "User Session Key", "Response Rating", "Chat Rating Quality", "Chat Rating Fluency", "Chat Rating Coherency", "Chat Rating Diversity", "Chat Rating Contingency", "Chat Rating Empathy", "Chat Rating Comments"]) 41 | for record in all_records: 42 | rating = ratings.get(record.chat_id) 43 | writer.writerow([record.id, 44 | record.chat_id, 45 | record.timestamp.isoformat(), 46 | record.chatbot_id, 47 | record.message, 48 | record.response, 49 | record.message_history, 50 | record.study_key, 51 | record.user_session_id, 52 | record.rating, 53 | rating.quality if rating else "", 54 | rating.fluency if rating else "", 55 | rating.coherency if rating else "", 56 | rating.diversity if rating else "", 57 | rating.contingency if rating else "", 58 | rating.empathy if rating else "", 59 | rating.comments if rating else "", 60 | ]) 61 | 62 | return response 63 | 64 | 65 | def _download_rating_csv(request, study_key=None): 66 | all_ratings = ChatRating.objects.order_by('timestamp') 67 | 68 | if study_key: 69 | all_ratings = all_ratings.filter(study_key=study_key) 70 | 71 | all_ratings = all_ratings.all() 72 | 73 | chat_lengths = ChatRecord.objects.values('chat_id').annotate(Count('chat_id')) 74 | chat_lengths = {chat['chat_id']:chat['chat_id__count'] for chat in chat_lengths} 75 | 76 | response = HttpResponse(content_type='text/csv') 77 | response['Content-Disposition'] = 'attachment; filename="chat_ratings{0}.csv"'.format( 78 | "_" + study_key if study_key else "") 79 | 80 | writer = csv.writer(response) 81 | writer.writerow(["ID", 82 | "Chat ID", 83 | "Datetime", 84 | "Chatbot ID", 85 | "User Session Key", 86 | "Quality", 87 | "Coherency", 88 | "Fluency", 89 | "Diversity", 90 | "Contingency", 91 | "Empathy", 92 | "Comments", 93 | "Chat Length", 94 | "MTurk Code", 95 | "MTurk User ID", 96 | "Study Key"]) 97 | 98 | for rating in all_ratings: 99 | writer.writerow([rating.id, 100 | rating.chat_id, 101 | rating.timestamp.isoformat(), 102 | rating.chatbot_id, 103 | rating.user_session_id, 104 | rating.quality, 105 | rating.coherency, 106 | rating.fluency, 107 | rating.diversity, 108 | rating.contingency, 109 | rating.empathy, 110 | rating.comments, 111 | chat_lengths.get(rating.chat_id), 112 | rating.mturk_code, 113 | rating.mturk_user_id, 114 | rating.study_key, 115 | ]) 116 | 117 | return response 118 | 119 | 120 | @register(ChatRecord) 121 | class ChatRecordAdmin(admin.ModelAdmin): 122 | list_display = ('id', 'chat_id', 'chatbot_id', 'user_session_id', 'message', 'response', 'timestamp', 'rating', 'study_key') 123 | search_fields = ('chat_id', 'chatbot_id', 'study_key', 'user_session_id') 124 | 125 | change_list_template = 'chatrecordchangelist.html' 126 | 127 | def get_urls(self): 128 | urls = super(ChatRecordAdmin, self).get_urls() 129 | my_urls = [ 130 | url(r'download_records_csv/$', self.admin_site.admin_view(_download_records_csv), name="download_records_csv"), 131 | ] 132 | return my_urls + urls 133 | 134 | 135 | 136 | @register(ChatRating) 137 | class ChatRatingAdmin(admin.ModelAdmin): 138 | list_display = ('id', 'chat_id', 'study_key', 'chatbot_id', 'user_session_id', 'mturk_code', 'mturk_user_id', 'timestamp', 'quality', 'coherency', 'fluency', 'diversity', 'contingency', 'empathy', 'comments') 139 | search_fields = ('study_key', 'chat_id', 'chatbot_id', 'user_session_id', 'mturk_user_id') 140 | change_list_template = 'chatratingchangelist.html' 141 | 142 | def get_urls(self): 143 | urls = super(ChatRatingAdmin, self).get_urls() 144 | my_urls = [ 145 | url(r'download_rating_csv/$', self.admin_site.admin_view(_download_rating_csv), name="download_rating_csv"), 146 | ] 147 | return my_urls + urls 148 | 149 | 150 | BOT_CHOICES = tuple((key, chatbots[key].name) for key in chatbots) 151 | 152 | 153 | class JSONMultipleChoiceField(forms.MultipleChoiceField): 154 | widget = FilteredSelectMultiple("Bots", is_stacked=False, choices=BOT_CHOICES) 155 | 156 | 157 | class StudyForm(forms.ModelForm): 158 | 159 | def __init__(self, *args, **kwargs): 160 | super(StudyForm, self).__init__(*args, **kwargs) 161 | if self.instance.pk: 162 | self.initial["bot_list"] = simplejson.loads(self.instance.bot_list) 163 | 164 | def save(self, commit=True): 165 | m = super(StudyForm, self).save(commit=False) 166 | m.bot_list = simplejson.dumps(self.cleaned_data["bot_list"]) 167 | if commit: 168 | m.save() 169 | return m 170 | 171 | class Meta: 172 | model = Study 173 | fields = '__all__' 174 | 175 | bot_list = JSONMultipleChoiceField(choices=BOT_CHOICES) 176 | 177 | 178 | @register(Study) 179 | class StudyAdmin(admin.ModelAdmin): 180 | list_display = ('key', 'name', 'link', 'is_random_bot', 'bot_list', 'is_mturk', 'is_anonymized') 181 | readonly_fields = ('key',) 182 | form = StudyForm 183 | 184 | change_form_template = 'studychange.html' 185 | 186 | def get_urls(self): 187 | urls = super(StudyAdmin, self).get_urls() 188 | my_urls = [ 189 | url(r'download_records_csv/(?P\w+)/', self.admin_site.admin_view(_download_records_csv), name="download_study_records_csv"), 190 | url(r'download_rating_csv/(?P\w+)/', self.admin_site.admin_view(_download_rating_csv), name="download_study_ratings_csv"), 191 | ] 192 | return my_urls + urls 193 | 194 | def link(self, obj): 195 | return mark_safe(u'Link'.format(reverse("index", kwargs=dict(study_key=obj.key)))) 196 | 197 | -------------------------------------------------------------------------------- /dialog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class DialogConfig(AppConfig): 4 | name = 'dialog' 5 | 6 | 7 | -------------------------------------------------------------------------------- /dialog/chatbots.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from abc import ABC, abstractmethod 3 | from django.conf import settings 4 | 5 | 6 | chatbots = OrderedDict() 7 | 8 | try: 9 | from web_chatbots import chatbots as aibots 10 | chatbots = aibots 11 | except ImportError: 12 | pass 13 | 14 | 15 | def registerbot(botclass): 16 | bot = botclass() 17 | chatbots[bot.id] = bot 18 | 19 | 20 | class Chatbot(ABC): 21 | def __init__(self, id, name, is_test_bot=False): 22 | """ 23 | All chatbots should extend this class and be registered with the @registerbot decorator 24 | :param id: An id string, must be unique! 25 | :param name: A user-friendly string shown to the end user to identify the chatbot. Should be unique. 26 | """ 27 | self.id = id 28 | self.name = name 29 | self.is_test_bot = is_test_bot 30 | 31 | @abstractmethod 32 | def handle_messages(self, messages): 33 | """ 34 | Takes a list of messages, and combines those with magic to return a response string 35 | :param messages: list of strings 36 | :return: string 37 | """ 38 | pass 39 | 40 | if not settings.IS_PRODUCTION: 41 | @registerbot 42 | class Bot1(Chatbot): 43 | def __init__(self): 44 | super().__init__("bot1", "Albert") 45 | 46 | def handle_messages(self, messages): 47 | return "HI I AM ALBERT" 48 | 49 | 50 | @registerbot 51 | class Bot2(Chatbot): 52 | def __init__(self): 53 | super().__init__("bot2", "Betty") 54 | 55 | def handle_messages(self, messages): 56 | return "HI I AM BETTY" 57 | 58 | 59 | @registerbot 60 | class Bot3(Chatbot): 61 | def __init__(self): 62 | super().__init__("bot3", "Cuthbert") 63 | 64 | def handle_messages(self, messages): 65 | return "HI I AM CUTHBERT" 66 | 67 | 68 | @registerbot 69 | class Bot4(Chatbot): 70 | def __init__(self): 71 | super().__init__("bot4", "Diane", True) 72 | 73 | def handle_messages(self, messages): 74 | return "HI I AM DIANE" 75 | 76 | 77 | @registerbot 78 | class Pretzelbot(Chatbot): 79 | def __init__(self): 80 | super().__init__("pretzel", "PretzelBot") 81 | 82 | def handle_messages(self, messages): 83 | return u'\U0001F968' 84 | -------------------------------------------------------------------------------- /dialog/chatbottest.py: -------------------------------------------------------------------------------- 1 | from chatbots import chatbots 2 | 3 | if __name__ == "__main__": 4 | 5 | username = None 6 | chatbotid = None 7 | 8 | messages = [] 9 | 10 | while not username: 11 | username = input("Please enter your name.\n> ") 12 | 13 | while chatbotid not in chatbots.keys(): 14 | chatbotid = input("Please enter the chatbot ID of your choice.\nValid choices are [" + ", ".join(chatbots.keys()) + "]:\n> ") 15 | 16 | chatbot = chatbots[chatbotid] 17 | 18 | print("\nPlease enjoy your chat with {0}! Type 'exit' or 'quit' to end the chat at any point.\n".format(chatbot.name)) 19 | 20 | while True: 21 | 22 | message = input("[User]: ") 23 | 24 | if message == "exit" or message == "quit": 25 | print("Goodbye!") 26 | quit() 27 | 28 | if not message: 29 | continue 30 | 31 | messages.append(message) 32 | messages = messages[-5:] 33 | 34 | response = chatbot.handle_messages(messages) 35 | 36 | print("[" + chatbot.name + "]: " + response) 37 | -------------------------------------------------------------------------------- /dialog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.7 on 2019-05-29 21:36 2 | 3 | import dialog.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | replaces = [('dialog', '0001_initial'), ('dialog', '0002_auto_20190221_1646'), ('dialog', '0003_auto_20190221_1647'), ('dialog', '0004_remove_chatrecord_key'), ('dialog', '0005_auto_20190221_2350'), ('dialog', '0006_chatrating_study'), ('dialog', '0007_auto_20190312_1455'), ('dialog', '0008_chatrecord_study_key'), ('dialog', '0009_chatrating_study_key'), ('dialog', '0010_study_name'), ('dialog', '0011_chatrecord_rating'), ('dialog', '0012_chatrating_contingency'), ('dialog', '0013_auto_20190403_1758'), ('dialog', '0014_auto_20190408_2059'), ('dialog', '0015_auto_20190419_1901'), ('dialog', '0016_chatrating_quality'), ('dialog', '0017_chatrating_empathetic'), ('dialog', '0018_auto_20190422_2023'), ('dialog', '0019_auto_20190422_2038'), ('dialog', '0020_auto_20190506_1541'), ('dialog', '0021_chatrating_mturk_user_id'), ('dialog', '0022_study_is_anonymized')] 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ChatRecord', 17 | fields=[ 18 | ('id', models.AutoField(primary_key=True, serialize=False)), 19 | ('date_created', models.DateTimeField(auto_now_add=True)), 20 | ('date_updated', models.DateTimeField(auto_now=True)), 21 | ('chat_id', models.CharField(db_index=True, max_length=64)), 22 | ('timestamp', models.DateTimeField()), 23 | ('chatbot_id', models.CharField(max_length=16)), 24 | ('message', models.TextField()), 25 | ('message_history', dialog.models.JsonField()), 26 | ('response', models.TextField()), 27 | ], 28 | options={ 29 | 'abstract': False, 30 | }, 31 | ), 32 | migrations.RunSQL( 33 | sql='ALTER TABLE dialog_chatrecord CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci', 34 | ), 35 | migrations.RunSQL( 36 | sql='ALTER TABLE dialog_chatrecord CHANGE response response TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL', 37 | ), 38 | migrations.RunSQL( 39 | sql='ALTER TABLE dialog_chatrecord CHANGE message_history message_history TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL', 40 | ), 41 | migrations.RunSQL( 42 | sql='ALTER TABLE dialog_chatrecord CHANGE message message TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL', 43 | ), 44 | migrations.CreateModel( 45 | name='ChatRating', 46 | fields=[ 47 | ('id', models.AutoField(primary_key=True, serialize=False)), 48 | ('date_created', models.DateTimeField(auto_now_add=True)), 49 | ('date_updated', models.DateTimeField(auto_now=True)), 50 | ('chat_id', models.CharField(db_index=True, max_length=128)), 51 | ('timestamp', models.DateTimeField()), 52 | ('chatbot_id', models.CharField(max_length=128)), 53 | ('coherency', models.IntegerField(null=True)), 54 | ('fluency', models.IntegerField()), 55 | ('diversity', models.IntegerField()), 56 | ('comments', models.TextField(blank=True)), 57 | ('study_key', models.CharField(max_length=128)), 58 | ('contingency', models.IntegerField(default=1)), 59 | ('user_session_id', models.CharField(max_length=128)), 60 | ('mturk_code', models.CharField(max_length=128)), 61 | ('quality', models.IntegerField(default=0)), 62 | ('empathy', models.IntegerField(default=0)), 63 | ('mturk_user_id', models.CharField(blank=True, max_length=128)), 64 | ], 65 | options={ 66 | 'abstract': False, 67 | }, 68 | ), 69 | migrations.CreateModel( 70 | name='Study', 71 | fields=[ 72 | ('id', models.AutoField(primary_key=True, serialize=False)), 73 | ('date_created', models.DateTimeField(auto_now_add=True)), 74 | ('date_updated', models.DateTimeField(auto_now=True)), 75 | ('key', models.CharField(blank=True, db_index=True, max_length=128, unique=True)), 76 | ('is_random_bot', models.BooleanField(blank=True, default=False)), 77 | ('bot_list', models.TextField(blank=True)), 78 | ('name', models.CharField(max_length=128)), 79 | ('is_mturk', models.BooleanField(blank=True, default=False)), 80 | ('is_anonymized', models.BooleanField(blank=True, default=False)), 81 | ], 82 | options={ 83 | 'abstract': False, 84 | 'verbose_name_plural': 'studies', 85 | }, 86 | ), 87 | migrations.AddField( 88 | model_name='chatrecord', 89 | name='study_key', 90 | field=models.CharField(max_length=128), 91 | ), 92 | migrations.AddField( 93 | model_name='chatrecord', 94 | name='rating', 95 | field=models.IntegerField(default=0), 96 | ), 97 | migrations.AddField( 98 | model_name='chatrecord', 99 | name='user_session_id', 100 | field=models.CharField(max_length=128), 101 | ), 102 | migrations.AlterField( 103 | model_name='chatrecord', 104 | name='chat_id', 105 | field=models.CharField(db_index=True, max_length=128), 106 | ), 107 | migrations.AlterField( 108 | model_name='chatrecord', 109 | name='chatbot_id', 110 | field=models.CharField(max_length=128), 111 | ), 112 | ] 113 | -------------------------------------------------------------------------------- /dialog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/dialog/migrations/__init__.py -------------------------------------------------------------------------------- /dialog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | import string, random, simplejson 3 | 4 | 5 | class JsonField(models.TextField): 6 | pass 7 | 8 | 9 | def key_generator(size=10, chars=string.ascii_uppercase + string.digits): 10 | return ''.join(random.choice(chars) for _ in range(size)) 11 | 12 | 13 | class BaseModel(models.Model): 14 | ''' 15 | Base model from which all other models should inherit. It has a unique key and other nice fields 16 | ''' 17 | id = models.AutoField(primary_key=True) 18 | date_created = models.DateTimeField(auto_now_add=True) 19 | date_updated = models.DateTimeField(auto_now=True) 20 | 21 | class Meta: 22 | abstract = True 23 | 24 | 25 | class ChatRecord(BaseModel): 26 | chat_id = models.CharField(max_length=128, db_index=True) 27 | timestamp = models.DateTimeField() 28 | chatbot_id = models.CharField(max_length=128) 29 | message = models.TextField() 30 | message_history = JsonField() 31 | response = models.TextField() 32 | study_key = models.CharField(max_length=128) 33 | rating = models.IntegerField(default=0) 34 | user_session_id = models.CharField(max_length=128) 35 | 36 | 37 | class ChatRating(BaseModel): 38 | chat_id = models.CharField(max_length=128, db_index=True) 39 | timestamp = models.DateTimeField() 40 | chatbot_id = models.CharField(max_length=128) 41 | quality = models.IntegerField() 42 | coherency = models.IntegerField(null=True) 43 | fluency = models.IntegerField() 44 | diversity = models.IntegerField() 45 | contingency = models.IntegerField() 46 | empathy = models.IntegerField() 47 | comments = models.TextField(blank=True) 48 | study_key = models.CharField(max_length=128) 49 | user_session_id = models.CharField(max_length=128) 50 | mturk_code = models.CharField(max_length=128) 51 | mturk_user_id = models.CharField(max_length=128, blank=True) 52 | 53 | def generate_mturk_code(self): 54 | if not self.mturk_code: 55 | for _ in range(6): 56 | mturk_code = key_generator(6) 57 | mturk_code = mturk_code[:3] + "-" + mturk_code[3:] 58 | if not type(self).objects.filter(mturk_code=mturk_code).count(): 59 | self.mturk_code = mturk_code 60 | break 61 | 62 | def should_generate_mturk_code(self): 63 | try: 64 | study = Study.objects.get(key=self.study_key) 65 | except: 66 | return False 67 | 68 | if study.is_mturk: 69 | return True 70 | # num_previous_ratings = ChatRating.objects.filter(user_session_id=self.user_session_id, study_key=self.study_key).count() 71 | # if num_previous_ratings == 2: 72 | # return True 73 | return False 74 | 75 | 76 | class Study(BaseModel): 77 | key = models.CharField(max_length=128, unique=True, db_index=True, blank=True) 78 | name = models.CharField(max_length=128) 79 | is_random_bot = models.BooleanField(blank=True, default=False) 80 | bot_list = models.TextField(blank=True) 81 | is_mturk = models.BooleanField(blank=True, default=False) 82 | is_anonymized = models.BooleanField(blank=True, default=False) 83 | 84 | class Meta: 85 | verbose_name_plural = "studies" 86 | 87 | def generate_key(self): 88 | if not self.key: 89 | for _ in range(10): 90 | key = key_generator(10) 91 | if not type(self).objects.filter(key=key).count(): 92 | self.key = key 93 | break 94 | 95 | def save(self, *args, **kwargs): 96 | self.generate_key() 97 | super(BaseModel, self).save(*args, **kwargs) 98 | 99 | -------------------------------------------------------------------------------- /dialog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /dialog/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | from django.views.generic import RedirectView 3 | 4 | from . import views 5 | 6 | urlpatterns = [ 7 | url(r'^chat_message/', views.chat_message, name='chat_message'), 8 | url(r'^rate_chat/', views.rate_chat, name='rate_chat'), 9 | url(r'^rate_chat_response/', views.rate_chat_response, name='rate_chat_response'), 10 | url(r'^error/', views.error, name='error'), 11 | url(r'^dialogadmins/', views.bot_test_page, name='bot_test_page'), 12 | url(r'^(?P\w+)/', views.index, name='index'), 13 | url(r'^$', views.index, name='index'), 14 | ] 15 | -------------------------------------------------------------------------------- /dialog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http.response import HttpResponse 3 | import simplejson 4 | from django.views.decorators.http import require_POST 5 | from django.views.decorators.csrf import csrf_exempt 6 | from .chatbots import chatbots 7 | from .models import ChatRecord, Study, ChatRating 8 | import datetime 9 | import pytz 10 | from django.shortcuts import get_object_or_404 11 | import random 12 | from django.utils.timezone import make_aware 13 | import re 14 | import uuid 15 | from django.conf import settings 16 | 17 | def render_to_json(**kwargs): 18 | return HttpResponse(simplejson.dumps(kwargs)) 19 | 20 | 21 | def index(request, study_key=None): 22 | 23 | if not study_key: 24 | bot_list = list(chatbots.keys()) 25 | 26 | study = Study(bot_list=simplejson.dumps(bot_list), is_random_bot=True, key="index", is_anonymized=True) 27 | 28 | else: 29 | study = get_object_or_404(Study, key=study_key) 30 | 31 | botlist = [key for key in simplejson.loads(study.bot_list) if key in chatbots.keys()] 32 | 33 | if study.is_mturk: 34 | # don't repeat any bots that've already been seen by this user, but only for mturk studies, and only if they haven't already seen all the bots 35 | botlist = [bot for bot in botlist if not getattr(chatbots[bot], "is_test_bot", None)] 36 | 37 | ratings = ChatRating.objects.filter(user_session_id=_get_session_id(request)) 38 | seen_bots = [rating.chatbot_id for rating in ratings] 39 | unseen_bots = [bot for bot in botlist if bot not in seen_bots] 40 | 41 | if unseen_bots: 42 | botlist = unseen_bots 43 | 44 | if study.is_random_bot: 45 | botlist = [bot for bot in botlist if not getattr(chatbots[bot], "is_test_bot", None)] 46 | botlist = [random.choice(botlist)] if botlist else botlist 47 | 48 | return render(request, "index.html", context=dict(study=study, chatbots={key: chatbots[key] for key in botlist})) 49 | 50 | 51 | def bot_test_page(request): 52 | study = Study(bot_list=simplejson.dumps(list(chatbots.keys())), key="bot_test_page") 53 | 54 | botlist = [key for key in simplejson.loads(study.bot_list) if key in chatbots.keys()] 55 | 56 | return render(request, "index.html", context=dict(study=study, chatbots={key: chatbots[key] for key in botlist})) 57 | 58 | 59 | @csrf_exempt 60 | @require_POST 61 | def chat_message(request): 62 | 63 | message = request.POST.get("message") 64 | chatbot_id = request.POST.get("chatbotID") 65 | chat_id = request.POST.get("chatID") 66 | timestamp = request.POST.get("timestamp") 67 | message_history = request.POST.get("messageHistory") 68 | study_key = request.POST.get("studyKey") 69 | 70 | chatbot = chatbots[chatbot_id] 71 | response = chatbot.handle_messages(simplejson.loads(message_history)) 72 | 73 | chatrecord = ChatRecord(message=message, 74 | chat_id=chat_id, 75 | chatbot_id=chatbot_id, 76 | timestamp=datetime.datetime.fromtimestamp(int(timestamp)/1000).replace(tzinfo=pytz.UTC), 77 | message_history=message_history, 78 | study_key=study_key, 79 | response=response, 80 | user_session_id=_get_session_id(request), 81 | ) 82 | chatrecord.save() 83 | 84 | emoji_regex = re.compile(u"[^\U00000000-\U0000d7ff\U0000e000-\U0000ffff]", flags=re.UNICODE) 85 | response_stripped = emoji_regex.sub(u'', response).strip() 86 | 87 | return render_to_json(success=True, response=response, response_stripped=response_stripped, response_id=chatrecord.id) 88 | 89 | 90 | @csrf_exempt 91 | @require_POST 92 | def rate_chat_response(request): 93 | chatrecord_id = request.POST.get("response_id") 94 | rating = int(request.POST.get("rating")) 95 | 96 | chatrecord = get_object_or_404(ChatRecord, pk=chatrecord_id) 97 | 98 | make_vote_active = chatrecord.rating != rating 99 | chatrecord.rating = rating if make_vote_active else 0 100 | chatrecord.save() 101 | 102 | return render_to_json(success=True, make_vote_active=make_vote_active) 103 | 104 | 105 | @require_POST 106 | def rate_chat(request): 107 | 108 | chat_id = request.POST.get("chat_id") 109 | chatbot_id = request.POST.get("chatbot_id") 110 | study_key = request.POST.get("study_key") 111 | quality = request.POST.get("quality") 112 | fluency = request.POST.get("fluency") 113 | diversity = request.POST.get("diversity") 114 | contingency = request.POST.get("contingency") 115 | empathy = request.POST.get("empathy") 116 | comments = request.POST.get("comments") 117 | mturk_user_id = request.POST.get("mturkUserId", "") 118 | 119 | chat_rating = ChatRating( 120 | chat_id=chat_id, 121 | chatbot_id=chatbot_id, 122 | study_key=study_key, 123 | quality=quality, 124 | fluency=fluency, 125 | diversity=diversity, 126 | contingency=contingency, 127 | empathy=empathy, 128 | comments=comments, 129 | timestamp=make_aware(datetime.datetime.utcnow()), 130 | user_session_id=_get_session_id(request), 131 | mturk_user_id=mturk_user_id, 132 | ) 133 | 134 | if chat_rating.should_generate_mturk_code(): 135 | chat_rating.generate_mturk_code() 136 | 137 | chat_rating.save() 138 | 139 | return render_to_json(success=True, mturk_code=chat_rating.mturk_code) 140 | 141 | 142 | def error(request): 143 | return render_to_json(value=5/0) 144 | 145 | 146 | def _get_session_id(request): 147 | if not request.session.get("uuid"): 148 | request.session["uuid"] = str(uuid.uuid4()) 149 | return request.session["uuid"] 150 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/project/__init__.py -------------------------------------------------------------------------------- /project/environment.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/project/environment.py -------------------------------------------------------------------------------- /project/jinja2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/project/jinja2/__init__.py -------------------------------------------------------------------------------- /project/jinja2/environment.py: -------------------------------------------------------------------------------- 1 | from django.contrib.staticfiles.storage import staticfiles_storage 2 | from django.urls import reverse 3 | from jinja2 import Environment 4 | from jinja2_pluralize import pluralize_dj 5 | import simplejson 6 | from django.conf import settings 7 | 8 | def cachebreak(): 9 | return "?c=" + settings.STARTUP_TIMESTAMP 10 | 11 | 12 | def environment(**options): 13 | env = Environment(**options) 14 | env.filters.update({ 15 | 'pluralize': pluralize_dj, 16 | 'json': simplejson.dumps, 17 | }) 18 | env.globals.update({ 19 | 'static': staticfiles_storage.url, 20 | 'url': reverse, 21 | 'list': list, 22 | 'cachebreak': cachebreak, 23 | }) 24 | return env 25 | -------------------------------------------------------------------------------- /project/prodsettings.py: -------------------------------------------------------------------------------- 1 | from .settings import * 2 | 3 | DEBUG = False 4 | -------------------------------------------------------------------------------- /project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for project project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | import datetime 15 | import getpass 16 | 17 | try: 18 | import pymysql 19 | 20 | pymysql.install_as_MySQLdb() 21 | except: 22 | pass 23 | 24 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 25 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 26 | 27 | # Quick-start development settings - unsuitable for production 28 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 29 | 30 | # SECURITY WARNING: keep the secret key used in production secret! 31 | SECRET_KEY = '[[ EDITME - change this to a random string! ]]' 32 | 33 | # SECURITY WARNING: don't run with debug turned on in production! 34 | IS_PRODUCTION = getpass.getuser() == "www-data" 35 | DEBUG = not IS_PRODUCTION 36 | 37 | ALLOWED_HOSTS = ['*'] 38 | 39 | STARTUP_TIMESTAMP = str(int(datetime.datetime.now().timestamp())) 40 | 41 | # Application definition 42 | 43 | INSTALLED_APPS = [ 44 | 'django.contrib.admin', 45 | 'django.contrib.auth', 46 | 'django.contrib.contenttypes', 47 | 'django.contrib.sessions', 48 | 'django.contrib.messages', 49 | 'django.contrib.staticfiles', 50 | 'dialog', 51 | ] 52 | 53 | MIDDLEWARE = [ 54 | 'django.middleware.security.SecurityMiddleware', 55 | 'django.contrib.sessions.middleware.SessionMiddleware', 56 | 'django.middleware.common.CommonMiddleware', 57 | 'django.middleware.csrf.CsrfViewMiddleware', 58 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 59 | 'django.contrib.messages.middleware.MessageMiddleware', 60 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 61 | ] 62 | 63 | ROOT_URLCONF = 'project.urls' 64 | 65 | TEMPLATES = [ 66 | { 67 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 68 | 'DIRS': [ 69 | os.path.join(BASE_DIR, "templatesadmin"), 70 | ], 71 | 'APP_DIRS': True, 72 | 'OPTIONS': { 73 | 'context_processors': [ 74 | 'django.template.context_processors.debug', 75 | 'django.template.context_processors.request', 76 | 'django.contrib.auth.context_processors.auth', 77 | 'django.contrib.messages.context_processors.messages', 78 | ], 79 | }, 80 | }, 81 | { 82 | 'BACKEND': 'django.template.backends.jinja2.Jinja2', 83 | 'DIRS': [ 84 | os.path.join(BASE_DIR, "templates"), 85 | ], 86 | 'OPTIONS': { 87 | 'environment': 'project.jinja2.environment.environment', 88 | }, 89 | }, 90 | ] 91 | 92 | WSGI_APPLICATION = 'project.wsgi.application' 93 | 94 | # Database 95 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 96 | 97 | DATABASES = { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.mysql', 100 | 'NAME': '[[ EDITME - Mysql Database Name ]]', 101 | 'USER': '[[ EDITME - MYSQL Username ]]', 102 | 'PASSWORD': '[[ EDITME - MYSQL Password ]]', 103 | 'HOST': '', 104 | 'PORT': '', 105 | 'OPTIONS': { 106 | 'init_command': 'SET default_storage_engine=INNODB', 107 | 'charset': 'utf8mb4', 108 | } 109 | } 110 | } 111 | 112 | # Password validation 113 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 114 | 115 | AUTH_PASSWORD_VALIDATORS = [ 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 121 | }, 122 | { 123 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 124 | }, 125 | { 126 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 127 | }, 128 | ] 129 | 130 | # Internationalization 131 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 132 | 133 | LANGUAGE_CODE = 'en-us' 134 | 135 | TIME_ZONE = 'UTC' 136 | 137 | USE_I18N = True 138 | 139 | USE_L10N = True 140 | 141 | USE_TZ = True 142 | 143 | # Static files (CSS, JavaScript, Images) 144 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 145 | 146 | STATIC_URL = '/static/' 147 | 148 | STATICFILES_DIRS = ( 149 | os.path.join(BASE_DIR, "static"), 150 | ) 151 | 152 | STATIC_ROOT = '/opt/staticfiles/' 153 | 154 | APPEND_SLASH = True 155 | 156 | LOGGING = { 157 | 'version': 1, 158 | 'disable_existing_loggers': False, 159 | 'filters': { 160 | 'require_debug_false': { 161 | '()': 'django.utils.log.RequireDebugFalse', 162 | }, 163 | 'require_debug_true': { 164 | '()': 'django.utils.log.RequireDebugTrue', 165 | }, 166 | }, 167 | 'formatters': { 168 | 'django.server': { 169 | '()': 'django.utils.log.ServerFormatter', 170 | 'format': '[%(server_time)s] %(message)s', 171 | }, 172 | 'simple': { 173 | 'format': '%(levelname)s %(asctime)s %(name)s.%(funcName)s:%(lineno)s- %(message)s' 174 | }, 175 | }, 176 | 'handlers': { 177 | 'console': { 178 | 'level': 'INFO', 179 | 'filters': ['require_debug_true'], 180 | 'class': 'logging.StreamHandler', 181 | 'formatter': 'simple', 182 | }, 183 | 'console_debug_false': { 184 | 'level': 'ERROR', 185 | 'filters': ['require_debug_false'], 186 | 'class': 'logging.StreamHandler', 187 | }, 188 | 'django.server': { 189 | 'level': 'INFO', 190 | 'class': 'logging.StreamHandler', 191 | 'formatter': 'django.server', 192 | }, 193 | 'logfile': { 194 | 'class': 'logging.handlers.WatchedFileHandler', 195 | 'filename': '/var/log/django/django.log', 196 | 'formatter': 'simple', 197 | }, 198 | }, 199 | 'loggers': { 200 | 'django': { 201 | 'handlers': ['console', 'console_debug_false', 'logfile'], 202 | 'level': 'INFO', 203 | }, 204 | 'django.server': { 205 | 'handlers': ['django.server'], 206 | 'level': 'INFO', 207 | 'propagate': False, 208 | } 209 | } 210 | } 211 | 212 | SESSION_COOKIE_AGE = 60 * 60 * 24 * 30 213 | -------------------------------------------------------------------------------- /project/urls.py: -------------------------------------------------------------------------------- 1 | """project URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import include, path 18 | from django.conf import settings 19 | from django.conf.urls.static import static 20 | 21 | urlpatterns = [ 22 | path('admin/', admin.site.urls), 23 | path('', include('dialog.urls')), 24 | ] 25 | 26 | 27 | if settings.DEBUG: 28 | urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) 29 | 30 | -------------------------------------------------------------------------------- /project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for project project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django==2.1.7 2 | inflect==2.1.0 3 | Jinja2==2.10 4 | jinja2-pluralize==0.3.0 5 | MarkupSafe==1.1.0 6 | pytz==2018.9 7 | simplejson==3.16.0 8 | mysqlclient==1.4.2.post1 9 | Fabric3==1.14.post1 10 | sqlparse==0.2.4 11 | PyMySQL==0.9.3 12 | -------------------------------------------------------------------------------- /static/image/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/static/image/close.png -------------------------------------------------------------------------------- /static/image/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/static/image/loading.png -------------------------------------------------------------------------------- /static/image/tutorial/closeandrate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/static/image/tutorial/closeandrate.png -------------------------------------------------------------------------------- /static/image/tutorial/upvotes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asmadotgh/neural_chat_web/1ef29cae7349ad945180adbd0a6d005087fe1365/static/image/tutorial/upvotes.png -------------------------------------------------------------------------------- /static/js/cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.2.0 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 6 | * Released under the MIT license 7 | */ 8 | ;(function (factory) { 9 | var registeredInModuleLoader = false; 10 | if (typeof define === 'function' && define.amd) { 11 | define(factory); 12 | registeredInModuleLoader = true; 13 | } 14 | if (typeof exports === 'object') { 15 | module.exports = factory(); 16 | registeredInModuleLoader = true; 17 | } 18 | if (!registeredInModuleLoader) { 19 | var OldCookies = window.Cookies; 20 | var api = window.Cookies = factory(); 21 | api.noConflict = function () { 22 | window.Cookies = OldCookies; 23 | return api; 24 | }; 25 | } 26 | }(function () { 27 | function extend () { 28 | var i = 0; 29 | var result = {}; 30 | for (; i < arguments.length; i++) { 31 | var attributes = arguments[ i ]; 32 | for (var key in attributes) { 33 | result[key] = attributes[key]; 34 | } 35 | } 36 | return result; 37 | } 38 | 39 | function init (converter) { 40 | function api (key, value, attributes) { 41 | var result; 42 | if (typeof document === 'undefined') { 43 | return; 44 | } 45 | 46 | // Write 47 | 48 | if (arguments.length > 1) { 49 | attributes = extend({ 50 | path: '/' 51 | }, api.defaults, attributes); 52 | 53 | if (typeof attributes.expires === 'number') { 54 | var expires = new Date(); 55 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 56 | attributes.expires = expires; 57 | } 58 | 59 | // We're using "expires" because "max-age" is not supported by IE 60 | attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; 61 | 62 | try { 63 | result = JSON.stringify(value); 64 | if (/^[\{\[]/.test(result)) { 65 | value = result; 66 | } 67 | } catch (e) {} 68 | 69 | if (!converter.write) { 70 | value = encodeURIComponent(String(value)) 71 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 72 | } else { 73 | value = converter.write(value, key); 74 | } 75 | 76 | key = encodeURIComponent(String(key)); 77 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 78 | key = key.replace(/[\(\)]/g, escape); 79 | 80 | var stringifiedAttributes = ''; 81 | 82 | for (var attributeName in attributes) { 83 | if (!attributes[attributeName]) { 84 | continue; 85 | } 86 | stringifiedAttributes += '; ' + attributeName; 87 | if (attributes[attributeName] === true) { 88 | continue; 89 | } 90 | stringifiedAttributes += '=' + attributes[attributeName]; 91 | } 92 | return (document.cookie = key + '=' + value + stringifiedAttributes); 93 | } 94 | 95 | // Read 96 | 97 | if (!key) { 98 | result = {}; 99 | } 100 | 101 | // To prevent the for loop in the first place assign an empty array 102 | // in case there are no cookies at all. Also prevents odd result when 103 | // calling "get()" 104 | var cookies = document.cookie ? document.cookie.split('; ') : []; 105 | var rdecode = /(%[0-9A-Z]{2})+/g; 106 | var i = 0; 107 | 108 | for (; i < cookies.length; i++) { 109 | var parts = cookies[i].split('='); 110 | var cookie = parts.slice(1).join('='); 111 | 112 | if (!this.json && cookie.charAt(0) === '"') { 113 | cookie = cookie.slice(1, -1); 114 | } 115 | 116 | try { 117 | var name = parts[0].replace(rdecode, decodeURIComponent); 118 | cookie = converter.read ? 119 | converter.read(cookie, name) : converter(cookie, name) || 120 | cookie.replace(rdecode, decodeURIComponent); 121 | 122 | if (this.json) { 123 | try { 124 | cookie = JSON.parse(cookie); 125 | } catch (e) {} 126 | } 127 | 128 | if (key === name) { 129 | result = cookie; 130 | break; 131 | } 132 | 133 | if (!key) { 134 | result[name] = cookie; 135 | } 136 | } catch (e) {} 137 | } 138 | 139 | return result; 140 | } 141 | 142 | api.set = api; 143 | api.get = function (key) { 144 | return api.call(api, key); 145 | }; 146 | api.getJSON = function () { 147 | return api.apply({ 148 | json: true 149 | }, [].slice.call(arguments)); 150 | }; 151 | api.defaults = {}; 152 | 153 | api.remove = function (key, attributes) { 154 | api(key, '', extend(attributes, { 155 | expires: -1 156 | })); 157 | }; 158 | 159 | api.withConverter = init; 160 | 161 | return api; 162 | } 163 | 164 | return init(function () {}); 165 | })); 166 | -------------------------------------------------------------------------------- /static/perfectscrollbar/perfect-scrollbar.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Container style 3 | */ 4 | .ps { 5 | overflow: hidden !important; 6 | overflow-anchor: none; 7 | -ms-overflow-style: none; 8 | touch-action: auto; 9 | -ms-touch-action: auto; 10 | } 11 | 12 | /* 13 | * Scrollbar rail styles 14 | */ 15 | .ps__rail-x { 16 | display: none; 17 | opacity: 0; 18 | transition: background-color .2s linear, opacity .2s linear; 19 | -webkit-transition: background-color .2s linear, opacity .2s linear; 20 | height: 15px; 21 | /* there must be 'bottom' or 'top' for ps__rail-x */ 22 | bottom: 0px; 23 | /* please don't change 'position' */ 24 | position: absolute; 25 | } 26 | 27 | .ps__rail-y { 28 | display: none; 29 | opacity: 0; 30 | transition: background-color .2s linear, opacity .2s linear; 31 | -webkit-transition: background-color .2s linear, opacity .2s linear; 32 | width: 15px; 33 | /* there must be 'right' or 'left' for ps__rail-y */ 34 | right: 0; 35 | /* please don't change 'position' */ 36 | position: absolute; 37 | } 38 | 39 | .ps--active-x > .ps__rail-x, 40 | .ps--active-y > .ps__rail-y { 41 | display: block; 42 | background-color: transparent; 43 | } 44 | 45 | .ps:hover > .ps__rail-x, 46 | .ps:hover > .ps__rail-y, 47 | .ps--focus > .ps__rail-x, 48 | .ps--focus > .ps__rail-y, 49 | .ps--scrolling-x > .ps__rail-x, 50 | .ps--scrolling-y > .ps__rail-y { 51 | opacity: 0.6; 52 | } 53 | 54 | .ps .ps__rail-x:hover, 55 | .ps .ps__rail-y:hover, 56 | .ps .ps__rail-x:focus, 57 | .ps .ps__rail-y:focus, 58 | .ps .ps__rail-x.ps--clicking, 59 | .ps .ps__rail-y.ps--clicking { 60 | background-color: #eee; 61 | opacity: 0.9; 62 | } 63 | 64 | /* 65 | * Scrollbar thumb styles 66 | */ 67 | .ps__thumb-x { 68 | background-color: #aaa; 69 | border-radius: 6px; 70 | transition: background-color .2s linear, height .2s ease-in-out; 71 | -webkit-transition: background-color .2s linear, height .2s ease-in-out; 72 | height: 6px; 73 | /* there must be 'bottom' for ps__thumb-x */ 74 | bottom: 2px; 75 | /* please don't change 'position' */ 76 | position: absolute; 77 | } 78 | 79 | .ps__thumb-y { 80 | background-color: #aaa; 81 | border-radius: 6px; 82 | transition: background-color .2s linear, width .2s ease-in-out; 83 | -webkit-transition: background-color .2s linear, width .2s ease-in-out; 84 | width: 6px; 85 | /* there must be 'right' for ps__thumb-y */ 86 | right: 2px; 87 | /* please don't change 'position' */ 88 | position: absolute; 89 | } 90 | 91 | .ps__rail-x:hover > .ps__thumb-x, 92 | .ps__rail-x:focus > .ps__thumb-x, 93 | .ps__rail-x.ps--clicking .ps__thumb-x { 94 | background-color: #999; 95 | height: 11px; 96 | } 97 | 98 | .ps__rail-y:hover > .ps__thumb-y, 99 | .ps__rail-y:focus > .ps__thumb-y, 100 | .ps__rail-y.ps--clicking .ps__thumb-y { 101 | background-color: #999; 102 | width: 11px; 103 | } 104 | 105 | /* MS supports */ 106 | @supports (-ms-overflow-style: none) { 107 | .ps { 108 | overflow: auto !important; 109 | } 110 | } 111 | 112 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 113 | .ps { 114 | overflow: auto !important; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /static/perfectscrollbar/perfect-scrollbar.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * perfect-scrollbar v1.4.0 3 | * (c) 2018 Hyunje Jun 4 | * @license MIT 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.PerfectScrollbar=e()}(this,function(){"use strict";function t(t){return getComputedStyle(t)}function e(t,e){for(var i in e){var r=e[i];"number"==typeof r&&(r+="px"),t.style[i]=r}return t}function i(t){var e=document.createElement("div");return e.className=t,e}function r(t,e){if(!v)throw new Error("No element matching method supported");return v.call(t,e)}function l(t){t.remove?t.remove():t.parentNode&&t.parentNode.removeChild(t)}function n(t,e){return Array.prototype.filter.call(t.children,function(t){return r(t,e)})}function o(t,e){var i=t.element.classList,r=m.state.scrolling(e);i.contains(r)?clearTimeout(Y[e]):i.add(r)}function s(t,e){Y[e]=setTimeout(function(){return t.isAlive&&t.element.classList.remove(m.state.scrolling(e))},t.settings.scrollingThreshold)}function a(t,e){o(t,e),s(t,e)}function c(t){if("function"==typeof window.CustomEvent)return new CustomEvent(t);var e=document.createEvent("CustomEvent");return e.initCustomEvent(t,!1,!1,void 0),e}function h(t,e,i,r,l){var n=i[0],o=i[1],s=i[2],h=i[3],u=i[4],d=i[5];void 0===r&&(r=!0),void 0===l&&(l=!1);var f=t.element;t.reach[h]=null,f[s]<1&&(t.reach[h]="start"),f[s]>t[n]-t[o]-1&&(t.reach[h]="end"),e&&(f.dispatchEvent(c("ps-scroll-"+h)),e<0?f.dispatchEvent(c("ps-scroll-"+u)):e>0&&f.dispatchEvent(c("ps-scroll-"+d)),r&&a(t,h)),t.reach[h]&&(e||l)&&f.dispatchEvent(c("ps-"+h+"-reach-"+t.reach[h]))}function u(t){return parseInt(t,10)||0}function d(t){return r(t,"input,[contenteditable]")||r(t,"select,[contenteditable]")||r(t,"textarea,[contenteditable]")||r(t,"button,[contenteditable]")}function f(e){var i=t(e);return u(i.width)+u(i.paddingLeft)+u(i.paddingRight)+u(i.borderLeftWidth)+u(i.borderRightWidth)}function p(t,e){return t.settings.minScrollbarLength&&(e=Math.max(e,t.settings.minScrollbarLength)),t.settings.maxScrollbarLength&&(e=Math.min(e,t.settings.maxScrollbarLength)),e}function b(t,i){var r={width:i.railXWidth},l=Math.floor(t.scrollTop);i.isRtl?r.left=i.negativeScrollAdjustment+t.scrollLeft+i.containerWidth-i.contentWidth:r.left=t.scrollLeft,i.isScrollbarXUsingBottom?r.bottom=i.scrollbarXBottom-l:r.top=i.scrollbarXTop+l,e(i.scrollbarXRail,r);var n={top:l,height:i.railYHeight};i.isScrollbarYUsingRight?i.isRtl?n.right=i.contentWidth-(i.negativeScrollAdjustment+t.scrollLeft)-i.scrollbarYRight-i.scrollbarYOuterWidth:n.right=i.scrollbarYRight-t.scrollLeft:i.isRtl?n.left=i.negativeScrollAdjustment+t.scrollLeft+2*i.containerWidth-i.contentWidth-i.scrollbarYLeft-i.scrollbarYOuterWidth:n.left=i.scrollbarYLeft+t.scrollLeft,e(i.scrollbarYRail,n),e(i.scrollbarX,{left:i.scrollbarXLeft,width:i.scrollbarXWidth-i.railBorderXWidth}),e(i.scrollbarY,{top:i.scrollbarYTop,height:i.scrollbarYHeight-i.railBorderYWidth})}function g(t,e){function i(e){b[d]=g+Y*(e[a]-v),o(t,f),R(t),e.stopPropagation(),e.preventDefault()}function r(){s(t,f),t[p].classList.remove(m.state.clicking),t.event.unbind(t.ownerDocument,"mousemove",i)}var l=e[0],n=e[1],a=e[2],c=e[3],h=e[4],u=e[5],d=e[6],f=e[7],p=e[8],b=t.element,g=null,v=null,Y=null;t.event.bind(t[h],"mousedown",function(e){g=b[d],v=e[a],Y=(t[n]-t[l])/(t[c]-t[u]),t.event.bind(t.ownerDocument,"mousemove",i),t.event.once(t.ownerDocument,"mouseup",r),t[p].classList.add(m.state.clicking),e.stopPropagation(),e.preventDefault()})}var v="undefined"!=typeof Element&&(Element.prototype.matches||Element.prototype.webkitMatchesSelector||Element.prototype.mozMatchesSelector||Element.prototype.msMatchesSelector),m={main:"ps",element:{thumb:function(t){return"ps__thumb-"+t},rail:function(t){return"ps__rail-"+t},consuming:"ps__child--consume"},state:{focus:"ps--focus",clicking:"ps--clicking",active:function(t){return"ps--active-"+t},scrolling:function(t){return"ps--scrolling-"+t}}},Y={x:null,y:null},X=function(t){this.element=t,this.handlers={}},w={isEmpty:{configurable:!0}};X.prototype.bind=function(t,e){void 0===this.handlers[t]&&(this.handlers[t]=[]),this.handlers[t].push(e),this.element.addEventListener(t,e,!1)},X.prototype.unbind=function(t,e){var i=this;this.handlers[t]=this.handlers[t].filter(function(r){return!(!e||r===e)||(i.element.removeEventListener(t,r,!1),!1)})},X.prototype.unbindAll=function(){var t=this;for(var e in t.handlers)t.unbind(e)},w.isEmpty.get=function(){var t=this;return Object.keys(this.handlers).every(function(e){return 0===t.handlers[e].length})},Object.defineProperties(X.prototype,w);var y=function(){this.eventElements=[]};y.prototype.eventElement=function(t){var e=this.eventElements.filter(function(e){return e.element===t})[0];return e||(e=new X(t),this.eventElements.push(e)),e},y.prototype.bind=function(t,e,i){this.eventElement(t).bind(e,i)},y.prototype.unbind=function(t,e,i){var r=this.eventElement(t);r.unbind(e,i),r.isEmpty&&this.eventElements.splice(this.eventElements.indexOf(r),1)},y.prototype.unbindAll=function(){this.eventElements.forEach(function(t){return t.unbindAll()}),this.eventElements=[]},y.prototype.once=function(t,e,i){var r=this.eventElement(t),l=function(t){r.unbind(e,l),i(t)};r.bind(e,l)};var W=function(t,e,i,r,l){void 0===r&&(r=!0),void 0===l&&(l=!1);var n;if("top"===e)n=["contentHeight","containerHeight","scrollTop","y","up","down"];else{if("left"!==e)throw new Error("A proper axis should be provided");n=["contentWidth","containerWidth","scrollLeft","x","left","right"]}h(t,i,n,r,l)},L={isWebKit:"undefined"!=typeof document&&"WebkitAppearance"in document.documentElement.style,supportsTouch:"undefined"!=typeof window&&("ontouchstart"in window||window.DocumentTouch&&document instanceof window.DocumentTouch),supportsIePointer:"undefined"!=typeof navigator&&navigator.msMaxTouchPoints,isChrome:"undefined"!=typeof navigator&&/Chrome/i.test(navigator&&navigator.userAgent)},R=function(t){var e=t.element,i=Math.floor(e.scrollTop);t.containerWidth=e.clientWidth,t.containerHeight=e.clientHeight,t.contentWidth=e.scrollWidth,t.contentHeight=e.scrollHeight,e.contains(t.scrollbarXRail)||(n(e,m.element.rail("x")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarXRail)),e.contains(t.scrollbarYRail)||(n(e,m.element.rail("y")).forEach(function(t){return l(t)}),e.appendChild(t.scrollbarYRail)),!t.settings.suppressScrollX&&t.containerWidth+t.settings.scrollXMarginOffset=t.railXWidth-t.scrollbarXWidth&&(t.scrollbarXLeft=t.railXWidth-t.scrollbarXWidth),t.scrollbarYTop>=t.railYHeight-t.scrollbarYHeight&&(t.scrollbarYTop=t.railYHeight-t.scrollbarYHeight),b(e,t),t.scrollbarXActive?e.classList.add(m.state.active("x")):(e.classList.remove(m.state.active("x")),t.scrollbarXWidth=0,t.scrollbarXLeft=0,e.scrollLeft=0),t.scrollbarYActive?e.classList.add(m.state.active("y")):(e.classList.remove(m.state.active("y")),t.scrollbarYHeight=0,t.scrollbarYTop=0,e.scrollTop=0)},T={"click-rail":function(t){t.event.bind(t.scrollbarY,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarYRail,"mousedown",function(e){var i=e.pageY-window.pageYOffset-t.scrollbarYRail.getBoundingClientRect().top>t.scrollbarYTop?1:-1;t.element.scrollTop+=i*t.containerHeight,R(t),e.stopPropagation()}),t.event.bind(t.scrollbarX,"mousedown",function(t){return t.stopPropagation()}),t.event.bind(t.scrollbarXRail,"mousedown",function(e){var i=e.pageX-window.pageXOffset-t.scrollbarXRail.getBoundingClientRect().left>t.scrollbarXLeft?1:-1;t.element.scrollLeft+=i*t.containerWidth,R(t),e.stopPropagation()})},"drag-thumb":function(t){g(t,["containerWidth","contentWidth","pageX","railXWidth","scrollbarX","scrollbarXWidth","scrollLeft","x","scrollbarXRail"]),g(t,["containerHeight","contentHeight","pageY","railYHeight","scrollbarY","scrollbarYHeight","scrollTop","y","scrollbarYRail"])},keyboard:function(t){function e(e,r){var l=Math.floor(i.scrollTop);if(0===e){if(!t.scrollbarYActive)return!1;if(0===l&&r>0||l>=t.contentHeight-t.containerHeight&&r<0)return!t.settings.wheelPropagation}var n=i.scrollLeft;if(0===r){if(!t.scrollbarXActive)return!1;if(0===n&&e<0||n>=t.contentWidth-t.containerWidth&&e>0)return!t.settings.wheelPropagation}return!0}var i=t.element,l=function(){return r(i,":hover")},n=function(){return r(t.scrollbarX,":focus")||r(t.scrollbarY,":focus")};t.event.bind(t.ownerDocument,"keydown",function(r){if(!(r.isDefaultPrevented&&r.isDefaultPrevented()||r.defaultPrevented)&&(l()||n())){var o=document.activeElement?document.activeElement:t.ownerDocument.activeElement;if(o){if("IFRAME"===o.tagName)o=o.contentDocument.activeElement;else for(;o.shadowRoot;)o=o.shadowRoot.activeElement;if(d(o))return}var s=0,a=0;switch(r.which){case 37:s=r.metaKey?-t.contentWidth:r.altKey?-t.containerWidth:-30;break;case 38:a=r.metaKey?t.contentHeight:r.altKey?t.containerHeight:30;break;case 39:s=r.metaKey?t.contentWidth:r.altKey?t.containerWidth:30;break;case 40:a=r.metaKey?-t.contentHeight:r.altKey?-t.containerHeight:-30;break;case 32:a=r.shiftKey?t.containerHeight:-t.containerHeight;break;case 33:a=t.containerHeight;break;case 34:a=-t.containerHeight;break;case 36:a=t.contentHeight;break;case 35:a=-t.contentHeight;break;default:return}t.settings.suppressScrollX&&0!==s||t.settings.suppressScrollY&&0!==a||(i.scrollTop-=a,i.scrollLeft+=s,R(t),e(s,a)&&r.preventDefault())}})},wheel:function(e){function i(t,i){var r=Math.floor(o.scrollTop),l=0===o.scrollTop,n=r+o.offsetHeight===o.scrollHeight,s=0===o.scrollLeft,a=o.scrollLeft+o.offsetWidth===o.scrollWidth;return!(Math.abs(i)>Math.abs(t)?l||n:s||a)||!e.settings.wheelPropagation}function r(t){var e=t.deltaX,i=-1*t.deltaY;return void 0!==e&&void 0!==i||(e=-1*t.wheelDeltaX/6,i=t.wheelDeltaY/6),t.deltaMode&&1===t.deltaMode&&(e*=10,i*=10),e!==e&&i!==i&&(e=0,i=t.wheelDelta),t.shiftKey?[-i,-e]:[e,i]}function l(e,i,r){if(!L.isWebKit&&o.querySelector("select:focus"))return!0;if(!o.contains(e))return!1;for(var l=e;l&&l!==o;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var s=l.scrollHeight-l.clientHeight;if(s>0&&!(0===l.scrollTop&&r>0||l.scrollTop===s&&r<0))return!0;var a=l.scrollWidth-l.clientWidth;if(a>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===a&&i>0))return!0}l=l.parentNode}return!1}function n(t){var n=r(t),s=n[0],a=n[1];if(!l(t.target,s,a)){var c=!1;e.settings.useBothWheelAxes?e.scrollbarYActive&&!e.scrollbarXActive?(a?o.scrollTop-=a*e.settings.wheelSpeed:o.scrollTop+=s*e.settings.wheelSpeed,c=!0):e.scrollbarXActive&&!e.scrollbarYActive&&(s?o.scrollLeft+=s*e.settings.wheelSpeed:o.scrollLeft-=a*e.settings.wheelSpeed,c=!0):(o.scrollTop-=a*e.settings.wheelSpeed,o.scrollLeft+=s*e.settings.wheelSpeed),R(e),(c=c||i(s,a))&&!t.ctrlKey&&(t.stopPropagation(),t.preventDefault())}}var o=e.element;void 0!==window.onwheel?e.event.bind(o,"wheel",n):void 0!==window.onmousewheel&&e.event.bind(o,"mousewheel",n)},touch:function(e){function i(t,i){var r=Math.floor(h.scrollTop),l=h.scrollLeft,n=Math.abs(t),o=Math.abs(i);if(o>n){if(i<0&&r===e.contentHeight-e.containerHeight||i>0&&0===r)return 0===window.scrollY&&i>0&&L.isChrome}else if(n>o&&(t<0&&l===e.contentWidth-e.containerWidth||t>0&&0===l))return!0;return!0}function r(t,i){h.scrollTop-=i,h.scrollLeft-=t,R(e)}function l(t){return t.targetTouches?t.targetTouches[0]:t}function n(t){return!(t.pointerType&&"pen"===t.pointerType&&0===t.buttons||(!t.targetTouches||1!==t.targetTouches.length)&&(!t.pointerType||"mouse"===t.pointerType||t.pointerType===t.MSPOINTER_TYPE_MOUSE))}function o(t){if(n(t)){var e=l(t);u.pageX=e.pageX,u.pageY=e.pageY,d=(new Date).getTime(),null!==p&&clearInterval(p)}}function s(e,i,r){if(!h.contains(e))return!1;for(var l=e;l&&l!==h;){if(l.classList.contains(m.element.consuming))return!0;var n=t(l);if([n.overflow,n.overflowX,n.overflowY].join("").match(/(scroll|auto)/)){var o=l.scrollHeight-l.clientHeight;if(o>0&&!(0===l.scrollTop&&r>0||l.scrollTop===o&&r<0))return!0;var s=l.scrollLeft-l.clientWidth;if(s>0&&!(0===l.scrollLeft&&i<0||l.scrollLeft===s&&i>0))return!0}l=l.parentNode}return!1}function a(t){if(n(t)){var e=l(t),o={pageX:e.pageX,pageY:e.pageY},a=o.pageX-u.pageX,c=o.pageY-u.pageY;if(s(t.target,a,c))return;r(a,c),u=o;var h=(new Date).getTime(),p=h-d;p>0&&(f.x=a/p,f.y=c/p,d=h),i(a,c)&&t.preventDefault()}}function c(){e.settings.swipeEasing&&(clearInterval(p),p=setInterval(function(){e.isInitialized?clearInterval(p):f.x||f.y?Math.abs(f.x)<.01&&Math.abs(f.y)<.01?clearInterval(p):(r(30*f.x,30*f.y),f.x*=.8,f.y*=.8):clearInterval(p)},10))}if(L.supportsTouch||L.supportsIePointer){var h=e.element,u={},d=0,f={},p=null;L.supportsTouch?(e.event.bind(h,"touchstart",o),e.event.bind(h,"touchmove",a),e.event.bind(h,"touchend",c)):L.supportsIePointer&&(window.PointerEvent?(e.event.bind(h,"pointerdown",o),e.event.bind(h,"pointermove",a),e.event.bind(h,"pointerup",c)):window.MSPointerEvent&&(e.event.bind(h,"MSPointerDown",o),e.event.bind(h,"MSPointerMove",a),e.event.bind(h,"MSPointerUp",c)))}}},H=function(r,l){var n=this;if(void 0===l&&(l={}),"string"==typeof r&&(r=document.querySelector(r)),!r||!r.nodeName)throw new Error("no element is specified to initialize PerfectScrollbar");this.element=r,r.classList.add(m.main),this.settings={handlers:["click-rail","drag-thumb","keyboard","wheel","touch"],maxScrollbarLength:null,minScrollbarLength:null,scrollingThreshold:1e3,scrollXMarginOffset:0,scrollYMarginOffset:0,suppressScrollX:!1,suppressScrollY:!1,swipeEasing:!0,useBothWheelAxes:!1,wheelPropagation:!0,wheelSpeed:1};for(var o in l)n.settings[o]=l[o];this.containerWidth=null,this.containerHeight=null,this.contentWidth=null,this.contentHeight=null;var s=function(){return r.classList.add(m.state.focus)},a=function(){return r.classList.remove(m.state.focus)};this.isRtl="rtl"===t(r).direction,this.isNegativeScroll=function(){var t=r.scrollLeft,e=null;return r.scrollLeft=-1,e=r.scrollLeft<0,r.scrollLeft=t,e}(),this.negativeScrollAdjustment=this.isNegativeScroll?r.scrollWidth-r.clientWidth:0,this.event=new y,this.ownerDocument=r.ownerDocument||document,this.scrollbarXRail=i(m.element.rail("x")),r.appendChild(this.scrollbarXRail),this.scrollbarX=i(m.element.thumb("x")),this.scrollbarXRail.appendChild(this.scrollbarX),this.scrollbarX.setAttribute("tabindex",0),this.event.bind(this.scrollbarX,"focus",s),this.event.bind(this.scrollbarX,"blur",a),this.scrollbarXActive=null,this.scrollbarXWidth=null,this.scrollbarXLeft=null;var c=t(this.scrollbarXRail);this.scrollbarXBottom=parseInt(c.bottom,10),isNaN(this.scrollbarXBottom)?(this.isScrollbarXUsingBottom=!1,this.scrollbarXTop=u(c.top)):this.isScrollbarXUsingBottom=!0,this.railBorderXWidth=u(c.borderLeftWidth)+u(c.borderRightWidth),e(this.scrollbarXRail,{display:"block"}),this.railXMarginWidth=u(c.marginLeft)+u(c.marginRight),e(this.scrollbarXRail,{display:""}),this.railXWidth=null,this.railXRatio=null,this.scrollbarYRail=i(m.element.rail("y")),r.appendChild(this.scrollbarYRail),this.scrollbarY=i(m.element.thumb("y")),this.scrollbarYRail.appendChild(this.scrollbarY),this.scrollbarY.setAttribute("tabindex",0),this.event.bind(this.scrollbarY,"focus",s),this.event.bind(this.scrollbarY,"blur",a),this.scrollbarYActive=null,this.scrollbarYHeight=null,this.scrollbarYTop=null;var h=t(this.scrollbarYRail);this.scrollbarYRight=parseInt(h.right,10),isNaN(this.scrollbarYRight)?(this.isScrollbarYUsingRight=!1,this.scrollbarYLeft=u(h.left)):this.isScrollbarYUsingRight=!0,this.scrollbarYOuterWidth=this.isRtl?f(this.scrollbarY):null,this.railBorderYWidth=u(h.borderTopWidth)+u(h.borderBottomWidth),e(this.scrollbarYRail,{display:"block"}),this.railYMarginHeight=u(h.marginTop)+u(h.marginBottom),e(this.scrollbarYRail,{display:""}),this.railYHeight=null,this.railYRatio=null,this.reach={x:r.scrollLeft<=0?"start":r.scrollLeft>=this.contentWidth-this.containerWidth?"end":null,y:r.scrollTop<=0?"start":r.scrollTop>=this.contentHeight-this.containerHeight?"end":null},this.isAlive=!0,this.settings.handlers.forEach(function(t){return T[t](n)}),this.lastScrollTop=Math.floor(r.scrollTop),this.lastScrollLeft=r.scrollLeft,this.event.bind(this.element,"scroll",function(t){return n.onScroll(t)}),R(this)};return H.prototype.update=function(){this.isAlive&&(this.negativeScrollAdjustment=this.isNegativeScroll?this.element.scrollWidth-this.element.clientWidth:0,e(this.scrollbarXRail,{display:"block"}),e(this.scrollbarYRail,{display:"block"}),this.railXMarginWidth=u(t(this.scrollbarXRail).marginLeft)+u(t(this.scrollbarXRail).marginRight),this.railYMarginHeight=u(t(this.scrollbarYRail).marginTop)+u(t(this.scrollbarYRail).marginBottom),e(this.scrollbarXRail,{display:"none"}),e(this.scrollbarYRail,{display:"none"}),R(this),W(this,"top",0,!1,!0),W(this,"left",0,!1,!0),e(this.scrollbarXRail,{display:""}),e(this.scrollbarYRail,{display:""}))},H.prototype.onScroll=function(t){this.isAlive&&(R(this),W(this,"top",this.element.scrollTop-this.lastScrollTop),W(this,"left",this.element.scrollLeft-this.lastScrollLeft),this.lastScrollTop=Math.floor(this.element.scrollTop),this.lastScrollLeft=this.element.scrollLeft)},H.prototype.destroy=function(){this.isAlive&&(this.event.unbindAll(),l(this.scrollbarX),l(this.scrollbarY),l(this.scrollbarXRail),l(this.scrollbarYRail),this.removePsClasses(),this.element=null,this.scrollbarX=null,this.scrollbarY=null,this.scrollbarXRail=null,this.scrollbarYRail=null,this.isAlive=!1)},H.prototype.removePsClasses=function(){this.element.className=this.element.className.split(" ").filter(function(t){return!t.match(/^ps([-_].+|)$/)}).join(" ")},H}); -------------------------------------------------------------------------------- /static/reset.css: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, embed, 11 | figure, figcaption, footer, header, hgroup, 12 | menu, nav, output, ruby, section, summary, 13 | time, mark, audio, video { 14 | margin: 0; 15 | padding: 0; 16 | border: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | ol, ul { 27 | list-style: none; 28 | } 29 | blockquote, q { 30 | quotes: none; 31 | } 32 | blockquote:before, blockquote:after, 33 | q:before, q:after { 34 | content: ''; 35 | content: none; 36 | } 37 | table { 38 | border-collapse: collapse; 39 | border-spacing: 0; 40 | } 41 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | const ps = new PerfectScrollbar('#chatlines-container', {suppressScrollX:true}); 2 | new PerfectScrollbar('#chatbotchoices', {suppressScrollX:true}); 3 | new PerfectScrollbar('#feedbackwindow', {suppressScrollX:true}); 4 | new PerfectScrollbar('#informedconsentcontent', {suppressScrollX:true}); 5 | new PerfectScrollbar('#review-chat-history', {suppressScrollX:true}); 6 | 7 | 8 | $( document ).ready(function() { 9 | 10 | var chatUUID = generateUUID(); 11 | var chatbotid, chatbotname; 12 | var messageHistory = []; 13 | var chatCount = 0; 14 | 15 | setIsChatting(false); 16 | 17 | $(".consentbutton").click(function() { 18 | $("#informedconsent").hide(); 19 | Cookies.set("consentcookie", 1); 20 | showTutorial(); 21 | }); 22 | 23 | if (! Cookies.get("consentcookie")) { 24 | $("#informedconsent").show(); 25 | } 26 | 27 | $(".choice").click(function() { 28 | $("#robotchoices").hide(); 29 | chatbotid = $(this).data("botid"); 30 | chatbotname = $(this).data("name"); 31 | $("#botname").html(chatbotname); 32 | 33 | $("#message").focus(); 34 | setIsChatting(true); 35 | 36 | $("#chatbot_id").val(chatbotid); 37 | $("#chat_id").val(chatUUID); 38 | $("#loading").find(".user").text('[' + chatbotname + ']: '); 39 | 40 | if (window.STUDY_KEY == "index") { 41 | var indexPrompts = ["Hello! So, what’s on your mind?", "Hi! how are you feeling today?", "Hey! Tell me about your day."]; 42 | var choice = indexPrompts[Math.floor(Math.random() * indexPrompts.length)]; 43 | addChatLine(chatbotname, choice, null); 44 | 45 | } 46 | }); 47 | 48 | if ($(".choice").length == 1) { 49 | $(".choice").click(); 50 | } 51 | 52 | $('.input').keypress(function (e) { 53 | if (e.which == 13) { 54 | $('#submit').click(); 55 | return false; 56 | } 57 | }); 58 | 59 | $("#submit").click(function() { 60 | $("#message").focus(); 61 | var message = $("#message").val(); 62 | if (! message) { 63 | return false; 64 | } 65 | $("#message").val(""); 66 | 67 | if (message.toLowerCase() == "quit" || message.toLowerCase() == "exit") { 68 | $(".close").click(); 69 | return false; 70 | } 71 | 72 | 73 | messageHistory.push(message); 74 | messageHistory = lastFive(messageHistory); 75 | 76 | $("#submit").attr('disabled','disabled'); 77 | $("#submit .loading").show(); 78 | $("#submit .text").fadeTo(0, 0); 79 | $("#loading").show(); 80 | 81 | addChatLine("User", message); 82 | chatCount += 1; 83 | 84 | var requestTime = new Date(); 85 | 86 | $.ajax("/chat_message/", { 87 | type: "POST", 88 | url: "/chat_message/", 89 | dataType: "json", 90 | data: {message:message, 91 | chatbotID:chatbotid, 92 | chatID:chatUUID, 93 | studyKey:window.STUDY_KEY, 94 | timestamp:new Date().getTime(), 95 | messageHistory:JSON.stringify(messageHistory) 96 | }, 97 | success: function(response) { 98 | 99 | var length = response.response_stripped.length; 100 | 101 | var botTypingDuration = (length * .04 + .15) * 1000 / 3; 102 | var realDuration = new Date() - requestTime; 103 | setTimeout(function() { 104 | $("#submit").attr('disabled',null); 105 | $("#submit .loading").hide(); 106 | $("#submit .text").fadeTo(0, 1); 107 | $("#loading").hide(); 108 | 109 | messageHistory.push(response.response_stripped); 110 | messageHistory = lastFive(messageHistory); 111 | 112 | addChatLine(chatbotname, response.response, response.response_id); 113 | 114 | }, Math.max(0, botTypingDuration - realDuration)); 115 | 116 | }, 117 | error: function() { 118 | $("#submit").attr('disabled',null); 119 | $("#submit .loading").hide(); 120 | $("#submit .text").fadeTo(0, 1); 121 | $("#loading").hide(); 122 | 123 | var $messageLine = $('
[SERVER ERROR TRY AGAIN]
'); 124 | $("#chatlines").append($messageLine); 125 | $("#chatlines").parent().scrollTop($("#chatlines").parent()[0].scrollHeight); 126 | } 127 | }) 128 | }) 129 | 130 | $(".close").click(function() { 131 | if (chatCount <3 ) { 132 | $("#chatrateconfirm").show(); 133 | return; 134 | } 135 | $("#chatlines").clone().appendTo($("#review-chat-history")); 136 | 137 | $("#review-chat-history .chatline").each(function() { 138 | initVoteButtons($(this)); 139 | }) 140 | $("#chatrate").show(); 141 | }); 142 | 143 | $("#confirm-rate").click(function() { 144 | $("#chatrateconfirm").hide(); 145 | setIsChatting(false); 146 | $("#chatratethanks").show(); 147 | }); 148 | 149 | $("#cancel-rate").click(function() { 150 | $("#chatrateconfirm").hide(); 151 | }); 152 | 153 | $("#rateform").submit(function(e) { 154 | var $form = $(this); 155 | e.stopPropagation(); 156 | 157 | var valid = true; 158 | 159 | $form.find(".likert").each(function() { 160 | var $field = $(this); 161 | $field.removeClass("error"); 162 | console.log($(this).find("input:checked")); 163 | if (! $(this).find("input:checked").val()) { 164 | $field.addClass("error"); 165 | valid = false; 166 | } 167 | }); 168 | 169 | if (valid) { 170 | $.ajax($form.attr("action"), 171 | { 172 | type:"POST", 173 | data: $form.serialize(), 174 | dataType: "json", 175 | error: function() { 176 | alert("There was an error submitting your rating. Please try again later!"); 177 | }, 178 | success: function(response) { 179 | setIsChatting(false); 180 | $("#chatrate").hide(); 181 | 182 | if (response.mturk_code) { 183 | $("#mturk-code").html(response.mturk_code); 184 | } else { 185 | $("#mturk-wrapper").hide(); 186 | } 187 | 188 | $("#chatratethanks").show(); 189 | } 190 | }); 191 | } 192 | 193 | return false; 194 | }) 195 | 196 | $("#confirm-close").click(function() { 197 | window.location = window.location; 198 | }); 199 | 200 | $("#tutorial .tutorialpage .button").click(function() { 201 | $("#tutorial .tutorialpage").hide(); 202 | var $next = $(this).parents(".tutorialpage").next(); 203 | if ($next.length > 0) { 204 | $next.show(); 205 | } else { 206 | $("#tutorial").hide(); 207 | } 208 | }); 209 | 210 | }); 211 | 212 | function showTutorial() { 213 | $("#tutorial").show(); 214 | $("#tutorial .tutorialpage").hide(); 215 | $("#tutorial .tutorialpage:nth-child(1)").show(); 216 | } 217 | 218 | 219 | function setIsChatting(isChatting) { 220 | window.isChatting = isChatting; 221 | if (isChatting) { 222 | $(window).bind('beforeunload', function(){ 223 | return "Please close your chat session and rate it before leaving."; 224 | }); 225 | } else { 226 | $(window).unbind('beforeunload'); 227 | } 228 | } 229 | 230 | function addChatLine(username, message, responseID) { 231 | var $messageLine = $('
'); 232 | $messageLine.find(".message").text(message); 233 | $messageLine.find(".user").text('[' + username + ']: '); 234 | $messageLine.data("responseid", responseID); 235 | $messageLine.attr("data-responseid", responseID); 236 | if (responseID) { 237 | initVoteButtons($messageLine); 238 | } else { 239 | $messageLine.find(".votes").empty(); 240 | } 241 | $("#chatlines").append($messageLine); 242 | ps.update(); 243 | $("#chatlines-container").scrollTop($("#chatlines-container")[0].scrollHeight); 244 | } 245 | 246 | function initVoteButtons($messageLine) { 247 | var responseID = $messageLine.data("responseid"); 248 | 249 | $messageLine.find(".upvote").click(function() { 250 | rateResponse(1, responseID, true); 251 | }); 252 | $messageLine.find(".downvote").click(function() { 253 | rateResponse(-1, responseID, false); 254 | }); 255 | } 256 | 257 | function rateResponse(rating, responseID, isUp) { 258 | $.ajax("/rate_chat_response/", { 259 | type:"post", 260 | dataType:"json", 261 | data:{ 262 | response_id:responseID, 263 | rating:rating, 264 | }, 265 | success: function(response) { 266 | var $voteButtons = $(".chatline[data-responseID='" + responseID + "'] .vote"); 267 | $voteButtons.removeClass("active"); 268 | if (response.make_vote_active) { 269 | $voteButtons.filter(isUp ? ".upvote" : ".downvote").addClass("active"); 270 | } 271 | }, 272 | }) 273 | } 274 | 275 | function generateUUID() { 276 | var d = new Date().getTime(); 277 | var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 278 | var r = (d + Math.random()*16)%16 | 0; 279 | d = Math.floor(d/16); 280 | return (c=='x' ? r : (r&0x3|0x8)).toString(16); 281 | }); 282 | return uuid; 283 | } 284 | 285 | function lastFive(arr) { 286 | return arr.slice(Math.max(arr.length - 5, 0)) 287 | } 288 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Overpass Mono', monospace; 3 | } 4 | html { 5 | font-family: 'Overpass Mono', monospace; 6 | background-color: black; 7 | color: #00FF00; 8 | font-size: 14px; 9 | height: 100%; 10 | width: 100%; 11 | box-sizing: border-box; 12 | overflow-x: hidden; 13 | } 14 | .hidden { 15 | display: none; 16 | } 17 | .content { 18 | margin: 0px auto; 19 | box-sizing: border-box; 20 | display: block; 21 | width: 100%; 22 | height: 100%; 23 | box-shadow: 0px 0px 60px rgba(0, 255, 0, 0.5), 20px 0px 70px rgba(0, 255, 0, 0.7), -20px 0px 70px rgba(0, 255, 0, 0.7); 24 | padding: 30px; 25 | background-color: #020; 26 | } 27 | strong { 28 | font-weight: 700; 29 | font-size: 1.05em; 30 | } 31 | .aside { 32 | font-style: italic; 33 | } 34 | a { 35 | color: #00FF00; 36 | } 37 | a:visited { 38 | color: #00FF00; 39 | } 40 | @keyframes flicker { 41 | 0% { 42 | opacity: 0.35218; 43 | } 44 | 5% { 45 | opacity: 0.54631; 46 | } 47 | 10% { 48 | opacity: 0.42162; 49 | } 50 | 15% { 51 | opacity: 0.32281; 52 | } 53 | 20% { 54 | opacity: 0.64841; 55 | } 56 | 25% { 57 | opacity: 0.44795; 58 | } 59 | 30% { 60 | opacity: 0.30339; 61 | } 62 | 35% { 63 | opacity: 0.96539; 64 | } 65 | 40% { 66 | opacity: 0.72515; 67 | } 68 | 45% { 69 | opacity: 0.65073; 70 | } 71 | 50% { 72 | opacity: 0.84213; 73 | } 74 | 55% { 75 | opacity: 0.60697; 76 | } 77 | 60% { 78 | opacity: 0.57798; 79 | } 80 | 65% { 81 | opacity: 0.38626; 82 | } 83 | 70% { 84 | opacity: 0.45701; 85 | } 86 | 75% { 87 | opacity: 0.76252; 88 | } 89 | 80% { 90 | opacity: 0.42619; 91 | } 92 | 85% { 93 | opacity: 0.83635; 94 | } 95 | 90% { 96 | opacity: 0.34095; 97 | } 98 | 95% { 99 | opacity: 0.55617; 100 | } 101 | 100% { 102 | opacity: 0.13109; 103 | } 104 | } 105 | .content { 106 | position: relative; 107 | overflow: hidden; 108 | } 109 | .content::after { 110 | content: " "; 111 | display: block; 112 | position: absolute; 113 | top: 0; 114 | left: 0; 115 | bottom: 0; 116 | right: 0; 117 | background: rgba(255, 255, 255, 0.01); 118 | opacity: 0; 119 | z-index: 2; 120 | pointer-events: none; 121 | animation: flicker 0.15s infinite; 122 | } 123 | .content::before { 124 | content: " "; 125 | display: block; 126 | position: absolute; 127 | top: 0; 128 | left: 0; 129 | bottom: 0; 130 | right: 0; 131 | background: linear-gradient(rgba(255, 255, 255, 0.03) 50%, rgba(230, 230, 230, 0.09) 50%), linear-gradient(90deg, rgba(255, 0, 0, 0.07), rgba(0, 255, 0, 0.02), rgba(0, 0, 255, 0.03)); 132 | z-index: 2; 133 | background-size: 100% 3px, 4px 100%; 134 | pointer-events: none; 135 | } 136 | .content .window { 137 | position: absolute; 138 | left: 0px; 139 | right: 0px; 140 | top: 0px; 141 | bottom: 0px; 142 | background: #020; 143 | overflow: hidden; 144 | z-index: 1000; 145 | } 146 | .content .window .windowcontent { 147 | position: absolute; 148 | left: 30px; 149 | right: 30px; 150 | top: 30px; 151 | bottom: 30px; 152 | border: 4px solid #00FF00; 153 | padding: 30px; 154 | overflow: hidden; 155 | display: flex; 156 | flex-flow: column; 157 | box-sizing: border-box; 158 | } 159 | .content .window .title { 160 | font-size: 28px; 161 | margin-bottom: 20px; 162 | text-align: center; 163 | } 164 | .content .window .subtitle { 165 | font-size: 16px; 166 | margin-bottom: 20px; 167 | text-align: center; 168 | } 169 | .content .window .chatbotchoicesborder { 170 | border: 4px solid #00FF00; 171 | width: 300px; 172 | margin: 0 auto; 173 | flex-grow: 1; 174 | position: relative; 175 | } 176 | .content .window .chatbotchoices { 177 | overflow: hidden; 178 | position: absolute; 179 | left: 0; 180 | top: -4px; 181 | right: -4px; 182 | bottom: -4px; 183 | } 184 | .content .window .chatbotchoices .choice { 185 | border: 4px solid #00FF00; 186 | background: rgba(0, 255, 0, 0.05); 187 | margin-left: -4px; 188 | display: block; 189 | margin-right: 21px; 190 | text-align: center; 191 | height: 60px; 192 | line-height: 60px; 193 | font-size: 18px; 194 | cursor: pointer; 195 | border-top: none; 196 | } 197 | .content .chat { 198 | font-size: 18px; 199 | height: 100%; 200 | width: 100%; 201 | position: relative; 202 | } 203 | .content .chat .chatlines { 204 | position: absolute; 205 | top: 0px; 206 | bottom: 60px; 207 | left: 0; 208 | right: 0px; 209 | padding-bottom: 80px; 210 | padding-left: 20px; 211 | padding-right: 25px; 212 | padding-top: 60px; 213 | box-sizing: border-box; 214 | overflow: hidden; 215 | } 216 | .content .chat .chatline { 217 | cursor: default; 218 | font-size: 18px; 219 | } 220 | .content .chat .close { 221 | position: absolute; 222 | top: 0px; 223 | right: 0px; 224 | border: 4px solid #00FF00; 225 | cursor: pointer; 226 | background: rgba(0, 255, 0, 0.05); 227 | padding: 0px 20px; 228 | background-color: #020; 229 | } 230 | .content .chat .close span { 231 | vertical-align: middle; 232 | } 233 | .content .chat .close img { 234 | width: 30px; 235 | height: 30px; 236 | margin-right: 0px; 237 | display: inline-block; 238 | vertical-align: middle; 239 | } 240 | .content .inputarea { 241 | position: absolute; 242 | bottom: 0; 243 | width: 100%; 244 | box-sizing: border-box; 245 | } 246 | .content .input { 247 | border: 4px solid #00FF00; 248 | line-height: 50px; 249 | padding: 0px 150px 0 20px; 250 | width: 100%; 251 | display: block; 252 | background-color: #020; 253 | color: #00FF00; 254 | font-size: 18px; 255 | outline-style: none; 256 | box-shadow: none; 257 | } 258 | .content .button { 259 | border: 4px solid #00FF00; 260 | line-height: 50px; 261 | padding: 0px 30px; 262 | cursor: pointer; 263 | display: inline-block; 264 | background-color: #020; 265 | color: #00FF00; 266 | font-size: 18px; 267 | outline-style: none; 268 | box-shadow: none; 269 | text-align: center; 270 | } 271 | .content .button .loading { 272 | height: 29px; 273 | width: 29px; 274 | display: none; 275 | position: absolute; 276 | top: 50%; 277 | left: 50%; 278 | margin-left: -14.5px; 279 | margin-top: -14.5px; 280 | animation-name: spin; 281 | animation-duration: 1000ms; 282 | animation-iteration-count: infinite; 283 | animation-timing-function: linear; 284 | } 285 | .content .chat .button { 286 | position: absolute; 287 | right: 0px; 288 | top: 0; 289 | float: right; 290 | } 291 | @keyframes spin { 292 | from { 293 | transform: rotate(0deg); 294 | } 295 | to { 296 | transform: rotate(360deg); 297 | } 298 | } 299 | @media only screen and (max-width: 800px) { 300 | html { 301 | padding: 0px; 302 | } 303 | .content { 304 | height: 100%; 305 | width: 100%; 306 | } 307 | .content .window .windowcontent { 308 | padding-left: 0; 309 | padding-right: 0; 310 | } 311 | .content .window .title { 312 | font-size: 18px; 313 | margin-top: 10px; 314 | } 315 | .content .window .subtitle { 316 | font-size: 16px !important; 317 | } 318 | .content .window .title, 319 | .content .window .subtitle { 320 | padding-left: 20px; 321 | padding-right: 20px; 322 | } 323 | .content .chat .chatlines { 324 | top: 80px; 325 | } 326 | .content .chat .chatlines .ps__rail-y { 327 | margin-top: 0; 328 | } 329 | .content .chat .chatline { 330 | font-size: 16px; 331 | } 332 | .content .chat .button { 333 | right: 0; 334 | } 335 | .content .chat .inputarea { 336 | padding-right: 0; 337 | } 338 | form .question-label { 339 | font-size: 16px; 340 | } 341 | .window.ratewindow .windowcontent { 342 | padding-left: 30px; 343 | padding-right: 40px; 344 | } 345 | .window.ratewindow .windowcontent .ps__rail-y { 346 | width: 15px; 347 | } 348 | .mturk .help { 349 | font-size: 16px !important; 350 | } 351 | } 352 | .ps__rail-y { 353 | width: 25px; 354 | box-sizing: border-box; 355 | background: rgba(0, 255, 0, 0.05) !important; 356 | border: 4px solid #00FF00; 357 | top: 50px; 358 | display: block; 359 | opacity: 1 !important; 360 | bottom: 0px; 361 | z-index: 100; 362 | } 363 | .ps__thumb-y { 364 | background: rgba(0, 255, 0, 0.2) !important; 365 | border: 4px solid #00FF00; 366 | border-radius: 0; 367 | width: 100% !important; 368 | right: -4px; 369 | margin-top: -4px; 370 | bottom: -4px; 371 | } 372 | .chat .ps__rail-y { 373 | margin-top: 70px; 374 | margin-bottom: 10px; 375 | } 376 | .windowcontent > .ps__rail-y { 377 | right: 6px; 378 | margin-top: 6px; 379 | margin-bottom: 6px; 380 | } 381 | form { 382 | text-align: center; 383 | } 384 | form .question-label { 385 | font-size: 18px; 386 | margin-top: 15px; 387 | margin-bottom: 10px; 388 | } 389 | form .form-question { 390 | margin-bottom: 40px; 391 | } 392 | form textarea { 393 | margin-top: 20px; 394 | border: 4px solid #00FF00; 395 | padding: 10px; 396 | width: 90%; 397 | max-width: 500px; 398 | display: inline-block; 399 | background-color: #020; 400 | color: #00FF00; 401 | font-size: 16px; 402 | outline-style: none; 403 | box-shadow: none; 404 | resize: none; 405 | } 406 | form textarea::placeholder { 407 | color: rgba(0, 255, 0, 0.5); 408 | } 409 | form .button { 410 | position: relative !important; 411 | float: none !important; 412 | } 413 | form .radios { 414 | width: 80%; 415 | max-width: 400px; 416 | display: flex; 417 | justify-content: space-between; 418 | margin: 20px auto 5px; 419 | padding-right: 20px; 420 | height: 35px; 421 | } 422 | form .likert-labels { 423 | font-size: 14px; 424 | margin-top: 5px; 425 | margin-bottom: 10px; 426 | display: inline-block; 427 | width: 90%; 428 | max-width: 450px; 429 | } 430 | form .likert-labels .left { 431 | float: left; 432 | } 433 | form .likert-labels .right { 434 | float: right; 435 | } 436 | .error-message { 437 | display: none; 438 | } 439 | .error .error-message { 440 | display: block; 441 | color: #cc0000; 442 | border-color: #cc0000; 443 | margin-top: -10px; 444 | margin-bottom: 5px; 445 | } 446 | .chatratethanks .button { 447 | width: 250px; 448 | margin: 40px auto; 449 | } 450 | /** 451 | * For the Radio Buttons 452 | */ 453 | /* Customize the label (the container) */ 454 | .radio { 455 | display: inline-block; 456 | position: relative; 457 | cursor: pointer; 458 | -webkit-user-select: none; 459 | -moz-user-select: none; 460 | -ms-user-select: none; 461 | user-select: none; 462 | /* Hide the browser's default radio button */ 463 | /* Create a custom radio button */ 464 | /* On mouse-over, add a grey background color */ 465 | /* When the radio button is checked, add a blue background */ 466 | } 467 | .radio input { 468 | position: absolute; 469 | opacity: 0; 470 | cursor: pointer; 471 | height: 0; 472 | width: 0; 473 | } 474 | .radio .checkmark { 475 | position: absolute; 476 | top: 0; 477 | left: 0; 478 | height: 25px; 479 | width: 25px; 480 | background-color: transparent; 481 | border-radius: 50%; 482 | border: 4px solid #00FF00; 483 | box-sizing: border-box; 484 | } 485 | .radio:hover input ~ .checkmark { 486 | background-color: rgba(0, 255, 0, 0.5); 487 | } 488 | .radio input:checked ~ .checkmark { 489 | background-color: #00FF00; 490 | } 491 | .votes { 492 | display: inline-block; 493 | position: relative; 494 | width: 24px; 495 | padding-right: 5px; 496 | margin-left: -20px; 497 | height: 18px; 498 | vertical-align: middle; 499 | } 500 | .votes .upvote { 501 | width: 0; 502 | height: 0; 503 | border-left: 12px solid transparent; 504 | border-right: 12px solid transparent; 505 | display: inline-block; 506 | cursor: pointer; 507 | top: -3px; 508 | position: absolute; 509 | border-radius: 3px; 510 | border-bottom: 10px solid rgba(0, 255, 0, 0.2); 511 | } 512 | .votes .upvote:hover { 513 | border-bottom-color: rgba(0, 255, 0, 0.4); 514 | } 515 | .votes .upvote.active { 516 | border-bottom-color: #00FF00; 517 | } 518 | .votes .downvote { 519 | width: 0; 520 | height: 0; 521 | display: inline-block; 522 | margin-top: 5px; 523 | border-left: 12px solid transparent; 524 | border-right: 12px solid transparent; 525 | cursor: pointer; 526 | bottom: -3px; 527 | position: absolute; 528 | border-radius: 3px; 529 | border-top: 10px solid rgba(0, 255, 0, 0.2); 530 | } 531 | .votes .downvote:hover { 532 | border-top-color: rgba(0, 255, 0, 0.4); 533 | } 534 | .votes .downvote.active { 535 | border-top-color: #00FF00; 536 | } 537 | .buttons-container { 538 | text-align: center; 539 | } 540 | .buttons-container .button { 541 | margin: 50px 20px; 542 | width: 170px; 543 | } 544 | .buttons-container .confirm { 545 | background-color: rgba(0, 255, 0, 0.1); 546 | } 547 | .content .window .consentform { 548 | padding-right: 35px; 549 | } 550 | .content .window .consentform .subtitle { 551 | font-size: 1.4em; 552 | margin: 10px auto 10px; 553 | max-width: 900px; 554 | text-align: center; 555 | padding: 0px 40px; 556 | } 557 | .content .window .consentform h2 { 558 | font-size: 1.8em; 559 | font-weight: 600; 560 | text-align: center; 561 | margin: 30px 0 30px; 562 | } 563 | .content .window .consentform h3 { 564 | font-size: 1.3em; 565 | font-weight: 600; 566 | text-align: center; 567 | } 568 | .content .window .consentform p { 569 | margin: 10px 0 20px; 570 | } 571 | .content .window .consentform .consenttext { 572 | margin: 0 auto; 573 | max-width: 900px; 574 | padding: 0 20px; 575 | } 576 | .content .window .consentform .confirmtext { 577 | font-size: 1.6em; 578 | margin: 40px auto 20px; 579 | text-align: center; 580 | } 581 | .content .window .consentform .consentbutton { 582 | text-align: center; 583 | margin-top: 20px; 584 | margin-bottom: 30px; 585 | } 586 | .content .window .consentform .consentbutton .button { 587 | width: 200px; 588 | } 589 | .loading:after { 590 | overflow: hidden; 591 | display: inline-block; 592 | vertical-align: bottom; 593 | -webkit-animation: ellipsis steps(4, end) 1000ms infinite; 594 | animation: ellipsis steps(4, end) 1000ms infinite; 595 | content: "..."; 596 | /* ascii code for the ellipsis character */ 597 | width: 0px; 598 | } 599 | @keyframes ellipsis { 600 | to { 601 | width: 2.5em; 602 | } 603 | } 604 | @-webkit-keyframes ellipsis { 605 | to { 606 | width: 2.5em; 607 | } 608 | } 609 | .content .window .windowcontent.widescreenonly { 610 | display: none; 611 | } 612 | @media only screen and (min-width: 1200px) { 613 | .content .window .windowcontent.widescreenonly { 614 | display: block; 615 | } 616 | .content .window .windowcontent.left { 617 | position: absolute; 618 | left: 30px; 619 | right: 60%; 620 | margin-right: 15px; 621 | } 622 | .content .window .windowcontent.right { 623 | position: absolute; 624 | left: 40%; 625 | margin-left: 15px; 626 | } 627 | .content .window .windowcontent.chathistory { 628 | font-size: 18px; 629 | } 630 | .content .window .windowcontent.chathistory h3 { 631 | font-size: 24px; 632 | margin-bottom: 10px; 633 | } 634 | } 635 | .content .window.tutorial .windowcontent { 636 | padding: 30px; 637 | top: 50%; 638 | transform: perspective(1px) translateY(-50%); 639 | bottom: auto; 640 | padding-bottom: 100px; 641 | } 642 | @media all and (min-width: 800px) { 643 | .content .window.tutorial .windowcontent { 644 | width: 600px; 645 | left: 50%; 646 | right: auto; 647 | transform: perspective(1px) translateY(-50%) translateX(-50%); 648 | } 649 | } 650 | .content .window.tutorial .tutorialpage { 651 | display: none; 652 | } 653 | .content .window.tutorial .button { 654 | position: absolute; 655 | right: 30px; 656 | bottom: 30px; 657 | } 658 | .content .window.tutorial .title { 659 | margin-bottom: 30px; 660 | font-size: 26px; 661 | } 662 | .content .window.tutorial .title.titlebig { 663 | font-size: 26px; 664 | } 665 | .content .window.tutorial strong { 666 | font-size: 1.3em; 667 | } 668 | .content .window.tutorial p { 669 | font-size: 18px; 670 | text-align: center; 671 | margin-bottom: 30px; 672 | } 673 | .content .window.tutorial img { 674 | margin: 0px auto 30px; 675 | height: 100px; 676 | display: block; 677 | box-shadow: 0px 0px 5px 2px #00FF00; 678 | } 679 | .content .window.chatratethanks .mturk { 680 | text-align: center; 681 | margin: 35px 0; 682 | } 683 | .content .window.chatratethanks .mturk .help { 684 | font-size: 22px; 685 | margin-bottom: 10px; 686 | } 687 | .content .window.chatratethanks .mturk .subtitle { 688 | font-size: 28px; 689 | margin-bottom: 10px; 690 | } 691 | .content .window.chatratethanks .mturk-code { 692 | margin-top: 40px; 693 | font-size: 40px; 694 | } 695 | .content .window.chatratethanks .subtitle { 696 | font-size: 20px; 697 | margin-bottom: 20px; 698 | } 699 | /*# sourceMappingURL=style.css.map */ -------------------------------------------------------------------------------- /static/style.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["style.less"],"names":[],"mappings":"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","file":"style.css"} -------------------------------------------------------------------------------- /static/style.less: -------------------------------------------------------------------------------- 1 | @accent:#00FF00; 2 | @screen-background:#020; 3 | 4 | * { 5 | font-family: 'Overpass Mono', monospace; 6 | } 7 | html { 8 | font-family: 'Overpass Mono', monospace; 9 | background-color:black; 10 | color:@accent; 11 | font-size:14px; 12 | height:100%; 13 | width:100%; 14 | box-sizing: border-box; 15 | overflow-x: hidden; 16 | } 17 | .hidden { 18 | display: none; 19 | } 20 | 21 | .content { 22 | margin:0px auto; 23 | box-sizing: border-box; 24 | display:block; 25 | width:100%; 26 | height:100%; 27 | box-shadow: 0px 0px 60px fade(@accent,50), 20px 0px 70px fade(@accent,70), -20px 0px 70px fade(@accent,70); 28 | padding:30px; 29 | background-color:@screen-background; 30 | } 31 | strong { 32 | font-weight: 700; 33 | font-size:1.05em; 34 | } 35 | .aside { 36 | font-style: italic; 37 | } 38 | a { 39 | color: @accent; 40 | 41 | &:visited { 42 | color: @accent; 43 | } 44 | } 45 | 46 | 47 | @screen-overlay:#FFF; // #121010; 48 | 49 | @keyframes flicker { 50 | 0% { 51 | opacity: 0.35218; 52 | } 53 | 5% { 54 | opacity: 0.54631; 55 | } 56 | 10% { 57 | opacity: 0.42162; 58 | } 59 | 15% { 60 | opacity: 0.32281; 61 | } 62 | 20% { 63 | opacity: 0.64841; 64 | } 65 | 25% { 66 | opacity: 0.44795; 67 | } 68 | 30% { 69 | opacity: 0.30339; 70 | } 71 | 35% { 72 | opacity: 0.96539; 73 | } 74 | 40% { 75 | opacity: 0.72515; 76 | } 77 | 45% { 78 | opacity: 0.65073; 79 | } 80 | 50% { 81 | opacity: 0.84213; 82 | } 83 | 55% { 84 | opacity: 0.60697; 85 | } 86 | 60% { 87 | opacity: 0.57798; 88 | } 89 | 65% { 90 | opacity: 0.38626; 91 | } 92 | 70% { 93 | opacity: 0.45701; 94 | } 95 | 75% { 96 | opacity: 0.76252; 97 | } 98 | 80% { 99 | opacity: 0.42619; 100 | } 101 | 85% { 102 | opacity: 0.83635; 103 | } 104 | 90% { 105 | opacity: 0.34095; 106 | } 107 | 95% { 108 | opacity: 0.55617; 109 | } 110 | 100% { 111 | opacity: 0.13109; 112 | } 113 | } 114 | 115 | .content{ 116 | position:relative; 117 | overflow:hidden; 118 | 119 | // flicker 120 | &::after{ 121 | content:" "; 122 | display:block; 123 | position:absolute; 124 | top:0; 125 | left:0; 126 | bottom:0; 127 | right:0; 128 | background:fade(@screen-overlay,1); 129 | opacity:0; 130 | z-index:2; 131 | pointer-events:none; 132 | animation:flicker 0.15s infinite; 133 | } 134 | // scanlines 135 | &::before{ 136 | content:" "; 137 | display:block; 138 | position:absolute; 139 | top:0; 140 | left:0; 141 | bottom:0; 142 | right:0; 143 | background:linear-gradient(fade(@screen-overlay,3) 50%, fade(darken(@screen-overlay,10),9) 50%), linear-gradient(90deg,fade(#ff0000,7),fade(#00ff00,2),fade(#0000ff,3)); 144 | z-index:2; 145 | background-size:100% 3px, 4px 100%; 146 | pointer-events:none; 147 | } 148 | } 149 | 150 | 151 | .content { 152 | .window { 153 | position:absolute; 154 | left:0px; 155 | right:0px; 156 | top:0px; 157 | bottom:0px; 158 | background: @screen-background; 159 | overflow:hidden; 160 | z-index:1000; 161 | 162 | .windowcontent { 163 | position:absolute; 164 | left:30px; 165 | right:30px; 166 | top:30px; 167 | bottom:30px; 168 | border:4px solid @accent; 169 | padding:30px; 170 | overflow:hidden; 171 | display: flex; 172 | flex-flow: column; 173 | box-sizing: border-box; 174 | } 175 | 176 | .title { 177 | font-size:28px; 178 | margin-bottom:20px; 179 | text-align: center; 180 | } 181 | .subtitle { 182 | font-size:16px; 183 | margin-bottom:20px; 184 | text-align: center; 185 | } 186 | .chatbotchoicesborder { 187 | border:4px solid @accent; 188 | width:300px; 189 | margin: 0 auto; 190 | flex-grow : 1; 191 | position:relative; 192 | } 193 | .chatbotchoices { 194 | overflow: hidden; 195 | position:absolute; 196 | left: 0; 197 | top:-4px; 198 | right:-4px; 199 | bottom:-4px; 200 | 201 | .choice { 202 | border:4px solid @accent; 203 | background: fade(@accent,5); 204 | margin-left:-4px; 205 | display:block; 206 | margin-right:21px; 207 | text-align: center; 208 | height:60px; 209 | line-height:60px; 210 | font-size:18px; 211 | cursor:pointer; 212 | border-top:none; 213 | } 214 | } 215 | } 216 | 217 | .chat { 218 | font-size:18px; 219 | height:100%; 220 | width:100%; 221 | position:relative; 222 | 223 | .chatlines { 224 | position:absolute; 225 | top:0px; 226 | bottom:60px; 227 | left:0; 228 | right: 0px; 229 | padding-bottom:80px; 230 | padding-left:20px; 231 | padding-right:25px; 232 | padding-top:60px; 233 | box-sizing: border-box; 234 | overflow:hidden; 235 | 236 | } 237 | .chatline { 238 | cursor:default; 239 | font-size:18px; 240 | } 241 | 242 | .close { 243 | position:absolute; 244 | top:0px; 245 | right:0px; 246 | border:4px solid @accent; 247 | cursor:pointer; 248 | background: fade(@accent,5); 249 | padding:0px 20px; 250 | background-color:@screen-background; 251 | 252 | span { 253 | vertical-align: middle; 254 | } 255 | 256 | img { 257 | width:30px; 258 | height:30px; 259 | margin-right:0px; 260 | display:inline-block; 261 | vertical-align: middle; 262 | } 263 | } 264 | } 265 | .inputarea { 266 | position:absolute; 267 | bottom:0; 268 | width:100%; 269 | box-sizing: border-box; 270 | } 271 | .input { 272 | border:4px solid @accent; 273 | line-height:50px; 274 | padding:0px 150px 0 20px; 275 | width:100%; 276 | display:block; 277 | background-color:@screen-background; 278 | color:@accent; 279 | font-size:18px; 280 | outline-style:none; 281 | box-shadow:none; 282 | } 283 | .button { 284 | border:4px solid @accent; 285 | line-height:50px; 286 | padding:0px 30px; 287 | cursor:pointer; 288 | display:inline-block; 289 | background-color:@screen-background; 290 | color:@accent; 291 | font-size:18px; 292 | outline-style:none; 293 | box-shadow:none; 294 | text-align: center; 295 | 296 | @loadingsize:29px; 297 | .loading { 298 | height:@loadingsize; 299 | width:@loadingsize; 300 | display: none; 301 | position:absolute; 302 | top:50%; 303 | left:50%; 304 | margin-left:-(@loadingsize/2); 305 | margin-top:-(@loadingsize/2); 306 | animation-name: spin; 307 | animation-duration: 1000ms; 308 | animation-iteration-count: infinite; 309 | animation-timing-function: linear; 310 | } 311 | } 312 | .chat .button { 313 | position:absolute; 314 | right:0px; 315 | top:0; 316 | float:right; 317 | } 318 | } 319 | 320 | @keyframes spin { 321 | from { 322 | transform:rotate(0deg); 323 | } 324 | to { 325 | transform:rotate(360deg); 326 | } 327 | } 328 | 329 | 330 | @media only screen and (max-width: 800px) { 331 | html { 332 | padding:0px; 333 | } 334 | .content { 335 | height:100%; 336 | width:100%; 337 | .window { 338 | .windowcontent { 339 | padding-left:0; 340 | padding-right:0; 341 | } 342 | .title { 343 | font-size:18px; 344 | margin-top:10px; 345 | } 346 | .subtitle { 347 | font-size:16px !important; 348 | } 349 | .title, .subtitle { 350 | padding-left:20px; 351 | padding-right:20px; 352 | } 353 | } 354 | } 355 | .content .chat .chatlines { 356 | top: 80px; 357 | .ps__rail-y { 358 | margin-top:0; 359 | } 360 | } 361 | .content .chat .chatline { 362 | font-size:16px; 363 | } 364 | .content .chat .button { 365 | right:0; 366 | } 367 | .content .chat .inputarea { 368 | padding-right:0; 369 | } 370 | form { 371 | .question-label { 372 | font-size:16px; 373 | } 374 | } 375 | .window.ratewindow .windowcontent { 376 | padding-left:30px; 377 | padding-right:40px; 378 | 379 | .ps__rail-y { 380 | width:15px; 381 | } 382 | } 383 | 384 | .mturk .help { 385 | font-size:16px !important; 386 | } 387 | 388 | } 389 | 390 | 391 | .ps__rail-y { 392 | width: 25px; 393 | box-sizing: border-box; 394 | background: fade(@accent,5) !important; 395 | border:4px solid @accent; 396 | top:50px; 397 | display: block; 398 | opacity:1 !important; 399 | bottom: 0px; 400 | z-index: 100; 401 | } 402 | .ps__thumb-y { 403 | background: fade(@accent,20) !important; 404 | border:4px solid @accent; 405 | border-radius: 0; 406 | width:100% !important; 407 | right: -4px; 408 | margin-top: -4px; 409 | bottom:-4px; 410 | } 411 | .chat .ps__rail-y { 412 | margin-top:70px; 413 | margin-bottom:10px; 414 | } 415 | .windowcontent > .ps__rail-y { 416 | right:6px; 417 | margin-top:6px; 418 | margin-bottom:6px; 419 | } 420 | 421 | 422 | form { 423 | text-align: center; 424 | .question-label { 425 | font-size:18px; 426 | margin-top:15px; 427 | margin-bottom:10px; 428 | } 429 | .form-question { 430 | margin-bottom:40px; 431 | } 432 | 433 | textarea { 434 | margin-top:20px; 435 | border:4px solid @accent; 436 | padding:10px; 437 | width:90%; 438 | max-width:500px; 439 | display:inline-block; 440 | background-color:@screen-background; 441 | color:@accent; 442 | font-size:16px; 443 | outline-style:none; 444 | box-shadow:none; 445 | resize:none; 446 | 447 | &::placeholder { 448 | color: fade(@accent, 50); 449 | } 450 | } 451 | .button { 452 | position:relative !important; 453 | float:none !important; 454 | } 455 | .radios { 456 | width: 80%; 457 | max-width: 400px; 458 | display: flex; 459 | justify-content: space-between; 460 | margin: 20px auto 5px; 461 | padding-right: 20px; 462 | height:35px; 463 | } 464 | .likert-labels { 465 | font-size:14px; 466 | margin-top:5px; 467 | margin-bottom:10px; 468 | display:inline-block; 469 | width:90%; 470 | max-width:450px; 471 | 472 | .left { 473 | float:left; 474 | } 475 | .right { 476 | float:right; 477 | } 478 | } 479 | } 480 | .error-message { 481 | display:none; 482 | } 483 | .error { 484 | 485 | .error-message { 486 | display:block; 487 | color: #cc0000; 488 | border-color: #cc0000; 489 | margin-top: -10px; 490 | margin-bottom: 5px; 491 | } 492 | } 493 | .chatratethanks { 494 | .button { 495 | width:250px; 496 | margin:40px auto; 497 | } 498 | } 499 | 500 | 501 | /** 502 | * For the Radio Buttons 503 | */ 504 | 505 | /* Customize the label (the container) */ 506 | .radio { 507 | display: inline-block; 508 | position: relative; 509 | cursor: pointer; 510 | -webkit-user-select: none; 511 | -moz-user-select: none; 512 | -ms-user-select: none; 513 | user-select: none; 514 | 515 | /* Hide the browser's default radio button */ 516 | input { 517 | position: absolute; 518 | opacity: 0; 519 | cursor: pointer; 520 | height: 0; 521 | width: 0; 522 | } 523 | 524 | /* Create a custom radio button */ 525 | .checkmark { 526 | position: absolute; 527 | top: 0; 528 | left: 0; 529 | height: 25px; 530 | width: 25px; 531 | background-color: transparent; 532 | border-radius: 50%; 533 | border:4px solid @accent; 534 | box-sizing: border-box; 535 | } 536 | 537 | /* On mouse-over, add a grey background color */ 538 | &:hover input ~ .checkmark { 539 | background-color: fade(@accent, 50); 540 | } 541 | 542 | /* When the radio button is checked, add a blue background */ 543 | input:checked ~ .checkmark { 544 | background-color: @accent; 545 | } 546 | 547 | } 548 | 549 | 550 | .votes { 551 | @vote-height:10px; 552 | @vote-width:12px; 553 | 554 | display:inline-block; 555 | position:relative; 556 | width:@vote-width*2; 557 | padding-right:5px; 558 | margin-left:-20px; 559 | height: 18px; 560 | vertical-align: middle; 561 | 562 | 563 | .upvote { 564 | width: 0; 565 | height: 0; 566 | border-left: @vote-width solid transparent; 567 | border-right: @vote-width solid transparent; 568 | display:inline-block; 569 | cursor:pointer; 570 | top:-3px; 571 | position:absolute; 572 | border-radius:3px; 573 | 574 | border-bottom: @vote-height solid fade(@accent, 20); 575 | 576 | &:hover { 577 | border-bottom-color: fade(@accent, 40); 578 | } 579 | 580 | &.active { 581 | border-bottom-color: @accent; 582 | } 583 | } 584 | 585 | .downvote { 586 | width: 0; 587 | height: 0; 588 | display:inline-block; 589 | margin-top:5px; 590 | border-left: @vote-width solid transparent; 591 | border-right: @vote-width solid transparent; 592 | cursor:pointer; 593 | bottom:-3px; 594 | position:absolute; 595 | border-radius:3px; 596 | 597 | border-top: @vote-height solid fade(@accent, 20); 598 | 599 | &:hover { 600 | border-top-color: fade(@accent, 40); 601 | } 602 | 603 | &.active { 604 | border-top-color: @accent; 605 | } 606 | } 607 | 608 | } 609 | 610 | .buttons-container { 611 | text-align: center; 612 | 613 | .button { 614 | margin: 50px 20px; 615 | width: 170px; 616 | } 617 | .confirm { 618 | background-color:fade(@accent,10); 619 | } 620 | } 621 | 622 | 623 | .content .window .consentform { 624 | padding-right:35px; 625 | 626 | .subtitle { 627 | font-size:1.4em; 628 | margin:10px auto 10px; 629 | max-width:900px; 630 | text-align: center; 631 | padding: 0px 40px; 632 | } 633 | 634 | h2 { 635 | font-size:1.8em; 636 | font-weight:600; 637 | text-align: center; 638 | margin:30px 0 30px; 639 | } 640 | h3 { 641 | font-size:1.3em; 642 | font-weight:600; 643 | text-align: center; 644 | } 645 | p { 646 | margin:10px 0 20px; 647 | } 648 | .consenttext { 649 | margin:0 auto; 650 | max-width:900px; 651 | padding:0 20px; 652 | } 653 | .confirmtext { 654 | font-size:1.6em; 655 | margin: 40px auto 20px; 656 | text-align: center; 657 | } 658 | .consentbutton { 659 | text-align:center; 660 | margin-top:20px; 661 | margin-bottom:30px; 662 | .button { 663 | width:200px; 664 | } 665 | } 666 | } 667 | 668 | 669 | .loading:after { 670 | overflow: hidden; 671 | display: inline-block; 672 | vertical-align: bottom; 673 | -webkit-animation: ellipsis steps(4,end) 1000ms infinite; 674 | animation: ellipsis steps(4,end) 1000ms infinite; 675 | content: "..."; /* ascii code for the ellipsis character */ 676 | width: 0px; 677 | } 678 | 679 | @keyframes ellipsis { 680 | to { 681 | width: 2.5em; 682 | } 683 | } 684 | 685 | @-webkit-keyframes ellipsis { 686 | to { 687 | width: 2.5em; 688 | } 689 | } 690 | 691 | 692 | .content .window .windowcontent.widescreenonly { 693 | display:none; 694 | } 695 | @media only screen and (min-width: 1200px) { 696 | .content .window { 697 | .windowcontent.widescreenonly { 698 | display:block; 699 | } 700 | .windowcontent.left { 701 | position:absolute; 702 | left:30px; 703 | right:60%; 704 | margin-right:15px; 705 | } 706 | .windowcontent.right { 707 | position:absolute; 708 | left:40%; 709 | margin-left:15px; 710 | } 711 | .windowcontent.chathistory { 712 | font-size:18px; 713 | 714 | h3 { 715 | font-size:24px; 716 | margin-bottom:10px; 717 | } 718 | } 719 | } 720 | 721 | } 722 | 723 | 724 | .content .window.tutorial { 725 | .windowcontent { 726 | padding:30px; 727 | top: 50%; 728 | transform: perspective(1px) translateY(-50%); 729 | bottom:auto; 730 | padding-bottom:100px; 731 | } 732 | @media all and (min-width: 800px) { 733 | .windowcontent { 734 | width: 600px; 735 | left: 50%; 736 | right:auto; 737 | transform: perspective(1px) translateY(-50%) translateX(-50%); 738 | } 739 | } 740 | @media all and (max-width: 800px) { 741 | .windowcontent { 742 | } 743 | } 744 | .tutorialpage { 745 | display:none; 746 | } 747 | .button { 748 | position:absolute; 749 | right:30px; 750 | bottom:30px; 751 | } 752 | .title { 753 | margin-bottom:30px; 754 | font-size:26px; 755 | } 756 | .title.titlebig { 757 | font-size:26px; 758 | } 759 | strong { 760 | font-size:1.3em; 761 | } 762 | p { 763 | font-size:18px; 764 | text-align: center; 765 | margin-bottom: 30px; 766 | } 767 | img { 768 | margin:0px auto 30px; 769 | height:100px; 770 | display:block; 771 | box-shadow: 0px 0px 5px 2px @accent; 772 | } 773 | } 774 | 775 | .content .window.chatratethanks { 776 | .mturk { 777 | text-align: center; 778 | margin: 35px 0; 779 | 780 | .help { 781 | font-size: 22px; 782 | margin-bottom: 10px; 783 | } 784 | .subtitle { 785 | font-size:28px; 786 | margin-bottom:10px; 787 | } 788 | } 789 | .mturk-code { 790 | margin-top: 40px; 791 | font-size: 40px; 792 | } 793 | .subtitle { 794 | font-size: 20px; 795 | margin-bottom: 20px; 796 | } 797 | } 798 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% block title %}E.L.S.A.{% endblock %} 4 | 5 | {% block extracss %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | {% block content %}{% endblock %} 15 |
16 | 17 | {% block extrascripts %}{% endblock %} 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if chatbots.keys()|length > 0 %} 6 | 7 |
8 |
9 |
10 | 11 |
12 | 13 |
>
14 |
15 |
16 | 17 | Close Chat and Rate 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 |
27 |
Welcome To Empathy Learning Socially-aware Agents (E.L.S.A.)
28 |
Please Select Your Chat Experience
29 | 30 |
31 |
32 | {% for chatbot in chatbots.values() %} 33 |
{{ chatbot.name }}
34 | {% endfor %} 35 |
36 |
37 |
38 |
39 | 40 | 147 | 148 | 163 | 175 | 176 | 177 | 233 | 234 | 235 | 256 | 257 | {% else %} 258 |
259 |
260 |
No Chatbots Found!
261 |
Sorry, it looks like we disabled the chatbots you're looking for.
262 |
Please try again later!
263 |
264 |
265 | {% endif %} 266 | 267 | {% endblock %} 268 | 269 | 270 | {% block extrascripts %} 271 | 274 | {% endblock %} 275 | -------------------------------------------------------------------------------- /templatesadmin/chatratingchangelist.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | {% load i18n admin_urls static admin_modify %} 3 | 4 | {% block object-tools-items %} 5 |
  • 6 | Download all as CSV 7 |
  • 8 | {{ block.super }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templatesadmin/chatrecordchangelist.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_list.html' %} 2 | {% load i18n admin_urls static admin_modify %} 3 | 4 | {% block object-tools-items %} 5 |
  • 6 | Download all as CSV 7 |
  • 8 | {{ block.super }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /templatesadmin/studychange.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/change_form.html' %} 2 | {% load i18n admin_urls static admin_modify %} 3 | 4 | 5 | {% block object-tools-items %} 6 | {% if original %} 7 |
  • 8 | Download Chat Records as CSV 9 |
  • 10 |
  • 11 | Download Chat Ratings as CSV 12 |
  • 13 | {% endif %} 14 | {{ block.super }} 15 | {% endblock %} 16 | --------------------------------------------------------------------------------