diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..419f53fb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +.DS_Store +node_modules +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +**/*.db +_test \ No newline at end of file diff --git a/README.md b/README.md index b3e407f0..c794c6b1 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,9 @@ Also check our sibling project, [OllamaHub](https://ollamahub.com/), where you c - โš™๏ธ **Fine-Tuned Control with Advanced Parameters**: Gain a deeper level of control by adjusting parameters such as temperature and defining your system prompts to tailor the conversation to your specific preferences and needs. -- ๐Ÿ” **Auth Header Support**: Effortlessly enhance security by adding Authorization headers to Ollama requests directly from the web UI settings, ensuring access to secured Ollama servers. +- ๐Ÿ”— **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable. -- ๐Ÿ”— **External Ollama Server Connection**: Seamlessly link to an external Ollama server hosted on a different address by configuring the environment variable during the Docker build phase. Additionally, you can also set the external server connection URL from the web UI post-build. +- ๐Ÿ” **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators. - ๐Ÿ”’ **Backend Reverse Proxy Support**: Strengthen security by enabling direct communication between Ollama Web UI backend and Ollama, eliminating the need to expose Ollama over LAN. @@ -82,13 +82,17 @@ docker compose up -d --build This command will install both Ollama and Ollama Web UI on your system. #### Enable GPU + Use the additional Docker Compose file designed to enable GPU support by running the following command: + ```bash docker compose -f docker-compose.yml -f docker-compose.gpu.yml up -d --build ``` #### Expose Ollama API outside the container stack + Deploy the service with an additional Docker Compose file designed for API exposure: + ```bash docker compose -f docker-compose.yml -f docker-compose.api.yml up -d --build ``` @@ -105,17 +109,19 @@ After installing Ollama, verify that Ollama is running by accessing the followin #### Using Docker ๐Ÿณ +**Important:** When using Docker to install Ollama Web UI, make sure to include the `-v ollama-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. + If Ollama is hosted on your local machine and accessible at [http://127.0.0.1:11434/](http://127.0.0.1:11434/), run the following command: ```bash -docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main +docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main ``` Alternatively, if you prefer to build the container yourself, use the following command: ```bash docker build -t ollama-webui . -docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui +docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ollama-webui ``` Your Ollama Web UI should now be hosted at [http://localhost:3000](http://localhost:3000) and accessible over LAN (or Network). Enjoy! ๐Ÿ˜„ @@ -125,23 +131,25 @@ Your Ollama Web UI should now be hosted at [http://localhost:3000](http://localh Change `OLLAMA_API_BASE_URL` environment variable to match the external Ollama Server url: ```bash -docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main +docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v ollama-webui:/app/backend/data --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main ``` Alternatively, if you prefer to build the container yourself, use the following command: ```bash docker build -t ollama-webui . -docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api --name ollama-webui --restart always ollama-webui +docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v ollama-webui:/app/backend/data --name ollama-webui --restart always ollama-webui ``` ## How to Install Without Docker While we strongly recommend using our convenient Docker container installation for optimal support, we understand that some situations may require a non-Docker setup, especially for development purposes. Please note that non-Docker installations are not officially supported, and you might need to troubleshoot on your own. -**Warning: Backend Dependency for Proper Functionality** +### Project Components -In order to ensure the seamless operation of our application, it is crucial to run both the backend and frontend components simultaneously. Serving only the frontend in isolation is not supported and may lead to unpredictable outcomes, rendering the application inoperable. Attempting to raise an issue when solely serving the frontend will not be addressed, as it falls outside the intended usage. To achieve optimal results, please strictly adhere to the specified steps outlined in this documentation. Utilize the frontend solely for building static files, and subsequently run the complete application with the provided backend commands. Failure to follow these instructions may result in unsupported configurations, and we may not be able to provide assistance in such cases. Your cooperation in following the prescribed procedures is essential for a smooth user experience and effective issue resolution. +The Ollama Web UI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment. + +**Warning: Backend Dependency for Proper Functionality** ### TL;DR ๐Ÿš€ @@ -166,86 +174,6 @@ sh start.sh You should have the Ollama Web UI up and running at http://localhost:8080/. Enjoy! ๐Ÿ˜„ -### Project Components - -The Ollama Web UI consists of two primary components: the frontend and the backend (which serves as a reverse proxy, handling static frontend files, and additional features). Both need to be running concurrently for the development environment using `npm run dev`. Alternatively, you can set the `PUBLIC_API_BASE_URL` during the build process to have the frontend connect directly to your Ollama instance or build the frontend as static files and serve them with the backend. - -### Prerequisites - -1. **Clone and Enter the Project:** - - ```sh - git clone https://github.com/ollama-webui/ollama-webui.git - cd ollama-webui/ - ``` - -2. **Create and Edit `.env`:** - - ```sh - cp -RPp example.env .env - ``` - -### Building Ollama Web UI Frontend - -1. **Install Node Dependencies:** - - ```sh - npm install - ``` - -2. **Run in Dev Mode or Build for Deployment:** - - - Dev Mode (requires the backend to be running simultaneously): - - ```sh - npm run dev - ``` - - - Build for Deployment: - - ```sh - # `PUBLIC_API_BASE_URL` overwrites the value in `.env` - PUBLIC_API_BASE_URL='https://example.com/api' npm run build - ``` - -3. **Test the Build with `Caddy` (or your preferred server):** - - ```sh - curl https://webi.sh/caddy | sh - - PUBLIC_API_BASE_URL='https://localhost/api' npm run build - caddy run --envfile .env --config ./Caddyfile.localhost - ``` - -### Running Ollama Web UI Backend - -If you wish to run the backend for deployment, ensure that the frontend is built so that the backend can serve the frontend files along with the API route. - -#### Setup Instructions - -1. **Install Python Requirements:** - - ```sh - cd ./backend - pip install -r requirements.txt - ``` - -2. **Run Python Backend:** - - - Dev Mode with Hot Reloading: - - ```sh - sh dev.sh - ``` - - - Deployment: - - ```sh - sh start.sh - ``` - -Now, you should have the Ollama Web UI up and running at [http://localhost:8080/](http://localhost:8080/). Feel free to explore the features and functionalities of Ollama! If you encounter any issues, please refer to the instructions above or reach out to the community for assistance. - ## Troubleshooting See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubleshoot and/or join our [Ollama Web UI Discord community](https://discord.gg/5rJgQTnV4s). @@ -257,7 +185,10 @@ See [TROUBLESHOOTING.md](/TROUBLESHOOTING.md) for information on how to troubles Here are some exciting tasks on our roadmap: - ๐Ÿ“š **RAG Integration**: Experience first-class retrieval augmented generation support, enabling chat with your documents. -- ๐Ÿ” **Access Control**: Securely manage requests to Ollama by utilizing the backend as a reverse proxy gateway, ensuring only authenticated users can send specific requests. +- ๐ŸŒ **Web Browsing Capability**: Experience the convenience of seamlessly integrating web content directly into your chat. Easily browse and share information without leaving the conversation. +- ๐Ÿ”„ **Function Calling**: Empower your interactions by running code directly within the chat. Execute functions and commands effortlessly, enhancing the functionality of your conversations. +- โš™๏ธ **Custom Python Backend Actions**: Empower your Ollama Web UI by creating or downloading custom Python backend actions. Unleash the full potential of your web interface with tailored actions that suit your specific needs, enhancing functionality and versatility. +- ๐Ÿง  **Long-Term Memory**: Witness the power of persistent memory in our agents. Enjoy conversations that feel continuous as agents remember and reference past interactions, creating a more cohesive and personalized user experience. - ๐Ÿงช **Research-Centric Features**: Empower researchers in the fields of LLM and HCI with a comprehensive web UI for conducting user studies. Stay tuned for ongoing feature enhancements (e.g., surveys, analytics, and participant tracking) to facilitate their research. - ๐Ÿ“ˆ **User Study Tools**: Providing specialized tools, like heat maps and behavior tracking modules, to empower researchers in capturing and analyzing user behavior patterns with precision and accuracy. - ๐Ÿ“š **Enhanced Documentation**: Elevate your setup and customization experience with improved, comprehensive documentation. @@ -270,7 +201,11 @@ A big shoutout to our amazing supporters who's helping to make this project poss ### Platinum Sponsors ๐Ÿค -- [Prof. Lawrence Kim @ SFU](https://www.lhkim.com/) +- We're looking for Sponsors! + +### Acknowledgments + +Special thanks to [Prof. Lawrence Kim @ SFU](https://www.lhkim.com/) and [Prof. Nick Vincent @ SFU](https://www.nickmvincent.com/) for their invaluable support and guidance in shaping this project into a research endeavor. Grateful for your mentorship throughout the journey! ๐Ÿ™Œ ## License ๐Ÿ“œ diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 2fabe497..4399ffc5 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -1,22 +1,22 @@ # Ollama Web UI Troubleshooting Guide +## Ollama WebUI: Server Connection Error + +If you're running ollama-webui and have chosen to install webui and ollama separately, you might encounter connection issues. This is often due to the docker container being unable to reach the Ollama server at 127.0.0.1:11434(host.docker.internal:11434). To resolve this, you can use the `--network=host` flag in the docker command. When done so port would be changed from 3000 to 8080, and the link would be: http://localhost:8080. + +Here's an example of the command you should run: + +```bash +docker run -d --network=host -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main +``` + ## Connection Errors Make sure you have the **latest version of Ollama** installed before proceeding with the installation. You can find the latest version of Ollama at [https://ollama.ai/](https://ollama.ai/). If you encounter difficulties connecting to the Ollama server, please follow these steps to diagnose and resolve the issue: -**1. Verify Ollama Server Configuration** - -Ensure that the Ollama server is properly configured to accept incoming connections from all origins. To do this, make sure the server is launched with the `OLLAMA_ORIGINS=*` environment variable, as shown in the following command: - -```bash -OLLAMA_HOST=0.0.0.0 OLLAMA_ORIGINS=* ollama serve -``` - -This configuration allows Ollama to accept connections from any source. - -**2. Check Ollama URL Format** +**1. Check Ollama URL Format** Ensure that the Ollama URL is correctly formatted in the application settings. Follow these steps: @@ -28,33 +28,3 @@ Ensure that the Ollama URL is correctly formatted in the application settings. F It is crucial to include the `/api` at the end of the URL to ensure that the Ollama Web UI can communicate with the server. By following these troubleshooting steps, you should be able to identify and resolve connection issues with your Ollama server configuration. If you require further assistance or have additional questions, please don't hesitate to reach out or refer to our documentation for comprehensive guidance. - -## Running ollama-webui as a container on Apple Silicon Mac - -If you are running Docker on a M{1..3} based Mac and have taken the steps to run an x86 container, add "--platform linux/amd64" to the docker run command to prevent a warning. - -Example: - -```bash -docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=http://example.com:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main -``` - -Becomes - -``` -docker run --platform linux/amd64 -d -p 3000:8080 -e OLLAMA_API_BASE_URL=http://example.com:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main -``` - -## Running ollama-webui as a container on WSL Ubuntu -If you're running ollama-webui via docker on WSL Ubuntu and have chosen to install webui and ollama separately, you might encounter connection issues. This is often due to the docker container being unable to reach the Ollama server at 127.0.0.1:11434. To resolve this, you can use the `--network=host` flag in the docker command. When done so port would be changed from 3000 to 8080, and the link would be: http://localhost:8080. - -Here's an example of the command you should run: - -```bash -docker run -d --network=host -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name ollama-webui --restart always ghcr.io/ollama-webui/ollama-webui:main -``` - -## References - -[Change Docker Desktop Settings on Mac](https://docs.docker.com/desktop/settings/mac/) Search for "x86" in that page. -[Run x86 (Intel) and ARM based images on Apple Silicon (M1) Macs?](https://forums.docker.com/t/run-x86-intel-and-arm-based-images-on-apple-silicon-m1-macs/117123) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..11f9256f --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index bbb8ba18..11f9256f 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,4 +1,7 @@ __pycache__ .env _old -uploads \ No newline at end of file +uploads +.ipynb_checkpoints +*.db +_test \ No newline at end of file diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index a2a09fc7..64c6361e 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -25,7 +25,7 @@ TARGET_SERVER_URL = OLLAMA_API_BASE_URL def proxy(path): # Combine the base URL of the target server with the requested path target_url = f"{TARGET_SERVER_URL}/{path}" - print(path) + print(target_url) # Get data from the original request data = request.get_data() @@ -61,6 +61,11 @@ def proxy(path): r = None + headers.pop("Host", None) + headers.pop("Authorization", None) + headers.pop("Origin", None) + headers.pop("Referer", None) + try: # Make a request to the target server r = requests.request( @@ -86,8 +91,10 @@ def proxy(path): return response except Exception as e: + print(e) error_detail = "Ollama WebUI: Server Connection Error" if r != None: + print(r.text) res = r.json() if "error" in res: error_detail = f"Ollama: {res['error']}" diff --git a/backend/apps/web/internal/db.py b/backend/apps/web/internal/db.py new file mode 100644 index 00000000..3d639f3c --- /dev/null +++ b/backend/apps/web/internal/db.py @@ -0,0 +1,4 @@ +from peewee import * + +DB = SqliteDatabase("./data/ollama.db") +DB.connect() diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 854f1626..03273f8b 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI, Request, Depends, HTTPException from fastapi.middleware.cors import CORSMiddleware -from apps.web.routers import auths, users, utils +from apps.web.routers import auths, users, chats, modelfiles, utils from config import WEBUI_VERSION, WEBUI_AUTH app = FastAPI() @@ -19,6 +19,10 @@ app.add_middleware( app.include_router(auths.router, prefix="/auths", tags=["auths"]) app.include_router(users.router, prefix="/users", tags=["users"]) +app.include_router(chats.router, prefix="/chats", tags=["chats"]) +app.include_router(modelfiles.router, prefix="/modelfiles", tags=["modelfiles"]) + + app.include_router(utils.router, prefix="/utils", tags=["utils"]) diff --git a/backend/apps/web/models/auths.py b/backend/apps/web/models/auths.py index 41c82efd..c066dbef 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -2,6 +2,7 @@ from pydantic import BaseModel from typing import List, Union, Optional import time import uuid +from peewee import * from apps.web.models.users import UserModel, Users @@ -12,15 +13,23 @@ from utils.utils import ( create_token, ) -import config - -DB = config.DB +from apps.web.internal.db import DB #################### # DB MODEL #################### +class Auth(Model): + id = CharField(unique=True) + email = CharField() + password = CharField() + active = BooleanField() + + class Meta: + database = DB + + class AuthModel(BaseModel): id: str email: str @@ -64,7 +73,7 @@ class SignupForm(BaseModel): class AuthsTable: def __init__(self, db): self.db = db - self.table = db.auths + self.db.create_tables([Auth]) def insert_new_auth( self, email: str, password: str, name: str, role: str = "pending" @@ -76,27 +85,28 @@ class AuthsTable: auth = AuthModel( **{"id": id, "email": email, "password": password, "active": True} ) - result = self.table.insert_one(auth.model_dump()) + result = Auth.create(**auth.model_dump()) + user = Users.insert_new_user(id, name, email, role) - print(result, user) if result and user: return user else: return None def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: - print("authenticate_user") - - auth = self.table.find_one({"email": email, "active": True}) - - if auth: - if verify_password(password, auth["password"]): - user = self.db.users.find_one({"id": auth["id"]}) - return UserModel(**user) + print("authenticate_user", email) + try: + auth = Auth.get(Auth.email == email, Auth.active == True) + if auth: + if verify_password(password, auth.password): + user = Users.get_user_by_id(auth.id) + return user + else: + return None else: return None - else: + except: return None diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py new file mode 100644 index 00000000..8f075e40 --- /dev/null +++ b/backend/apps/web/models/chats.py @@ -0,0 +1,157 @@ +from pydantic import BaseModel +from typing import List, Union, Optional +from peewee import * +from playhouse.shortcuts import model_to_dict + + +import json +import uuid +import time + +from apps.web.internal.db import DB + + +#################### +# Chat DB Schema +#################### + + +class Chat(Model): + id = CharField(unique=True) + user_id = CharField() + title = CharField() + chat = TextField() # Save Chat JSON as Text + timestamp = DateField() + + class Meta: + database = DB + + +class ChatModel(BaseModel): + id: str + user_id: str + title: str + chat: str + timestamp: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class ChatForm(BaseModel): + chat: dict + + +class ChatTitleForm(BaseModel): + title: str + + +class ChatResponse(BaseModel): + id: str + user_id: str + title: str + chat: dict + timestamp: int # timestamp in epoch + + +class ChatTitleIdResponse(BaseModel): + id: str + title: str + + +class ChatTable: + def __init__(self, db): + self.db = db + db.create_tables([Chat]) + + def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]: + id = str(uuid.uuid4()) + chat = ChatModel( + **{ + "id": id, + "user_id": user_id, + "title": form_data.chat["title"] + if "title" in form_data.chat + else "New Chat", + "chat": json.dumps(form_data.chat), + "timestamp": int(time.time()), + } + ) + + result = Chat.create(**chat.model_dump()) + return chat if result else None + + def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: + try: + query = Chat.update( + chat=json.dumps(chat), + title=chat["title"] if "title" in chat else "New Chat", + timestamp=int(time.time()), + ).where(Chat.id == id) + query.execute() + + chat = Chat.get(Chat.id == id) + return ChatModel(**model_to_dict(chat)) + except: + return None + + def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: + try: + query = Chat.update( + chat=json.dumps(chat), + title=chat["title"] if "title" in chat else "New Chat", + timestamp=int(time.time()), + ).where(Chat.id == id) + query.execute() + + chat = Chat.get(Chat.id == id) + return ChatModel(**model_to_dict(chat)) + except: + return None + + def get_chat_lists_by_user_id( + self, user_id: str, skip: int = 0, limit: int = 50 + ) -> List[ChatModel]: + return [ + ChatModel(**model_to_dict(chat)) + for chat in Chat.select() + .where(Chat.user_id == user_id) + .order_by(Chat.timestamp.desc()) + # .limit(limit) + # .offset(skip) + ] + + def get_all_chats_by_user_id(self, user_id: str) -> List[ChatModel]: + return [ + ChatModel(**model_to_dict(chat)) + for chat in Chat.select() + .where(Chat.user_id == user_id) + .order_by(Chat.timestamp.desc()) + ] + + def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: + try: + chat = Chat.get(Chat.id == id, Chat.user_id == user_id) + return ChatModel(**model_to_dict(chat)) + except: + return None + + def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]: + return [ + ChatModel(**model_to_dict(chat)) + for chat in Chat.select().limit(limit).offset(skip) + ] + + def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool: + try: + query = Chat.delete().where((Chat.id == id) & (Chat.user_id == user_id)) + query.execute() # Remove the rows, return number of rows removed. + + return True + except: + return False + + +Chats = ChatTable(DB) diff --git a/backend/apps/web/models/modelfiles.py b/backend/apps/web/models/modelfiles.py new file mode 100644 index 00000000..4d8202db --- /dev/null +++ b/backend/apps/web/models/modelfiles.py @@ -0,0 +1,135 @@ +from pydantic import BaseModel +from peewee import * +from playhouse.shortcuts import model_to_dict +from typing import List, Union, Optional +import time + +from utils.utils import decode_token +from utils.misc import get_gravatar_url + +from apps.web.internal.db import DB + +import json + +#################### +# User DB Schema +#################### + + +class Modelfile(Model): + tag_name = CharField(unique=True) + user_id = CharField() + modelfile = TextField() + timestamp = DateField() + + class Meta: + database = DB + + +class ModelfileModel(BaseModel): + tag_name: str + user_id: str + modelfile: str + timestamp: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class ModelfileForm(BaseModel): + modelfile: dict + + +class ModelfileTagNameForm(BaseModel): + tag_name: str + + +class ModelfileUpdateForm(ModelfileForm, ModelfileTagNameForm): + pass + + +class ModelfileResponse(BaseModel): + tag_name: str + user_id: str + modelfile: dict + timestamp: int # timestamp in epoch + + +class ModelfilesTable: + def __init__(self, db): + self.db = db + self.db.create_tables([Modelfile]) + + def insert_new_modelfile( + self, user_id: str, form_data: ModelfileForm + ) -> Optional[ModelfileModel]: + if "tagName" in form_data.modelfile: + modelfile = ModelfileModel( + **{ + "user_id": user_id, + "tag_name": form_data.modelfile["tagName"], + "modelfile": json.dumps(form_data.modelfile), + "timestamp": int(time.time()), + } + ) + + try: + result = Modelfile.create(**modelfile.model_dump()) + if result: + return modelfile + else: + return None + except: + return None + + else: + return None + + def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]: + try: + modelfile = Modelfile.get(Modelfile.tag_name == tag_name) + return ModelfileModel(**model_to_dict(modelfile)) + except: + return None + + def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]: + return [ + ModelfileResponse( + **{ + **model_to_dict(modelfile), + "modelfile": json.loads(modelfile.modelfile), + } + ) + for modelfile in Modelfile.select() + # .limit(limit).offset(skip) + ] + + def update_modelfile_by_tag_name( + self, tag_name: str, modelfile: dict + ) -> Optional[ModelfileModel]: + try: + query = Modelfile.update( + modelfile=json.dumps(modelfile), + timestamp=int(time.time()), + ).where(Modelfile.tag_name == tag_name) + + query.execute() + + modelfile = Modelfile.get(Modelfile.tag_name == tag_name) + return ModelfileModel(**model_to_dict(modelfile)) + except: + return None + + def delete_modelfile_by_tag_name(self, tag_name: str) -> bool: + try: + query = Modelfile.delete().where((Modelfile.tag_name == tag_name)) + query.execute() # Remove the rows, return number of rows removed. + + return True + except: + return False + + +Modelfiles = ModelfilesTable(DB) diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index 4dc3fc7a..782b7f47 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -1,25 +1,38 @@ from pydantic import BaseModel +from peewee import * +from playhouse.shortcuts import model_to_dict from typing import List, Union, Optional -from pymongo import ReturnDocument import time from utils.utils import decode_token from utils.misc import get_gravatar_url -from config import DB +from apps.web.internal.db import DB #################### # User DB Schema #################### +class User(Model): + id = CharField(unique=True) + name = CharField() + email = CharField() + role = CharField() + profile_image_url = CharField() + timestamp = DateField() + + class Meta: + database = DB + + class UserModel(BaseModel): id: str name: str email: str role: str = "pending" profile_image_url: str = "/user.png" - created_at: int # timestamp in epoch + timestamp: int # timestamp in epoch #################### @@ -35,7 +48,7 @@ class UserRoleUpdateForm(BaseModel): class UsersTable: def __init__(self, db): self.db = db - self.table = db.users + self.db.create_tables([User]) def insert_new_user( self, id: str, name: str, email: str, role: str = "pending" @@ -47,22 +60,27 @@ class UsersTable: "email": email, "role": role, "profile_image_url": get_gravatar_url(email), - "created_at": int(time.time()), + "timestamp": int(time.time()), } ) - result = self.table.insert_one(user.model_dump()) - + result = User.create(**user.model_dump()) if result: return user else: return None - def get_user_by_email(self, email: str) -> Optional[UserModel]: - user = self.table.find_one({"email": email}, {"_id": False}) + def get_user_by_id(self, id: str) -> Optional[UserModel]: + try: + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None - if user: - return UserModel(**user) - else: + def get_user_by_email(self, email: str) -> Optional[UserModel]: + try: + user = User.get(User.email == email) + return UserModel(**model_to_dict(user)) + except: return None def get_user_by_token(self, token: str) -> Optional[UserModel]: @@ -75,23 +93,22 @@ class UsersTable: def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]: return [ - UserModel(**user) - for user in list( - self.table.find({}, {"_id": False}).skip(skip).limit(limit) - ) + UserModel(**model_to_dict(user)) + for user in User.select().limit(limit).offset(skip) ] def get_num_users(self) -> Optional[int]: - return self.table.count_documents({}) - - def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: - user = self.table.find_one_and_update( - {"id": id}, {"$set": updated}, return_document=ReturnDocument.AFTER - ) - return UserModel(**user) + return User.select().count() def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]: - return self.update_user_by_id(id, {"role": role}) + try: + query = User.update(role=role).where(User.id == id) + query.execute() + + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None Users = UsersTable(DB) diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 023e1914..02b77248 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -104,8 +104,8 @@ async def signup(form_data: SignupForm): "profile_image_url": user.profile_image_url, } else: - raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) except Exception as err: raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) else: - raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py new file mode 100644 index 00000000..798972e7 --- /dev/null +++ b/backend/apps/web/routers/chats.py @@ -0,0 +1,161 @@ +from fastapi import Response +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.web.models.users import Users +from apps.web.models.chats import ( + ChatModel, + ChatResponse, + ChatTitleForm, + ChatForm, + ChatTitleIdResponse, + Chats, +) + +from utils.utils import ( + bearer_scheme, +) +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetChats +############################ + + +@router.get("/", response_model=List[ChatTitleIdResponse]) +async def get_user_chats(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Chats.get_chat_lists_by_user_id(user.id, skip, limit) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# GetAllChats +############################ + + +@router.get("/all", response_model=List[ChatResponse]) +async def get_all_user_chats(cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return [ + ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + for chat in Chats.get_all_chats_by_user_id(user.id) + ] + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# CreateNewChat +############################ + + +@router.post("/new", response_model=Optional[ChatResponse]) +async def create_new_chat(form_data: ChatForm, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + chat = Chats.insert_new_chat(user.id, form_data) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# GetChatById +############################ + + +@router.get("/{id}", response_model=Optional[ChatResponse]) +async def get_chat_by_id(id: str, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + + if chat: + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# UpdateChatById +############################ + + +@router.post("/{id}", response_model=Optional[ChatResponse]) +async def update_chat_by_id(id: str, form_data: ChatForm, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + updated_chat = {**json.loads(chat.chat), **form_data.chat} + + chat = Chats.update_chat_by_id(id, updated_chat) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# DeleteChatById +############################ + + +@router.delete("/{id}", response_model=bool) +async def delete_chat_by_id(id: str, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + result = Chats.delete_chat_by_id_and_user_id(id, user.id) + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) diff --git a/backend/apps/web/routers/modelfiles.py b/backend/apps/web/routers/modelfiles.py new file mode 100644 index 00000000..dd1f6cc5 --- /dev/null +++ b/backend/apps/web/routers/modelfiles.py @@ -0,0 +1,191 @@ +from fastapi import Response +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.web.models.users import Users +from apps.web.models.modelfiles import ( + Modelfiles, + ModelfileForm, + ModelfileTagNameForm, + ModelfileUpdateForm, + ModelfileResponse, +) + +from utils.utils import ( + bearer_scheme, +) +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetModelfiles +############################ + + +@router.get("/", response_model=List[ModelfileResponse]) +async def get_modelfiles(skip: int = 0, limit: int = 50, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + return Modelfiles.get_modelfiles(skip, limit) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# CreateNewModelfile +############################ + + +@router.post("/create", response_model=Optional[ModelfileResponse]) +async def create_new_modelfile(form_data: ModelfileForm, cred=Depends(bearer_scheme)): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + # Admin Only + if user.role == "admin": + modelfile = Modelfiles.insert_new_modelfile(user.id, form_data) + + if modelfile: + return ModelfileResponse( + **{ + **modelfile.model_dump(), + "modelfile": json.loads(modelfile.modelfile), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.DEFAULT(), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# GetModelfileByTagName +############################ + + +@router.post("/", response_model=Optional[ModelfileResponse]) +async def get_modelfile_by_tag_name( + form_data: ModelfileTagNameForm, cred=Depends(bearer_scheme) +): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) + + if modelfile: + return ModelfileResponse( + **{ + **modelfile.model_dump(), + "modelfile": json.loads(modelfile.modelfile), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# UpdateModelfileByTagName +############################ + + +@router.post("/update", response_model=Optional[ModelfileResponse]) +async def update_modelfile_by_tag_name( + form_data: ModelfileUpdateForm, cred=Depends(bearer_scheme) +): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + if user.role == "admin": + modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) + if modelfile: + updated_modelfile = { + **json.loads(modelfile.modelfile), + **form_data.modelfile, + } + + modelfile = Modelfiles.update_modelfile_by_tag_name( + form_data.tag_name, updated_modelfile + ) + + return ModelfileResponse( + **{ + **modelfile.model_dump(), + "modelfile": json.loads(modelfile.modelfile), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + + +############################ +# DeleteModelfileByTagName +############################ + + +@router.delete("/delete", response_model=bool) +async def delete_modelfile_by_tag_name( + form_data: ModelfileTagNameForm, cred=Depends(bearer_scheme) +): + token = cred.credentials + user = Users.get_user_by_token(token) + + if user: + if user.role == "admin": + result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name) + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) diff --git a/backend/config.py b/backend/config.py index cf9eae02..8e100fe5 100644 --- a/backend/config.py +++ b/backend/config.py @@ -1,9 +1,10 @@ from dotenv import load_dotenv, find_dotenv -from pymongo import MongoClient + from constants import ERROR_MESSAGES from secrets import token_bytes from base64 import b64encode + import os load_dotenv(find_dotenv("../.env")) @@ -30,30 +31,13 @@ if ENV == "prod": # WEBUI_VERSION #################################### -WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.40") +WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.42") #################################### -# WEBUI_AUTH +# WEBUI_AUTH (Required for security) #################################### - -WEBUI_AUTH = True if os.environ.get("WEBUI_AUTH", "FALSE") == "TRUE" else False - - -#################################### -# WEBUI_DB (Deprecated, Should be removed) -#################################### - - -WEBUI_DB_URL = os.environ.get("WEBUI_DB_URL", "mongodb://root:root@localhost:27017/") - -if WEBUI_AUTH and WEBUI_DB_URL == "": - raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) - - -DB_CLIENT = MongoClient(f"{WEBUI_DB_URL}?authSource=admin") -DB = DB_CLIENT["ollama-webui"] - +WEBUI_AUTH = True #################################### # WEBUI_JWT_SECRET_KEY diff --git a/backend/constants.py b/backend/constants.py index b383957b..06d67eec 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -11,6 +11,11 @@ class ERROR_MESSAGES(str, Enum): DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}" ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." + CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." + EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." + USERNAME_TAKEN = ( + "Uh-oh! This username is already registered. Please choose another username." + ) INVALID_TOKEN = ( "Your session has expired or the token is invalid. Please sign in again." ) @@ -20,5 +25,6 @@ class ERROR_MESSAGES(str, Enum): ACTION_PROHIBITED = ( "The requested action has been restricted as a security measure." ) + NOT_FOUND = "We could not find what you're looking for :/" USER_NOT_FOUND = "We could not find what you're looking for :/" MALICIOUS = "Unusual activities detected, please try again in a few minutes." diff --git a/backend/data/readme.txt b/backend/data/readme.txt new file mode 100644 index 00000000..30c12ace --- /dev/null +++ b/backend/data/readme.txt @@ -0,0 +1 @@ +dir for backend files (db, documents, etc.) \ No newline at end of file diff --git a/backend/dev.sh b/backend/dev.sh index 27793999..e4e1aaa1 100644 --- a/backend/dev.sh +++ b/backend/dev.sh @@ -1 +1 @@ -uvicorn main:app --port 8080 --reload \ No newline at end of file +uvicorn main:app --port 8080 --host 0.0.0.0 --reload \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 1568f7bb..2644d559 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,8 +13,8 @@ uuid requests aiohttp -pymongo +peewee bcrypt PyJWT -pyjwt[crypto] \ No newline at end of file +pyjwt[crypto] diff --git a/docker-compose.yml b/docker-compose.yml index 427f8580..a7357740 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,8 @@ services: dockerfile: Dockerfile image: ollama-webui:latest container_name: ollama-webui + volumes: + - ollama-webui:/app/backend/data depends_on: - ollama ports: @@ -30,3 +32,4 @@ services: volumes: ollama: {} + ollama-webui: {} diff --git a/run.sh b/run.sh index 584c7f64..0ada65d1 100644 --- a/run.sh +++ b/run.sh @@ -1,5 +1,5 @@ docker stop ollama-webui || true docker rm ollama-webui || true docker build -t ollama-webui . -docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway --name ollama-webui --restart always ollama-webui +docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v ollama-webui:/app/backend/data --name ollama-webui --restart always ollama-webui docker image prune -f \ No newline at end of file diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts new file mode 100644 index 00000000..41c397f9 --- /dev/null +++ b/src/lib/apis/auths/index.ts @@ -0,0 +1,90 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getSessionUser = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignIn = async (email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignUp = async (name: string, email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts new file mode 100644 index 00000000..354e4f74 --- /dev/null +++ b/src/lib/apis/chats/index.ts @@ -0,0 +1,193 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewChat = async (token: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChatById = async (token: string, id: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts new file mode 100644 index 00000000..91512166 --- /dev/null +++ b/src/lib/apis/index.ts @@ -0,0 +1,23 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getBackendConfig = async () => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + return res; +}; diff --git a/src/lib/apis/modelfiles/index.ts b/src/lib/apis/modelfiles/index.ts new file mode 100644 index 00000000..91af5e38 --- /dev/null +++ b/src/lib/apis/modelfiles/index.ts @@ -0,0 +1,173 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewModelfile = async (token: string, modelfile: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + modelfile: modelfile + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelfiles = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((modelfile) => modelfile.modelfile); +}; + +export const getModelfileByTagName = async (token: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + tag_name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res.modelfile; +}; + +export const updateModelfileByTagName = async ( + token: string, + tagName: string, + modelfile: object +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + tag_name: tagName, + modelfile: modelfile + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModelfileByTagName = async (token: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/modelfiles/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + tag_name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts new file mode 100644 index 00000000..198ea418 --- /dev/null +++ b/src/lib/apis/ollama/index.ts @@ -0,0 +1,204 @@ +import { OLLAMA_API_BASE_URL } from '$lib/constants'; + +export const getOllamaVersion = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/version`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.version ?? ''; +}; + +export const getOllamaModels = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.models ?? []; +}; + +export const generateTitle = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '', + model: string, + prompt: string +) => { + let error = null; + + const res = await fetch(`${base_url}/generate`, { + method: 'POST', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${prompt}`, + stream: false + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.response ?? 'New Chat'; +}; + +export const generateChatCompletion = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string = '', + body: object +) => { + let error = null; + + const res = await fetch(`${base_url}/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(body) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createModel = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string, + tagName: string, + content: string +) => { + let error = null; + + const res = await fetch(`${base_url}/create`, { + method: 'POST', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName, + modelfile: content + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModel = async ( + base_url: string = OLLAMA_API_BASE_URL, + token: string, + tagName: string +) => { + let error = null; + + const res = await fetch(`${base_url}/delete`, { + method: 'DELETE', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + console.log(json); + return true; + }) + .catch((err) => { + console.log(err); + error = err.error; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts new file mode 100644 index 00000000..89776268 --- /dev/null +++ b/src/lib/apis/openai/index.ts @@ -0,0 +1,33 @@ +export const getOpenAIModels = async ( + base_url: string = 'https://api.openai.com/v1', + api_key: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${api_key}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return null; + }); + + if (error) { + throw error; + } + + let models = Array.isArray(res) ? res : res?.data ?? null; + + return models + .map((model) => ({ name: model.id, external: true })) + .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)); +}; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts new file mode 100644 index 00000000..5939d39d --- /dev/null +++ b/src/lib/apis/users/index.ts @@ -0,0 +1,58 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const updateUserRole = async (token: string, id: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + id: id, + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUsers = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((error) => { + console.log(error); + return null; + }); + + if (error) { + throw error; + } + + return res ? res : []; +}; diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte index 543ce6a5..8516dc40 100644 --- a/src/lib/components/chat/Messages.svelte +++ b/src/lib/components/chat/Messages.svelte @@ -8,11 +8,13 @@ import auto_render from 'katex/dist/contrib/auto-render.mjs'; import 'katex/dist/katex.min.css'; - import { chatId, config, db, modelfiles, settings, user } from '$lib/stores'; + import { chats, config, db, modelfiles, settings, user } from '$lib/stores'; import { tick } from 'svelte'; import toast from 'svelte-french-toast'; + import { getChatList, updateChatById } from '$lib/apis/chats'; + export let chatId = ''; export let sendPrompt: Function; export let regenerateResponse: Function; @@ -27,14 +29,25 @@ $: if (messages && messages.length > 0 && (messages.at(-1).done ?? false)) { (async () => { await tick(); + + [...document.querySelectorAll('*')].forEach((node) => { + if (node._tippy) { + node._tippy.destroy(); + } + }); + + console.log('rendering message'); + renderLatex(); hljs.highlightAll(); createCopyCodeBlockButton(); for (const message of messages) { if (message.info) { + console.log(message); + tippy(`#info-${message.id}`, { - content: `token/s: ${ + content: `token/s: ${ `${ Math.round( ((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100 @@ -81,7 +94,7 @@ blocks.forEach((block) => { // only add button if browser supports Clipboard API - if (navigator.clipboard && block.childNodes.length < 2 && block.id !== 'user-message') { + if (block.childNodes.length < 2 && block.id !== 'user-message') { let code = block.querySelector('code'); code.style.borderTopRightRadius = 0; code.style.borderTopLeftRadius = 0; @@ -119,10 +132,6 @@ topBarDiv.appendChild(button); block.prepend(topBarDiv); - - // button.addEventListener('click', async () => { - // await copyCode(block, button); - // }); } }); @@ -130,7 +139,7 @@ let code = block.querySelector('code'); let text = code.innerText; - await navigator.clipboard.writeText(text); + await copyToClipboard(text); // visual feedback that task is completed button.innerText = 'Copied!'; @@ -239,7 +248,7 @@ history.currentId = userMessageId; await tick(); - await sendPrompt(userPrompt, userMessageId, $chatId); + await sendPrompt(userPrompt, userMessageId, chatId); }; const confirmEditResponseMessage = async (messageId) => { @@ -253,6 +262,7 @@ }; const rateMessage = async (messageIdx, rating) => { + // TODO: Move this function to parent messages = messages.map((message, idx) => { if (messageIdx === idx) { message.rating = rating; @@ -260,10 +270,12 @@ return message; }); - $db.updateChatById(chatId, { + await updateChatById(localStorage.token, chatId, { messages: messages, history: history }); + + await chats.set(await getChatList(localStorage.token)); }; const showPreviousMessage = async (message) => { diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 4baf6c91..8ef24d06 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -1,18 +1,23 @@ @@ -741,6 +790,32 @@
Add-ons
+ + {#if !$config || ($config && !$config.auth)} + + + +
+
{ uploadModelHandler(); }} >
-
Upload a GGUF model
+
+ Upload a GGUF model (Experimental) +
-
- {:else if selectedTab === 'external'} @@ -1472,6 +1554,150 @@ + {:else if selectedTab === 'chats'} +
+
+ + + +
+ +
{:else if selectedTab === 'auth'}
Ollama Version
- {$info?.ollama?.version ?? 'N/A'} + {ollamaVersion ?? 'N/A'}
diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte index bcd66ee9..fb350fb0 100644 --- a/src/lib/components/layout/Navbar.svelte +++ b/src/lib/components/layout/Navbar.svelte @@ -1,18 +1,17 @@
@@ -91,8 +65,11 @@ on:click={async () => { goto('/'); - await chatId.set(uuidv4()); - // createNewChat(); + const newChatButton = document.getElementById('new-chat-button'); + + if (newChatButton) { + newChatButton.click(); + } }} >
@@ -121,39 +98,41 @@
-
- -
+
+
Modelfiles
+
+ +
+ {/if}
-
+
@@ -394,148 +373,9 @@
-
+
-
- - - -
- {#if showDeleteHistoryConfirm} -
-
- - - - Are you sure? -
- -
- - -
-
- {:else} - - {/if} - {#if $user !== undefined} + + +
+
+
+
+
+ {:else if checkVersion(REQUIRED_OLLAMA_VERSION, ollamaVersion ?? '0')} +
+
@@ -254,15 +171,16 @@ />We've detected either a connection hiccup or observed that you're using an older version. Ensure you're on the latest Ollama version (version - {requiredOllamaVersion} or higher) - or check your connection. + {REQUIRED_OLLAMA_VERSION} or higher) or check your connection.
+
+
+
+
+
+ {:else if localDBChats.length > 0} +
+
+
+
+
+ Important Update
Action Required for Chat Log Storage +
+ +
+ Saving chat logs directly to your browser's storage is no longer supported. Please + take a moment to download and delete your chat logs by clicking the button below. + Don't worry, you can easily re-import your chat logs to the backend through Settings > Chats > Import Chats. This ensures that your valuable conversations are securely saved to your backend + database. Thank you! +
+ +
+ + +
@@ -285,9 +253,7 @@ class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-800 min-h-screen overflow-auto flex flex-row" > - -
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 0d1055f9..6f272a62 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -2,23 +2,27 @@ import { v4 as uuidv4 } from 'uuid'; import toast from 'svelte-french-toast'; - import { OLLAMA_API_BASE_URL } from '$lib/constants'; import { onMount, tick } from 'svelte'; - import { splitStream } from '$lib/utils'; import { goto } from '$app/navigation'; + import { page } from '$app/stores'; - import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; + import { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; + import { OLLAMA_API_BASE_URL } from '$lib/constants'; + + import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; + import { copyToClipboard, splitStream } from '$lib/utils'; import MessageInput from '$lib/components/chat/MessageInput.svelte'; import Messages from '$lib/components/chat/Messages.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte'; - import { page } from '$app/stores'; + import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats'; let stopResponseFlag = false; let autoScroll = true; let selectedModels = ['']; + let selectedModelfile = null; $: selectedModelfile = selectedModels.length === 1 && @@ -26,10 +30,11 @@ ? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0] : null; + let chat = null; + let title = ''; let prompt = ''; let files = []; - let messages = []; let history = { messages: {}, @@ -50,16 +55,8 @@ messages = []; } - $: if (files) { - console.log(files); - } - onMount(async () => { - await chatId.set(uuidv4()); - - chatId.subscribe(async () => { - await initNewChat(); - }); + await initNewChat(); }); ////////////////////////// @@ -67,6 +64,11 @@ ////////////////////////// const initNewChat = async () => { + window.history.replaceState(history.state, '', `/`); + + console.log('initNewChat'); + + await chatId.set(''); console.log($chatId); autoScroll = true; @@ -82,68 +84,33 @@ : $settings.models ?? ['']; let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); - console.log(_settings); settings.set({ ..._settings }); }; - const copyToClipboard = (text) => { - if (!navigator.clipboard) { - var textArea = document.createElement('textarea'); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.top = '0'; - textArea.style.left = '0'; - textArea.style.position = 'fixed'; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - var successful = document.execCommand('copy'); - var msg = successful ? 'successful' : 'unsuccessful'; - console.log('Fallback: Copying text command was ' + msg); - } catch (err) { - console.error('Fallback: Oops, unable to copy', err); - } - - document.body.removeChild(textArea); - return; - } - navigator.clipboard.writeText(text).then( - function () { - console.log('Async: Copying to clipboard was successful!'); - }, - function (err) { - console.error('Async: Could not copy text: ', err); - } - ); - }; - ////////////////////////// // Ollama functions ////////////////////////// - const sendPrompt = async (userPrompt, parentId, _chatId) => { + const sendPrompt = async (prompt, parentId) => { + const _chatId = JSON.parse(JSON.stringify($chatId)); await Promise.all( selectedModels.map(async (model) => { console.log(model); if ($models.filter((m) => m.name === model)[0].external) { - await sendPromptOpenAI(model, userPrompt, parentId, _chatId); + await sendPromptOpenAI(model, prompt, parentId, _chatId); } else { - await sendPromptOllama(model, userPrompt, parentId, _chatId); + await sendPromptOllama(model, prompt, parentId, _chatId); } }) ); - await chats.set(await $db.getChats()); + await chats.set(await getChatList(localStorage.token)); }; const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { - console.log('sendPromptOllama'); + // Create response message let responseMessageId = uuidv4(); let responseMessage = { parentId: parentId, @@ -154,8 +121,11 @@ model: model }; + // Add message to history and Set currentId to messageId history.messages[responseMessageId] = responseMessage; history.currentId = responseMessageId; + + // Append messageId to childrenIds of parent message if (parentId !== null) { history.messages[parentId].childrenIds = [ ...history.messages[parentId].childrenIds, @@ -163,17 +133,16 @@ ]; } + // Wait until history/message have been updated await tick(); + + // Scroll down window.scrollTo({ top: document.body.scrollHeight }); - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ + const res = await generateChatCompletion( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + { model: model, messages: [ $settings.system @@ -195,20 +164,11 @@ }) })), options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, ...($settings.options ?? {}) }, format: $settings.requestFormat ?? undefined - }) - }).catch((err) => { - console.log(err); - return null; - }); + } + ); if (res && res.ok) { const reader = res.body @@ -297,23 +257,14 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { - title: title === '' ? 'New Chat' : title, - models: selectedModels, - system: $settings.system ?? undefined, - options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, - ...($settings.options ?? {}) - }, + if ($chatId == _chatId) { + chat = await updateChatById(localStorage.token, _chatId, { messages: messages, history: history }); + await chats.set(await getChatList(localStorage.token)); } } else { if (res !== null) { @@ -339,6 +290,7 @@ stopResponseFlag = false; await tick(); + if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } @@ -481,23 +433,14 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { - title: title === '' ? 'New Chat' : title, - models: selectedModels, - system: $settings.system ?? undefined, - options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, - ...($settings.options ?? {}) - }, + if ($chatId == _chatId) { + chat = await updateChatById(localStorage.token, _chatId, { messages: messages, history: history }); + await chats.set(await getChatList(localStorage.token)); } } else { if (res !== null) { @@ -542,16 +485,18 @@ }; const submitPrompt = async (userPrompt) => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('submitPrompt', _chatId); + console.log('submitPrompt', $chatId); if (selectedModels.includes('')) { toast.error('Model not selected'); } else if (messages.length != 0 && messages.at(-1).done != true) { + // Response not done console.log('wait'); } else { + // Reset chat message textarea height document.getElementById('chat-textarea').style.height = ''; + // Create user message let userMessageId = uuidv4(); let userMessage = { id: userMessageId, @@ -562,42 +507,43 @@ files: files.length > 0 ? files : undefined }; + // Add message to history and Set currentId to messageId + history.messages[userMessageId] = userMessage; + history.currentId = userMessageId; + + // Append messageId to childrenIds of parent message if (messages.length !== 0) { history.messages[messages.at(-1).id].childrenIds.push(userMessageId); } - history.messages[userMessageId] = userMessage; - history.currentId = userMessageId; - + // Wait until history/message have been updated await tick(); + + // Create new chat if only one message in messages if (messages.length == 1) { - await $db.createNewChat({ - id: _chatId, + chat = await createNewChat(localStorage.token, { + id: $chatId, title: 'New Chat', models: selectedModels, system: $settings.system ?? undefined, options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, ...($settings.options ?? {}) }, messages: messages, - history: history + history: history, + timestamp: Date.now() }); + await chats.set(await getChatList(localStorage.token)); + await chatId.set(chat.id); + await tick(); } + // Reset chat input textarea prompt = ''; files = []; - setTimeout(() => { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }, 50); - - await sendPrompt(userPrompt, userMessageId, _chatId); + // Send prompt + await sendPrompt(userPrompt, userMessageId); } }; @@ -607,9 +553,7 @@ }; const regenerateResponse = async () => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('regenerateResponse', _chatId); - + console.log('regenerateResponse'); if (messages.length != 0 && messages.at(-1).done == true) { messages.splice(messages.length - 1, 1); messages = messages; @@ -617,41 +561,21 @@ let userMessage = messages.at(-1); let userPrompt = userMessage.content; - await sendPrompt(userPrompt, userMessage.id, _chatId); + await sendPrompt(userPrompt, userMessage.id); } }; const generateChatTitle = async (_chatId, userPrompt) => { if ($settings.titleAutoGenerate ?? true) { - console.log('generateChatTitle'); + const title = await generateTitle( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + selectedModels[0], + userPrompt + ); - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ - model: selectedModels[0], - prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`, - stream: false - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - if ('detail' in error) { - toast.error(error.detail); - } - console.log(error); - return null; - }); - - if (res) { - await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response); + if (title) { + await setChatTitle(_chatId, title); } } else { await setChatTitle(_chatId, `${userPrompt}`); @@ -659,10 +583,12 @@ }; const setChatTitle = async (_chatId, _title) => { - await $db.updateChatById(_chatId, { title: _title }); if (_chatId === $chatId) { title = _title; } + + chat = await updateChatById(localStorage.token, _chatId, { title: _title }); + await chats.set(await getChatList(localStorage.token)); }; @@ -672,7 +598,7 @@ }} /> - 0} /> + 0} {initNewChat} />
@@ -681,6 +607,7 @@
{ - const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.token}` - }, - body: JSON.stringify({ - id: id, - role: role - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - toast.error(error.detail); - return null; - }); + const updateRoleHandler = async (id, role) => { + const res = await updateUserRole(localStorage.token, id, role).catch((error) => { + toast.error(error); + return null; + }); if (res) { - await getUsers(); + users = await getUsers(localStorage.token); } }; - const getUsers = async () => { - const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - toast.error(error.detail); - return null; - }); - - users = res ? res : []; - }; - onMount(async () => { - if ($config === null || !$config.auth || ($config.auth && $user && $user.role !== 'admin')) { + if ($user?.role !== 'admin') { await goto('/'); } else { - await getUsers(); + users = await getUsers(localStorage.token); } loaded = true; }); @@ -115,11 +80,11 @@ class=" dark:text-white underline" on:click={() => { if (user.role === 'user') { - updateUserRole(user.id, 'admin'); + updateRoleHandler(user.id, 'admin'); } else if (user.role === 'pending') { - updateUserRole(user.id, 'user'); + updateRoleHandler(user.id, 'user'); } else { - updateUserRole(user.id, 'pending'); + updateRoleHandler(user.id, 'pending'); } }}>{user.role} diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte index 1b2c7053..9600f298 100644 --- a/src/routes/(app)/c/[id]/+page.svelte +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -2,17 +2,21 @@ import { v4 as uuidv4 } from 'uuid'; import toast from 'svelte-french-toast'; - import { OLLAMA_API_BASE_URL } from '$lib/constants'; import { onMount, tick } from 'svelte'; - import { convertMessagesToHistory, splitStream } from '$lib/utils'; import { goto } from '$app/navigation'; - import { config, models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; + import { page } from '$app/stores'; + + import { models, modelfiles, user, settings, db, chats, chatId } from '$lib/stores'; + import { OLLAMA_API_BASE_URL } from '$lib/constants'; + + import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; + import { copyToClipboard, splitStream } from '$lib/utils'; import MessageInput from '$lib/components/chat/MessageInput.svelte'; import Messages from '$lib/components/chat/Messages.svelte'; import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte'; - import { page } from '$app/stores'; + import { createNewChat, getChatById, getChatList, updateChatById } from '$lib/apis/chats'; let loaded = false; let stopResponseFlag = false; @@ -27,6 +31,8 @@ ? $modelfiles.filter((modelfile) => modelfile.tagName === selectedModels[0])[0] : null; + let chat = null; + let title = ''; let prompt = ''; let files = []; @@ -53,10 +59,8 @@ $: if ($page.params.id) { (async () => { - let chat = await loadChat(); - - await tick(); - if (chat) { + if (await loadChat()) { + await tick(); loaded = true; } else { await goto('/'); @@ -70,94 +74,70 @@ const loadChat = async () => { await chatId.set($page.params.id); - const chat = await $db.getChatById($chatId); + chat = await getChatById(localStorage.token, $chatId).catch(async (error) => { + await goto('/'); + return null; + }); if (chat) { - console.log(chat); + const chatContent = chat.chat; - selectedModels = (chat?.models ?? undefined) !== undefined ? chat.models : [chat.model ?? '']; - history = - (chat?.history ?? undefined) !== undefined - ? chat.history - : convertMessagesToHistory(chat.messages); - title = chat.title; + if (chatContent) { + console.log(chatContent); - let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); - await settings.set({ - ..._settings, - system: chat.system ?? _settings.system, - options: chat.options ?? _settings.options - }); - autoScroll = true; + selectedModels = + (chatContent?.models ?? undefined) !== undefined + ? chatContent.models + : [chatContent.model ?? '']; + history = + (chatContent?.history ?? undefined) !== undefined + ? chatContent.history + : convertMessagesToHistory(chatContent.messages); + title = chatContent.title; - await tick(); - if (messages.length > 0) { - history.messages[messages.at(-1).id].done = true; + let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); + await settings.set({ + ..._settings, + system: chatContent.system ?? _settings.system, + options: chatContent.options ?? _settings.options + }); + autoScroll = true; + await tick(); + + if (messages.length > 0) { + history.messages[messages.at(-1).id].done = true; + } + await tick(); + + return true; + } else { + return null; } - await tick(); - - return chat; - } else { - return null; } }; - const copyToClipboard = (text) => { - if (!navigator.clipboard) { - var textArea = document.createElement('textarea'); - textArea.value = text; - - // Avoid scrolling to bottom - textArea.style.top = '0'; - textArea.style.left = '0'; - textArea.style.position = 'fixed'; - - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - try { - var successful = document.execCommand('copy'); - var msg = successful ? 'successful' : 'unsuccessful'; - console.log('Fallback: Copying text command was ' + msg); - } catch (err) { - console.error('Fallback: Oops, unable to copy', err); - } - - document.body.removeChild(textArea); - return; - } - navigator.clipboard.writeText(text).then( - function () { - console.log('Async: Copying to clipboard was successful!'); - }, - function (err) { - console.error('Async: Could not copy text: ', err); - } - ); - }; - ////////////////////////// // Ollama functions ////////////////////////// - const sendPrompt = async (userPrompt, parentId, _chatId) => { + const sendPrompt = async (prompt, parentId) => { + const _chatId = JSON.parse(JSON.stringify($chatId)); await Promise.all( selectedModels.map(async (model) => { console.log(model); if ($models.filter((m) => m.name === model)[0].external) { - await sendPromptOpenAI(model, userPrompt, parentId, _chatId); + await sendPromptOpenAI(model, prompt, parentId, _chatId); } else { - await sendPromptOllama(model, userPrompt, parentId, _chatId); + await sendPromptOllama(model, prompt, parentId, _chatId); } }) ); - await chats.set(await $db.getChats()); + await chats.set(await getChatList(localStorage.token)); }; const sendPromptOllama = async (model, userPrompt, parentId, _chatId) => { - console.log('sendPromptOllama'); + // Create response message let responseMessageId = uuidv4(); let responseMessage = { parentId: parentId, @@ -168,8 +148,11 @@ model: model }; + // Add message to history and Set currentId to messageId history.messages[responseMessageId] = responseMessage; history.currentId = responseMessageId; + + // Append messageId to childrenIds of parent message if (parentId !== null) { history.messages[parentId].childrenIds = [ ...history.messages[parentId].childrenIds, @@ -177,17 +160,16 @@ ]; } + // Wait until history/message have been updated await tick(); + + // Scroll down window.scrollTo({ top: document.body.scrollHeight }); - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/chat`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ + const res = await generateChatCompletion( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + { model: model, messages: [ $settings.system @@ -209,20 +191,11 @@ }) })), options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, ...($settings.options ?? {}) }, format: $settings.requestFormat ?? undefined - }) - }).catch((err) => { - console.log(err); - return null; - }); + } + ); if (res && res.ok) { const reader = res.body @@ -311,23 +284,14 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { - title: title === '' ? 'New Chat' : title, - models: selectedModels, - system: $settings.system ?? undefined, - options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, - ...($settings.options ?? {}) - }, + if ($chatId == _chatId) { + chat = await updateChatById(localStorage.token, _chatId, { messages: messages, history: history }); + await chats.set(await getChatList(localStorage.token)); } } else { if (res !== null) { @@ -353,6 +317,7 @@ stopResponseFlag = false; await tick(); + if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } @@ -495,23 +460,14 @@ if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - await $db.updateChatById(_chatId, { - title: title === '' ? 'New Chat' : title, - models: selectedModels, - system: $settings.system ?? undefined, - options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, - ...($settings.options ?? {}) - }, + if ($chatId == _chatId) { + chat = await updateChatById(localStorage.token, _chatId, { messages: messages, history: history }); + await chats.set(await getChatList(localStorage.token)); } } else { if (res !== null) { @@ -556,16 +512,18 @@ }; const submitPrompt = async (userPrompt) => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('submitPrompt', _chatId); + console.log('submitPrompt', $chatId); if (selectedModels.includes('')) { toast.error('Model not selected'); } else if (messages.length != 0 && messages.at(-1).done != true) { + // Response not done console.log('wait'); } else { + // Reset chat message textarea height document.getElementById('chat-textarea').style.height = ''; + // Create user message let userMessageId = uuidv4(); let userMessage = { id: userMessageId, @@ -576,42 +534,43 @@ files: files.length > 0 ? files : undefined }; + // Add message to history and Set currentId to messageId + history.messages[userMessageId] = userMessage; + history.currentId = userMessageId; + + // Append messageId to childrenIds of parent message if (messages.length !== 0) { history.messages[messages.at(-1).id].childrenIds.push(userMessageId); } - history.messages[userMessageId] = userMessage; - history.currentId = userMessageId; - + // Wait until history/message have been updated await tick(); + + // Create new chat if only one message in messages if (messages.length == 1) { - await $db.createNewChat({ - id: _chatId, + chat = await createNewChat(localStorage.token, { + id: $chatId, title: 'New Chat', models: selectedModels, system: $settings.system ?? undefined, options: { - seed: $settings.seed ?? undefined, - temperature: $settings.temperature ?? undefined, - repeat_penalty: $settings.repeat_penalty ?? undefined, - top_k: $settings.top_k ?? undefined, - top_p: $settings.top_p ?? undefined, - num_ctx: $settings.num_ctx ?? undefined, ...($settings.options ?? {}) }, messages: messages, - history: history + history: history, + timestamp: Date.now() }); + await chats.set(await getChatList(localStorage.token)); + await chatId.set(chat.id); + await tick(); } + // Reset chat input textarea prompt = ''; files = []; - setTimeout(() => { - window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); - }, 50); - - await sendPrompt(userPrompt, userMessageId, _chatId); + // Send prompt + await sendPrompt(userPrompt, userMessageId); } }; @@ -621,9 +580,7 @@ }; const regenerateResponse = async () => { - const _chatId = JSON.parse(JSON.stringify($chatId)); - console.log('regenerateResponse', _chatId); - + console.log('regenerateResponse'); if (messages.length != 0 && messages.at(-1).done == true) { messages.splice(messages.length - 1, 1); messages = messages; @@ -631,41 +588,21 @@ let userMessage = messages.at(-1); let userPrompt = userMessage.content; - await sendPrompt(userPrompt, userMessage.id, _chatId); + await sendPrompt(userPrompt, userMessage.id); } }; const generateChatTitle = async (_chatId, userPrompt) => { if ($settings.titleAutoGenerate ?? true) { - console.log('generateChatTitle'); + const title = await generateTitle( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + selectedModels[0], + userPrompt + ); - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/generate`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ - model: selectedModels[0], - prompt: `Generate a brief 3-5 word title for this question, excluding the term 'title.' Then, please reply with only the title: ${userPrompt}`, - stream: false - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - if ('detail' in error) { - toast.error(error.detail); - } - console.log(error); - return null; - }); - - if (res) { - await setChatTitle(_chatId, res.response === '' ? 'New Chat' : res.response); + if (title) { + await setChatTitle(_chatId, title); } } else { await setChatTitle(_chatId, `${userPrompt}`); @@ -673,10 +610,12 @@ }; const setChatTitle = async (_chatId, _title) => { - await $db.updateChatById(_chatId, { title: _title }); if (_chatId === $chatId) { title = _title; } + + chat = await updateChatById(localStorage.token, _chatId, { title: _title }); + await chats.set(await getChatList(localStorage.token)); }; @@ -687,7 +626,13 @@ /> {#if loaded} - 0} /> + 0} + initNewChat={() => { + goto('/'); + }} + />
@@ -696,6 +641,7 @@
- import { modelfiles, settings, user } from '$lib/stores'; - import { onMount } from 'svelte'; import toast from 'svelte-french-toast'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + import { onMount } from 'svelte'; + + import { modelfiles, settings, user } from '$lib/stores'; import { OLLAMA_API_BASE_URL } from '$lib/constants'; + import { createModel, deleteModel } from '$lib/apis/ollama'; + import { + createNewModelfile, + deleteModelfileByTagName, + getModelfiles + } from '$lib/apis/modelfiles'; + + let localModelfiles = []; const deleteModelHandler = async (tagName) => { let success = null; - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/delete`, { - method: 'DELETE', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ - name: tagName - }) - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .then((json) => { - console.log(json); - toast.success(`Deleted ${tagName}`); - success = true; - return json; - }) - .catch((err) => { - console.log(err); - toast.error(err.error); - return null; - }); + + success = await deleteModel( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + tagName + ); + + if (success) { + toast.success(`Deleted ${tagName}`); + } return success; }; - const deleteModelfilebyTagName = async (tagName) => { + const deleteModelfile = async (tagName) => { await deleteModelHandler(tagName); - await modelfiles.set($modelfiles.filter((modelfile) => modelfile.tagName != tagName)); - localStorage.setItem('modelfiles', JSON.stringify($modelfiles)); + await deleteModelfileByTagName(localStorage.token, tagName); + await modelfiles.set(await getModelfiles(localStorage.token)); }; const shareModelfile = async (modelfile) => { @@ -60,6 +55,21 @@ false ); }; + + const saveModelfiles = async (modelfiles) => { + let blob = new Blob([JSON.stringify(modelfiles)], { + type: 'application/json' + }); + saveAs(blob, `modelfiles-export-${Date.now()}.json`); + }; + + onMount(() => { + localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); + + if (localModelfiles) { + console.log(localModelfiles); + } + });
@@ -167,7 +177,7 @@ class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" type="button" on:click={() => { - deleteModelfilebyTagName(modelfile.tagName); + deleteModelfile(modelfile.tagName); }} > {/each} + {#if localModelfiles.length > 0} +
+ +
+
+ {localModelfiles.length} Local Modelfiles Detected +
+ +
+ + + +
+
+ {/if} +
Made by OllamaHub Community
diff --git a/src/routes/(app)/modelfiles/create/+page.svelte b/src/routes/(app)/modelfiles/create/+page.svelte index 505ab02d..506edb9f 100644 --- a/src/routes/(app)/modelfiles/create/+page.svelte +++ b/src/routes/(app)/modelfiles/create/+page.svelte @@ -8,6 +8,8 @@ import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; import { splitStream } from '$lib/utils'; import { onMount, tick } from 'svelte'; + import { createModel } from '$lib/apis/ollama'; + import { createNewModelfile, getModelfileByTagName, getModelfiles } from '$lib/apis/modelfiles'; let loading = false; @@ -93,11 +95,8 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, ''); }; const saveModelfile = async (modelfile) => { - await modelfiles.set([ - ...$modelfiles.filter((m) => m.tagName !== modelfile.tagName), - modelfile - ]); - localStorage.setItem('modelfiles', JSON.stringify($modelfiles)); + await createNewModelfile(localStorage.token, modelfile); + await modelfiles.set(await getModelfiles(localStorage.token)); }; const submitHandler = async () => { @@ -112,7 +111,10 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, ''); return success; } - if ($models.includes(tagName)) { + if ( + $models.map((model) => model.name).includes(tagName) || + (await getModelfileByTagName(localStorage.token, tagName).catch(() => false)) + ) { toast.error( `Uh-oh! It looks like you already have a model named '${tagName}'. Please choose a different name to complete your modelfile.` ); @@ -128,18 +130,12 @@ SYSTEM """${system}"""`.replace(/^\s*\n/gm, ''); Object.keys(categories).filter((category) => categories[category]).length > 0 && !$models.includes(tagName) ) { - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ - name: tagName, - modelfile: content - }) - }); + const res = await createModel( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + tagName, + content + ); if (res) { const reader = res.body diff --git a/src/routes/(app)/modelfiles/edit/+page.svelte b/src/routes/(app)/modelfiles/edit/+page.svelte index 48d12239..19b7f1f0 100644 --- a/src/routes/(app)/modelfiles/edit/+page.svelte +++ b/src/routes/(app)/modelfiles/edit/+page.svelte @@ -2,14 +2,20 @@ import { v4 as uuidv4 } from 'uuid'; import { toast } from 'svelte-french-toast'; import { goto } from '$app/navigation'; - import { OLLAMA_API_BASE_URL } from '$lib/constants'; - import { settings, db, user, config, modelfiles } from '$lib/stores'; - import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; - import { splitStream } from '$lib/utils'; import { onMount } from 'svelte'; import { page } from '$app/stores'; + import { settings, db, user, config, modelfiles } from '$lib/stores'; + + import { OLLAMA_API_BASE_URL } from '$lib/constants'; + import { splitStream } from '$lib/utils'; + + import { createModel } from '$lib/apis/ollama'; + import { getModelfiles, updateModelfileByTagName } from '$lib/apis/modelfiles'; + + import Advanced from '$lib/components/chat/Settings/Advanced.svelte'; + let loading = false; let filesInputElement; @@ -78,17 +84,9 @@ } }); - const saveModelfile = async (modelfile) => { - await modelfiles.set( - $modelfiles.map((e) => { - if (e.tagName === modelfile.tagName) { - return modelfile; - } else { - return e; - } - }) - ); - localStorage.setItem('modelfiles', JSON.stringify($modelfiles)); + const updateModelfile = async (modelfile) => { + await updateModelfileByTagName(localStorage.token, modelfile.tagName, modelfile); + await modelfiles.set(await getModelfiles(localStorage.token)); }; const updateHandler = async () => { @@ -106,18 +104,12 @@ content !== '' && Object.keys(categories).filter((category) => categories[category]).length > 0 ) { - const res = await fetch(`${$settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL}/create`, { - method: 'POST', - headers: { - 'Content-Type': 'text/event-stream', - ...($settings.authHeader && { Authorization: $settings.authHeader }), - ...($user && { Authorization: `Bearer ${localStorage.token}` }) - }, - body: JSON.stringify({ - name: tagName, - modelfile: content - }) - }); + const res = await createModel( + $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, + localStorage.token, + tagName, + content + ); if (res) { const reader = res.body @@ -178,7 +170,7 @@ } if (success) { - await saveModelfile({ + await updateModelfile({ tagName: tagName, imageUrl: imageUrl, title: title, diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 7479f559..78b550d2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,56 +2,39 @@ import { onMount, tick } from 'svelte'; import { config, user } from '$lib/stores'; import { goto } from '$app/navigation'; - import { WEBUI_API_BASE_URL } from '$lib/constants'; import toast, { Toaster } from 'svelte-french-toast'; + import { getBackendConfig } from '$lib/apis'; + import { getSessionUser } from '$lib/apis/auths'; + import '../app.css'; import '../tailwind.css'; import 'tippy.js/dist/tippy.css'; + let loaded = false; onMount(async () => { - const resBackend = await fetch(`${WEBUI_API_BASE_URL}/`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json' - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - return null; - }); + // Check Backend Status + const backendConfig = await getBackendConfig(); - console.log(resBackend); - await config.set(resBackend); + if (backendConfig) { + // Save Backend Status to Store + await config.set(backendConfig); + console.log(backendConfig); - if ($config) { - if ($config.auth) { + if ($config) { if (localStorage.token) { - const res = await fetch(`${WEBUI_API_BASE_URL}/auths`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${localStorage.token}` - } - }) - .then(async (res) => { - if (!res.ok) throw await res.json(); - return res.json(); - }) - .catch((error) => { - console.log(error); - toast.error(error.detail); - return null; - }); + // Get Session User Info + const sessionUser = await getSessionUser(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); - if (res) { - await user.set(res); + if (sessionUser) { + // Save Session User to Store + await user.set(sessionUser); } else { + // Redirect Invalid Session User to /auth Page localStorage.removeItem('token'); await goto('/auth'); } @@ -59,6 +42,9 @@ await goto('/auth'); } } + } else { + // Redirect to /error when Backend Not Detected + await goto(`/error`); } await tick(); @@ -69,8 +55,9 @@ Ollama - -{#if $config !== undefined && loaded} +{#if loaded} {/if} + + diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index a3d33f2f..451c746d 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -1,5 +1,6 @@ -{#if loaded && $config && $config.auth} +{#if loaded}
@@ -91,7 +67,7 @@
-