diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e402fa61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Database +*.db +/data \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..1c4e3d13 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +## Code of Conduct + +Our chatbot is an inclusive community that values respect, collaboration, and innovation. We welcome contributors of all backgrounds and skill levels to join us in building a positive and productive community. To ensure that our community remains safe, respectful, and welcoming, we ask all contributors to abide by this code of conduct. + +### 1. Respectful Communication + +We value open communication and encourage contributors to express themselves in a respectful and constructive manner. We do not tolerate discrimination, harassment, or abuse of any kind, including but not limited to: + +- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, race, ethnicity, age, nationality, religion, or physical appearance +- Threats, intimidation, or bullying +- Inappropriate sexual advances or imagery + +### 2. Collaborative Development + +We encourage collaboration and teamwork among contributors and ask that everyone work together in a constructive and positive manner. We do not tolerate disruptive behavior, including but not limited to: + +- Spamming, trolling, or flaming +- Hijacking discussions or derailing conversations +- Refusal to consider alternative viewpoints or approaches + +### 3. Innovative Contributions + +We welcome contributions of all kinds and encourage innovation and experimentation. However, we do not tolerate the use of our chatbot for any form of misinformation, including but not limited to: + +- The dissemination of false information, rumors, or hoaxes +- The promotion of conspiracy theories or fake news +- The use of our chatbot for malicious purposes, including but not limited to fraud, scams, or phishing + +### 4. Consequences of Unacceptable Behavior + +We take all reports of unacceptable behavior seriously and will investigate all incidents promptly and thoroughly. We reserve the right to take any action deemed necessary, including but not limited to: + +- Warning the individual responsible for the unacceptable behavior +- Temporarily or permanently revoking their access to the chatbot +- Banning them from future participation in the community + +### 5. Reporting Unacceptable Behavior + +If you experience or witness behavior that violates this code of conduct, please report it immediately to the chatbot administrator. All reports will be kept confidential and will be investigated promptly and thoroughly. + +### 6. Acknowledgment of Code of Conduct + +By contributing to our chatbot, you acknowledge that you have read and agree to abide by this code of conduct. You also acknowledge that you have the responsibility to report any violations of this code of conduct. diff --git a/Change Log.md b/Change Log.md new file mode 100644 index 00000000..7249c715 --- /dev/null +++ b/Change Log.md @@ -0,0 +1,9 @@ +# GPT4ALL-Webui Change Log + +# V 0.0.1 +1 - Interaction with the bot in threaded discussion +2 - List of past discussions +3 - New Discussion +4 - Edit discussion name +5 - Remove discussion +6 - Export database as json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..673bcd5f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10 + +WORKDIR /srv +COPY ./requirements.txt . + +RUN python3.10 -m venv env +RUN . env/bin/activate +RUN python3.10 -m pip install -r requirements.txt --upgrade pip + +COPY ./app.py /srv/app.py +COPY ./static /srv/static +COPY ./templates /srv/templates + +CMD ["python", "app.py", "--host", "0.0.0.0", "--port", "4685", "--db_path", "data/database.db"] diff --git a/README.md b/README.md new file mode 100644 index 00000000..b264221d --- /dev/null +++ b/README.md @@ -0,0 +1,158 @@ +# Gpt4All Web UI + +![GitHub license](https://img.shields.io/github/license/ParisNeo/Gpt4All-webui) + +![GitHub issues](https://img.shields.io/github/issues/ParisNeo/Gpt4All-webui) + +![GitHub stars](https://img.shields.io/github/stars/ParisNeo/Gpt4All-webui) + +![GitHub forks](https://img.shields.io/github/forks/ParisNeo/Gpt4All-webui) + +This is a Flask web application that provides a chat UI for interacting with the GPT4All chatbot. + +[Discord server](https://discord.gg/DZ4wsgg4) + +## What is GPT4All + +GPT4All is a language model built by Nomic-AI, a company specializing in natural language processing. The app uses Nomic-AI's library to communicate with the GPT4All model, which runs locally on the user's PC. For more details about this project, head on to their [github repository](https://github.com/nomic-ai/gpt4all). You can also reald their [Technical report](https://s3.amazonaws.com/static.nomic.ai/gpt4all/2023_GPT4All_Technical_Report.pdf) for more information about the training process, the batabase etc. + +The app allows users to send messages to the chatbot and view its responses in real-time. Additionally, users can export the entire chat history in text or JSON format. + +The model has just been released and it may evolve over time, this webui is meant for community to get easy and fully local access to a chatbot that may become better with time. + +## Disclaimer + +The model used by GPT4ALL has been fine-tuned using the LORA technique on LLAMA 7B weights (for now). It is important to note that the LLAMA weights are under commercial proprietary license, and therefore, this model cannot be used for commercial purposes. We do not provide the weights ourselves, but have built a UI wrapper on top of the Nomic library, which downloads the weights automatically upon running the program. + +It is important to understand that we are not responsible for any misuse of this tool. Please use it responsibly and at your own risk. While we hope that Nomic will address this issue in the future by providing clean weights that can be used freely, for now, this model is intended for testing purposes only. + +## UI screenshot +![image](https://user-images.githubusercontent.com/827993/229951093-27114d9f-0e1f-4d84-b103-e35cd3f9310d.png) + +**Note for Windows users:** At the moment, Nomic-AI has not provided a wheel for Windows, so you will need to use the app with the Windows Subsystem for Linux (WSL). To install WSL, follow these steps: + +- Open the Windows Features settings (you can find this by searching for "Windows Features" in the Start menu). +- Enable the "Windows Subsystem for Linux" feature. +- Restart your computer when prompted. +- Install a Linux distribution from the Microsoft Store (e.g., Ubuntu). +- Open the Linux distribution and follow the prompts to create a new user account. +- We apologize for any inconvenience this may cause. We are working on a more widespread version. + +## Installation + +To install the app, follow these steps: + +1. Clone the GitHub repository: + +``` +git clone https://github.com/ParisNeo/Gpt4All-webui +``` + +1. Navigate to the project directory: + +``` +cd Gpt4All-webui +``` + +1. Run the appropriate installation script for your platform: + +On Windows with WSL: + +- When Nomic add windows support you would be able to use this : + + ``` + install.bat + ``` +- On linux/ Mac os + + ``` +./install.sh + ``` + +On Linux/MacOS, if you have issues, refer more details are presented [here](docs/Linux_Osx_Install.md) + +These scripts will create a Python virtual environment and install the required dependencies. + +## Usage + +To run the Flask server, execute the following command: +```bash +python app.py [--port PORT] [--host HOST] [--temp TEMP] [--n-predict N_PREDICT] [--top-k TOP_K] [--top-p TOP_P] [--repeat-penalty REPEAT_PENALTY] [--repeat-last-n REPEAT_LAST_N] [--ctx-size CTX_SIZE] +``` + +On Linux/MacOS more details are [here](docs/Linux_Osx_Usage.md) + + +## Options + +* `--port`: the port on which to run the server (default: 9600) +* `--host`: the host address on which to run the server (default: localhost) +* `--temp`: the sampling temperature for the model (default: 0.1) +* `--n-predict`: the number of tokens to predict at a time (default: 128) +* `--top-k`: the number of top-k candidates to consider for sampling (default: 40) +* `--top-p`: the cumulative probability threshold for top-p sampling (default: 0.90) +* `--repeat-penalty`: the penalty to apply for repeated n-grams (default: 1.3) +* `--repeat-last-n`: the number of tokens to use for detecting repeated n-grams (default: 64) +* `--ctx-size`: the maximum context size to use for generating responses (default: 2048) + +Note: All options are optional, and have default values. + +Once the server is running, open your web browser and navigate to http://localhost:9600 (or http://your host name:your port number if you have selected different values for those) to access the chatbot UI. To use the app, open a web browser and navigate to this URL. + +Make sure to adjust the default values and descriptions of the options to match your specific application. + +## Contribute + +This is an open-source project by the community for the community. Our chatbot is a UI wrapper for Nomic AI's model, which enables natural language processing and machine learning capabilities. + +We welcome contributions from anyone who is interested in improving our chatbot. Whether you want to report a bug, suggest a feature, or submit a pull request, we encourage you to get involved and help us make our chatbot even better. + +Before contributing, please take a moment to review our [code of conduct](./CODE_OF_CONDUCT.md). We expect all contributors to abide by this code of conduct, which outlines our expectations for respectful communication, collaborative development, and innovative contributions. + +### Reporting Bugs + +If you find a bug or other issue with our chatbot, please report it by [opening an issue](https://github.com/your-username/your-chatbot/issues/new). Be sure to provide as much detail as possible, including steps to reproduce the issue and any relevant error messages. + +### Suggesting Features + +If you have an idea for a new feature or improvement to our chatbot, we encourage you to [open an issue](https://github.com/your-username/your-chatbot/issues/new) to discuss it. We welcome feedback and ideas from the community and will consider all suggestions that align with our project goals. + +### Contributing Code + +If you want to contribute code to our chatbot, please follow these steps: + +1. Fork the repository and create a new branch for your changes. +2. Make your changes and ensure that they follow our coding conventions. +3. Test your changes to ensure that they work as expected. +4. Submit a pull request with a clear description of your changes and the problem they solve. + +We will review your pull request as soon as possible and provide feedback on any necessary changes. We appreciate your contributions and look forward to working with you! + +Please note that all contributions are subject to review and approval by our project maintainers. We reserve the right to reject any contribution that does not align with our project goals or standards. + +## Future Plans + +Here are some of the future plans for this project: + +**Enhanced control of chatbot parameters:** We plan to improve the user interface (UI) of the chatbot to allow users to control the parameters of the chatbot such as temperature and other variables. This will give users more control over the chatbot's responses, and allow for a more customized experience. + +**Extension system for plugins:** We are also working on an extension system that will allow developers to create plugins for the chatbot. These plugins will be able to add new features and capabilities to the chatbot, and allow for greater customization of the chatbot's behavior. + +**Enhanced UI with themes and skins:** Additionally, we plan to enhance the user interface of the chatbot to allow for themes and skins. This will allow users to personalize the appearance of the chatbot, and make it more visually appealing. + +We are excited about these future plans for the project and look forward to implementing them in the near future. Stay tuned for updates! + +## License + +This project is licensed under the Apache 2.0 License. See the [LICENSE](https://github.com/ParisNeo/Gpt4All-webui/blob/main/LICENSE) file for details. + +## Special thanks + + +Special thanks to : +- [cclaar-byte](https://github.com/cclaar-byte) +- [CybearWarfare](https://github.com/CybearWarfare) +- [Jan Brummelte](https://github.com/brummelte) +- [higorvaz](https://github.com/higorvaz) + +for their contributions. diff --git a/app.py b/app.py new file mode 100644 index 00000000..4be17f7b --- /dev/null +++ b/app.py @@ -0,0 +1,355 @@ +from flask import Flask, jsonify, request, render_template, Response, stream_with_context +from nomic.gpt4all import GPT4All +import argparse +import threading +from io import StringIO +import sys +import re +import sqlite3 +from datetime import datetime + +import sqlite3 +import json +import time +import traceback + +import select + +#=================================== Database ================================================================== +class Discussion: + def __init__(self, discussion_id, db_path='database.db'): + self.discussion_id = discussion_id + self.db_path = db_path + + @staticmethod + def create_discussion(db_path='database.db', title='untitled'): + with sqlite3.connect(db_path) as conn: + cur = conn.cursor() + cur.execute("INSERT INTO discussion (title) VALUES (?)", (title,)) + discussion_id = cur.lastrowid + conn.commit() + return Discussion(discussion_id, db_path) + + @staticmethod + def get_discussion(db_path='database.db', id=0): + return Discussion(id, db_path) + + def add_message(self, sender, content): + with sqlite3.connect(self.db_path) as conn: + cur = conn.cursor() + cur.execute('INSERT INTO message (sender, content, discussion_id) VALUES (?, ?, ?)', + (sender, content, self.discussion_id)) + message_id = cur.lastrowid + conn.commit() + return message_id + @staticmethod + def get_discussions(db_path): + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM discussion') + rows = cursor.fetchall() + return [{'id': row[0], 'title': row[1]} for row in rows] + + @staticmethod + def rename(db_path, discussion_id, title): + with sqlite3.connect(db_path) as conn: + cursor = conn.cursor() + cursor.execute('UPDATE discussion SET title=? WHERE id=?', (title, discussion_id)) + conn.commit() + + def delete_discussion(self): + with sqlite3.connect(self.db_path) as conn: + cur = conn.cursor() + cur.execute('DELETE FROM message WHERE discussion_id=?', (self.discussion_id,)) + cur.execute('DELETE FROM discussion WHERE id=?', (self.discussion_id,)) + conn.commit() + + def get_messages(self): + with sqlite3.connect(self.db_path) as conn: + cur = conn.cursor() + cur.execute('SELECT * FROM message WHERE discussion_id=?', (self.discussion_id,)) + rows = cur.fetchall() + return [{'sender': row[1], 'content': row[2], 'id':row[0]} for row in rows] + + + + def update_message(self, message_id, new_content): + with sqlite3.connect(self.db_path) as conn: + cur = conn.cursor() + cur.execute('UPDATE message SET content = ? WHERE id = ?', (new_content, message_id)) + conn.commit() + + def remove_discussion(self): + with sqlite3.connect(self.db_path) as conn: + conn.cursor().execute('DELETE FROM discussion WHERE id=?', (self.discussion_id,)) + conn.commit() + +def last_discussion_has_messages(db_path='database.db'): + with sqlite3.connect(db_path) as conn: + c = conn.cursor() + c.execute("SELECT * FROM message ORDER BY id DESC LIMIT 1") + last_message = c.fetchone() + return last_message is not None + +def export_to_json(db_path='database.db'): + with sqlite3.connect(db_path) as conn: + cur = conn.cursor() + cur.execute('SELECT * FROM discussion') + discussions = [] + for row in cur.fetchall(): + discussion_id = row[0] + discussion = {'id': discussion_id, 'messages': []} + cur.execute('SELECT * FROM message WHERE discussion_id=?', (discussion_id,)) + for message_row in cur.fetchall(): + discussion['messages'].append({'sender': message_row[1], 'content': message_row[2]}) + discussions.append(discussion) + return discussions + +def remove_discussions(db_path='database.db'): + with sqlite3.connect(db_path) as conn: + cur = conn.cursor() + cur.execute('DELETE FROM message') + cur.execute('DELETE FROM discussion') + conn.commit() + +# create database schema +def check_discussion_db(db_path): + print("Checking discussions database...") + with sqlite3.connect(db_path) as conn: + cur = conn.cursor() + cur.execute(''' + CREATE TABLE IF NOT EXISTS discussion ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT + ) + ''') + cur.execute(''' + CREATE TABLE IF NOT EXISTS message ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sender TEXT NOT NULL, + content TEXT NOT NULL, + discussion_id INTEGER NOT NULL, + FOREIGN KEY (discussion_id) REFERENCES discussion(id) + ) + ''') + conn.commit() + + print("Ok") + +# ======================================================================================================================== + + + +app = Flask("GPT4All-WebUI", static_url_path='/static', static_folder='static') +class Gpt4AllWebUI(): + def __init__(self, chatbot_bindings, app, db_path='database.db') -> None: + self.current_discussion = None + self.chatbot_bindings = chatbot_bindings + self.app=app + self.db_path= db_path + self.add_endpoint('/', '', self.index, methods=['GET']) + self.add_endpoint('/stream', 'stream', self.stream, methods=['GET']) + self.add_endpoint('/export', 'export', self.export, methods=['GET']) + self.add_endpoint('/new_discussion', 'new_discussion', self.new_discussion, methods=['GET']) + self.add_endpoint('/bot', 'bot', self.bot, methods=['POST']) + self.add_endpoint('/discussions', 'discussions', self.discussions, methods=['GET']) + self.add_endpoint('/rename', 'rename', self.rename, methods=['POST']) + self.add_endpoint('/get_messages', 'get_messages', self.get_messages, methods=['POST']) + self.add_endpoint('/delete_discussion', 'delete_discussion', self.delete_discussion, methods=['POST']) + + self.add_endpoint('/update_message', 'update_message', self.update_message, methods=['GET']) + + + + # Chatbot conditionning + # response = self.chatbot_bindings.prompt("This is a discussion between A user and an AI. AI responds to user questions in a helpful manner. AI is not allowed to lie or deceive. AI welcomes the user\n### Response:") + # print(response) + + def add_endpoint(self, endpoint=None, endpoint_name=None, handler=None, methods=['GET'], *args, **kwargs): + self.app.add_url_rule(endpoint, endpoint_name, handler, methods=methods, *args, **kwargs) + + def index(self): + return render_template('chat.html') + + def format_message(self, message): + # Look for a code block within the message + pattern = re.compile(r"(```.*?```)", re.DOTALL) + match = pattern.search(message) + + # If a code block is found, replace it with a tag + if match: + code_block = match.group(1) + message = message.replace(code_block, f"{code_block[3:-3]}") + + # Return the formatted message + return message + + + def stream(self): + def generate(): + # Replace this with your text-generating code + for i in range(10): + yield f'This is line {i+1}\n' + time.sleep(1) + + return Response(stream_with_context(generate())) + + def export(self): + return jsonify(export_to_json(self.db_path)) + + + @stream_with_context + def parse_to_prompt_stream(self, message, message_id): + bot_says = [''] + point = b'' + bot = self.chatbot_bindings.bot + self.stop=False + + # very important. This is the maximum time we wait for the model + wait_val = 15.0 # At the beginning the server may need time to send data. we wait 15s + + # send the message to the bot + print(f"Received message : {message}") + bot = self.chatbot_bindings.bot + bot.stdin.write(message.encode('utf-8')) + bot.stdin.write(b"\n") + bot.stdin.flush() + + # First we need to send the new message ID to the client + response_id = self.current_discussion.add_message("GPT4All",'') # first the content is empty, but we'll fill it at the end + yield(json.dumps({'type':'input_message_infos','message':message, 'id':message_id, 'response_id':response_id})) + + #Now let's wait for the bot to answer + while not self.stop: + readable, _, _ = select.select([bot.stdout], [], [], wait_val) + wait_val = 4.0 # Once started, the process doesn't take that much so we reduce the wait + if bot.stdout in readable: + point += bot.stdout.read(1) + try: + character = point.decode("utf-8") + if character == "\n": + bot_says.append('\n') + yield '\n' + else: + bot_says[-1] += character + yield character + point = b'' + + except UnicodeDecodeError: + if len(point) > 4: + point = b'' + else: + self.current_discussion.update_message(response_id,bot_says) + return "\n".join(bot_says) + + def bot(self): + self.stop=True + with sqlite3.connect(self.db_path) as conn: + try: + if self.current_discussion is None or not last_discussion_has_messages(self.db_path): + self.current_discussion=Discussion.create_discussion(self.db_path) + + message_id = self.current_discussion.add_message("user", request.json['message']) + message = f"{request.json['message']}" + + # Segmented (the user receives the output as it comes) + # We will first send a json entry that contains the message id and so on, then the text as it goes + return Response(stream_with_context(self.parse_to_prompt_stream(message, message_id))) + except Exception as ex: + print(ex) + msg = traceback.print_exc() + return "Exception :"+str(ex)+"
"+traceback.format_exc()+"
Please report exception" + + def discussions(self): + try: + discussions = Discussion.get_discussions(self.db_path) + return jsonify(discussions) + except Exception as ex: + print(ex) + msg = traceback.print_exc() + return "Exception :"+str(ex)+"
"+traceback.format_exc()+"
Please report exception" + + def rename(self): + data = request.get_json() + id = data['id'] + title = data['title'] + Discussion.rename(self.db_path, id, title) + return "renamed successfully" + + def get_messages(self): + data = request.get_json() + id = data['id'] + self.current_discussion = Discussion(id,self.db_path) + messages = self.current_discussion.get_messages() + return jsonify(messages) + + + def delete_discussion(self): + data = request.get_json() + id = data['id'] + self.current_discussion = Discussion(id, self.db_path) + self.current_discussion.delete_discussion() + self.current_discussion = None + return jsonify({}) + + def update_message(self): + try: + id = request.args.get('id') + new_message = request.args.get('message') + self.current_discussion.update_message(id, new_message) + return jsonify({"status":'ok'}) + except Exception as ex: + print(ex) + msg = traceback.print_exc() + return "Exception :"+str(ex)+"
"+traceback.format_exc()+"
Please report exception" + + def new_discussion(self): + title = request.args.get('title') + self.current_discussion= Discussion.create_discussion(self.db_path, title) + # Get the current timestamp + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # add a new discussion + self.chatbot_bindings.close() + self.chatbot_bindings.open() + + # Return a success response + return json.dumps({'id': self.current_discussion.discussion_id}) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Start the chatbot Flask app.') + + parser.add_argument('--temp', type=float, default=0.1, help='Temperature parameter for the model.') + parser.add_argument('--n_predict', type=int, default=128, help='Number of tokens to predict at each step.') + parser.add_argument('--top_k', type=int, default=40, help='Value for the top-k sampling.') + parser.add_argument('--top_p', type=float, default=0.95, help='Value for the top-p sampling.') + parser.add_argument('--repeat_penalty', type=float, default=1.3, help='Penalty for repeated tokens.') + parser.add_argument('--repeat_last_n', type=int, default=64, help='Number of previous tokens to consider for the repeat penalty.') + parser.add_argument('--ctx_size', type=int, default=2048, help='Size of the context window for the model.') + parser.add_argument('--debug', dest='debug', action='store_true', help='launch Flask server in debug mode') + parser.add_argument('--host', type=str, default='localhost', help='the hostname to listen on') + parser.add_argument('--port', type=int, default=9600, help='the port to listen on') + parser.add_argument('--db_path', type=str, default='database.db', help='Database path') + parser.set_defaults(debug=False) + + args = parser.parse_args() + + chatbot_bindings = GPT4All(decoder_config = { + 'temp': args.temp, + 'n_predict':args.n_predict, + 'top_k':args.top_k, + 'top_p':args.top_p, + #'color': True,#"## Instruction", + 'repeat_penalty': args.repeat_penalty, + 'repeat_last_n':args.repeat_last_n, + 'ctx_size': args.ctx_size + }) + chatbot_bindings.open() + check_discussion_db(args.db_path) + bot = Gpt4AllWebUI(chatbot_bindings, app, args.db_path) + + if args.debug: + app.run(debug=True, host=args.host, port=args.port) + else: + app.run(host=args.host, port=args.port) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..1b53de2d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.8' + +services: + webui: + build: + context: . + dockerfile: Dockerfile + volumes: + - ./data:/srv/data + - ./data/.nomic:/root/.nomic/ + ports: + - "4685:4685" diff --git a/docs/Linux_Osx_Install.md b/docs/Linux_Osx_Install.md new file mode 100644 index 00000000..086a285e --- /dev/null +++ b/docs/Linux_Osx_Install.md @@ -0,0 +1,55 @@ +# Installing GPT4All-Webui on Linux or macOS: + +\- Install requirements +python3.11 -m pip install -r requirements.txt  + +![](https://user-images.githubusercontent.com/9384127/230159652-120e60f3-b737-434a-ac01-15819a0e7698.png) + +\- Review the install script 🙏🏻 + +``` +nano -lASimYsh install.sh +``` + +![](https://user-images.githubusercontent.com/9384127/229646387-9fea98c6-fb13-496b-b8eb-9db6fe241556.png) + +\- Make it runnable + +``` +chmod +x install.sh +``` + +\- Run the install script + +``` +./install.sh +``` + +![](https://user-images.githubusercontent.com/9384127/229650379-e70a54b3-a8c0-44c6-a44f-26b96dfbcf4e.png) + +\- Install nomic  + +``` +pip install nomic +``` + +or force pip to install with Python 3.11 + +``` +python3.11 -m pip install nomic +``` + +![](https://user-images.githubusercontent.com/9384127/229660511-ea6ef97e-712a-4e59-81d4-b4162e796728.png) + +![](https://user-images.githubusercontent.com/9384127/229660570-a960cfc3-4634-4354-868f-259ba9ffe888.png) + +\- Install/updt venv  + +``` +sudo apt install python3.11-venv +``` + +![](https://user-images.githubusercontent.com/9384127/229801745-3c84e89e-c62c-460d-9e79-dafe5aa518d5.png) + +\- ToDo + diff --git a/docs/Linux_Osx_Usage.md b/docs/Linux_Osx_Usage.md new file mode 100644 index 00000000..b40e641e --- /dev/null +++ b/docs/Linux_Osx_Usage.md @@ -0,0 +1,26 @@ +# Using GPT4All-Webui on Linux or macOS: + +To run the Flask server, execute the following command: + +```bash +python app.py [--port PORT] [--host HOST] [--temp TEMP] [--n-predict N_PREDICT] [--top-k TOP_K] [--top-p TOP_P] [--repeat-penalty REPEAT_PENALTY] [--repeat-last-n REPEAT_LAST_N] [--ctx-size CTX_SIZE] +``` + +On Kali Linux it runned well but Ubuntu requires some upgrades: + - python3.11 -m pip install numpy --upgrade + + + +![](https://user-images.githubusercontent.com/9384127/229806717-1b260484-723f-4780-b69b-d19c7375a84e.png) + +![](https://user-images.githubusercontent.com/9384127/229807131-623e9017-1536-473c-9e54-58d64f007991.png) + +![](https://user-images.githubusercontent.com/9384127/229809099-3ef4d87f-18ce-4873-b43b-e6f9d7accb50.png) + +![Magic Memes](https://www.memesmonkey.com/images/memesmonkey/77/771330e9f7a2a22e7b412187a657045c.jpeg) + +😅 + +?Root? + +![](https://user-images.githubusercontent.com/9384127/230199605-ab29926d-07dc-4d4d-9fd9-c51f9e117dfb.jpeg) \ No newline at end of file diff --git a/install.3.10.sh b/install.3.10.sh new file mode 100644 index 00000000..171c0fb6 --- /dev/null +++ b/install.3.10.sh @@ -0,0 +1,63 @@ +#!/usr/bin/bash + +# Install Python 3.10 and pip +echo -n "Checking for python3.10..." +if command -v python3.10 > /dev/null 2>&1; then + echo "OK" +else + read -p "Python3.10 is not installed. Would you like to install Python3.10? [Y/N] " choice + if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; then + echo "Installing Python3.10..." + sudo apt update + sudo apt install -y python3.10 python3.10-venv + else + echo "Please install Python3.10 and try again." + exit 1 + fi +fi + +# Install venv module +echo -n "Checking for venv module..." +if python3.10 -m venv env > /dev/null 2>&1; then + echo "OK" +else + read -p "venv module is not available. Would you like to install it? [Y/N] " choice + if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; then + echo "Installing venv module..." + sudo apt update + sudo apt install -y python3.10-venv + else + echo "Please install venv module and try again." + exit 1 + fi +fi + +# Create a new virtual environment +echo -n "Creating virtual environment..." +python3.10 -m venv env +if [ $? -ne 0 ]; then + echo "Failed to create virtual environment. Please check your Python installation and try again." + exit 1 +else + echo "OK" +fi + +# Activate the virtual environment +echo -n "Activating virtual environment..." +source env/bin/activate +echo "OK" + +# Install the required packages +echo "Installing requirements..." +export DS_BUILD_OPS=0 +export DS_BUILD_AIO=0 +python3.10 -m pip install pip --upgrade +python3.10 -m pip install -r requirements.txt + +if [ $? -ne 0 ]; then + echo "Failed to install required packages. Please check your internet connection and try again." + exit 1 +fi + +echo "Virtual environment created and packages installed successfully." +exit 0 diff --git a/install.bat b/install.bat new file mode 100644 index 00000000..1388eb6a --- /dev/null +++ b/install.bat @@ -0,0 +1,96 @@ +@echo off + +REM Check if Python is installed +set /p="Checking for python..." nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo Python is not installed. Would you like to install Python? [Y/N] + set /p choice= + if /i "%choice%" equ "Y" ( + REM Download Python installer + echo Downloading Python installer... + powershell -Command "Invoke-WebRequest -Uri 'https://www.python.org/ftp/python/3.10.0/python-3.10.0-amd64.exe' -OutFile 'python.exe'" + REM Install Python + echo Installing Python... + python.exe /quiet /norestart + ) else ( + echo Please install Python and try again. + pause + exit /b 1 + ) +) else ( + echo OK +) + +REM Check if pip is installed +set /p="Checking for pip..." nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo Pip is not installed. Would you like to install pip? [Y/N] + set /p choice= + if /i "%choice%" equ "Y" ( + REM Download get-pip.py + echo Downloading get-pip.py... + powershell -Command "Invoke-WebRequest -Uri 'https://bootstrap.pypa.io/get-pip.py' -OutFile 'get-pip.py'" + REM Install pip + echo Installing pip... + python get-pip.py + ) else ( + echo Please install pip and try again. + pause + exit /b 1 + ) +) else ( + echo OK +) + +REM Check if venv module is available +set /p="Checking for venv..." nul 2>&1 +if %ERRORLEVEL% neq 0 ( + echo venv module is not available. Would you like to upgrade Python to the latest version? [Y/N] + set /p choice= + if /i "%choice%" equ "Y" ( + REM Upgrade Python + echo Upgrading Python... + python -m pip install --upgrade pip setuptools wheel + python -m pip install --upgrade --user python + ) else ( + echo Please upgrade your Python installation and try again. + pause + exit /b 1 + ) +) else ( + echo OK +) + +REM Create a new virtual environment +set /p="Creating virtual environment ..." /dev/null 2>&1; then + echo "OK" +else + read -p "Python3.11 is not installed. Would you like to install Python3.11? [Y/N] " choice + if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; then + echo "Installing Python3.11..." + sudo apt update + sudo apt install -y python3.11 python3.11-venv + else + echo "Please install Python3.11 and try again." + exit 1 + fi +fi + +# Install venv module +echo -n "Checking for venv module..." +if python3.11 -m venv env > /dev/null 2>&1; then + echo "OK" +else + read -p "venv module is not available. Would you like to install it? [Y/N] " choice + if [ "$choice" = "Y" ] || [ "$choice" = "y" ]; then + echo "Installing venv module..." + sudo apt update + sudo apt install -y python3.11-venv + else + echo "Please install venv module and try again." + exit 1 + fi +fi + +# Create a new virtual environment +echo -n "Creating virtual environment..." +python3.11 -m venv env +if [ $? -ne 0 ]; then + echo "Failed to create virtual environment. Please check your Python installation and try again." + exit 1 +else + echo "OK" +fi + +# Activate the virtual environment +echo -n "Activating virtual environment..." +source env/bin/activate +echo "OK" + +# Install the required packages +echo "Installing requirements..." +export DS_BUILD_OPS=0 +export DS_BUILD_AIO=0 +python3.11 -m pip install pip --upgrade +python3.11 -m pip install -r requirements.txt + +if [ $? -ne 0 ]; then + echo "Failed to install required packages. Please check your internet connection and try again." + exit 1 +fi + +echo "Virtual environment created and packages installed successfully." +exit 0 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..c02e5403 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +nomic +pytest +pyllamacpp \ No newline at end of file diff --git a/static/css/chat.css b/static/css/chat.css new file mode 100644 index 00000000..d8cc9024 --- /dev/null +++ b/static/css/chat.css @@ -0,0 +1,788 @@ +/* +! tailwindcss v3.1.4 | MIT License | https://tailwindcss.com +*//* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; /* 1 */ + border-width: 0; /* 2 */ + border-style: solid; /* 2 */ + border-color: #e5e7eb; /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +*/ + +html { + line-height: 1.5; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -moz-tab-size: 4; /* 3 */ + -o-tab-size: 4; + tab-size: 4; /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 4 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; /* 1 */ + line-height: inherit; /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + font-weight: inherit; /* 1 */ + line-height: inherit; /* 1 */ + color: inherit; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; /* 1 */ + background-color: transparent; /* 2 */ + background-image: none; /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +input:-ms-input-placeholder, textarea:-ms-input-placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; /* 1 */ + color: #9ca3af; /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::-webkit-backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} +.mx-6 { + margin-left: 1.5rem; + margin-right: 1.5rem; +} +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} +.my-1 { + margin-top: 0.25rem; + margin-bottom: 0.25rem; +} +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} +.flex { + display: flex; +} +.h-screen { + height: 100vh; +} +.h-20 { + height: 5rem; +} +.h-12 { + height: 3rem; +} +.h-full { + height: 100%; +} +.max-h-full { + max-height: 100%; +} +.w-screen { + width: 100vw; +} +.w-full { + width: 100%; +} +.w-12 { + width: 3rem; +} +.flex-col { + flex-direction: column; +} +.items-center { + align-items: center; +} +.justify-between { + justify-content: space-between; +} +.space-y-0 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); +} +.space-x-2 > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} +.overflow-hidden { + overflow: hidden; +} +.overflow-y-auto { + overflow-y: auto; +} +.rounded-md { + border-radius: 0.375rem; +} +.border-b { + border-bottom-width: 1px; +} +.border-t { + border-top-width: 1px; +} +.border-accent { + border-color: var(--accent); +} +.bg-primary { + background-color: var(--primary); +} +.bg-tertiary { + background-color: var(--tertiary); +} +.bg-secondary { + background-color: var(--secondary); +} +.bg-accent { + background-color: var(--accent); +} +.p-4 { + padding: 1rem; +} +.p-2 { + padding: 0.5rem; +} +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} +.text-2xl { + font-size: 1.5rem; + line-height: 2rem; +} +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} +.font-bold { + font-weight: 700; +} +.font-normal { + font-weight: 400; +} +.font-medium { + font-weight: 500; +} +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} +.drop-shadow-sm { + --tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / 0.05)); + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} +.transition-colors { + transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +:root { + --primary: #1a1d21; + --secondary: #212529; + --teriary: #23282c; + --accent: #6691e7; +} + +.hover\:bg-\[\#7ba0ea\]:hover { + --tw-bg-opacity: 1; + background-color: rgb(123 160 234 / var(--tw-bg-opacity)); +} + +.active\:bg-\[\#3d73e1\]:active { + --tw-bg-opacity: 1; + background-color: rgb(61 115 225 / var(--tw-bg-opacity)); +} + +@media (min-width: 640px) { + + .sm\:space-x-reverse > :not([hidden]) ~ :not([hidden]) { + --tw-space-x-reverse: 1; + } + + .sm\:border-b { + border-bottom-width: 1px; + } +} + +@media (min-width: 768px) { + + .md\:h-1\/2 { + height: 50%; + } + + .md\:w-1\/3 { + width: 33.333333%; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-col { + flex-direction: column; + } + + .md\:border-r { + border-right-width: 1px; + } + + .md\:border-b-0 { + border-bottom-width: 0px; + } + + .md\:border-b { + border-bottom-width: 1px; + } +} + +@media (min-width: 1024px) { + + .lg\:w-1\/4 { + width: 25%; + } +} + +@media (min-width: 1280px) { + + .xl\:w-1\/5 { + width: 20%; + } +} + +.collapsible-header { + cursor: pointer; +} + +.collapsible-content { + display: none; +} + + +/* Wait animation */ +.lds-facebook { + display: inline-block; + position: relative; + width: 40px; + height: 40px; +} +.lds-facebook div { + display: inline-block; + position: absolute; + left: 8px; + width: 8px; + background: #fff; + animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; +} +.lds-facebook div:nth-child(1) { + left: 4px; + animation-delay: -0.24s; +} +.lds-facebook div:nth-child(2) { + left: 16px; + animation-delay: -0.12s; +} +.lds-facebook div:nth-child(3) { + left: 28px; + animation-delay: 0; +} +@keyframes lds-facebook { + 0% { + top: 8px; + height: 34px; + } + 50%, 100% { + top: 12px; + height: 18px; + } +} \ No newline at end of file diff --git a/static/images/delete_discussion.png b/static/images/delete_discussion.png new file mode 100644 index 00000000..08ee7a69 Binary files /dev/null and b/static/images/delete_discussion.png differ diff --git a/static/images/edit_discussion.png b/static/images/edit_discussion.png new file mode 100644 index 00000000..56301b1b Binary files /dev/null and b/static/images/edit_discussion.png differ diff --git a/static/images/icon.png b/static/images/icon.png new file mode 100644 index 00000000..6b2027a5 Binary files /dev/null and b/static/images/icon.png differ diff --git a/static/images/rename_discussion.svg b/static/images/rename_discussion.svg new file mode 100644 index 00000000..27e3a491 --- /dev/null +++ b/static/images/rename_discussion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/js/chat.js b/static/js/chat.js new file mode 100644 index 00000000..c39377bd --- /dev/null +++ b/static/js/chat.js @@ -0,0 +1,541 @@ +const chatWindow = document.getElementById('chat-window'); +const chatForm = document.getElementById('chat-form'); +const userInput = document.getElementById('user-input'); + +chatForm.addEventListener('submit', event => { + event.preventDefault(); + + // get user input and clear input field + message = userInput.value; + userInput.value = ''; + + // add user message to chat window + const sendbtn = document.querySelector("#submit-input") + const waitAnimation = document.querySelector("#wait-animation") + sendbtn.style.display="none"; + waitAnimation.style.display="block"; + fetch('/bot', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ message }) + }).then(function(response) { + const stream = new ReadableStream({ + start(controller) { + const reader = response.body.getReader(); + function push() { + reader.read().then(function(result) { + if (result.done) { + sendbtn.style.display="block"; + waitAnimation.style.display="none"; + controller.close(); + return; + } + controller.enqueue(result.value); + push(); + }) + } + push(); + } + }); + const textDecoder = new TextDecoder(); + const readableStreamDefaultReader = stream.getReader(); + let entry_counter = 0 + function readStream() { + readableStreamDefaultReader.read().then(function(result) { + if (result.done) { + return; + } + + text = textDecoder.decode(result.value); + + // The server will first send a json containing information about the message just sent + if(entry_counter==0) + { + // We parse it and + infos = JSON.parse(text) + addMessage('User', infos.message, infos.id, true); + elements = addMessage('GPT4ALL', '', infos.response_id, true); + messageTextElement=elements['messageTextElement']; + hiddenElement=elements['hiddenElement']; + entry_counter ++; + } + else{ + // For the other enrtries, these are just the text of the chatbot + for (const char of text) { + txt = hiddenElement.innerHTML; + if (char != '\f') { + txt += char + hiddenElement.innerHTML = txt + messageTextElement.innerHTML = txt.replace(/\n/g, "
") + } + + // scroll to bottom of chat window + chatWindow.scrollTop = chatWindow.scrollHeight; + } + entry_counter ++; + } + + readStream(); + }); + } + readStream(); + }); + +}); + + +function addMessage(sender, message, id, can_edit=false) { + console.log(id) + const messageElement = document.createElement('div'); + messageElement.classList.add('bg-secondary', 'drop-shadow-sm', 'p-4', 'mx-6', 'my-4', 'flex', 'flex-col', 'space-x-2'); + messageElement.classList.add(sender); + messageElement.setAttribute('id', id); + + const senderElement = document.createElement('div'); + senderElement.classList.add('font-normal', 'underline', 'text-sm'); + senderElement.innerHTML = sender; + + const messageTextElement = document.createElement('div'); + messageTextElement.classList.add('font-medium', 'text-md'); + messageTextElement.innerHTML = message; + // Create a hidden div element needed to buffer responses before commiting them to the visible message + const hiddenElement = document.createElement('div'); + hiddenElement.style.display = 'none'; + hiddenElement.innerHTML = ''; + + messageElement.appendChild(senderElement); + messageElement.appendChild(messageTextElement); + if(can_edit) + { + const editButton = document.createElement('button'); + editButton.classList.add('bg-blue-500', 'hover:bg-blue-700', 'text-white', 'font-bold', 'py-2', 'px-4', 'rounded', 'my-2'); + editButton.innerHTML = 'Edit'; + editButton.addEventListener('click', () => { + const inputField = document.createElement('input'); + inputField.type = 'text'; + inputField.classList.add('font-medium', 'text-md', 'border', 'border-gray-300', 'p-1'); + inputField.value = messageTextElement.innerHTML; + + const saveButton = document.createElement('button'); + saveButton.classList.add('bg-green-500', 'hover:bg-green-700', 'text-white', 'font-bold', 'py-2', 'px-4', 'rounded', 'my-2', 'ml-2'); + saveButton.innerHTML = 'Save'; + saveButton.addEventListener('click', () => { + const newText = inputField.value; + messageTextElement.innerHTML = newText; + // make request to update message + const url = `/update_message?id=${id}&message=${newText}`; + fetch(url) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + else{ + console.log("Updated") + } + }) + .catch(error => { + console.error('There was a problem updating the message:', error); + }); + messageElement.replaceChild(messageTextElement, inputField); + //messageElement.removeChild(inputField); + messageElement.removeChild(saveButton); + }); + + messageElement.replaceChild(inputField, messageTextElement); + messageElement.appendChild(saveButton); + inputField.focus(); + }); + + messageElement.appendChild(editButton); + } + chatWindow.appendChild(messageElement); + chatWindow.appendChild(hiddenElement); + + // scroll to bottom of chat window + chatWindow.scrollTop = chatWindow.scrollHeight; + + // Return all needed stuff + return {'messageTextElement':messageTextElement, 'hiddenElement':hiddenElement} +} + + + +const exportButton = document.getElementById('export-button'); + +exportButton.addEventListener('click', () => { + const messages = Array.from(chatWindow.querySelectorAll('.message')).map(messageElement => { + const senderElement = messageElement.querySelector('.sender'); + const messageTextElement= messageElement.querySelector('.message-text'); + const sender = senderElement.textContent; + const messageText = messageTextElement.textContent; + return { sender, messageText }; + }); + const exportFormat = 'json'; // replace with desired export format + + if (exportFormat === 'text') { + const exportText = messages.map(({ sender, messageText }) => `${sender}: ${messageText}`).join('\n'); + downloadTextFile(exportText); + } else if (exportFormat === 'json') { + fetch('/export') + .then(response => response.json()) + .then(data => { + db_data = JSON.stringify(data) + // Do something with the data, such as displaying it on the page + console.log(db_data); + downloadJsonFile(db_data); + }) + .catch(error => { + // Handle any errors that occur + console.error(error); + }); + } else { + console.error(`Unsupported export format: ${exportFormat}`); + } +}); + +function downloadTextFile(text) { +const blob = new Blob([text], { type: 'text/plain' }); +const url = URL.createObjectURL(blob); +downloadUrl(url); +} + +function downloadJsonFile(json) { +const blob = new Blob([json], { type: 'application/json' }); +const url = URL.createObjectURL(blob); +downloadUrl(url); +} + +function downloadUrl(url) { +const link = document.createElement('a'); +link.href = url; +link.download = 'chat.txt'; +link.click(); +} + + + +const newDiscussionBtn = document.querySelector('#new-discussion-btn'); + +newDiscussionBtn.addEventListener('click', () => { + const discussionName = prompt('Enter a name for the new discussion:'); + if (discussionName) { + const sendbtn = document.querySelector("#submit-input") + const waitAnimation = document.querySelector("#wait-animation") + sendbtn.style.display="none"; + waitAnimation.style.display="block"; + + // Add the discussion to the discussion list + const discussionItem = document.createElement('li'); + discussionItem.textContent = discussionName; + fetch(`/new_discussion?title=${discussionName}`) + .then(response => response.json()) + .then(data => { + console.log(`New chat ${data}`) + // Select the new discussion + //selectDiscussion(discussionId); + chatWindow.innerHTML="" + addMessage("GPT4ALL", welcome_message,0); + populate_discussions_list() + sendbtn.style.display="block"; + waitAnimation.style.display="none"; + }) + .catch(error => { + // Handle any errors that occur + console.error(error); + }); + + + } +}); + +function populate_discussions_list() +{ + // Populate discussions list + const discussionsList = document.querySelector('#discussions-list'); + discussionsList.innerHTML = ""; + fetch('/discussions') + .then(response => response.json()) + .then(discussions => { + discussions.forEach(discussion => { + const buttonWrapper = document.createElement('div'); + //buttonWrapper.classList.add('flex', 'space-x-2', 'mt-2'); + buttonWrapper.classList.add('flex', 'items-center', 'mt-2', 'py-4', 'text-left'); + + const renameButton = document.createElement('button'); + renameButton.classList.add('bg-green-500', 'hover:bg-green-700', 'text-white', 'font-bold', 'py-0', 'px-0', 'rounded', 'mr-2'); + const renameImg = document.createElement('img'); + renameImg.src = "/static/images/edit_discussion.png"; + renameImg.style.width='20px' + renameImg.style.height='20px' + renameButton.appendChild(renameImg); + + //renameButton.style.backgroundImage = "/rename_discussion.svg"; //.textContent = 'Rename'; + renameButton.addEventListener('click', () => { + const dialog = document.createElement('dialog'); + dialog.classList.add('bg-white', 'rounded', 'p-4'); + + const inputLabel = document.createElement('label'); + inputLabel.textContent = 'New name: '; + const inputField = document.createElement('input'); + inputField.classList.add('border', 'border-gray-400', 'rounded', 'py-1', 'px-2'); + inputField.setAttribute('type', 'text'); + inputField.setAttribute('name', 'title'); + inputField.setAttribute('value', discussion.title); + inputLabel.appendChild(inputField); + dialog.appendChild(inputLabel); + + const cancelButton = document.createElement('button'); + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => { + dialog.close(); + }); + + const renameConfirmButton = document.createElement('button'); + renameConfirmButton.classList.add('bg-green-500', 'hover:bg-green-700', 'text-white', 'font-bold', 'py-2', 'px-4', 'rounded', 'ml-2'); + renameConfirmButton.textContent = 'Rename'; + renameConfirmButton.addEventListener('click', () => { + const newTitle = inputField.value; + if (newTitle === '') { + alert('New name cannot be empty'); + } else { + fetch('/rename', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: discussion.id, title: newTitle }) + }) + .then(response => { + if (response.ok) { + discussion.title = newTitle; + discussionButton.textContent = newTitle; + dialog.close(); + } else { + alert('Failed to rename discussion'); + } + }) + .catch(error => { + console.error('Failed to rename discussion:', error); + alert('Failed to rename discussion'); + }); + } + }); + + dialog.appendChild(cancelButton); + dialog.appendChild(renameConfirmButton); + document.body.appendChild(dialog); + dialog.showModal(); + }); + const deleteButton = document.createElement('button'); + deleteButton.classList.add('bg-green-500', 'hover:bg-green-700', 'text-white', 'font-bold', 'py-0', 'px-0', 'rounded', 'ml-2'); + const deleteImg = document.createElement('img'); + deleteImg.src = "/static/images/delete_discussion.png"; + deleteImg.style.width='20px' + deleteImg.style.height='20px' + + deleteButton.addEventListener('click', () => { + fetch('/delete_discussion', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: discussion.id}) + }) + .then(response => { + if (response.ok) { + buttonWrapper.remove(); + } else { + alert('Failed to delete discussion'); + } + }) + .catch(error => { + console.error('Failed to delete discussion:', error); + alert('Failed to delete discussion'); + }); + + }); + + deleteButton.appendChild(deleteImg); + deleteButton.addEventListener('click', () => { + + }); + + const discussionButton = document.createElement('button'); + discussionButton.classList.add('flex-grow', 'w-full', 'bg-blue-500', 'hover:bg-blue-700', 'text-white', 'font-bold', 'py-2', 'px-4', 'rounded', 'text-left', 'hover:text-white'); + discussionButton.textContent = discussion.title; + discussionButton.addEventListener('click', () => { + // send query with discussion id to reveal discussion messages + fetch('/get_messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id: discussion.id }) + }) + .then(response => { + if (response.ok) { + response.text().then(data => { + const messages = JSON.parse(data); + console.log(messages) + // process messages + var container = document.getElementById('chat-window'); + container.innerHTML = ''; + messages.forEach(message => { + addMessage(message.sender, message.content, message.id, true); + }); + }); + } else { + alert('Failed to query the discussion'); + } + }) + .catch(error => { + console.error('Failed to get messages:', error); + alert('Failed to get messages'); + }); + console.log(`Showing messages for discussion ${discussion.id}`); + }); + + + buttonWrapper.appendChild(renameButton); + buttonWrapper.appendChild(deleteButton); + buttonWrapper.appendChild(discussionButton); + discussionsList.appendChild(buttonWrapper); + }); + }) + .catch(error => { + console.error('Failed to get discussions:', error); + alert('Failed to get discussions'); + }); +} + +// First time we populate the discussions list +populate_discussions_list() + + + + + +function add_collapsible_div(discussion_title, text, id) { + // Create the outer box element + const box = document.createElement('div'); + box.classList.add('bg-gray-100', 'rounded-lg', 'p-4'); + + // Create the title element + const title = document.createElement('h2'); + title.classList.add('text-lg', 'font-medium'); + title.textContent = discussion_title; + + // Create the toggle button element + const toggleBtn = document.createElement('button'); + toggleBtn.classList.add('focus:outline-none'); + toggleBtn.id = `${id}-toggle-btn`; + + // Create the expand icon element + const expandIcon = document.createElement('path'); + expandIcon.id = `${id}-expand-icon`; + expandIcon.setAttribute('d', 'M5 5h10v10H5z'); + + // Create the collapse icon element + const collapseIcon = document.createElement('path'); + collapseIcon.id = `${id}-collapse-icon`; + collapseIcon.setAttribute('d', 'M7 10h6'); + + // Add the icons to the toggle button element + toggleBtn.appendChild(expandIcon); + toggleBtn.appendChild(collapseIcon); + + // Create the content element + const content = document.createElement('div'); + content.id = `${id}-box-content`; + content.classList.add('mt-4'); + content.textContent = text; + // Add the title, toggle button, and content to the box element + // Create the title and toggle button container element + const titleToggleContainer = document.createElement('div'); + titleToggleContainer.classList.add('flex', 'justify-between', 'items-center'); + + // Add the title and toggle button to the container element + titleToggleContainer.appendChild(title); + titleToggleContainer.appendChild(toggleBtn); + + // Add the container element to the box element + box.appendChild(titleToggleContainer); + box.appendChild(content); + + // Add the box to the document + document.body.appendChild(box); + + // Add the CSS styles to the head of the document + const css = ` + #${id}-box-content { + max-height: 0; + overflow: hidden; + transition: max-height 0.2s ease-out; + } + + #${id}-box-content.expanded { + max-height: 1000px; + transition: max-height 0.5s ease-in; + } + + #${id}-toggle-btn:focus #${id}-collapse-icon { + display: block; + } + + #${id}-toggle-btn:focus #${id}-expand-icon { + display: none; + } + + #${id}-collapse-icon { + display: none; + } + `; + const head = document.head || document.getElementsByTagName('head')[0]; + const style = document.createElement('style'); + style.type = 'text/css'; + style.appendChild(document.createTextNode(css)); + head.appendChild(style); + + // Add the JavaScript code to toggle the box + const toggleBtnEl = document.querySelector(`#${id}-toggle-btn`); + const boxContentEl = document.querySelector(`#${id}-box-content`); + + toggleBtnEl.addEventListener('click', function() { + boxContentEl.classList.toggle('expanded'); + }); + return box + } + +const welcome_message = ` +
+This is a very early testing Web UI of GPT4All chatbot. +
Keep in mind that this is a 7B parameters model running on your own PC's CPU. It is literally 24 times smaller than GPT-3 in terms of parameter count. +
While it is still new and not as powerful as GPT-3.5 or GPT-4, it can still be useful for many applications. +
Any feedback and contribution is welcomed. +
This Web UI is a binding to the GPT4All model that allows you to test a chatbot locally on your machine. Feel free to ask questions or give instructions.
+ +
Examples:
+ +- A color description has been provided. Find the CSS code associated with that color. A light red color with a medium light shade of pink.
+- Come up with an interesting idea for a new movie plot. Your plot should be described with a title and a summary.
+- Reverse a string in python.
+- List 10 dogs.
+- Write me a poem about the fall of Julius Ceasar into a ceasar salad in iambic pentameter.
+- What is a three word topic describing the following keywords: baseball, football, soccer.
+- Act as ChefAI an AI that has the ability to create recipes for any occasion. Instruction: Give me a recipe for my next anniversary.
+
+
+`; +//welcome_message = add_collapsible_div("Note:", text, 'hints'); + +addMessage("GPT4ALL",welcome_message,0); + +// Code for collapsable text +const collapsibles = document.querySelectorAll('.collapsible'); +function uncollapse(id){ + console.log("uncollapsing") + const content = document.querySelector(`#${id}`); + content.classList.toggle('active'); +} diff --git a/static/js/marked.min.js b/static/js/marked.min.js new file mode 100644 index 00000000..9402998a --- /dev/null +++ b/static/js/marked.min.js @@ -0,0 +1,6 @@ +/** + * marked v4.3.0 - a markdown parser + * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,function(r){"use strict";function i(e,t){for(var u=0;ue.length)&&(t=e.length);for(var u=0,n=new Array(t);u=e.length?{done:!0}:{done:!1,value:e[u++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function e(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}r.defaults=e();function u(e){return t[e]}var n=/[&<>"']/,l=new RegExp(n.source,"g"),o=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,a=new RegExp(o.source,"g"),t={"&":"&","<":"<",">":">",'"':""","'":"'"};function A(e,t){if(t){if(n.test(e))return e.replace(l,u)}else if(o.test(e))return e.replace(a,u);return e}var c=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function x(e){return e.replace(c,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var h=/(^|[^\[])\^/g;function p(u,e){u="string"==typeof u?u:u.source,e=e||"";var n={replace:function(e,t){return t=(t=t.source||t).replace(h,"$1"),u=u.replace(e,t),n},getRegex:function(){return new RegExp(u,e)}};return n}var Z=/[^\w:]/g,O=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function f(e,t,u){if(e){try{n=decodeURIComponent(x(u)).replace(Z,"").toLowerCase()}catch(e){return null}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return null}var n;t&&!O.test(u)&&(e=u,g[" "+(n=t)]||(q.test(n)?g[" "+n]=n+"/":g[" "+n]=C(n,"/",!0)),t=-1===(n=g[" "+n]).indexOf(":"),u="//"===e.substring(0,2)?t?e:n.replace(j,"$1")+e:"/"===e.charAt(0)?t?e:n.replace(P,"$1")+e:n+e);try{u=encodeURI(u).replace(/%25/g,"%")}catch(e){return null}return u}var g={},q=/^[^:]+:\/*[^/]*$/,j=/^([^:]+:)[\s\S]*$/,P=/^([^:]+:\/*[^/]*)[\s\S]*$/;var k={exec:function(){}};function d(e,t){var u=e.replace(/\|/g,function(e,t,u){for(var n=!1,r=t;0<=--r&&"\\"===u[r];)n=!n;return n?"|":" |"}).split(/ \|/),n=0;if(u[0].trim()||u.shift(),0t)u.splice(t);else for(;u.length>=1,e+=e;return u+e}function m(e,t,u,n){var r=t.href,t=t.title?A(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?(n.state.inLink=!0,e={type:"link",raw:u,href:r,title:t,text:i,tokens:n.inlineTokens(i)},n.state.inLink=!1,e):{type:"image",raw:u,href:r,title:t,text:A(i)}}var b=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e&&0=r.length?e.slice(r.length):e}).join("\n")),{type:"code",raw:t,lang:e[2]&&e[2].trim().replace(this.rules.inline._escapes,"$1"),text:u}},t.heading=function(e){var t,u,e=this.rules.block.heading.exec(e);if(e)return t=e[2].trim(),/#$/.test(t)&&(u=C(t,"#"),!this.options.pedantic&&u&&!/ $/.test(u)||(t=u.trim())),{type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:this.lexer.inline(t)}},t.hr=function(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}},t.blockquote=function(e){var t,u,n,e=this.rules.block.blockquote.exec(e);if(e)return t=e[0].replace(/^ *>[ \t]?/gm,""),u=this.lexer.state.top,this.lexer.state.top=!0,n=this.lexer.blockTokens(t),this.lexer.state.top=u,{type:"blockquote",raw:e[0],tokens:n,text:t}},t.list=function(e){var t=this.rules.block.list.exec(e);if(t){var u,n,r,i,s,l,o,a,D,c,h,p=1<(g=t[1].trim()).length,f={type:"list",raw:"",ordered:p,start:p?+g.slice(0,-1):"",loose:!1,items:[]},g=p?"\\d{1,9}\\"+g.slice(-1):"\\"+g;this.options.pedantic&&(g=p?g:"[*+-]");for(var F=new RegExp("^( {0,3}"+g+")((?:[\t ][^\\n]*)?(?:\\n|$))");e&&(h=!1,t=F.exec(e))&&!this.rules.block.hr.test(e);){if(u=t[0],e=e.substring(u.length),o=t[2].split("\n",1)[0].replace(/^\t+/,function(e){return" ".repeat(3*e.length)}),a=e.split("\n",1)[0],this.options.pedantic?(i=2,c=o.trimLeft()):(i=t[2].search(/[^ ]/),c=o.slice(i=4=i||!a.trim())c+="\n"+a.slice(i);else{if(s)break;if(4<=o.search(/[^ ]/))break;if(d.test(o))break;if(C.test(o))break;if(k.test(o))break;c+="\n"+a}s||a.trim()||(s=!0),u+=D+"\n",e=e.substring(D.length+1),o=a.slice(i)}f.loose||(l?f.loose=!0:/\n *\n *$/.test(u)&&(l=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(c))&&(r="[ ] "!==n[0],c=c.replace(/^\[[ xX]\] +/,"")),f.items.push({type:"list_item",raw:u,task:!!n,checked:r,loose:!1,text:c}),f.raw+=u}f.items[f.items.length-1].raw=u.trimRight(),f.items[f.items.length-1].text=c.trimRight(),f.raw=f.raw.trimRight();for(var E,x=f.items.length,m=0;m$/,"$1").replace(this.rules.inline._escapes,"$1"):"",n=e[3]&&e[3].substring(1,e[3].length-1).replace(this.rules.inline._escapes,"$1"),{type:"def",tag:t,raw:e[0],href:u,title:n}},t.table=function(e){e=this.rules.block.table.exec(e);if(e){var t={type:"table",header:d(e[1]).map(function(e){return{text:e}}),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(t.header.length===t.align.length){t.raw=e[0];for(var u,n,r,i=t.align.length,s=0;s/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):A(e[0]):e[0]}},t.link=function(e){e=this.rules.inline.link.exec(e);if(e){var t=e[2].trim();if(!this.options.pedantic&&/^$/.test(t))return;var u=C(t.slice(0,-1),"\\");if((t.length-u.length)%2==0)return}else{u=function(e,t){if(-1!==e.indexOf(t[1]))for(var u=e.length,n=0,r=0;r$/.test(t)?u.slice(1):u.slice(1,-1):u)&&u.replace(this.rules.inline._escapes,"$1"),title:r&&r.replace(this.rules.inline._escapes,"$1")},e[0],this.lexer)}},t.reflink=function(e,t){var u;if(u=(u=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e))return(e=t[(e=(u[2]||u[1]).replace(/\s+/g," ")).toLowerCase()])?m(u,e,u[0],this.lexer):{type:"text",raw:t=u[0].charAt(0),text:t}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&(!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF38\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var r=n[1]||n[2]||"";if(!r||""===u||this.rules.inline.punctuation.exec(u)){var i=n[0].length-1,s=i,l=0,o="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(o.lastIndex=0,t=t.slice(-1*e.length+i);null!=(n=o.exec(t));){var a,D=n[1]||n[2]||n[3]||n[4]||n[5]||n[6];if(D)if(a=D.length,n[3]||n[4])s+=a;else if((n[5]||n[6])&&i%3&&!((i+a)%3))l+=a;else if(!(0<(s-=a)))return a=Math.min(a,a+s+l),D=e.slice(0,i+n.index+(n[0].length-D.length)+a),Math.min(i,a)%2?(a=D.slice(1,-1),{type:"em",raw:D,text:a,tokens:this.lexer.inlineTokens(a)}):(a=D.slice(2,-2),{type:"strong",raw:D,text:a,tokens:this.lexer.inlineTokens(a)})}}}},t.codespan=function(e){var t,u,n,e=this.rules.inline.code.exec(e);if(e)return n=e[2].replace(/\n/g," "),t=/[^ ]/.test(n),u=/^ /.test(n)&&/ $/.test(n),n=A(n=t&&u?n.substring(1,n.length-1):n,!0),{type:"codespan",raw:e[0],text:n}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}},t.autolink=function(e,t){var u,e=this.rules.inline.autolink.exec(e);if(e)return t="@"===e[2]?"mailto:"+(u=A(this.options.mangle?t(e[1]):e[1])):u=A(e[1]),{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=A(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=A(u[0]),r="www."===u[1]?"http://"+u[0]:u[0]}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t){e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):A(e[0]):e[0]:A(this.options.smartypants?t(e[0]):e[0]),{type:"text",raw:e[0],text:t}},e}(),B={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:k,lheading:/^((?:.|\n(?!\n))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/},w=(B.def=p(B.def).replace("label",B._label).replace("title",B._title).getRegex(),B.bullet=/(?:[*+-]|\d{1,9}[.)])/,B.listItemStart=p(/^( *)(bull) */).replace("bull",B.bullet).getRegex(),B.list=p(B.list).replace(/bull/g,B.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+B.def.source+")").getRegex(),B._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",B._comment=/|$)/,B.html=p(B.html,"i").replace("comment",B._comment).replace("tag",B._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),B.paragraph=p(B._paragraph).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.blockquote=p(B.blockquote).replace("paragraph",B.paragraph).getRegex(),B.normal=F({},B),B.gfm=F({},B.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),B.gfm.table=p(B.gfm.table).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.gfm.paragraph=p(B._paragraph).replace("hr",B.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",B.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",B._tag).getRegex(),B.pedantic=F({},B.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",B._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(B.normal._paragraph).replace("hr",B.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",B.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),{escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:k,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/^(?:[^_*\\]|\\.)*?\_\_(?:[^_*\\]|\\.)*?\*(?:[^_*\\]|\\.)*?(?=\_\_)|(?:[^*\\]|\\.)+(?=[^*])|[punct_](\*+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|(?:[^punct*_\s\\]|\\.)(\*+)(?=[^punct*_\s])/,rDelimUnd:/^(?:[^_*\\]|\\.)*?\*\*(?:[^_*\\]|\\.)*?\_(?:[^_*\\]|\\.)*?(?=\*\*)|(?:[^_\\]|\\.)+(?=[^_])|[punct*](\_+)(?=[\s]|$)|(?:[^punct*_\s\\]|\\.)(\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:k,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",w.punctuation=p(w.punctuation).replace(/punctuation/g,w._punctuation).getRegex(),w.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,w.escapedEmSt=/(?:^|[^\\])(?:\\\\)*\\[*_]/g,w._comment=p(B._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),w.emStrong.lDelim=p(w.emStrong.lDelim).replace(/punct/g,w._punctuation).getRegex(),w.emStrong.rDelimAst=p(w.emStrong.rDelimAst,"g").replace(/punct/g,w._punctuation).getRegex(),w.emStrong.rDelimUnd=p(w.emStrong.rDelimUnd,"g").replace(/punct/g,w._punctuation).getRegex(),w._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,w._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,w._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,w.autolink=p(w.autolink).replace("scheme",w._scheme).replace("email",w._email).getRegex(),w._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,w.tag=p(w.tag).replace("comment",w._comment).replace("attribute",w._attribute).getRegex(),w._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,w._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,w._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,w.link=p(w.link).replace("label",w._label).replace("href",w._href).replace("title",w._title).getRegex(),w.reflink=p(w.reflink).replace("label",w._label).replace("ref",B._label).getRegex(),w.nolink=p(w.nolink).replace("ref",B._label).getRegex(),w.reflinkSearch=p(w.reflinkSearch,"g").replace("reflink",w.reflink).replace("nolink",w.nolink).getRegex(),w.normal=F({},w),w.pedantic=F({},w.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",w._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",w._label).getRegex()}),w.gfm=F({},w.normal,{escape:p(w.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\'+(u?e:A(e,!0))+"\n":"
"+(u?e:A(e,!0))+"
\n"},t.blockquote=function(e){return"
\n"+e+"
\n"},t.html=function(e){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"'+e+"\n":""+e+"\n"},t.hr=function(){return this.options.xhtml?"
\n":"
\n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"\n"},t.listitem=function(e){return"
  • "+e+"
  • \n"},t.checkbox=function(e){return" "},t.paragraph=function(e){return"

    "+e+"

    \n"},t.table=function(e,t){return"\n\n"+e+"\n"+(t=t&&""+t+"")+"
    \n"},t.tablerow=function(e){return"\n"+e+"\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"\n"},t.strong=function(e){return""+e+""},t.em=function(e){return""+e+""},t.codespan=function(e){return""+e+""},t.br=function(){return this.options.xhtml?"
    ":"
    "},t.del=function(e){return""+e+""},t.link=function(e,t,u){return null===(e=f(this.options.sanitize,this.options.baseUrl,e))?u:(e='"+u+"")},t.image=function(e,t,u){return null===(e=f(this.options.sanitize,this.options.baseUrl,e))?u:(e=''+u+'":">"))},t.text=function(e){return e},e}(),z=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),$=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});e=this.serialize(e);return this.getNextSafeSlug(e,t.dryrun)},e}(),S=function(){function u(e){this.options=e||r.defaults,this.options.renderer=this.options.renderer||new _,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new z,this.slugger=new $}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,l,o,a,D,c,h,p,f,g,F,A,k="",d=e.length,C=0;C",i?Promise.resolve(t):s?void s(null,t):t;if(i)return Promise.reject(e);if(!s)throw e;s(e)});if(null==e)return l(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof e)return l(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected"));if((t=u)&&t.sanitize&&!t.silent&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options"),u.hooks&&(u.hooks.options=u),n){var o,a=u.highlight;try{u.hooks&&(e=u.hooks.preprocess(e)),o=f(e,u)}catch(e){return l(e)}var D,c=function(t){var e;if(!t)try{u.walkTokens&&I.walkTokens(o,u.walkTokens),e=g(o,u),u.hooks&&(e=u.hooks.postprocess(e))}catch(e){t=e}return u.highlight=a,t?l(t):n(null,e)};return!a||a.length<3?c():(delete u.highlight,o.length?(D=0,I.walkTokens(o,function(u){"code"===u.type&&(D++,setTimeout(function(){a(u.text,u.lang,function(e,t){if(e)return c(e);null!=t&&t!==u.text&&(u.text=t,u.escaped=!0),0===--D&&c()})},0))}),void(0===D&&c())):c())}if(u.async)return Promise.resolve(u.hooks?u.hooks.preprocess(e):e).then(function(e){return f(e,u)}).then(function(e){return u.walkTokens?Promise.all(I.walkTokens(e,u.walkTokens)).then(function(){return e}):e}).then(function(e){return g(e,u)}).then(function(e){return u.hooks?u.hooks.postprocess(e):e}).catch(l);try{u.hooks&&(e=u.hooks.preprocess(e));var h=f(e,u),p=(u.walkTokens&&I.walkTokens(h,u.walkTokens),g(h,u));return p=u.hooks?u.hooks.postprocess(p):p}catch(e){return l(e)}}}function I(e,t,u){return R(v.lex,S.parse)(e,t,u)}T.passThroughHooks=new Set(["preprocess","postprocess"]),I.options=I.setOptions=function(e){return I.defaults=F({},I.defaults,e),e=I.defaults,r.defaults=e,I},I.getDefaults=e,I.defaults=r.defaults,I.use=function(){for(var D=I.defaults.extensions||{renderers:{},childTokens:{}},e=arguments.length,t=new Array(e),u=0;u + + + GPT4All - WEBUI + + + +
    +
    +

    GPT4All - WEBUI

    +
    +
    +
    +
    +
    +

    Discussions

    +
    +
    + +
    +
    +
    +

    Settings

    +
    +
    +
    + +
    +
    +
    + + +
    + + + +
    +
    + + + + diff --git a/test/test_app.py b/test/test_app.py new file mode 100644 index 00000000..c02e1508 --- /dev/null +++ b/test/test_app.py @@ -0,0 +1,12 @@ +import pytest +from app import app + +@pytest.fixture +def client(): + with app.test_client() as client: + yield client + +def test_homepage(client): + response = client.get('/') + assert response.status_code == 200 + assert b"Welcome to my Flask app" in response.data diff --git a/uninstall.bat b/uninstall.bat new file mode 100644 index 00000000..e580b373 --- /dev/null +++ b/uninstall.bat @@ -0,0 +1,16 @@ +@echo off + +echo This will uninstall the environment. Are you sure? [Y/N] +set /p choice= +if /i "%choice%" equ "Y" ( + REM Download Python installer + echo -n + set /p="Removing virtual environment..."