├── .dockerignore ├── .github └── workflows │ ├── build.yaml │ └── deploy.yaml ├── .gitignore ├── Dockerfile.production ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 56d28e9010c2_.py │ ├── 6d5fa536a4f3_.py │ ├── 8d2e0763fc8c_.py │ └── fda6a5c1ed77_.py ├── assets ├── favicon.ico └── logo.jpg ├── compose.prod.yaml ├── devops └── playbook.yaml ├── reflex-gpt.code-workspace ├── reflex_gpt ├── __init__.py ├── chat │ ├── __init__.py │ ├── ai.py │ ├── form.py │ ├── page.py │ └── state.py ├── models.py ├── navigation │ ├── __init__.py │ ├── routes.py │ └── state.py ├── pages │ ├── __init__.py │ ├── about.py │ └── home.py ├── reflex_gpt.py └── ui │ ├── __init__.py │ ├── base.py │ ├── footer.py │ └── navbar.py ├── requirements.txt └── rxconfig.py /.dockerignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.py[cod] 3 | .web 4 | __pycache__/ 5 | assets/external/ 6 | .DS_Store 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 117 | .pdm.toml 118 | .pdm-python 119 | .pdm-build/ 120 | 121 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 122 | __pypackages__/ 123 | 124 | # Celery stuff 125 | celerybeat-schedule 126 | celerybeat.pid 127 | 128 | # SageMath parsed files 129 | *.sage.py 130 | 131 | # Environments 132 | .env 133 | .venv 134 | env/ 135 | venv/ 136 | ENV/ 137 | env.bak/ 138 | venv.bak/ 139 | 140 | # Spyder project settings 141 | .spyderproject 142 | .spyproject 143 | 144 | # Rope project settings 145 | .ropeproject 146 | 147 | # mkdocs documentation 148 | /site 149 | 150 | # mypy 151 | .mypy_cache/ 152 | .dmypy.json 153 | dmypy.json 154 | 155 | # Pyre type checker 156 | .pyre/ 157 | 158 | # pytype static type analyzer 159 | .pytype/ 160 | 161 | # Cython debug symbols 162 | cython_debug/ 163 | 164 | # PyCharm 165 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 166 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 167 | # and can be added to the global gitignore or merged into this file. For a more nuclear 168 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 169 | #.idea/ 170 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Push Container 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ "main" ] 7 | paths: 8 | - "Dockerfile.production" 9 | - "compose.prod.yaml" 10 | - "requirements.txt" 11 | - assets/ 12 | - reflex_gpt/ 13 | - rxconfig.py 14 | - alembic.ini 15 | - alembic/ 16 | - .github/workflows/build.yaml 17 | 18 | env: 19 | # DOCKER_IMAGE: codingforentrepreneurs/reflex-gpt 20 | # uncomment if using 21 | DOCKER_IMAGE: ${{ secrets.DOCKERHUB_REPO }} 22 | 23 | jobs: 24 | build-and-push: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v2 31 | 32 | - name: Login to DockerHub 33 | uses: docker/login-action@v2 34 | with: 35 | username: ${{ secrets.DOCKERHUB_USERNAME }} 36 | password: ${{ secrets.DOCKERHUB_TOKEN }} 37 | 38 | # For Reflex to build a container, 39 | # injecting your environment variables at 40 | # container build time is often required. 41 | - name: Create build env file 42 | run: | 43 | cat << EOF > .build-env 44 | OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} 45 | DATABASE_URL=${{ secrets.DATABASE_URL }} 46 | EOF 47 | 48 | - name: Build and push 49 | run: | 50 | docker build -f Dockerfile.production -t ${{ env.DOCKER_IMAGE }}:latest . 51 | docker tag ${{ env.DOCKER_IMAGE }}:latest ${{ env.DOCKER_IMAGE }}:${{ github.sha }} 52 | docker push ${{ env.DOCKER_IMAGE }} --all-tags 53 | 54 | - name: Remove build env file 55 | run: rm .build-env 56 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy with Ansible 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | DOCKER_IMAGE: codingforentrepreneurs/reflex-gpt 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install Ansible 16 | run: | 17 | sudo apt update 18 | sudo apt install -y ansible 19 | 20 | - name: Set up SSH key 21 | uses: webfactory/ssh-agent@v0.5.0 22 | with: 23 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 24 | 25 | - name: Add host key to known hosts 26 | run: | 27 | mkdir -p ~/.ssh 28 | ssh-keyscan ${{ secrets.VIRTUAL_MACHINE_IP }} >> ~/.ssh/known_hosts 29 | 30 | - name: Create Ansible inventory 31 | run: | 32 | echo "[linode]" > inventory.ini 33 | echo "${{ secrets.VIRTUAL_MACHINE_IP }} ansible_user=root" >> inventory.ini 34 | 35 | - name: Create env file 36 | run: | 37 | cat << EOF > .env 38 | DATABASE_URL=${{ secrets.DATABASE_URL }} 39 | DOCKERHUB_TOKEN=${{ secrets.DOCKERHUB_TOKEN }} 40 | DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }} 41 | OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }} 42 | EOF 43 | 44 | - name: Create Ansible Vars file 45 | run: | 46 | cat << EOF > devops/vars.yaml 47 | dockerhub_token: ${{ secrets.DOCKERHUB_TOKEN }} 48 | dockerhub_username: ${{ secrets.DOCKERHUB_USERNAME }} 49 | EOF 50 | 51 | - name: Run Ansible playbook 52 | env: 53 | ANSIBLE_HOST_KEY_CHECKING: False 54 | run: | 55 | ansible-playbook -i inventory.ini devops/playbook.yaml 56 | 57 | - name: Remove vars file 58 | run: rm devops/vars.yaml && rm .env 59 | if: always() 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.db 2 | *.py[cod] 3 | .web 4 | __pycache__/ 5 | assets/external/ 6 | .DS_Store 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | build/ 19 | develop-eggs/ 20 | dist/ 21 | downloads/ 22 | eggs/ 23 | .eggs/ 24 | lib/ 25 | lib64/ 26 | parts/ 27 | sdist/ 28 | var/ 29 | wheels/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | cover/ 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 117 | .pdm.toml 118 | .pdm-python 119 | .pdm-build/ 120 | 121 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 122 | __pypackages__/ 123 | 124 | # Celery stuff 125 | celerybeat-schedule 126 | celerybeat.pid 127 | 128 | # SageMath parsed files 129 | *.sage.py 130 | 131 | # Environments 132 | .env 133 | .venv 134 | env/ 135 | venv/ 136 | ENV/ 137 | env.bak/ 138 | venv.bak/ 139 | 140 | # Spyder project settings 141 | .spyderproject 142 | .spyproject 143 | 144 | # Rope project settings 145 | .ropeproject 146 | 147 | # mkdocs documentation 148 | /site 149 | 150 | # mypy 151 | .mypy_cache/ 152 | .dmypy.json 153 | dmypy.json 154 | 155 | # Pyre type checker 156 | .pyre/ 157 | 158 | # pytype static type analyzer 159 | .pytype/ 160 | 161 | # Cython debug symbols 162 | cython_debug/ 163 | 164 | # PyCharm 165 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 166 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 167 | # and can be added to the global gitignore or merged into this file. For a more nuclear 168 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 169 | #.idea/ 170 | reflex-gpt 171 | reflex-gpt.pub 172 | devops/vars.yaml 173 | inventory.ini 174 | -------------------------------------------------------------------------------- /Dockerfile.production: -------------------------------------------------------------------------------- 1 | FROM python:3.11-slim 2 | 3 | WORKDIR /app 4 | 5 | ARG NODE_VERSION=20.x 6 | 7 | # Install necessary tools, Node.js, and unzip 8 | RUN apt-get update && apt-get install -y \ 9 | curl \ 10 | libpq-dev \ 11 | gnupg \ 12 | unzip \ 13 | && curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION} | bash - \ 14 | && apt-get install -y nodejs \ 15 | && apt-get clean \ 16 | && rm -rf /var/lib/apt/lists/* 17 | 18 | # Verify Node.js installation 19 | RUN node --version && npm --version 20 | 21 | # Create reflex user 22 | RUN adduser --disabled-password --home /app reflex 23 | 24 | # Set up Python environment 25 | RUN python -m venv /app/.venv 26 | ENV PATH="/app/.venv/bin:$PATH" 27 | 28 | # Copy the application files 29 | COPY --chown=reflex:reflex . /app 30 | 31 | # Move .build-env to .env if it exists 32 | RUN if [ -f .build-env ]; then mv .build-env .env; fi 33 | 34 | # Set permissions 35 | RUN chown -R reflex:reflex /app 36 | 37 | # Switch to reflex user 38 | USER reflex 39 | 40 | # Install Python dependencies 41 | RUN pip install --no-cache-dir -r requirements.txt 42 | 43 | # Initialize Reflex 44 | RUN reflex init 45 | 46 | # Remove .env file after reflex init 47 | RUN rm -f .env 48 | 49 | # Ensure all environment variables are set 50 | ENV PATH="/app/.venv/bin:/usr/local/bin:/usr/bin:/bin:$PATH" 51 | ENV NODE_PATH="/usr/lib/node_modules" 52 | ENV REFLEX_DB_URL="sqlite:///reflex.db" 53 | 54 | # Needed until Reflex properly passes SIGTERM on backend. 55 | STOPSIGNAL SIGKILL 56 | 57 | # Always apply migrations before starting the backend. 58 | CMD ["sh", "-c", "reflex db migrate && reflex run --env prod --backend-only"] 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reflex GPT 2 | 3 | Build a Python Reflex App with OpenAI, a [Neon Postgres + Vector Database](https://neon.tech/cfe), and Deploy to a Virtual Machine. -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | # Use forward slashes (/) also on windows to provide an os agnostic path 6 | script_location = alembic 7 | 8 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 9 | # Uncomment the line below if you want the files to be prepended with date and time 10 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 11 | # for all available tokens 12 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 13 | 14 | # sys.path path, will be prepended to sys.path if present. 15 | # defaults to the current working directory. 16 | prepend_sys_path = . 17 | 18 | # timezone to use when rendering the date within the migration file 19 | # as well as the filename. 20 | # If specified, requires the python>=3.9 or backports.zoneinfo library. 21 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements 22 | # string value is passed to ZoneInfo() 23 | # leave blank for localtime 24 | # timezone = 25 | 26 | # max length of characters to apply to the "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to alembic/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:alembic/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # set to 'true' to search source files recursively 55 | # in each "version_locations" directory 56 | # new in Alembic version 1.10 57 | # recursive_version_locations = false 58 | 59 | # the output encoding used when revision files 60 | # are written from script.py.mako 61 | # output_encoding = utf-8 62 | 63 | sqlalchemy.url = driver://user:pass@localhost/dbname 64 | 65 | 66 | [post_write_hooks] 67 | # post_write_hooks defines scripts or Python functions that are run 68 | # on newly generated revision scripts. See the documentation for further 69 | # detail and examples 70 | 71 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 72 | # hooks = black 73 | # black.type = console_scripts 74 | # black.entrypoint = black 75 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 76 | 77 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 78 | # hooks = ruff 79 | # ruff.type = exec 80 | # ruff.executable = %(here)s/.venv/bin/ruff 81 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 82 | 83 | # Logging configuration 84 | [loggers] 85 | keys = root,sqlalchemy,alembic 86 | 87 | [handlers] 88 | keys = console 89 | 90 | [formatters] 91 | keys = generic 92 | 93 | [logger_root] 94 | level = WARN 95 | handlers = console 96 | qualname = 97 | 98 | [logger_sqlalchemy] 99 | level = WARN 100 | handlers = 101 | qualname = sqlalchemy.engine 102 | 103 | [logger_alembic] 104 | level = INFO 105 | handlers = 106 | qualname = alembic 107 | 108 | [handler_console] 109 | class = StreamHandler 110 | args = (sys.stderr,) 111 | level = NOTSET 112 | formatter = generic 113 | 114 | [formatter_generic] 115 | format = %(levelname)-5.5s [%(name)s] %(message)s 116 | datefmt = %H:%M:%S 117 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | if config.config_file_name is not None: 15 | fileConfig(config.config_file_name) 16 | 17 | # add your model's MetaData object here 18 | # for 'autogenerate' support 19 | # from myapp import mymodel 20 | # target_metadata = mymodel.Base.metadata 21 | target_metadata = None 22 | 23 | # other values from the config, defined by the needs of env.py, 24 | # can be acquired: 25 | # my_important_option = config.get_main_option("my_important_option") 26 | # ... etc. 27 | 28 | 29 | def run_migrations_offline() -> None: 30 | """Run migrations in 'offline' mode. 31 | 32 | This configures the context with just a URL 33 | and not an Engine, though an Engine is acceptable 34 | here as well. By skipping the Engine creation 35 | we don't even need a DBAPI to be available. 36 | 37 | Calls to context.execute() here emit the given string to the 38 | script output. 39 | 40 | """ 41 | url = config.get_main_option("sqlalchemy.url") 42 | context.configure( 43 | url=url, 44 | target_metadata=target_metadata, 45 | literal_binds=True, 46 | dialect_opts={"paramstyle": "named"}, 47 | ) 48 | 49 | with context.begin_transaction(): 50 | context.run_migrations() 51 | 52 | 53 | def run_migrations_online() -> None: 54 | """Run migrations in 'online' mode. 55 | 56 | In this scenario we need to create an Engine 57 | and associate a connection with the context. 58 | 59 | """ 60 | connectable = engine_from_config( 61 | config.get_section(config.config_ini_section, {}), 62 | prefix="sqlalchemy.", 63 | poolclass=pool.NullPool, 64 | ) 65 | 66 | with connectable.connect() as connection: 67 | context.configure( 68 | connection=connection, target_metadata=target_metadata 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | if context.is_offline_mode(): 76 | run_migrations_offline() 77 | else: 78 | run_migrations_online() 79 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /alembic/versions/56d28e9010c2_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 56d28e9010c2 4 | Revises: fda6a5c1ed77 5 | Create Date: 2024-08-28 15:26:58.914489 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | import sqlmodel 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '56d28e9010c2' 16 | down_revision: Union[str, None] = 'fda6a5c1ed77' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('chatsessionmessagemodel', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('session_id', sa.Integer(), nullable=True), 26 | sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 27 | sa.Column('role', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 28 | sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 29 | sa.ForeignKeyConstraint(['session_id'], ['chatsession.id'], ), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade() -> None: 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_table('chatsessionmessagemodel') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /alembic/versions/6d5fa536a4f3_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6d5fa536a4f3 4 | Revises: 8d2e0763fc8c 5 | Create Date: 2024-08-28 15:16:53.853333 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | import sqlmodel 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '6d5fa536a4f3' 16 | down_revision: Union[str, None] = '8d2e0763fc8c' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | with op.batch_alter_table('chat', schema=None) as batch_op: 24 | batch_op.drop_column('title') 25 | 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | with op.batch_alter_table('chat', schema=None) as batch_op: 32 | batch_op.add_column(sa.Column('title', sa.VARCHAR(), autoincrement=False, nullable=False)) 33 | 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /alembic/versions/8d2e0763fc8c_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 8d2e0763fc8c 4 | Revises: 5 | Create Date: 2024-08-28 14:59:22.733662 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | import sqlmodel 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '8d2e0763fc8c' 16 | down_revision: Union[str, None] = None 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('chat', 24 | sa.Column('id', sa.Integer(), nullable=False), 25 | sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False), 26 | sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 27 | sa.Column('update_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade() -> None: 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('chat') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /alembic/versions/fda6a5c1ed77_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: fda6a5c1ed77 4 | Revises: 6d5fa536a4f3 5 | Create Date: 2024-08-28 15:21:18.090094 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | from sqlalchemy.dialects import postgresql 13 | import sqlmodel 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = 'fda6a5c1ed77' 17 | down_revision: Union[str, None] = '6d5fa536a4f3' 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.create_table('chatsession', 25 | sa.Column('id', sa.Integer(), nullable=False), 26 | sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 27 | sa.Column('update_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.drop_table('chat') 31 | # ### end Alembic commands ### 32 | 33 | 34 | def downgrade() -> None: 35 | # ### commands auto generated by Alembic - please adjust! ### 36 | op.create_table('chat', 37 | sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), 38 | sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), 39 | sa.Column('update_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('now()'), autoincrement=False, nullable=False), 40 | sa.PrimaryKeyConstraint('id', name='chat_pkey') 41 | ) 42 | op.drop_table('chatsession') 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/reflex-gpt/0eef5136613b1a1c7edf2fe60ea9ebabfd383411/assets/favicon.ico -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/reflex-gpt/0eef5136613b1a1c7edf2fe60ea9ebabfd383411/assets/logo.jpg -------------------------------------------------------------------------------- /compose.prod.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | frontend: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.production 6 | image: codingforentrepreneurs/reflex-gpt:latest 7 | env_file: 8 | - .env 9 | ports: 10 | - 80:3000 11 | command: reflex run --env prod --frontend-only 12 | app: 13 | build: 14 | context: . 15 | dockerfile: Dockerfile.production 16 | image: codingforentrepreneurs/reflex-gpt:latest 17 | env_file: 18 | - .env 19 | ports: 20 | - 8000:8000 21 | -------------------------------------------------------------------------------- /devops/playbook.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: linode 3 | become: true 4 | vars: 5 | project_dir: /app/ 6 | remote_env_file: "{{ project_dir }}/.env" 7 | compose_file: "{{ project_dir }}/compose.prod.yaml" 8 | env_file: "{{ playbook_dir | dirname }}/.env" 9 | vars_files: 10 | - "{{playbook_dir}}/vars.yaml" 11 | tasks: 12 | - name: Download Docker installation script 13 | get_url: 14 | url: https://get.docker.com 15 | dest: /tmp/get-docker.sh 16 | mode: '0755' 17 | 18 | - name: Run Docker installation script 19 | shell: /tmp/get-docker.sh 20 | args: 21 | creates: /usr/bin/docker 22 | 23 | - name: Ensure Docker service is running 24 | systemd: 25 | name: docker 26 | state: started 27 | enabled: yes 28 | 29 | - name: Create project directory 30 | file: 31 | path: "{{ project_dir }}" 32 | state: directory 33 | mode: '0755' 34 | 35 | - name: Copy .env file to server 36 | copy: 37 | src: "{{ env_file }}" 38 | dest: "{{ remote_env_file }}" 39 | no_log: false 40 | 41 | - name: Copy Docker Compose file to server 42 | copy: 43 | src: "{{ playbook_dir | dirname }}/compose.prod.yaml" 44 | dest: "{{ compose_file }}" 45 | 46 | - name: Login to Docker Hub 47 | shell: echo {{ dockerhub_token }} | docker login -u {{ dockerhub_username }} --password-stdin 48 | no_log: true 49 | 50 | - name: Pull latest Docker images 51 | shell: 52 | cmd: docker compose -f compose.prod.yaml pull 53 | args: 54 | chdir: "{{ project_dir }}" 55 | 56 | - name: Update Docker services 57 | shell: 58 | cmd: | 59 | services=$(docker compose -f compose.prod.yaml config --services) 60 | for service in $services; do 61 | if docker compose -f compose.prod.yaml ps --status running $service | grep -q $service; then 62 | echo "Updating running service: $service" 63 | docker compose -f compose.prod.yaml up -d --no-deps "$service" 64 | else 65 | echo "Starting service: $service" 66 | docker compose -f compose.prod.yaml up -d --no-deps "$service" 67 | fi 68 | done 69 | args: 70 | chdir: "{{ project_dir }}" 71 | 72 | - name: Remove orphaned containers 73 | shell: 74 | cmd: docker compose -f compose.prod.yaml up -d --remove-orphans 75 | args: 76 | chdir: "{{ project_dir }}" 77 | 78 | - name: Prune Docker system 79 | shell: 80 | cmd: docker system prune -f 81 | args: 82 | chdir: "{{ project_dir }}" 83 | -------------------------------------------------------------------------------- /reflex-gpt.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /reflex_gpt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codingforentrepreneurs/reflex-gpt/0eef5136613b1a1c7edf2fe60ea9ebabfd383411/reflex_gpt/__init__.py -------------------------------------------------------------------------------- /reflex_gpt/chat/__init__.py: -------------------------------------------------------------------------------- 1 | from . import state 2 | from .page import chat_page 3 | 4 | 5 | __all__ = [ 6 | 'chat_page', 7 | 'state' 8 | ] -------------------------------------------------------------------------------- /reflex_gpt/chat/ai.py: -------------------------------------------------------------------------------- 1 | from decouple import config 2 | from openai import OpenAI 3 | 4 | OPENAI_API_KEY= config("OPENAI_API_KEY", cast=str, default=None) 5 | OPENAI_MODEL="gpt-4o-mini" 6 | 7 | def get_client(): 8 | return OpenAI(api_key=OPENAI_API_KEY) 9 | 10 | def get_llm_response(gpt_messages): 11 | client = get_client() 12 | completion = client.chat.completions.create( 13 | model=OPENAI_MODEL, 14 | messages=gpt_messages 15 | ) 16 | return completion.choices[0].message.content -------------------------------------------------------------------------------- /reflex_gpt/chat/form.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from .state import ChatState 4 | 5 | def chat_form() -> rx.Component: 6 | return rx.form( 7 | rx.vstack( 8 | rx.text_area( 9 | name='message', 10 | placeholder='Your message', 11 | required=True, 12 | width='100%' 13 | ), 14 | rx.hstack( 15 | rx.button('Submit', type='submit'), 16 | rx.cond( 17 | ChatState.user_did_submit, 18 | rx.text("Submitted"), 19 | rx.fragment(), 20 | ) 21 | 22 | ) 23 | ), 24 | on_submit=ChatState.handle_submit, 25 | reset_on_submit=True 26 | ) -------------------------------------------------------------------------------- /reflex_gpt/chat/page.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from reflex_gpt import ui 4 | 5 | from .state import ChatMessage, ChatState 6 | from .form import chat_form 7 | 8 | 9 | message_style = dict( 10 | display="inline-block", 11 | padding="1em", 12 | border_radius="8px", 13 | max_width=["30em", "30em", "50em", "50em", "50em", "50em"] 14 | ) 15 | 16 | 17 | def message_box(chat_message: ChatMessage) -> rx.Component: 18 | return rx.box( 19 | rx.box( 20 | rx.markdown( 21 | chat_message.message, 22 | background_color=rx.cond(chat_message.is_bot, rx.color("mauve", 4), rx.color('blue', 4)), 23 | color=rx.cond(chat_message.is_bot, rx.color("mauve", 12), rx.color('blue', 12)), 24 | **message_style, 25 | ), 26 | text_align=rx.cond(chat_message.is_bot, "left", "right"), 27 | margin_top="1em", 28 | ), 29 | width="100%", 30 | ) 31 | 32 | def chat_page(): 33 | 34 | return ui.base_layout( 35 | rx.vstack( 36 | rx.hstack( 37 | rx.heading("Chat Here", size="5"), 38 | rx.cond(ChatState.not_found, "Not found", "Found"), 39 | rx.button("+ New Chat", on_click=ChatState.create_new_and_redirect) 40 | ), 41 | rx.box( 42 | rx.foreach(ChatState.messages, message_box), 43 | width='100%' 44 | ), 45 | chat_form(), 46 | margin="3rem auto", 47 | spacing="5", 48 | justify="center", 49 | min_height="85vh", 50 | ) 51 | ) -------------------------------------------------------------------------------- /reflex_gpt/chat/state.py: -------------------------------------------------------------------------------- 1 | # import time 2 | import reflex as rx 3 | import sqlmodel 4 | 5 | from typing import List, Optional 6 | from reflex_gpt.models import ChatSession, ChatSessionMessageModel 7 | 8 | from . import ai 9 | 10 | class ChatMessage(rx.Base): 11 | message: str 12 | is_bot: bool = False 13 | 14 | 15 | class ChatState(rx.State): 16 | chat_session: ChatSession = None 17 | not_found: Optional[bool] = None 18 | did_submit: bool = False 19 | messages: List[ChatMessage] = [] 20 | 21 | @rx.var 22 | def user_did_submit(self) -> bool: 23 | return self.did_submit 24 | 25 | def get_session_id(self) -> int: 26 | try: 27 | my_session_id = int(self.router.page.params.get('session_id')) 28 | except: 29 | my_session_id = None 30 | return my_session_id 31 | 32 | def create_new_chat_session(self): 33 | with rx.session() as db_session: 34 | obj = ChatSession() 35 | db_session.add(obj) # prepare to save 36 | db_session.commit() # actually save 37 | db_session.refresh(obj) 38 | self.chat_session = obj 39 | return obj 40 | 41 | def clear_ui(self): 42 | self.chat_session = None 43 | self.not_found = None 44 | self.did_submit = False 45 | self.messages = [] 46 | 47 | def create_new_and_redirect(self): 48 | self.clear_ui() 49 | new_chat_session = self.create_new_chat_session() 50 | return rx.redirect(f"/chat/{new_chat_session.id}") 51 | 52 | def clear_and_start_new(self): 53 | self.clear_ui() 54 | self.create_new_chat_session() 55 | yield 56 | 57 | def get_session_from_db(self, session_id=None): 58 | if session_id is None: 59 | session_id = self.get_session_id() 60 | # ChatSession.id == session_id 61 | with rx.session() as db_session: 62 | sql_statement = sqlmodel.select( 63 | ChatSession 64 | ).where( 65 | ChatSession.id == session_id 66 | ) 67 | result = db_session.exec(sql_statement).one_or_none() 68 | if result is None: 69 | self.not_found = True 70 | else: 71 | self.not_found = False 72 | self.chat_session = result 73 | messages = result.messages 74 | for msg_obj in messages: 75 | msg_txt = msg_obj.content 76 | is_bot = False if msg_obj.role == "user" else True 77 | self.append_message_to_ui(msg_txt, is_bot=is_bot) 78 | 79 | 80 | def on_detail_load(self): 81 | session_id = self.get_session_id() 82 | reload_detail = False 83 | if not self.chat_session: 84 | reload_detail = True 85 | else: 86 | """has a session""" 87 | if self.chat_session.id != session_id: 88 | reload_detail = True 89 | 90 | if reload_detail: 91 | self.clear_ui() 92 | if isinstance(session_id, int): 93 | self.get_session_from_db(session_id=session_id) 94 | 95 | def on_load(self): 96 | print("running on load") 97 | self.clear_ui() 98 | self.create_new_chat_session() 99 | 100 | def insert_message_to_db(self, content, role='unknown'): 101 | print("insert message data to db") 102 | if self.chat_session is None: 103 | return 104 | if not isinstance(self.chat_session, ChatSession): 105 | return 106 | with rx.session() as db_session: 107 | data = { 108 | "session_id": self.chat_session.id, 109 | "content": content, 110 | "role": role 111 | } 112 | obj = ChatSessionMessageModel(**data) 113 | db_session.add(obj) # prepare to save 114 | db_session.commit() # actually save 115 | 116 | def append_message_to_ui(self, message, is_bot:bool=False): 117 | self.messages.append( 118 | ChatMessage( 119 | message=message, 120 | is_bot = is_bot 121 | ) 122 | ) 123 | 124 | def get_gpt_messages(self): 125 | gpt_messages = [ 126 | { 127 | "role": "system", 128 | "content": "You are an expert at creating recipes like an elite chef. Respond in markdown" 129 | } 130 | ] 131 | for chat_message in self.messages: 132 | role = 'user' 133 | if chat_message.is_bot: 134 | role = 'system' 135 | gpt_messages.append({ 136 | "role": role, 137 | "content": chat_message.message 138 | }) 139 | return gpt_messages 140 | 141 | async def handle_submit(self, form_data:dict): 142 | print('here is our form data', form_data) 143 | user_message = form_data.get('message') 144 | if user_message: 145 | self.did_submit = True 146 | self.append_message_to_ui(user_message, is_bot=False) 147 | self.insert_message_to_db(user_message, role='user') 148 | yield 149 | gpt_messages = self.get_gpt_messages() 150 | bot_response = ai.get_llm_response(gpt_messages) 151 | self.did_submit = False 152 | self.append_message_to_ui(bot_response, is_bot=True) 153 | self.insert_message_to_db(bot_response, role='system') 154 | yield -------------------------------------------------------------------------------- /reflex_gpt/models.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import reflex as rx 3 | import sqlalchemy 4 | 5 | from datetime import datetime, timezone 6 | from sqlmodel import Field, Relationship 7 | 8 | 9 | def get_utc_now() -> datetime: 10 | return datetime.now(timezone.utc) 11 | 12 | 13 | class ChatSession(rx.Model, table=True): 14 | # id 15 | messages: List['ChatSessionMessageModel'] = Relationship(back_populates='session') 16 | # title: str 17 | created_at: datetime = Field( 18 | default_factory=get_utc_now, 19 | sa_type=sqlalchemy.DateTime(timezone=True), 20 | sa_column_kwargs={ 21 | 'server_default': sqlalchemy.func.now() 22 | }, 23 | nullable=False, 24 | ) 25 | update_at: datetime = Field( 26 | default_factory=get_utc_now, 27 | sa_type=sqlalchemy.DateTime(timezone=True), 28 | sa_column_kwargs={ 29 | 'onupdate': sqlalchemy.func.now(), 30 | 'server_default': sqlalchemy.func.now() 31 | }, 32 | nullable=False, 33 | ) 34 | 35 | class ChatSessionMessageModel(rx.Model, table=True): 36 | session_id: int = Field(default=None, foreign_key='chatsession.id') 37 | session: ChatSession = Relationship(back_populates="messages") 38 | content: str 39 | role: str 40 | created_at: datetime = Field( 41 | default_factory=get_utc_now, 42 | sa_type=sqlalchemy.DateTime(timezone=True), 43 | sa_column_kwargs={ 44 | 'server_default': sqlalchemy.func.now() 45 | }, 46 | nullable=False, 47 | ) -------------------------------------------------------------------------------- /reflex_gpt/navigation/__init__.py: -------------------------------------------------------------------------------- 1 | from . import routes, state 2 | 3 | __all__ = [ 4 | 'routes' 5 | 'state', 6 | ] -------------------------------------------------------------------------------- /reflex_gpt/navigation/routes.py: -------------------------------------------------------------------------------- 1 | HOME_ROUTE="/" 2 | ABOUT_US_ROUTE="/about" 3 | CHAT_ROUTE='/chat' -------------------------------------------------------------------------------- /reflex_gpt/navigation/state.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from . import routes 4 | 5 | 6 | class NavState(rx.State): 7 | def to_home(self): 8 | """ 9 | on_click event 10 | """ 11 | return rx.redirect(routes.HOME_ROUTE) 12 | 13 | def to_about_us(self): 14 | """ 15 | on_click event 16 | """ 17 | return rx.redirect(routes.ABOUT_US_ROUTE) 18 | 19 | def to_chat(self): 20 | return rx.redirect(routes.CHAT_ROUTE) -------------------------------------------------------------------------------- /reflex_gpt/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .about import about_us_page 2 | from .home import home_page 3 | 4 | 5 | __all__ = [ 6 | 'about_us_page', 7 | 'home_page' 8 | ] -------------------------------------------------------------------------------- /reflex_gpt/pages/about.py: -------------------------------------------------------------------------------- 1 | """Welcome to Reflex! This file outlines the steps to create a basic app.""" 2 | 3 | import reflex as rx 4 | 5 | from reflex_gpt import ui 6 | 7 | def about_us_page() -> rx.Component: 8 | # About us Page 9 | return ui.base_layout( 10 | rx.vstack( 11 | rx.heading("Welcome to Reflex About!", size="9"), 12 | spacing="5", 13 | justify="center", 14 | min_height="85vh", 15 | ), 16 | rx.logo(), 17 | ) -------------------------------------------------------------------------------- /reflex_gpt/pages/home.py: -------------------------------------------------------------------------------- 1 | """Welcome to Reflex! This file outlines the steps to create a basic app.""" 2 | 3 | import reflex as rx 4 | 5 | from rxconfig import config 6 | 7 | from reflex_gpt import ui 8 | 9 | def home_page() -> rx.Component: 10 | # Welcome Page (Index) 11 | return ui.base_layout( 12 | rx.vstack( 13 | rx.heading("Welcome to Reflex GPT!", size="9"), 14 | rx.text( 15 | "Get started by editing something like ", 16 | rx.code(f"{config.app_name}/{config.app_name}.py"), 17 | size="5", 18 | ), 19 | rx.link( 20 | rx.button("Check out our docs!"), 21 | href="https://reflex.dev/docs/getting-started/introduction/", 22 | is_external=True, 23 | ), 24 | spacing="5", 25 | justify="center", 26 | min_height="85vh", 27 | ), 28 | rx.logo(), 29 | ) 30 | -------------------------------------------------------------------------------- /reflex_gpt/reflex_gpt.py: -------------------------------------------------------------------------------- 1 | """Welcome to Reflex! This file outlines the steps to create a basic app.""" 2 | 3 | import reflex as rx 4 | 5 | from . import chat, pages, navigation 6 | 7 | app = rx.App() 8 | app.add_page(pages.home_page, route=navigation.routes.HOME_ROUTE) 9 | app.add_page(pages.about_us_page, route=navigation.routes.ABOUT_US_ROUTE) 10 | app.add_page( 11 | chat.chat_page, 12 | route=f"{navigation.routes.CHAT_ROUTE}/[session_id]", 13 | on_load=chat.state.ChatState.on_detail_load 14 | ) 15 | 16 | app.add_page( 17 | chat.chat_page, 18 | route=navigation.routes.CHAT_ROUTE, 19 | on_load=chat.state.ChatState.on_load 20 | ) 21 | 22 | -------------------------------------------------------------------------------- /reflex_gpt/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import base_layout 2 | 3 | __all__ = [ 4 | 'base_layout' 5 | ] -------------------------------------------------------------------------------- /reflex_gpt/ui/base.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from .navbar import base_navbar 4 | from .footer import base_footer 5 | 6 | def base_layout(*args, **kwargs) -> rx.Component: 7 | return rx.container( 8 | base_navbar(), 9 | rx.fragment( 10 | *args, 11 | **kwargs, 12 | ), 13 | base_footer(), 14 | id='my-base-container' 15 | ) -------------------------------------------------------------------------------- /reflex_gpt/ui/footer.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | def footer_item(text: str, href: str) -> rx.Component: 4 | return rx.link(rx.text(text, size="3"), href=href, ) 5 | 6 | 7 | def footer_items_1() -> rx.Component: 8 | return rx.flex( 9 | rx.heading( 10 | "PRODUCTS", size="4", weight="bold", as_="h3" 11 | ), 12 | footer_item("Web Design", "/#"), 13 | footer_item("Web Development", "/#"), 14 | footer_item("E-commerce", "/#"), 15 | footer_item("Content Management", "/#"), 16 | footer_item("Mobile Apps", "/#"), 17 | spacing="4", 18 | text_align=["center", "center", "start"], 19 | flex_direction="column", 20 | ) 21 | 22 | 23 | def footer_items_2() -> rx.Component: 24 | return rx.flex( 25 | rx.heading( 26 | "RESOURCES", size="4", weight="bold", as_="h3" 27 | ), 28 | footer_item("Blog", "/#"), 29 | footer_item("Case Studies", "/#"), 30 | footer_item("Whitepapers", "/#"), 31 | footer_item("Webinars", "/#"), 32 | footer_item("E-books", "/#"), 33 | spacing="4", 34 | text_align=["center", "center", "start"], 35 | flex_direction="column", 36 | ) 37 | 38 | 39 | def social_link(icon: str, href: str) -> rx.Component: 40 | return rx.link(rx.icon(icon), href=href) 41 | 42 | 43 | def socials() -> rx.Component: 44 | return rx.flex( 45 | social_link("instagram", "/#"), 46 | social_link("twitter", "/#"), 47 | social_link("facebook", "/#"), 48 | social_link("linkedin", "/#"), 49 | spacing="3", 50 | justify="end", 51 | width="100%", 52 | ) 53 | 54 | 55 | def base_footer() -> rx.Component: 56 | return rx.el.footer( 57 | rx.vstack( 58 | rx.flex( 59 | rx.vstack( 60 | rx.hstack( 61 | rx.image( 62 | src="/logo.jpg", 63 | width="2.25em", 64 | height="auto", 65 | border_radius="25%", 66 | ), 67 | rx.heading( 68 | "Reflex", 69 | size="7", 70 | weight="bold", 71 | ), 72 | align_items="center", 73 | ), 74 | rx.text( 75 | "© 2024 Reflex, Inc", 76 | size="3", 77 | white_space="nowrap", 78 | weight="medium", 79 | ), 80 | spacing="4", 81 | align_items=[ 82 | "center", 83 | "center", 84 | "start", 85 | ], 86 | ), 87 | footer_items_1(), 88 | footer_items_2(), 89 | justify="between", 90 | spacing="6", 91 | flex_direction=["column", "column", "row"], 92 | width="100%", 93 | ), 94 | rx.divider(), 95 | rx.hstack( 96 | rx.hstack( 97 | footer_item("Privacy Policy", "/#"), 98 | footer_item("Terms of Service", "/#"), 99 | spacing="4", 100 | align="center", 101 | width="100%", 102 | ), 103 | socials(), 104 | justify="between", 105 | width="100%", 106 | ), 107 | spacing="5", 108 | width="100%", 109 | ), 110 | width="100%", 111 | ) -------------------------------------------------------------------------------- /reflex_gpt/ui/navbar.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | 3 | from reflex_gpt import navigation 4 | 5 | def navbar_link(text: str, url: str) -> rx.Component: 6 | return rx.link( 7 | rx.text(text, size="4", weight="medium"), href=url 8 | ) 9 | 10 | 11 | def base_navbar() -> rx.Component: 12 | return rx.box( 13 | rx.desktop_only( 14 | rx.hstack( 15 | rx.hstack( 16 | rx.image( 17 | src="/logo.jpg", 18 | width="2.25em", 19 | height="auto", 20 | border_radius="25%", 21 | ), 22 | rx.heading( 23 | "Reflex GPT", size="7", weight="bold" 24 | ), 25 | align_items="center", 26 | ), 27 | rx.hstack( 28 | navbar_link("Home", navigation.routes.HOME_ROUTE), 29 | navbar_link("About", navigation.routes.ABOUT_US_ROUTE), 30 | navbar_link("Chat", navigation.routes.CHAT_ROUTE), 31 | justify="end", 32 | spacing="5", 33 | ), 34 | justify="between", 35 | align_items="center", 36 | ), 37 | ), 38 | rx.mobile_and_tablet( 39 | rx.hstack( 40 | rx.hstack( 41 | rx.image( 42 | src="/logo.jpg", 43 | width="2em", 44 | height="auto", 45 | border_radius="25%", 46 | ), 47 | rx.heading( 48 | "Reflex GPT", size="6", weight="bold" 49 | ), 50 | align_items="center", 51 | ), 52 | rx.menu.root( 53 | rx.menu.trigger( 54 | rx.icon("menu", size=30) 55 | ), 56 | rx.menu.content( 57 | rx.menu.item("Home", on_click=navigation.state.NavState.to_home), 58 | rx.menu.item("About", on_click=navigation.state.NavState.to_about_us), 59 | rx.menu.item("Chat", on_click=navigation.state.NavState.to_chat), 60 | ), 61 | justify="end", 62 | ), 63 | justify="between", 64 | align_items="center", 65 | ), 66 | ), 67 | bg=rx.color("accent", 3), 68 | padding="1em", 69 | # position="fixed", 70 | # top="0px", 71 | # z_index="5", 72 | width="100%", 73 | ) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | reflex==0.5.9 2 | python-decouple 3 | openai 4 | psycopg2-binary -------------------------------------------------------------------------------- /rxconfig.py: -------------------------------------------------------------------------------- 1 | import reflex as rx 2 | from decouple import config 3 | 4 | 5 | DATABASE_URL = config("DATABASE_URL") 6 | 7 | config = rx.Config( 8 | app_name="reflex_gpt", 9 | db_url=DATABASE_URL 10 | ) --------------------------------------------------------------------------------