├── .gitignore ├── README.md ├── backend ├── .env ├── .gitignore ├── chat │ ├── __init__.py │ ├── admin.py │ ├── agents │ │ ├── __init__.py │ │ ├── agent_factory.py │ │ └── callbacks.py │ ├── api_urls.py │ ├── apps.py │ ├── consumers.py │ ├── messages │ │ ├── __init__.py │ │ └── chat_message_repository.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_chatmessage_chat_id.py │ │ ├── 0003_chat_remove_chatmessage_chat_id_and_more.py │ │ ├── 0004_agent.py │ │ ├── 0005_alter_agent_token.py │ │ └── __init__.py │ ├── models.py │ ├── routing.py │ ├── serializers.py │ ├── tests.py │ ├── views.py │ └── websocket_urls.py ├── dump.rdb ├── manage.py ├── project │ ├── __init__.py │ ├── asgi.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── requirements.txt └── frontend ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── components │ └── chat │ │ ├── ChatBox.tsx │ │ ├── ChatInput.tsx │ │ ├── ChatMessage.tsx │ │ ├── LoadingIndicator.tsx │ │ ├── SettingsModal.tsx │ │ ├── Sidebar.tsx │ │ ├── TypingIndicator.tsx │ │ └── debug │ │ ├── ChatMenu.tsx │ │ └── DebugDrawer.tsx ├── css │ └── chat │ │ └── TypingIndicator.css ├── data │ └── Message.ts ├── index.css ├── index.tsx ├── logo.svg ├── react-app-env.d.ts ├── reportWebVitals.ts ├── setupTests.ts ├── types │ └── chat.ts └── utils │ └── DateFormatter.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | myenv/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | .DS_Store 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | .idea/ 164 | .idea 165 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 166 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 167 | 168 | # User-specific stuff 169 | .idea/**/workspace.xml 170 | .idea/**/tasks.xml 171 | .idea/**/usage.statistics.xml 172 | .idea/**/dictionaries 173 | .idea/**/shelf 174 | 175 | # AWS User-specific 176 | .idea/**/aws.xml 177 | 178 | # Generated files 179 | .idea/**/contentModel.xml 180 | 181 | # Sensitive or high-churn files 182 | .idea/**/dataSources/ 183 | .idea/**/dataSources.ids 184 | .idea/**/dataSources.local.xml 185 | .idea/**/sqlDataSources.xml 186 | .idea/**/dynamic.xml 187 | .idea/**/uiDesigner.xml 188 | .idea/**/dbnavigator.xml 189 | 190 | # Gradle 191 | .idea/**/gradle.xml 192 | .idea/**/libraries 193 | 194 | # Gradle and Maven with auto-import 195 | # When using Gradle or Maven with auto-import, you should exclude module files, 196 | # since they will be recreated, and may cause churn. Uncomment if using 197 | # auto-import. 198 | # .idea/artifacts 199 | # .idea/compiler.xml 200 | # .idea/jarRepositories.xml 201 | # .idea/modules.xml 202 | # .idea/*.iml 203 | # .idea/modules 204 | # *.iml 205 | # *.ipr 206 | 207 | # CMake 208 | cmake-build-*/ 209 | 210 | # Mongo Explorer plugin 211 | .idea/**/mongoSettings.xml 212 | 213 | # File-based project format 214 | *.iws 215 | 216 | # IntelliJ 217 | out/ 218 | 219 | # mpeltonen/sbt-idea plugin 220 | .idea_modules/ 221 | 222 | # JIRA plugin 223 | atlassian-ide-plugin.xml 224 | 225 | # Cursive Clojure plugin 226 | .idea/replstate.xml 227 | 228 | # SonarLint plugin 229 | .idea/sonarlint/ 230 | 231 | # Crashlytics plugin (for Android Studio and IntelliJ) 232 | com_crashlytics_export_strings.xml 233 | crashlytics.properties 234 | crashlytics-build.properties 235 | fabric.properties 236 | 237 | # Editor-based Rest Client 238 | .idea/httpRequests 239 | 240 | # Android studio 3.1+ serialized cache file 241 | .idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LLM-Powered Chat Application 2 | This repository contains all of the starter code needed to run an LLM-powered chat app on your local machine: 3 | 1. Django backend 4 | 2. React TypeScript frontend 5 | 3. LangChain Agents and LLMs 6 | 7 | ## Getting Started 🚀 8 | To run the chat app, you need to: 9 | 10 | 1. Clone this GitHub repo 11 | 2. Run the backend server 12 | 3. Run the frontend app 13 | 14 | ### 1. Clone this GitHub repo 📁 15 | To clone this GitHub repo, open up your Terminal (MacOS) or Bash terminal (Windows) and navigate to wherever you want to save this repo on your local machine. Then, run: 16 | 17 | ``` 18 | git clone https://github.com/virattt/chat_app.git 19 | ``` 20 | 21 | Make sure that you have git installed ([instructions](https://github.com/git-guides/install-git)). 22 | 23 | ### 2. Run the backend server 🖥️ 24 | Once you have this `chat_app` project cloned locally, navigate to the `backend` directory: 25 | 26 | ``` 27 | cd ~/path_to/chat_app/backend 28 | ``` 29 | 30 | Create and activate a virtual environment: 31 | 32 | ``` 33 | python3 -m venv myenv 34 | ``` 35 | 36 | For MacOS/Linux: 37 | ``` 38 | source myenv/bin/activate 39 | ``` 40 | 41 | For Windows: 42 | ``` 43 | myenv\Scripts\activate 44 | ``` 45 | 46 | Install the necessary libraries: 47 | ``` 48 | pip install -r requirements.txt 49 | ``` 50 | 51 | Make sure that you have Redis installed. You can find instructions [here](https://redis.io/docs/getting-started/installation/). 52 | Once installed, run redis: 53 | ``` 54 | redis-server 55 | ``` 56 | 57 | Run the backend server: 58 | ``` 59 | daphne project.asgi:application 60 | ``` 61 | 62 | If your backend server is running correctly, you should see something like this: 63 | ``` 64 | "WSCONNECTING /ws/chat/" - - 65 | "WSCONNECT /ws/chat/" - - 66 | ``` 67 | 68 | **Important**: In order to run the LLM, set your Open AI API key [here](https://github.com/virattt/chat_app/blob/main/backend/.env#L1). 69 | 70 | ### 3. Run the frontend app 💻 71 | In a new Terminal window (or tab), navigate to the `frontend` directory: 72 | ``` 73 | cd ~/path_to/chat_app/frontend 74 | ``` 75 | 76 | Make sure that you have Node and npm installed (MacOS [instructions](https://nodejs.org/en/download/package-manager#macos) and Windows [instructions](https://nodejs.org/en/download/package-manager#windows-1)) 77 | 78 | Install the necessary packages: 79 | ``` 80 | npm install 81 | ``` 82 | 83 | Run the frontend app: 84 | ``` 85 | npm start 86 | ``` 87 | 88 | If successful, your browser should open and navigate to http://localhost:3000/. The chat app should load automatically. 89 | 90 | ## The Chat App UX 🤖 91 | _As of May 17, 2023_ 92 | Screen Shot 2023-05-17 at 4 52 27 PM 93 | 94 | ## Troubleshooting ⚠️ 95 | If you encounter any issues, send me a message on [Twitter](https://twitter.com/virat)! 96 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY=YOUR_OPENAI_API_KEY 2 | SECRET_KEY=YOUR_DJANGO_SECRET_KEY # you can make this value up; primarily used for deploying 3 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | myenv/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | .DS_Store 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | .idea/ 164 | .idea 165 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 166 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 167 | 168 | # User-specific stuff 169 | .idea/**/workspace.xml 170 | .idea/**/tasks.xml 171 | .idea/**/usage.statistics.xml 172 | .idea/**/dictionaries 173 | .idea/**/shelf 174 | 175 | # AWS User-specific 176 | .idea/**/aws.xml 177 | 178 | # Generated files 179 | .idea/**/contentModel.xml 180 | 181 | # Sensitive or high-churn files 182 | .idea/**/dataSources/ 183 | .idea/**/dataSources.ids 184 | .idea/**/dataSources.local.xml 185 | .idea/**/sqlDataSources.xml 186 | .idea/**/dynamic.xml 187 | .idea/**/uiDesigner.xml 188 | .idea/**/dbnavigator.xml 189 | 190 | # Gradle 191 | .idea/**/gradle.xml 192 | .idea/**/libraries 193 | 194 | # Gradle and Maven with auto-import 195 | # When using Gradle or Maven with auto-import, you should exclude module files, 196 | # since they will be recreated, and may cause churn. Uncomment if using 197 | # auto-import. 198 | # .idea/artifacts 199 | # .idea/compiler.xml 200 | # .idea/jarRepositories.xml 201 | # .idea/modules.xml 202 | # .idea/*.iml 203 | # .idea/modules 204 | # *.iml 205 | # *.ipr 206 | 207 | # CMake 208 | cmake-build-*/ 209 | 210 | # Mongo Explorer plugin 211 | .idea/**/mongoSettings.xml 212 | 213 | # File-based project format 214 | *.iws 215 | 216 | # IntelliJ 217 | out/ 218 | 219 | # mpeltonen/sbt-idea plugin 220 | .idea_modules/ 221 | 222 | # JIRA plugin 223 | atlassian-ide-plugin.xml 224 | 225 | # Cursive Clojure plugin 226 | .idea/replstate.xml 227 | 228 | # SonarLint plugin 229 | .idea/sonarlint/ 230 | 231 | # Crashlytics plugin (for Android Studio and IntelliJ) 232 | com_crashlytics_export_strings.xml 233 | crashlytics.properties 234 | crashlytics-build.properties 235 | fabric.properties 236 | 237 | # Editor-based Rest Client 238 | .idea/httpRequests 239 | 240 | # Android studio 3.1+ serialized cache file 241 | .idea/caches/build_file_checksums.ser -------------------------------------------------------------------------------- /backend/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/__init__.py -------------------------------------------------------------------------------- /backend/chat/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /backend/chat/agents/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/agents/__init__.py -------------------------------------------------------------------------------- /backend/chat/agents/agent_factory.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from langchain.agents import initialize_agent, load_tools, AgentType, AgentExecutor 4 | from langchain.callbacks.base import BaseCallbackHandler 5 | from langchain.chat_models import ChatOpenAI 6 | from langchain.memory import ConversationBufferMemory 7 | 8 | from chat.messages.chat_message_repository import ChatMessageRepository 9 | from chat.models import MessageSender, ChatMessage 10 | from project import settings 11 | 12 | 13 | class AgentFactory: 14 | 15 | def __init__(self): 16 | self.chat_message_repository = ChatMessageRepository() 17 | 18 | async def create_agent( 19 | self, 20 | tool_names: List[str], 21 | chat_id: str = None, 22 | streaming=False, 23 | callback_handlers: List[BaseCallbackHandler] = None, 24 | ) -> AgentExecutor: 25 | # Instantiate the OpenAI LLM 26 | llm = ChatOpenAI( 27 | temperature=0, 28 | openai_api_key=settings.openai_api_key, 29 | streaming=streaming, 30 | callbacks=callback_handlers, 31 | ) 32 | 33 | # Load the Tools that the Agent will use 34 | tools = load_tools(tool_names, llm=llm) 35 | 36 | # Load the memory and populate it with any previous messages 37 | memory = await self._load_agent_memory(chat_id) 38 | 39 | # Initialize and return the agent 40 | return initialize_agent( 41 | tools=tools, 42 | llm=llm, 43 | agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, 44 | verbose=True, 45 | memory=memory 46 | ) 47 | 48 | async def _load_agent_memory( 49 | self, 50 | chat_id: str = None, 51 | ) -> ConversationBufferMemory: 52 | if not chat_id: 53 | return ConversationBufferMemory(memory_key="chat_history", return_messages=True) 54 | 55 | # Create the conversational memory for the agent 56 | memory = ConversationBufferMemory(memory_key="chat_history", return_messages=True) 57 | 58 | # Load the messages for the chat_id from the DB 59 | chat_messages: List[ChatMessage] = await self.chat_message_repository.get_chat_messages(chat_id) 60 | 61 | # Add the messages to the memory 62 | for message in chat_messages: 63 | if message.sender == MessageSender.USER.value: 64 | # Add user message to the memory 65 | memory.chat_memory.add_user_message(message.content) 66 | elif message.sender == MessageSender.AI.value: 67 | # Add AI message to the memory 68 | memory.chat_memory.add_ai_message(message.content) 69 | 70 | return memory 71 | -------------------------------------------------------------------------------- /backend/chat/agents/callbacks.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional, Any, Dict, List 3 | from uuid import UUID 4 | 5 | from channels.generic.websocket import AsyncWebsocketConsumer 6 | from langchain.callbacks.base import AsyncCallbackHandler 7 | from langchain.schema import LLMResult, BaseMessage 8 | 9 | 10 | class AsyncStreamingCallbackHandler(AsyncCallbackHandler): 11 | 12 | def __init__(self, consumer: AsyncWebsocketConsumer): 13 | self.consumer = consumer 14 | 15 | async def on_llm_new_token( 16 | self, 17 | token: str, 18 | *, 19 | run_id: UUID, 20 | parent_run_id: Optional[UUID] = None, 21 | **kwargs: Any, 22 | ) -> None: 23 | # Send the token to any consumers (e.g. frontend client) 24 | await self.consumer.send(text_data=json.dumps({'message': token, 'type': 'debug'})) 25 | 26 | async def on_llm_end( 27 | self, 28 | response: LLMResult, 29 | *, 30 | run_id: UUID, 31 | parent_run_id: Optional[UUID] = None, 32 | **kwargs: Any, 33 | ) -> None: 34 | # When the LLM ends, add a new line so that debug messages are spaced with new lines. 35 | await self.consumer.send(text_data=json.dumps({'message': '\n\n', 'type': 'debug'})) 36 | 37 | async def on_chat_model_start( 38 | self, serialized: Dict[str, Any], 39 | messages: List[List[BaseMessage]], 40 | *, 41 | run_id: UUID, 42 | parent_run_id: Optional[UUID] = None, 43 | **kwargs: Any 44 | ) -> Any: 45 | # Do nothing 46 | pass 47 | -------------------------------------------------------------------------------- /backend/chat/api_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import include, path 2 | from rest_framework.routers import DefaultRouter 3 | from .views import ChatViewSet, AgentViewSet 4 | 5 | router = DefaultRouter() 6 | router.register(r'chats', ChatViewSet) 7 | router.register(r'agents', AgentViewSet) 8 | 9 | urlpatterns = [ 10 | path('', include(router.urls)), 11 | ] -------------------------------------------------------------------------------- /backend/chat/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ChatConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'chat' 7 | -------------------------------------------------------------------------------- /backend/chat/consumers.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import django 5 | 6 | from chat.agents.agent_factory import AgentFactory 7 | from chat.agents.callbacks import AsyncStreamingCallbackHandler 8 | from chat.messages.chat_message_repository import ChatMessageRepository 9 | 10 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 11 | django.setup() 12 | 13 | from channels.generic.websocket import AsyncWebsocketConsumer 14 | from langchain.agents import AgentExecutor 15 | 16 | from chat.models import MessageSender 17 | 18 | 19 | class ChatConsumer(AsyncWebsocketConsumer): 20 | # The LLM agent for this chat application 21 | agent: AgentExecutor 22 | 23 | def __init__(self, *args, **kwargs): 24 | super().__init__(*args, **kwargs) 25 | self.agent_factory = AgentFactory() 26 | self.chat_message_repository = ChatMessageRepository() 27 | 28 | async def connect(self): 29 | # Get the chat_id from the client 30 | chat_id = self.scope['url_route']['kwargs'].get('chat_id') 31 | 32 | # Create the agent when the websocket connection with the client is established 33 | self.agent = await self.agent_factory.create_agent( 34 | tool_names=["llm-math"], 35 | chat_id=chat_id, 36 | streaming=True, 37 | callback_handlers=[AsyncStreamingCallbackHandler(self)], 38 | ) 39 | 40 | await self.accept() 41 | 42 | async def disconnect(self, close_code): 43 | pass 44 | 45 | async def receive(self, text_data): 46 | text_data_json = json.loads(text_data) 47 | message = text_data_json['message'] 48 | chat_id = text_data_json['chat_id'] 49 | 50 | # Forward the message to LangChain 51 | response = await self.message_agent(message, chat_id) 52 | 53 | # Send the response from the OpenAI Chat API to the frontend client 54 | await self.send(text_data=json.dumps({'message': response, 'type': 'answer'})) 55 | 56 | async def message_agent(self, message: str, chat_id: str): 57 | # Save the user message to the database 58 | await self.chat_message_repository.save_message(message=message, sender=MessageSender.USER.value, chat_id=chat_id) 59 | 60 | # Call the agent 61 | response = await self.agent.arun(message) 62 | 63 | # Save the AI message to the database 64 | await self.chat_message_repository.save_message(message=response, sender=MessageSender.AI.value, chat_id=chat_id) 65 | 66 | return response 67 | 68 | def my_callback(self, message): 69 | print("Callback received:", message) 70 | -------------------------------------------------------------------------------- /backend/chat/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/messages/__init__.py -------------------------------------------------------------------------------- /backend/chat/messages/chat_message_repository.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List 3 | 4 | import django 5 | from channels.db import database_sync_to_async 6 | 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 8 | django.setup() 9 | 10 | from chat.models import ChatMessage 11 | 12 | 13 | class ChatMessageRepository: 14 | 15 | @database_sync_to_async 16 | def get_chat_messages(self, chat_id: str, order_by='timestamp') -> List[ChatMessage]: 17 | # Retrieve the chat history for `chat_id` from the database 18 | return list(ChatMessage.objects.filter(chat_id=chat_id).order_by(order_by)) 19 | 20 | @database_sync_to_async 21 | def save_message(self, message: str, sender: str, chat_id: str): 22 | # Save the message to the database 23 | ChatMessage.objects.create(sender=sender, content=message, chat_id=chat_id) 24 | -------------------------------------------------------------------------------- /backend/chat/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-05-13 19:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='ChatMessage', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('content', models.TextField()), 19 | ('sender', models.CharField(choices=[('USER', 'USER'), ('SYSTEM', 'SYSTEM')], max_length=10)), 20 | ('timestamp', models.DateTimeField(auto_now_add=True)), 21 | ], 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/chat/migrations/0002_chatmessage_chat_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-05-15 13:45 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='chatmessage', 15 | name='chat_id', 16 | field=models.CharField(db_index=True, max_length=255), 17 | preserve_default=False, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /backend/chat/migrations/0003_chat_remove_chatmessage_chat_id_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-05-19 20:06 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('chat', '0002_chatmessage_chat_id'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Chat', 16 | fields=[ 17 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('name', models.CharField(max_length=255)), 19 | ('created_at', models.DateTimeField(auto_now_add=True)), 20 | ('updated_at', models.DateTimeField(auto_now=True)), 21 | ], 22 | ), 23 | migrations.RemoveField( 24 | model_name='chatmessage', 25 | name='chat_id', 26 | ), 27 | migrations.AlterField( 28 | model_name='chatmessage', 29 | name='sender', 30 | field=models.CharField(choices=[('USER', 'USER'), ('AI', 'AI')], max_length=10), 31 | ), 32 | migrations.AddField( 33 | model_name='chatmessage', 34 | name='chat', 35 | field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='chat.chat'), 36 | preserve_default=False, 37 | ), 38 | ] 39 | -------------------------------------------------------------------------------- /backend/chat/migrations/0004_agent.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-05-24 22:48 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0003_chat_remove_chatmessage_chat_id_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Agent', 15 | fields=[ 16 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 17 | ('name', models.CharField(max_length=255)), 18 | ('agent_type', models.CharField(max_length=255)), 19 | ('token', models.CharField(default='a_6e744ad2-4677-482f-8d52-02c34624a1d3', max_length=255, unique=True)), 20 | ('created_at', models.DateTimeField(auto_now_add=True)), 21 | ('updated_at', models.DateTimeField(auto_now=True)), 22 | ('is_active', models.BooleanField(default=True)), 23 | ], 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /backend/chat/migrations/0005_alter_agent_token.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2 on 2023-05-24 22:50 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('chat', '0004_agent'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='agent', 15 | name='token', 16 | field=models.CharField(db_index=True, default='a_d1e13ad0-7e9f-437f-9d66-5e18c45367d4', max_length=255, unique=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /backend/chat/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/chat/migrations/__init__.py -------------------------------------------------------------------------------- /backend/chat/models.py: -------------------------------------------------------------------------------- 1 | # models.py 2 | from enum import Enum 3 | import uuid 4 | 5 | from django.db import models 6 | 7 | 8 | class MessageSender(Enum): 9 | USER = 'USER' 10 | AI = 'AI' 11 | 12 | 13 | class Chat(models.Model): 14 | name = models.CharField(max_length=255) 15 | created_at = models.DateTimeField(auto_now_add=True) 16 | updated_at = models.DateTimeField(auto_now=True) 17 | 18 | 19 | class ChatMessage(models.Model): 20 | content = models.TextField() 21 | chat = models.ForeignKey(Chat, related_name="messages", on_delete=models.CASCADE) 22 | sender = models.CharField(max_length=10, choices=[(tag.value, tag.name) for tag in MessageSender]) 23 | timestamp = models.DateTimeField(auto_now_add=True) 24 | 25 | 26 | 27 | class Agent(models.Model): 28 | name = models.CharField(max_length=255) 29 | agent_type = models.CharField(max_length=255) 30 | token = models.CharField(max_length=255, default='a_' + str(uuid.uuid4()), unique=True, db_index=True) 31 | created_at = models.DateTimeField(auto_now_add=True) 32 | updated_at = models.DateTimeField(auto_now=True) 33 | is_active = models.BooleanField(default=True) 34 | 35 | def __str__(self): 36 | return self.name 37 | -------------------------------------------------------------------------------- /backend/chat/routing.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from . import consumers 3 | 4 | websocket_urlpatterns = [ 5 | path('ws/chat/', consumers.ChatConsumer.as_asgi()), 6 | ] 7 | -------------------------------------------------------------------------------- /backend/chat/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | 3 | from .models import Agent 4 | from .models import Chat, ChatMessage 5 | 6 | 7 | class ChatSerializer(serializers.ModelSerializer): 8 | class Meta: 9 | model = Chat 10 | fields = ['id', 'name', 'created_at', 'updated_at'] 11 | 12 | 13 | class ChatMessageSerializer(serializers.ModelSerializer): 14 | class Meta: 15 | model = ChatMessage 16 | fields = ['id', 'content', 'chat', 'sender', 'timestamp'] 17 | 18 | 19 | class AgentSerializer(serializers.ModelSerializer): 20 | class Meta: 21 | model = Agent 22 | fields = ['name', 'agent_type', 'token', 'created_at', 'updated_at', 'is_active'] 23 | -------------------------------------------------------------------------------- /backend/chat/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /backend/chat/views.py: -------------------------------------------------------------------------------- 1 | from django.http import JsonResponse 2 | from rest_framework import viewsets 3 | from rest_framework.decorators import action 4 | from rest_framework.response import Response 5 | 6 | from .models import Agent 7 | from .models import Chat, ChatMessage 8 | from .serializers import AgentSerializer 9 | from .serializers import ChatSerializer, ChatMessageSerializer 10 | 11 | 12 | class ChatViewSet(viewsets.ModelViewSet): 13 | queryset = Chat.objects.all() 14 | serializer_class = ChatSerializer 15 | 16 | def create(self, request, *args, **kwargs): 17 | serializer = self.get_serializer(data=request.data) 18 | serializer.is_valid(raise_exception=True) 19 | self.perform_create(serializer) 20 | return Response(serializer.data) 21 | 22 | def perform_destroy(self, instance): 23 | # Also delete related chat messages 24 | ChatMessage.objects.filter(chat_id=instance.id).delete() 25 | instance.delete() 26 | 27 | def list(self, request, *args, **kwargs): 28 | queryset = self.filter_queryset(self.get_queryset()) 29 | serializer = self.get_serializer(queryset, many=True) 30 | return JsonResponse({"chats": serializer.data}) 31 | 32 | @action(detail=True, methods=['get']) 33 | def messages(self, request, pk=None): 34 | chat = self.get_object() 35 | messages = ChatMessage.objects.filter(chat_id=chat.id).order_by('timestamp') 36 | serializer = ChatMessageSerializer(messages, many=True) 37 | return Response(serializer.data) 38 | 39 | 40 | class AgentViewSet(viewsets.ModelViewSet): 41 | queryset = Agent.objects.all() 42 | serializer_class = AgentSerializer 43 | lookup_field = 'token' 44 | 45 | def destroy(self, request, *args, **kwargs): 46 | agent = self.get_object() 47 | agent.is_active = False 48 | agent.save() 49 | return Response(status=204) 50 | 51 | def update(self, request, *args, **kwargs): 52 | agent = self.get_object() 53 | agent.agent_type = request.data.get('agent_type') 54 | agent.save() 55 | return Response(AgentSerializer(agent).data) 56 | -------------------------------------------------------------------------------- /backend/chat/websocket_urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | from django.urls import re_path 3 | from . import consumers 4 | 5 | websocket_urlpatterns = [ 6 | path('ws/chat/', consumers.ChatConsumer.as_asgi()), 7 | 8 | # Websocket URL for a given chat_id 9 | re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer.as_asgi()), 10 | ] 11 | -------------------------------------------------------------------------------- /backend/dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/dump.rdb -------------------------------------------------------------------------------- /backend/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /backend/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/backend/project/__init__.py -------------------------------------------------------------------------------- /backend/project/asgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.asgi import get_asgi_application 3 | from channels.routing import ProtocolTypeRouter, URLRouter 4 | from channels.auth import AuthMiddlewareStack 5 | import chat.websocket_urls 6 | 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 8 | 9 | application = ProtocolTypeRouter({ 10 | 'http': get_asgi_application(), 11 | 'websocket': AuthMiddlewareStack( 12 | URLRouter( 13 | chat.websocket_urls.websocket_urlpatterns 14 | ) 15 | ), 16 | }) 17 | -------------------------------------------------------------------------------- /backend/project/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for backend project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | import os 13 | from pathlib import Path 14 | 15 | from dotenv import load_dotenv 16 | 17 | # Load the environment variables from .env file 18 | load_dotenv() 19 | 20 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 21 | BASE_DIR = Path(__file__).resolve().parent.parent 22 | 23 | # Quick-start development settings - unsuitable for production 24 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 25 | 26 | # SECURITY WARNING: keep the secret key used in production secret! 27 | SECRET_KEY = os.environ.get('SECRET_KEY') 28 | 29 | # SECURITY WARNING: don't run with debug turned on in production! 30 | DEBUG = True 31 | 32 | ALLOWED_HOSTS = [] 33 | 34 | # Application definition 35 | 36 | INSTALLED_APPS = [ 37 | 'django.contrib.admin', 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 'channels', 44 | 'chat', 45 | 'corsheaders', 46 | ] 47 | 48 | # Django Channels 49 | CHANNEL_LAYERS = { 50 | 'default': { 51 | 'BACKEND': 'channels_redis.core.RedisChannelLayer', 52 | 'CONFIG': { 53 | 'hosts': [('127.0.0.1', 6379)], 54 | }, 55 | }, 56 | } 57 | 58 | # Use channels layer as default backend 59 | ASGI_APPLICATION = 'project.asgi.application' 60 | 61 | MIDDLEWARE = [ 62 | 'django.middleware.security.SecurityMiddleware', 63 | 'django.contrib.sessions.middleware.SessionMiddleware', 64 | 'django.middleware.common.CommonMiddleware', 65 | 'django.middleware.csrf.CsrfViewMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | 'django.contrib.messages.middleware.MessageMiddleware', 68 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 69 | 'corsheaders.middleware.CorsMiddleware', 70 | ] 71 | 72 | CORS_ORIGIN_WHITELIST = [ 73 | "http://localhost:3000", 74 | ] 75 | 76 | ROOT_URLCONF = 'project.urls' 77 | 78 | TEMPLATES = [ 79 | { 80 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 81 | 'DIRS': [], 82 | 'APP_DIRS': True, 83 | 'OPTIONS': { 84 | 'context_processors': [ 85 | 'django.template.context_processors.debug', 86 | 'django.template.context_processors.request', 87 | 'django.contrib.auth.context_processors.auth', 88 | 'django.contrib.messages.context_processors.messages', 89 | ], 90 | }, 91 | }, 92 | ] 93 | 94 | WSGI_APPLICATION = 'project.wsgi.application' 95 | 96 | # Database 97 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 98 | 99 | DATABASES = { 100 | 'default': { 101 | 'ENGINE': 'django.db.backends.sqlite3', 102 | 'NAME': BASE_DIR / 'db.sqlite3', 103 | } 104 | } 105 | 106 | # Password validation 107 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 108 | 109 | AUTH_PASSWORD_VALIDATORS = [ 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 115 | }, 116 | { 117 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 118 | }, 119 | { 120 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 121 | }, 122 | ] 123 | 124 | # Internationalization 125 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 126 | 127 | LANGUAGE_CODE = 'en-us' 128 | 129 | TIME_ZONE = 'UTC' 130 | 131 | USE_I18N = True 132 | 133 | USE_TZ = True 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 137 | 138 | STATIC_URL = 'static/' 139 | 140 | # Default primary key field type 141 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 142 | 143 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 144 | 145 | # Access the OPENAI_API_KEY environment variable 146 | openai_api_key = os.environ.get('OPENAI_API_KEY') 147 | -------------------------------------------------------------------------------- /backend/project/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for backend project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/4.2/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import include, path 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path('api/', include('chat.api_urls')), 23 | ] 24 | -------------------------------------------------------------------------------- /backend/project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for backend project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.4 2 | aiosignal==1.3.1 3 | asgiref==3.6.0 4 | async-timeout==4.0.2 5 | attrs==23.1.0 6 | autobahn==23.1.2 7 | Automat==22.10.0 8 | beautifulsoup4==4.12.2 9 | certifi==2022.12.7 10 | cffi==1.15.1 11 | channels==4.0.0 12 | channels-redis==4.1.0 13 | charset-normalizer==3.1.0 14 | constantly==15.1.0 15 | cryptography==40.0.2 16 | daphne==4.0.0 17 | dataclasses-json==0.5.7 18 | Django==4.2 19 | django-cors-headers==3.14.0 20 | djangorestframework==3.14.0 21 | frozenlist==1.3.3 22 | greenlet==2.0.2 23 | hyperlink==21.0.0 24 | idna==3.4 25 | incremental==22.10.0 26 | langchain==0.0.176 27 | lxml==4.9.2 28 | marshmallow==3.19.0 29 | marshmallow-enum==1.5.1 30 | msgpack==1.0.5 31 | multidict==6.0.4 32 | mypy-extensions==1.0.0 33 | numexpr==2.8.4 34 | numpy==1.24.3 35 | openai==0.27.6 36 | openapi-schema-pydantic==1.2.4 37 | packaging==23.1 38 | pandas==2.0.1 39 | pdfkit==1.0.0 40 | pyasn1==0.5.0 41 | pyasn1-modules==0.3.0 42 | pycparser==2.21 43 | pydantic==1.10.7 44 | pyOpenSSL==23.1.1 45 | python-dateutil==2.8.2 46 | python-dotenv==1.0.0 47 | pytz==2023.3 48 | PyYAML==6.0 49 | redis==4.5.4 50 | requests==2.29.0 51 | service-identity==21.1.0 52 | six==1.16.0 53 | soupsieve==2.4.1 54 | SQLAlchemy==2.0.13 55 | sqlparse==0.4.4 56 | tenacity==8.2.2 57 | tqdm==4.65.0 58 | Twisted==22.10.0 59 | txaio==23.1.1 60 | typing-inspect==0.8.0 61 | typing_extensions==4.5.0 62 | tzdata==2023.3 63 | urllib3==1.26.15 64 | yarl==1.9.2 65 | zope.interface==6.0 66 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat_frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.0", 7 | "@emotion/styled": "^11.11.0", 8 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 9 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 10 | "@fortawesome/react-fontawesome": "^0.2.0", 11 | "@mui/material": "^5.12.3", 12 | "@testing-library/jest-dom": "^5.16.5", 13 | "@testing-library/react": "^13.4.0", 14 | "@testing-library/user-event": "^13.5.0", 15 | "@types/jest": "^27.5.2", 16 | "@types/node": "^16.18.25", 17 | "@types/react": "^18.2.6", 18 | "@types/react-dom": "^18.2.4", 19 | "@types/styled-components": "^5.1.26", 20 | "@types/ws": "^8.5.4", 21 | "axios": "^1.4.0", 22 | "react": "^18.2.0", 23 | "react-dom": "^18.2.0", 24 | "react-scripts": "5.0.1", 25 | "react-syntax-highlighter": "^15.5.0", 26 | "react-textarea-autosize": "^8.4.1", 27 | "react-use-websocket": "^4.3.1", 28 | "react-websocket": "^2.1.0", 29 | "reconnecting-websocket": "^4.4.0", 30 | "styled-components": "^6.0.0-rc.1", 31 | "typescript": "^4.9.5", 32 | "web-vitals": "^2.1.4", 33 | "ws": "^8.13.0" 34 | }, 35 | "scripts": { 36 | "start": "react-scripts start", 37 | "build": "react-scripts build", 38 | "test": "react-scripts test", 39 | "eject": "react-scripts eject" 40 | }, 41 | "proxy": "http://localhost:8000", 42 | "eslintConfig": { 43 | "extends": [ 44 | "react-app", 45 | "react-app/jest" 46 | ] 47 | }, 48 | "browserslist": { 49 | "production": [ 50 | ">0.2%", 51 | "not dead", 52 | "not op_mini all" 53 | ], 54 | "development": [ 55 | "last 1 chrome version", 56 | "last 1 firefox version", 57 | "last 1 safari version" 58 | ] 59 | }, 60 | "devDependencies": { 61 | "@types/react-syntax-highlighter": "^15.5.6" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 18 | 19 | 28 | React App 29 | 30 | 31 | 32 |
33 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virattt/chat_app/c1cb01a440ada16c6983c192545d32eacf6bccf5/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | // App.tsx 2 | 3 | import React, { useEffect, useRef, useState } from 'react'; 4 | import { Sidebar } from './components/chat/Sidebar'; 5 | import { ChatBox } from './components/chat/ChatBox'; 6 | import { ChatInput } from "./components/chat/ChatInput"; 7 | import styled from 'styled-components'; 8 | import ReconnectingWebSocket from "reconnecting-websocket"; 9 | import { Message } from "./data/Message"; 10 | import { ChatMenu } from "./components/chat/debug/ChatMenu"; 11 | import { DebugDrawer } from "./components/chat/debug/DebugDrawer"; 12 | 13 | export const App = () => { 14 | const [currentChatId, setCurrentChatId] = useState(null); 15 | const [messages, setMessages] = useState([]); 16 | const webSocket = useRef(null); 17 | const [loading, setLoading] = useState(false); 18 | const [debugMessage, setDebugMessage] = useState(""); 19 | const [debugMode, setDebugMode] = useState(false); 20 | 21 | // Set up websocket connection when currentChatId changes 22 | useEffect(() => { 23 | if (currentChatId) { 24 | webSocket.current = new ReconnectingWebSocket(`ws://localhost:8000/ws/chat/${currentChatId}/`); 25 | webSocket.current.onmessage = (event) => { 26 | const data = JSON.parse(event.data); 27 | if (data.type === "debug") { 28 | // Debug message received. Replace newline characters with
tags 29 | const formattedToken = data.message.replace(/\n/g, '
'); 30 | setDebugMessage(prevMessage => prevMessage + formattedToken); 31 | } else { 32 | // Entire message received 33 | setLoading(false) 34 | const newMessage = {sender: 'AI', content: data['message']}; 35 | setMessages(prevMessages => [...prevMessages, newMessage]); 36 | } 37 | }; 38 | 39 | webSocket.current.onclose = () => { 40 | console.error('Chat socket closed unexpectedly'); 41 | }; 42 | // Fetch chat messages for currentChatId 43 | fetchMessages(currentChatId) 44 | } 45 | return () => { 46 | webSocket.current?.close(); 47 | }; 48 | }, [currentChatId]); 49 | 50 | const onChatSelected = (chatId: string | null) => { 51 | if (currentChatId === chatId) return; // Prevent unnecessary re-renders. 52 | if (chatId == null) { 53 | // Clear messages if no chat is selected 54 | setMessages([]) 55 | } 56 | setCurrentChatId(chatId); 57 | }; 58 | 59 | const onNewUserMessage = (chatId: string, message: Message) => { 60 | webSocket.current?.send( 61 | JSON.stringify({ 62 | message: message.content, 63 | chat_id: chatId, 64 | }) 65 | ); 66 | setMessages(prevMessages => [...prevMessages, message]); 67 | setLoading(true); // Set loading to true when sending a message 68 | }; 69 | 70 | const onNewChatCreated = (chatId: string) => { 71 | onChatSelected(chatId) 72 | }; 73 | 74 | const fetchMessages = (currentChatId: string | null) => { 75 | fetch(`http://localhost:8000/api/chats/${currentChatId}/messages/`) 76 | .then(response => response.json()) 77 | .then(data => { 78 | setMessages(data) 79 | }); 80 | } 81 | 82 | return ( 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | {debugMode && } 91 | 92 | ); 93 | }; 94 | 95 | const AppContainer = styled.div` 96 | display: flex; 97 | height: 100vh; 98 | `; 99 | 100 | const ChatContainer = styled.div<{ debugMode: boolean }>` 101 | display: flex; 102 | flex-direction: column; 103 | width: ${({debugMode}) => debugMode ? '70%' : '100%'}; 104 | transition: all 0.2s; // Smooth transition 105 | `; 106 | -------------------------------------------------------------------------------- /frontend/src/components/chat/ChatBox.tsx: -------------------------------------------------------------------------------- 1 | // ChatBox.tsx 2 | 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | import TypingIndicator from "./TypingIndicator"; 6 | import { ChatMessage } from "./ChatMessage"; 7 | 8 | type Message = { 9 | sender: string; 10 | content: string; 11 | }; 12 | 13 | type ChatBoxProps = { 14 | messages: Message[]; 15 | isLoading: boolean; 16 | }; 17 | 18 | export const ChatBox: React.FC = ({messages, isLoading}) => { 19 | return ( 20 | 21 | {messages.map((message, index) => ( 22 | 23 | ))} 24 | 25 | 26 | ); 27 | }; 28 | 29 | const MessageList = styled.div` 30 | flex-grow: 1; 31 | overflow-y: auto; 32 | `; 33 | -------------------------------------------------------------------------------- /frontend/src/components/chat/ChatInput.tsx: -------------------------------------------------------------------------------- 1 | // ChatInput.tsx 2 | 3 | import React, { useState } from 'react'; 4 | import TextareaAutosize from 'react-textarea-autosize'; 5 | import styled from 'styled-components'; 6 | import { Message } from "../../data/Message"; 7 | 8 | type ChatInputProps = { 9 | onNewUserMessage: (chatId: string, message: Message) => void; 10 | onNewChatCreated: (chatId: string) => void; 11 | chatId: string | null; 12 | }; 13 | 14 | export const ChatInput: React.FC = ({onNewUserMessage, onNewChatCreated, chatId}) => { 15 | const [message, setMessage] = useState(''); 16 | 17 | const handleSubmit = (event: React.FormEvent) => { 18 | event.preventDefault(); 19 | 20 | if (message.trim() === '') return; 21 | 22 | if (chatId) { 23 | // If there is a chatId, just send the message. 24 | const newMessage = {sender: 'USER', content: message}; 25 | onNewUserMessage(chatId, newMessage) 26 | } else { 27 | // If there is no chatId, create a new chat. 28 | createChat() 29 | } 30 | setMessage(''); // Clear the input message 31 | } 32 | 33 | const createChat = () => { 34 | fetch('http://localhost:8000/api/chats/', { 35 | method: 'POST', 36 | headers: {'Content-Type': 'application/json'}, 37 | body: JSON.stringify({name: 'New Chat'}) // Adjust this as necessary. 38 | }) 39 | .then((response) => response.json()) 40 | .then((newChat) => { 41 | // Update listeners that a new chat was created. 42 | onNewChatCreated(newChat.id) 43 | 44 | // Send the message after a timeout to ensure that the Chat has been created 45 | setTimeout(function () { 46 | // This block of code will be executed after 0.5 seconds 47 | onNewUserMessage(newChat.id, {sender: 'USER', content: message}) 48 | }, 500); 49 | }); 50 | }; 51 | 52 | return ( 53 |
54 | setMessage(e.target.value)} 57 | placeholder="Type a message..." 58 | maxRows={10} 59 | onKeyDown={(e) => { 60 | if (e.key === 'Enter' && !e.shiftKey) { 61 | e.preventDefault(); 62 | handleSubmit(e); 63 | } 64 | }} 65 | /> 66 | 67 | 68 | ); 69 | } 70 | ; 71 | 72 | const Form = styled.form` 73 | display: flex; 74 | align-items: flex-start; 75 | padding: 10px; 76 | border-top: 1px solid #eee; 77 | `; 78 | 79 | const StyledTextareaAutosize = styled(TextareaAutosize)` 80 | flex-grow: 1; 81 | border: 1px solid #eee; 82 | border-radius: 3px; 83 | padding: 10px; 84 | margin-right: 10px; 85 | resize: none; 86 | overflow: auto; 87 | font-family: inherit; 88 | font-size: 16px; 89 | min-height: 14px; // Initial height 90 | max-height: 500px; // Max height 91 | &:focus, 92 | &:active { 93 | border-color: #1C1C1C; 94 | outline: none; 95 | } 96 | `; 97 | 98 | const Button = styled.button` 99 | height: 40px; 100 | padding: 10px 20px; 101 | border: none; 102 | background-color: #1C1C1C; 103 | color: white; 104 | cursor: pointer; 105 | border-radius: 3px; 106 | font-size: 1em; 107 | align-self: flex-end; 108 | &:hover { 109 | background-color: #333333; /* Change this to the desired lighter shade */ 110 | } 111 | `; 112 | -------------------------------------------------------------------------------- /frontend/src/components/chat/ChatMessage.tsx: -------------------------------------------------------------------------------- 1 | // Message.tsx 2 | 3 | import React from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | type MessageProps = { 7 | sender: string; 8 | content: string; 9 | isUser: boolean; 10 | }; 11 | 12 | const Container = styled.div<{ isUser: boolean }>` 13 | background-color: ${({isUser}) => (isUser ? 'white' : '#F5F5F5')}; 14 | padding-top: 10px; 15 | padding-bottom: 10px; 16 | display: flex; 17 | justify-content: center; 18 | width: 100%; // Make the container full width 19 | border-top: 0.5px solid #c4c4c4; // Add a thin gray line to the bottom 20 | `; 21 | 22 | const Bubble = styled.div<{ isUser: boolean }>` 23 | margin: 10px; 24 | padding: 10px; 25 | border-radius: 10px; 26 | width: 50%; // Set a fixed width for the bubble 27 | display: flex; // Make it a flex container 28 | align-items: baseline; // Align items to the text baseline 29 | font-family: 'Inter', sans-serif; // Set the font to Inter 30 | font-size: 16px; 31 | `; 32 | 33 | const Content = styled.div` 34 | margin-left: 14px; 35 | line-height: 1.5; 36 | font-size: 16px; 37 | `; 38 | 39 | const Sender = styled.div` 40 | font-weight: 700; // Make the sender name bold 41 | font-family: 'Inter', sans-serif; // Set the font to Inter 42 | font-size: 16px; 43 | min-width: 50px; 44 | `; 45 | 46 | export const ChatMessage: React.FC = ({sender, content, isUser}) => ( 47 | 48 | 49 | {sender} 50 | 51 | {content.toString().split('\n').map((line, index) => ( 52 | line === '' ?
:
{line}
53 | ))} 54 |
55 |
56 |
57 | ); 58 | -------------------------------------------------------------------------------- /frontend/src/components/chat/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled, { keyframes } from 'styled-components'; 3 | 4 | interface LoadingIndicatorProps { 5 | isLoading: boolean; 6 | } 7 | 8 | // Define the pulsating animation 9 | const pulse = keyframes` 10 | 0% { 11 | background-color: rgba(0, 0, 0, 1); 12 | } 13 | 50% { 14 | background-color: rgba(0, 0, 0, 0.5); 15 | } 16 | 100% { 17 | background-color: rgba(0, 0, 0, 1); 18 | } 19 | `; 20 | 21 | const Container = styled.div` 22 | display: flex; 23 | justify-content: center; 24 | width: 100%; 25 | `; 26 | 27 | const InnerContainer = styled.div` 28 | display: flex; 29 | justify-content: flex-start; 30 | width: 40%; 31 | `; 32 | 33 | const Rectangle = styled.div` 34 | width: 8px; 35 | height: 16px; 36 | background-color: black; 37 | animation: ${pulse} 1s infinite ease-in-out; 38 | `; 39 | 40 | const LoadingIndicator: React.FC = ({isLoading}) => { 41 | if (!isLoading) return
42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | export default LoadingIndicator; 53 | -------------------------------------------------------------------------------- /frontend/src/components/chat/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | // SettingsModal.tsx 2 | 3 | import React, { useRef, useState } from 'react'; 4 | import styled from 'styled-components'; 5 | 6 | type SettingsModalProps = { 7 | setShowSettingsModal: (showSettingsModal: boolean) => void; 8 | }; 9 | 10 | const SettingsModal: React.FC = ({setShowSettingsModal}) => { 11 | const [apiKey, setApiKey] = useState(''); 12 | const [isApiKeyValid, setIsApiKeyValid] = useState(false); 13 | const modalContentRef = useRef(null); 14 | 15 | const handleApiKeyChange = (e: React.ChangeEvent) => { 16 | const value = e.target.value; 17 | setApiKey(value); 18 | setIsApiKeyValid(value.trim() !== ''); 19 | }; 20 | 21 | const handleSaveApiKey = () => { 22 | localStorage.setItem('OPENAI_API_KEY', apiKey); 23 | setShowSettingsModal(false); 24 | }; 25 | 26 | const handleOutsideClick = (e: React.MouseEvent) => { 27 | if (!modalContentRef.current?.contains(e.target as Node)) { 28 | setShowSettingsModal(false); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 35 | Enter your OpenAI API Key 36 | 37 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | const ModalContainer = styled.div` 55 | position: fixed; 56 | top: 0; 57 | left: 0; 58 | right: 0; 59 | bottom: 0; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | background-color: rgba(0, 0, 0, 0.5); 64 | `; 65 | 66 | const ModalContent = styled.div` 67 | width: 400px; 68 | background-color: #fff; 69 | border-radius: 5px; 70 | padding: 20px; 71 | `; 72 | 73 | const ModalHeader = styled.h2` 74 | font-size: 1.2em; 75 | margin-bottom: 10px; 76 | `; 77 | 78 | const ModalBody = styled.div` 79 | margin-bottom: 20px; 80 | `; 81 | 82 | const ApiKeyInput = styled.input` 83 | width: 100%; 84 | padding: 10px; 85 | font-size: 1em; 86 | border: 1px solid #ccc; 87 | border-radius: 3px; 88 | box-sizing: border-box; /* Add this line */ 89 | `; 90 | const ModalFooter = styled.div` 91 | display: flex; 92 | justify-content: flex-end; 93 | `; 94 | 95 | const Button = styled.button` 96 | padding: 10px 20px; 97 | font-size: 1em; 98 | border: none; 99 | border-radius: 3px; 100 | background-color: #1c1c1c; 101 | color: white; 102 | cursor: pointer; 103 | &:hover { 104 | background-color: #3f3f3f; 105 | } 106 | `; 107 | 108 | export default SettingsModal; 109 | -------------------------------------------------------------------------------- /frontend/src/components/chat/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import styled from 'styled-components'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import { faTrash } from '@fortawesome/free-solid-svg-icons'; 5 | import SettingsModal from "./SettingsModal"; 6 | import { formatDate } from "../../utils/DateFormatter"; 7 | import { Chat } from "../../types/chat"; 8 | 9 | type SidebarProps = { 10 | onChatSelected: (chatId: string | null) => void; 11 | selectedChatId: string | null; 12 | }; 13 | 14 | export const Sidebar: React.FC = ({onChatSelected, selectedChatId}) => { 15 | const [chats, setChats] = useState([]); 16 | const [showSettingsModal, setShowSettingsModal] = useState(false); 17 | 18 | // Fetch chats when the selectedChatId changes 19 | useEffect(() => { 20 | fetchChats(); 21 | }, [selectedChatId]); 22 | 23 | const fetchChats = () => { 24 | fetch('http://localhost:8000/api/chats/') 25 | .then((response) => response.json()) 26 | .then((data) => { 27 | const sortedChats = sortChats(data.chats) 28 | setChats(sortedChats); 29 | }); 30 | }; 31 | 32 | const sortChats = (chats: Chat[]) => { 33 | return chats.sort((a, b) => { 34 | const dateA = new Date(a.created_at); 35 | const dateB = new Date(b.created_at); 36 | 37 | // sort in descending order 38 | return dateB.getTime() - dateA.getTime(); 39 | }) 40 | } 41 | 42 | const createChat = () => { 43 | fetch('http://localhost:8000/api/chats/', { 44 | method: 'POST', 45 | headers: {'Content-Type': 'application/json'}, 46 | body: JSON.stringify({name: 'New Chat'}) // Adjust this as necessary. 47 | }) 48 | .then((response) => response.json()) 49 | .then((newChat) => { 50 | setChats((prevChats) => [...prevChats, newChat]); 51 | onChatSelected(newChat.id); // Select the new chat automatically 52 | }); 53 | }; 54 | 55 | const onDeleteChat = (chatId: string) => { 56 | fetch(`http://localhost:8000/api/chats/${chatId}/`, { 57 | method: 'DELETE' 58 | }) 59 | .then(() => { 60 | // Update the state to remove the deleted chat 61 | setChats((prevChats) => prevChats.filter((chat) => chat.id !== chatId)); 62 | // If the deleted chat was the currently selected one, nullify the selection 63 | if (chatId === selectedChatId) { 64 | onChatSelected(null); 65 | } 66 | }) 67 | .catch((error) => { 68 | console.error('Error:', error); 69 | }); 70 | }; 71 | 72 | const handleSettingsClick = () => { 73 | setShowSettingsModal(true); 74 | }; 75 | 76 | return ( 77 | 78 | 79 | 80 | {chats.map((chat) => ( 81 | onChatSelected(chat.id)} 84 | isSelected={chat.id === selectedChatId} 85 | > 86 | {formatDate(chat.created_at)} 87 | { 90 | e.stopPropagation(); // Prevent the chat row's onClick event from firing. 91 | onDeleteChat(chat.id); 92 | }} 93 | /> 94 | 95 | ))} 96 | 97 | Settings 98 | {showSettingsModal && ( 99 | 100 | )} 101 | 102 | ); 103 | }; 104 | 105 | const SidebarContainer = styled.div` 106 | width: 250px; 107 | background-color: #1c1c1c; 108 | display: flex; 109 | flex-direction: column; 110 | justify-content: space-between; 111 | height: 100vh; // Adjust the height to fit your layout 112 | `; 113 | 114 | const ChatListContainer = styled.div` 115 | overflow-y: auto; 116 | `; 117 | 118 | const ChatRow = styled.div<{ isSelected?: boolean }>` 119 | padding: 10px; 120 | cursor: pointer; 121 | background-color: ${(props) => (props.isSelected ? '#4c4c4c' : 'transparent')}; 122 | &:hover { 123 | background-color: #3f3f3f; 124 | } 125 | color: white; 126 | font-size: 14px; 127 | display: flex; 128 | justify-content: space-between; 129 | align-items: center; // ensure text and icon are aligned 130 | overflow: hidden; // add overflow handling 131 | 132 | & > span { // add a span tag around the text inside ChatRow 133 | white-space: nowrap; 134 | overflow: hidden; 135 | text-overflow: ellipsis; 136 | margin-right: 10px; 137 | } 138 | `; 139 | 140 | const Button = styled.button` 141 | padding: 20px; 142 | border: none; 143 | background-color: #1c1c1c; 144 | width: 100%; 145 | color: white; 146 | cursor: pointer; 147 | border-radius: 3px; 148 | border-color: #fff; 149 | font-size: 14px; 150 | &:hover { 151 | background-color: #3f3f3f; 152 | } 153 | `; 154 | 155 | const SettingsRow = styled.div` 156 | padding: 10px; 157 | margin: 10px; 158 | cursor: pointer; 159 | background-color: transparent; 160 | &:hover { 161 | background-color: #3f3f3f; 162 | } 163 | border-top: 1px solid #3f3f3f; 164 | color: white; 165 | font-size: 14px; 166 | display: flex; 167 | justify-content: center; 168 | align-items: center; 169 | `; 170 | -------------------------------------------------------------------------------- /frontend/src/components/chat/TypingIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../css/chat/TypingIndicator.css' 3 | import styled from "styled-components"; 4 | 5 | interface TypingIndicatorProps { 6 | isTyping: boolean; 7 | } 8 | 9 | const Container = styled.div` 10 | display: flex; 11 | justify-content: center; 12 | width: 100%; // Make the container full width 13 | `; 14 | 15 | const InnerContainer = styled.div` 16 | display: flex; 17 | justify-content: flex-start; 18 | width: 40%; // Set a fixed width for the inner container 19 | `; 20 | 21 | const TypingIndicator: React.FC = ({isTyping}) => { 22 | if (!isTyping) return
23 | 24 | return ( 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 | 33 | 34 | ); 35 | }; 36 | 37 | export default TypingIndicator; 38 | -------------------------------------------------------------------------------- /frontend/src/components/chat/debug/ChatMenu.tsx: -------------------------------------------------------------------------------- 1 | // ChatMenu.tsx 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | 5 | interface ChatMenuProps { 6 | debugMode: boolean; 7 | setDebugMode: (value: boolean) => void; 8 | } 9 | 10 | export const ChatMenu: React.FC = ({ debugMode, setDebugMode }) => { 11 | return ( 12 | 13 | 21 | 22 | ); 23 | }; 24 | 25 | const Menu = styled.div` 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | margin: 20px; 30 | `; 31 | -------------------------------------------------------------------------------- /frontend/src/components/chat/debug/DebugDrawer.tsx: -------------------------------------------------------------------------------- 1 | // DebugDrawer.tsx 2 | import React from 'react'; 3 | import styled from 'styled-components'; 4 | import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; 5 | import json from 'react-syntax-highlighter/dist/esm/languages/hljs/json'; 6 | import python from 'react-syntax-highlighter/dist/esm/languages/hljs/python'; 7 | 8 | SyntaxHighlighter.registerLanguage('json', json); 9 | SyntaxHighlighter.registerLanguage('python', python); 10 | 11 | interface DebugDrawerProps { 12 | message: string; 13 | debugMode: boolean; 14 | } 15 | 16 | export const DebugDrawer: React.FC = ({message, debugMode}) => { 17 | return ( 18 | 19 | Agent Thoughts 20 |

21 | 22 | ); 23 | }; 24 | 25 | const Drawer = styled.div<{ debugMode: boolean }>` 26 | width: ${({debugMode}) => debugMode ? '30%' : '0'}; 27 | height: 100vh; 28 | background: white; 29 | border-left: 1px solid gray; 30 | overflow: auto; 31 | padding: 20px; 32 | box-sizing: border-box; 33 | transition: all 0.2s; // Smooth transition 34 | `; 35 | 36 | const CenteredHeading = styled.h3` 37 | text-align: center; 38 | `; 39 | -------------------------------------------------------------------------------- /frontend/src/css/chat/TypingIndicator.css: -------------------------------------------------------------------------------- 1 | 2 | .typing-indicator-container { 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | height: 16px; 7 | margin-top: 8px; 8 | margin-bottom: 8px; 9 | padding: 0.3rem; 10 | border-radius: 1rem; 11 | background-color: #d1d1d1; 12 | opacity: 0; 13 | transition: opacity 0.3s; 14 | width: 40px; 15 | text-align: left; 16 | } 17 | 18 | .typing-indicator-container.visible { 19 | opacity: 1; 20 | } 21 | 22 | .typing-indicator-container.hidden { 23 | opacity: 0; 24 | } 25 | 26 | .typing-indicator-dot { 27 | width: 6px; 28 | height: 6px; 29 | border-radius: 50%; 30 | background-color: #ffffff; 31 | margin: 0 1px; 32 | animation: typing-indicator-bounce 1.2s infinite ease-in-out; 33 | } 34 | 35 | .typing-indicator-dot:nth-child(2) { 36 | animation-delay: 0.2s; 37 | } 38 | 39 | .typing-indicator-dot:nth-child(3) { 40 | animation-delay: 0.4s; 41 | } 42 | 43 | @keyframes typing-indicator-bounce { 44 | 0%, 80%, 100% { 45 | transform: translateY(0); 46 | } 47 | 48 | 40% { 49 | transform: translateY(-5px); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/data/Message.ts: -------------------------------------------------------------------------------- 1 | export type Message = { 2 | sender: string; 3 | content: string; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import reportWebVitals from './reportWebVitals'; 5 | import { App } from "./App"; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | 12 | 13 | 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'react-syntax-highlighter/dist/esm/languages/hljs/json'; 3 | declare module 'react-syntax-highlighter/dist/esm/languages/hljs/python'; 4 | declare module 'react-syntax-highlighter/dist/esm/styles/hljs/docco'; 5 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /frontend/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /frontend/src/types/chat.ts: -------------------------------------------------------------------------------- 1 | export type Chat = { 2 | id: string; 3 | name: string; 4 | created_at: string, 5 | }; -------------------------------------------------------------------------------- /frontend/src/utils/DateFormatter.ts: -------------------------------------------------------------------------------- 1 | export const formatDate = (datetime: string) => { 2 | const options: Intl.DateTimeFormatOptions = { 3 | hour: 'numeric', 4 | minute: 'numeric', 5 | second: 'numeric', 6 | hour12: true, 7 | }; 8 | 9 | const date = new Date(datetime); 10 | 11 | // Format the time using toLocaleTimeString 12 | const time = date.toLocaleTimeString('en-US', options); 13 | 14 | // Format the date manually 15 | const year = date.getFullYear(); 16 | const month = date.toLocaleString('en-US', { month: 'long' }); 17 | const day = date.getDate(); 18 | 19 | return `${time} on ${month} ${day}, ${year}`; 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | --------------------------------------------------------------------------------