diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 190270131..5a85d0879 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,6 @@ about: Create a report to help us improve title: '' labels: '' assignees: '' - --- # Bug Report @@ -31,6 +30,7 @@ assignees: '' ## Reproduction Details **Confirmation:** + - [ ] I have read and followed all the instructions provided in the README.md. - [ ] I have reviewed the troubleshooting.md document. - [ ] I have included the browser console logs. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index bbcbbe7d6..2f28cead0 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,7 +4,6 @@ about: Suggest an idea for this project title: '' labels: '' assignees: '' - --- **Is your feature request related to a problem? Please describe.** diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml new file mode 100644 index 000000000..9d767fa5b --- /dev/null +++ b/.github/workflows/format-backend.yaml @@ -0,0 +1,27 @@ +name: Python CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Format Backend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - latest + steps: + - uses: actions/checkout@v4 + - name: Use Python + uses: actions/setup-python@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install yapf + - name: Format backend + run: bun run format:backend diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml new file mode 100644 index 000000000..65916cf44 --- /dev/null +++ b/.github/workflows/format-build-frontend.yaml @@ -0,0 +1,22 @@ +name: Bun CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Format & Build Frontend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - run: bun --version + - name: Install frontend dependencies + run: bun install + - name: Format frontend + run: bun run format + - name: Build frontend + run: bun run build diff --git a/.github/workflows/lint-backend.disabled b/.github/workflows/lint-backend.disabled new file mode 100644 index 000000000..d220031cc --- /dev/null +++ b/.github/workflows/lint-backend.disabled @@ -0,0 +1,27 @@ +name: Python CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Lint Backend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - latest + steps: + - uses: actions/checkout@v4 + - name: Use Python + uses: actions/setup-python@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Lint backend + run: bun run lint:backend diff --git a/.github/workflows/lint-frontend.disabled b/.github/workflows/lint-frontend.disabled new file mode 100644 index 000000000..2c1cd3c5a --- /dev/null +++ b/.github/workflows/lint-frontend.disabled @@ -0,0 +1,21 @@ +name: Bun CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Lint Frontend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - run: bun --version + - name: Install frontend dependencies + run: bun install --frozen-lockfile + - run: bun run lint:frontend + - run: bun run lint:types + if: success() || failure() \ No newline at end of file diff --git a/.github/workflows/node.js.yaml b/.github/workflows/node.js.yaml deleted file mode 100644 index 20a04dc05..000000000 --- a/.github/workflows/node.js.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: Node.js CI -on: - push: - branches: ['main'] - pull_request: -jobs: - build: - name: 'Fmt, Lint, & Build' - env: - PUBLIC_API_BASE_URL: '' - runs-on: ubuntu-latest - strategy: - matrix: - node-version: - - latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - run: node --version - - run: npm clean-install - - run: npm run fmt - #- run: npm run lint - #- run: npm run lint:types - - run: npm run build diff --git a/Dockerfile b/Dockerfile index 2dd898136..2e57ee66a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,6 @@ FROM node:alpine as build -ARG OLLAMA_API_BASE_URL='/ollama/api' -RUN echo $OLLAMA_API_BASE_URL - -ENV PUBLIC_API_BASE_URL $OLLAMA_API_BASE_URL -RUN echo $PUBLIC_API_BASE_URL - WORKDIR /app COPY package.json package-lock.json ./ @@ -18,10 +12,13 @@ RUN npm run build FROM python:3.11-slim-buster as base -ARG OLLAMA_API_BASE_URL='/ollama/api' - ENV ENV=prod -ENV OLLAMA_API_BASE_URL $OLLAMA_API_BASE_URL + +ENV OLLAMA_API_BASE_URL "/ollama/api" + +ENV OPENAI_API_BASE_URL "" +ENV OPENAI_API_KEY "" + ENV WEBUI_JWT_SECRET_KEY "SECRET_KEY" WORKDIR /app diff --git a/README.md b/README.md index 808740ff8..f35d66783 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,15 @@ While we strongly recommend using our convenient Docker container installation f 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** +> [!IMPORTANT] +> The backend is required for proper functionality -### TL;DR πŸš€ +### Requirements πŸ“¦ + +- 🐰 [Bun](https://bun.sh) >= 1.0.21 or 🐒 [Node.js](https://nodejs.org/en) >= 20.10 +- 🐍 [Python](https://python.org) >= 3.11 + +### Build and Install πŸ› οΈ Run the following commands to install: @@ -211,13 +217,17 @@ cd ollama-webui/ # Copying required .env file cp -RPp example.env .env -# Building Frontend +# Building Frontend Using Node npm i npm run build +# or Building Frontend Using Bun +# bun install +# bun run build + # Serving Frontend with the Backend cd ./backend -pip install -r requirements.txt +pip install -r requirements.txt -U sh start.sh ``` diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md index 339c230fd..f7976550d 100644 --- a/TROUBLESHOOTING.md +++ b/TROUBLESHOOTING.md @@ -4,7 +4,7 @@ The Ollama WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. -- **How it Works**: When you make a request (like `/ollama/api/tags`) from the Ollama WebUI, it doesn’t go directly to the Ollama API. Instead, it first reaches the Ollama WebUI backend. The backend then forwards this request to the Ollama API via the route you define in the `OLLAMA_API_BASE_URL` environment variable. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend. +- **How it Works**: The Ollama WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Ollama WebUI backend via `/ollama/api` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_API_BASE_URL` environment variable. Therefore, a request made to `/ollama/api` in the WebUI is effectively the same as making a request to `OLLAMA_API_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_API_BASE_URL/tags` in the backend. - **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. @@ -27,6 +27,6 @@ docker run -d --network=host -v ollama-webui:/app/backend/data -e OLLAMA_API_BAS 1. **Verify Ollama URL Format**: - When running the Web UI container, ensure the `OLLAMA_API_BASE_URL` is correctly set, including the `/api` suffix. (e.g., `http://192.168.1.1:11434/api` for different host setups). - In the Ollama WebUI, navigate to "Settings" > "General". - - Confirm that the Ollama Server URL is correctly set to `/ollama/api`, including the `/api` suffix. + - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix. By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 7e138c39a..dc0c9d3fa 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -1,119 +1,111 @@ -from flask import Flask, request, Response, jsonify -from flask_cors import CORS - +from fastapi import FastAPI, Request, Response, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from fastapi.concurrency import run_in_threadpool import requests import json - +from pydantic import BaseModel from apps.web.models.users import Users from constants import ERROR_MESSAGES -from utils.utils import decode_token +from utils.utils import decode_token, get_current_user from config import OLLAMA_API_BASE_URL, WEBUI_AUTH -app = Flask(__name__) -CORS( - app -) # Enable Cross-Origin Resource Sharing (CORS) to allow requests from different domains +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) -# Define the target server URL -TARGET_SERVER_URL = OLLAMA_API_BASE_URL +app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL + +# TARGET_SERVER_URL = OLLAMA_API_BASE_URL -@app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE"]) -@app.route("/", methods=["GET", "POST", "PUT", "DELETE"]) -def proxy(path): - # Combine the base URL of the target server with the requested path - target_url = f"{TARGET_SERVER_URL}/{path}" - print(target_url) +@app.get("/url") +async def get_ollama_api_url(user=Depends(get_current_user)): + if user and user.role == "admin": + return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + else: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) - # Get data from the original request - data = request.get_data() + +class UrlUpdateForm(BaseModel): + url: str + + +@app.post("/url/update") +async def update_ollama_api_url( + form_data: UrlUpdateForm, user=Depends(get_current_user) +): + if user and user.role == "admin": + app.state.OLLAMA_API_BASE_URL = form_data.url + return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + else: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +async def proxy(path: str, request: Request, user=Depends(get_current_user)): + target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" + + body = await request.body() headers = dict(request.headers) - # Basic RBAC support - if WEBUI_AUTH: - if "Authorization" in headers: - _, credentials = headers["Authorization"].split() - token_data = decode_token(credentials) - if token_data is None or "email" not in token_data: - return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 - - user = Users.get_user_by_email(token_data["email"]) - if user: - # Only user and admin roles can access - if user.role in ["user", "admin"]: - if path in ["pull", "delete", "push", "copy", "create"]: - # Only admin role can perform actions above - if user.role == "admin": - pass - else: - return ( - jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), - 401, - ) - else: - pass - else: - return jsonify({"detail": ERROR_MESSAGES.ACCESS_PROHIBITED}), 401 - else: - return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 - else: - return jsonify({"detail": ERROR_MESSAGES.UNAUTHORIZED}), 401 + if user.role in ["user", "admin"]: + if path in ["pull", "delete", "push", "copy", "create"]: + if user.role != "admin": + raise HTTPException( + status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) else: - pass - - r = None + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) headers.pop("Host", None) headers.pop("Authorization", None) headers.pop("Origin", None) headers.pop("Referer", None) + r = None + + def get_request(): + nonlocal r + try: + r = requests.request( + method=request.method, + url=target_url, + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + return StreamingResponse( + r.iter_content(chunk_size=8192), + status_code=r.status_code, + headers=dict(r.headers), + ) + except Exception as e: + raise e + try: - # Make a request to the target server - r = requests.request( - method=request.method, - url=target_url, - data=data, - headers=headers, - stream=True, # Enable streaming for server-sent events - ) - - r.raise_for_status() - - # Proxy the target server's response to the client - def generate(): - for chunk in r.iter_content(chunk_size=8192): - yield chunk - - response = Response(generate(), status=r.status_code) - - # Copy headers from the target server's response to the client's response - for key, value in r.headers.items(): - response.headers[key] = value - - return response + return await run_in_threadpool(get_request) 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']}" - print(res) + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" - return ( - jsonify( - { - "detail": error_detail, - "message": str(e), - } - ), - 400, + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, ) - - -if __name__ == "__main__": - app.run(debug=True) diff --git a/backend/apps/ollama/old_main.py b/backend/apps/ollama/old_main.py new file mode 100644 index 000000000..961d19357 --- /dev/null +++ b/backend/apps/ollama/old_main.py @@ -0,0 +1,127 @@ +from fastapi import FastAPI, Request, Response, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse + +import requests +import json +from pydantic import BaseModel + +from apps.web.models.users import Users +from constants import ERROR_MESSAGES +from utils.utils import decode_token, get_current_user +from config import OLLAMA_API_BASE_URL, WEBUI_AUTH + +import aiohttp + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.OLLAMA_API_BASE_URL = OLLAMA_API_BASE_URL + +# TARGET_SERVER_URL = OLLAMA_API_BASE_URL + + +@app.get("/url") +async def get_ollama_api_url(user=Depends(get_current_user)): + if user and user.role == "admin": + return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + else: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +class UrlUpdateForm(BaseModel): + url: str + + +@app.post("/url/update") +async def update_ollama_api_url( + form_data: UrlUpdateForm, user=Depends(get_current_user) +): + if user and user.role == "admin": + app.state.OLLAMA_API_BASE_URL = form_data.url + return {"OLLAMA_API_BASE_URL": app.state.OLLAMA_API_BASE_URL} + else: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +# async def fetch_sse(method, target_url, body, headers): +# async with aiohttp.ClientSession() as session: +# try: +# async with session.request( +# method, target_url, data=body, headers=headers +# ) as response: +# print(response.status) +# async for line in response.content: +# yield line +# except Exception as e: +# print(e) +# error_detail = "Ollama WebUI: Server Connection Error" +# yield json.dumps({"error": error_detail, "message": str(e)}).encode() + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +async def proxy(path: str, request: Request, user=Depends(get_current_user)): + target_url = f"{app.state.OLLAMA_API_BASE_URL}/{path}" + print(target_url) + + body = await request.body() + headers = dict(request.headers) + + if user.role in ["user", "admin"]: + if path in ["pull", "delete", "push", "copy", "create"]: + if user.role != "admin": + raise HTTPException( + status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) + else: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + headers.pop("Host", None) + headers.pop("Authorization", None) + headers.pop("Origin", None) + headers.pop("Referer", None) + + session = aiohttp.ClientSession() + response = None + try: + response = await session.request( + request.method, target_url, data=body, headers=headers + ) + + print(response) + if not response.ok: + data = await response.json() + print(data) + response.raise_for_status() + + async def generate(): + async for line in response.content: + print(line) + yield line + await session.close() + + return StreamingResponse(generate(), response.status) + + except Exception as e: + print(e) + error_detail = "Ollama WebUI: Server Connection Error" + + if response is not None: + try: + res = await response.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + await session.close() + raise HTTPException( + status_code=response.status if response else 500, + detail=error_detail, + ) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py new file mode 100644 index 000000000..ef9330c55 --- /dev/null +++ b/backend/apps/openai/main.py @@ -0,0 +1,143 @@ +from fastapi import FastAPI, Request, Response, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, JSONResponse + +import requests +import json +from pydantic import BaseModel + +from apps.web.models.users import Users +from constants import ERROR_MESSAGES +from utils.utils import decode_token, get_current_user +from config import OPENAI_API_BASE_URL, OPENAI_API_KEY + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL +app.state.OPENAI_API_KEY = OPENAI_API_KEY + + +class UrlUpdateForm(BaseModel): + url: str + + +class KeyUpdateForm(BaseModel): + key: str + + +@app.get("/url") +async def get_openai_url(user=Depends(get_current_user)): + if user and user.role == "admin": + return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} + else: + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +@app.post("/url/update") +async def update_openai_url(form_data: UrlUpdateForm, + user=Depends(get_current_user)): + if user and user.role == "admin": + app.state.OPENAI_API_BASE_URL = form_data.url + return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL} + else: + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +@app.get("/key") +async def get_openai_key(user=Depends(get_current_user)): + if user and user.role == "admin": + return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} + else: + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +@app.post("/key/update") +async def update_openai_key(form_data: KeyUpdateForm, + user=Depends(get_current_user)): + if user and user.role == "admin": + app.state.OPENAI_API_KEY = form_data.key + return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY} + else: + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +async def proxy(path: str, request: Request, user=Depends(get_current_user)): + target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}" + print(target_url, app.state.OPENAI_API_KEY) + + if user.role not in ["user", "admin"]: + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED) + if app.state.OPENAI_API_KEY == "": + raise HTTPException(status_code=401, + detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + + body = await request.body() + # headers = dict(request.headers) + # print(headers) + + headers = {} + headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + try: + r = requests.request( + method=request.method, + url=target_url, + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + # Check if response is SSE + if "text/event-stream" in r.headers.get("Content-Type", ""): + return StreamingResponse( + r.iter_content(chunk_size=8192), + status_code=r.status_code, + headers=dict(r.headers), + ) + else: + # For non-SSE, read the response and return it + # response_data = ( + # r.json() + # if r.headers.get("Content-Type", "") + # == "application/json" + # else r.text + # ) + + response_data = r.json() + + print(type(response_data)) + + if "openai" in app.state.OPENAI_API_BASE_URL and path == "models": + response_data["data"] = list( + filter(lambda model: "gpt" in model["id"], + response_data["data"])) + + return response_data + except Exception as e: + print(e) + error_detail = "Ollama WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']}" + except: + error_detail = f"External: {e}" + + raise HTTPException(status_code=r.status_code, detail=error_detail) diff --git a/backend/apps/web/main.py b/backend/apps/web/main.py index 153b5dcbb..0030c25ed 100644 --- a/backend/apps/web/main.py +++ b/backend/apps/web/main.py @@ -22,10 +22,11 @@ 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(modelfiles.router, + prefix="/modelfiles", + tags=["modelfiles"]) app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) - app.include_router(configs.router, prefix="/configs", tags=["configs"]) 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 800750c3d..00c66a2c7 100644 --- a/backend/apps/web/models/auths.py +++ b/backend/apps/web/models/auths.py @@ -4,7 +4,6 @@ import time import uuid from peewee import * - from apps.web.models.users import UserModel, Users from utils.utils import ( verify_password, @@ -123,6 +122,15 @@ class AuthsTable: except: return False + def update_email_by_id(self, id: str, email: str) -> bool: + try: + query = Auth.update(email=email).where(Auth.id == id) + result = query.execute() + + return True if result == 1 else False + except: + return False + def delete_auth_by_id(self, id: str) -> bool: try: # Delete User diff --git a/backend/apps/web/models/chats.py b/backend/apps/web/models/chats.py index ebc17d9a5..bc4659de3 100644 --- a/backend/apps/web/models/chats.py +++ b/backend/apps/web/models/chats.py @@ -3,14 +3,12 @@ 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 #################### @@ -62,23 +60,23 @@ class ChatTitleIdResponse(BaseModel): 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]: + 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", + "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 @@ -111,27 +109,25 @@ class ChatTable: except: return None - def get_chat_lists_by_user_id( - self, user_id: str, skip: int = 0, limit: int = 50 - ) -> List[ChatModel]: + 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()) + 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()) + 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]: + 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)) @@ -146,7 +142,8 @@ class ChatTable: 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 = Chat.delete().where((Chat.id == id) + & (Chat.user_id == user_id)) query.execute() # Remove the rows, return number of rows removed. return True diff --git a/backend/apps/web/models/modelfiles.py b/backend/apps/web/models/modelfiles.py index 8231d8dff..51eccfb5e 100644 --- a/backend/apps/web/models/modelfiles.py +++ b/backend/apps/web/models/modelfiles.py @@ -58,13 +58,14 @@ class ModelfileResponse(BaseModel): 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]: + self, user_id: str, + form_data: ModelfileForm) -> Optional[ModelfileModel]: if "tagName" in form_data.modelfile: modelfile = ModelfileModel( **{ @@ -72,8 +73,7 @@ class ModelfilesTable: "tag_name": form_data.modelfile["tagName"], "modelfile": json.dumps(form_data.modelfile), "timestamp": int(time.time()), - } - ) + }) try: result = Modelfile.create(**modelfile.model_dump()) @@ -87,28 +87,29 @@ class ModelfilesTable: else: return None - def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]: + 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]: + 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() + "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]: + self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]: try: query = Modelfile.update( modelfile=json.dumps(modelfile), diff --git a/backend/apps/web/models/prompts.py b/backend/apps/web/models/prompts.py index bb0710b60..044a3697c 100644 --- a/backend/apps/web/models/prompts.py +++ b/backend/apps/web/models/prompts.py @@ -47,13 +47,13 @@ class PromptForm(BaseModel): class PromptsTable: + def __init__(self, db): self.db = db self.db.create_tables([Prompt]) - def insert_new_prompt( - self, user_id: str, form_data: PromptForm - ) -> Optional[PromptModel]: + def insert_new_prompt(self, user_id: str, + form_data: PromptForm) -> Optional[PromptModel]: prompt = PromptModel( **{ "user_id": user_id, @@ -61,8 +61,7 @@ class PromptsTable: "title": form_data.title, "content": form_data.content, "timestamp": int(time.time()), - } - ) + }) try: result = Prompt.create(**prompt.model_dump()) @@ -82,14 +81,13 @@ class PromptsTable: def get_prompts(self) -> List[PromptModel]: return [ - PromptModel(**model_to_dict(prompt)) - for prompt in Prompt.select() + PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select() # .limit(limit).offset(skip) ] def update_prompt_by_command( - self, command: str, form_data: PromptForm - ) -> Optional[PromptModel]: + self, command: str, + form_data: PromptForm) -> Optional[PromptModel]: try: query = Prompt.update( title=form_data.title, diff --git a/backend/apps/web/models/users.py b/backend/apps/web/models/users.py index b1de7c338..f86697f44 100644 --- a/backend/apps/web/models/users.py +++ b/backend/apps/web/models/users.py @@ -8,7 +8,6 @@ from utils.misc import get_gravatar_url from apps.web.internal.db import DB from apps.web.models.chats import Chats - #################### # User DB Schema #################### @@ -45,6 +44,13 @@ class UserRoleUpdateForm(BaseModel): role: str +class UserUpdateForm(BaseModel): + name: str + email: str + profile_image_url: str + password: Optional[str] = None + + class UsersTable: def __init__(self, db): self.db = db @@ -102,6 +108,16 @@ class UsersTable: except: return None + def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: + try: + query = User.update(**updated).where(User.id == id) + query.execute() + + user = User.get(User.id == id) + return UserModel(**model_to_dict(user)) + except: + return None + def delete_user_by_id(self, id: str) -> bool: try: # Delete User Chats diff --git a/backend/apps/web/routers/auths.py b/backend/apps/web/routers/auths.py index 714982e34..f245601dc 100644 --- a/backend/apps/web/routers/auths.py +++ b/backend/apps/web/routers/auths.py @@ -8,7 +8,6 @@ from pydantic import BaseModel import time import uuid - from apps.web.models.auths import ( SigninForm, SignupForm, @@ -19,12 +18,10 @@ from apps.web.models.auths import ( ) from apps.web.models.users import Users - from utils.utils import get_password_hash, get_current_user, create_token from utils.misc import get_gravatar_url, validate_email_format from constants import ERROR_MESSAGES - router = APIRouter() ############################ @@ -49,9 +46,8 @@ async def get_session_user(user=Depends(get_current_user)): @router.post("/update/password", response_model=bool) -async def update_password( - form_data: UpdatePasswordForm, session_user=Depends(get_current_user) -): +async def update_password(form_data: UpdatePasswordForm, + session_user=Depends(get_current_user)): if session_user: user = Auths.authenticate_user(session_user.email, form_data.password) @@ -101,9 +97,8 @@ async def signup(request: Request, form_data: SignupForm): try: role = "admin" if Users.get_num_users() == 0 else "pending" hashed = get_password_hash(form_data.password) - user = Auths.insert_new_auth( - form_data.email.lower(), hashed, form_data.name, role - ) + user = Auths.insert_new_auth(form_data.email.lower(), + hashed, form_data.name, role) if user: token = create_token(data={"email": user.email}) @@ -120,14 +115,15 @@ async def signup(request: Request, form_data: SignupForm): } else: raise HTTPException( - 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR - ) + 500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) except Exception as err: - raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + raise HTTPException(500, + detail=ERROR_MESSAGES.DEFAULT(err)) else: raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) else: - raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) + raise HTTPException(400, + detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT) else: raise HTTPException(400, detail=ERROR_MESSAGES.ACCESS_PROHIBITED) diff --git a/backend/apps/web/routers/chats.py b/backend/apps/web/routers/chats.py index 0eec45536..f061b145b 100644 --- a/backend/apps/web/routers/chats.py +++ b/backend/apps/web/routers/chats.py @@ -17,8 +17,7 @@ from apps.web.models.chats import ( ) from utils.utils import ( - bearer_scheme, -) + bearer_scheme, ) from constants import ERROR_MESSAGES router = APIRouter() @@ -30,8 +29,7 @@ router = APIRouter() @router.get("/", response_model=List[ChatTitleIdResponse]) async def get_user_chats( - user=Depends(get_current_user), skip: int = 0, limit: int = 50 -): + user=Depends(get_current_user), skip: int = 0, limit: int = 50): return Chats.get_chat_lists_by_user_id(user.id, skip, limit) @@ -43,8 +41,9 @@ async def get_user_chats( @router.get("/all", response_model=List[ChatResponse]) async def get_all_user_chats(user=Depends(get_current_user)): return [ - ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) - for chat in Chats.get_all_chats_by_user_id(user.id) + ChatResponse(**{ + **chat.model_dump(), "chat": json.loads(chat.chat) + }) for chat in Chats.get_all_chats_by_user_id(user.id) ] @@ -69,11 +68,12 @@ async def get_chat_by_id(id: str, user=Depends(get_current_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)}) + return ChatResponse(**{ + **chat.model_dump(), "chat": json.loads(chat.chat) + }) else: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND - ) + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND) ############################ @@ -82,15 +82,17 @@ async def get_chat_by_id(id: str, user=Depends(get_current_user)): @router.post("/{id}", response_model=Optional[ChatResponse]) -async def update_chat_by_id( - id: str, form_data: ChatForm, user=Depends(get_current_user) -): +async def update_chat_by_id(id: str, + form_data: ChatForm, + user=Depends(get_current_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)}) + return ChatResponse(**{ + **chat.model_dump(), "chat": json.loads(chat.chat) + }) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/apps/web/routers/configs.py b/backend/apps/web/routers/configs.py index b57fae3d5..4dfe79fd7 100644 --- a/backend/apps/web/routers/configs.py +++ b/backend/apps/web/routers/configs.py @@ -10,7 +10,6 @@ import uuid from apps.web.models.users import Users - from utils.utils import get_password_hash, get_current_user, create_token from utils.misc import get_gravatar_url, validate_email_format from constants import ERROR_MESSAGES @@ -28,9 +27,9 @@ class SetDefaultModelsForm(BaseModel): @router.post("/default/models", response_model=str) -async def set_global_default_models( - request: Request, form_data: SetDefaultModelsForm, user=Depends(get_current_user) -): +async def set_global_default_models(request: Request, + form_data: SetDefaultModelsForm, + user=Depends(get_current_user)): if user.role == "admin": request.app.state.DEFAULT_MODELS = form_data.models return request.app.state.DEFAULT_MODELS diff --git a/backend/apps/web/routers/modelfiles.py b/backend/apps/web/routers/modelfiles.py index 841d534d9..0af9ca0f5 100644 --- a/backend/apps/web/routers/modelfiles.py +++ b/backend/apps/web/routers/modelfiles.py @@ -24,7 +24,9 @@ router = APIRouter() @router.get("/", response_model=List[ModelfileResponse]) -async def get_modelfiles(skip: int = 0, limit: int = 50, user=Depends(get_current_user)): +async def get_modelfiles(skip: int = 0, + limit: int = 50, + user=Depends(get_current_user)): return Modelfiles.get_modelfiles(skip, limit) @@ -34,9 +36,8 @@ async def get_modelfiles(skip: int = 0, limit: int = 50, user=Depends(get_curren @router.post("/create", response_model=Optional[ModelfileResponse]) -async def create_new_modelfile( - form_data: ModelfileForm, user=Depends(get_current_user) -): +async def create_new_modelfile(form_data: ModelfileForm, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -49,9 +50,9 @@ async def create_new_modelfile( return ModelfileResponse( **{ **modelfile.model_dump(), - "modelfile": json.loads(modelfile.modelfile), - } - ) + "modelfile": + json.loads(modelfile.modelfile), + }) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -65,16 +66,17 @@ async def create_new_modelfile( @router.post("/", response_model=Optional[ModelfileResponse]) -async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, user=Depends(get_current_user)): +async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, + user=Depends(get_current_user)): modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) if modelfile: return ModelfileResponse( **{ **modelfile.model_dump(), - "modelfile": json.loads(modelfile.modelfile), - } - ) + "modelfile": + json.loads(modelfile.modelfile), + }) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -88,9 +90,8 @@ async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, user=Depend @router.post("/update", response_model=Optional[ModelfileResponse]) -async def update_modelfile_by_tag_name( - form_data: ModelfileUpdateForm, user=Depends(get_current_user) -): +async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -104,15 +105,14 @@ async def update_modelfile_by_tag_name( } modelfile = Modelfiles.update_modelfile_by_tag_name( - form_data.tag_name, updated_modelfile - ) + form_data.tag_name, updated_modelfile) return ModelfileResponse( **{ **modelfile.model_dump(), - "modelfile": json.loads(modelfile.modelfile), - } - ) + "modelfile": + json.loads(modelfile.modelfile), + }) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -126,9 +126,8 @@ async def update_modelfile_by_tag_name( @router.delete("/delete", response_model=bool) -async def delete_modelfile_by_tag_name( - form_data: ModelfileTagNameForm, user=Depends(get_current_user) -): +async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/apps/web/routers/prompts.py b/backend/apps/web/routers/prompts.py index 5a002c941..e812555d0 100644 --- a/backend/apps/web/routers/prompts.py +++ b/backend/apps/web/routers/prompts.py @@ -6,7 +6,6 @@ from fastapi import APIRouter from pydantic import BaseModel import json - from apps.web.models.prompts import Prompts, PromptForm, PromptModel from utils.utils import get_current_user @@ -30,7 +29,8 @@ async def get_prompts(user=Depends(get_current_user)): @router.post("/create", response_model=Optional[PromptModel]) -async def create_new_prompt(form_data: PromptForm, user=Depends(get_current_user)): +async def create_new_prompt(form_data: PromptForm, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -79,9 +79,9 @@ async def get_prompt_by_command(command: str, user=Depends(get_current_user)): @router.post("/{command}/update", response_model=Optional[PromptModel]) -async def update_prompt_by_command( - command: str, form_data: PromptForm, user=Depends(get_current_user) -): +async def update_prompt_by_command(command: str, + form_data: PromptForm, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, @@ -104,7 +104,8 @@ async def update_prompt_by_command( @router.delete("/{command}/delete", response_model=bool) -async def delete_prompt_by_command(command: str, user=Depends(get_current_user)): +async def delete_prompt_by_command(command: str, + user=Depends(get_current_user)): if user.role != "admin": raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index b3b552682..32d1e67f1 100644 --- a/backend/apps/web/routers/users.py +++ b/backend/apps/web/routers/users.py @@ -8,14 +8,12 @@ from pydantic import BaseModel import time import uuid -from apps.web.models.users import UserModel, UserRoleUpdateForm, Users +from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users from apps.web.models.auths import Auths - -from utils.utils import get_current_user +from utils.utils import get_current_user, get_password_hash from constants import ERROR_MESSAGES - router = APIRouter() ############################ @@ -57,6 +55,62 @@ async def update_user_role( ) +############################ +# UpdateUserById +############################ + + +@router.post("/{user_id}/update", response_model=Optional[UserModel]) +async def update_user_by_id( + user_id: str, form_data: UserUpdateForm, session_user=Depends(get_current_user) +): + if session_user.role != "admin": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + user = Users.get_user_by_id(user_id) + + if user: + if form_data.email.lower() != user.email: + email_user = Users.get_user_by_email(form_data.email.lower()) + if email_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.EMAIL_TAKEN, + ) + + if form_data.password: + hashed = get_password_hash(form_data.password) + print(hashed) + Auths.update_user_password_by_id(user_id, hashed) + + Auths.update_email_by_id(user_id, form_data.email.lower()) + updated_user = Users.update_user_by_id( + user_id, + { + "name": form_data.name, + "email": form_data.email.lower(), + "profile_image_url": form_data.profile_image_url, + }, + ) + + if updated_user: + return updated_user + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + ############################ # DeleteUserById ############################ diff --git a/backend/apps/web/routers/utils.py b/backend/apps/web/routers/utils.py index e9986bab4..588ef5a63 100644 --- a/backend/apps/web/routers/utils.py +++ b/backend/apps/web/routers/utils.py @@ -9,12 +9,10 @@ import os import aiohttp import json - from utils.misc import calculate_sha256 from config import OLLAMA_API_BASE_URL - router = APIRouter() @@ -42,7 +40,10 @@ def parse_huggingface_url(hf_url): return None -async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024): +async def download_file_stream(url, + file_path, + file_name, + chunk_size=1024 * 1024): done = False if os.path.exists(file_path): @@ -56,7 +57,8 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024 async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url, headers=headers) as response: - total_size = int(response.headers.get("content-length", 0)) + current_size + total_size = int(response.headers.get("content-length", + 0)) + current_size with open(file_path, "ab+") as file: async for data in response.content.iter_chunked(chunk_size): @@ -89,9 +91,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024 @router.get("/download") -async def download( - url: str, -): +async def download(url: str, ): # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" file_name = parse_huggingface_url(url) @@ -161,4 +161,5 @@ async def upload(file: UploadFile = File(...)): res = {"error": str(e)} yield f"data: {json.dumps(res)}\n\n" - return StreamingResponse(file_write_stream(), media_type="text/event-stream") + return StreamingResponse(file_write_stream(), + media_type="text/event-stream") diff --git a/backend/config.py b/backend/config.py index 8e100fe55..4c518d139 100644 --- a/backend/config.py +++ b/backend/config.py @@ -19,19 +19,28 @@ ENV = os.environ.get("ENV", "dev") # OLLAMA_API_BASE_URL #################################### -OLLAMA_API_BASE_URL = os.environ.get( - "OLLAMA_API_BASE_URL", "http://localhost:11434/api" -) +OLLAMA_API_BASE_URL = os.environ.get("OLLAMA_API_BASE_URL", + "http://localhost:11434/api") if ENV == "prod": if OLLAMA_API_BASE_URL == "/ollama/api": OLLAMA_API_BASE_URL = "http://host.docker.internal:11434/api" +#################################### +# OPENAI_API +#################################### + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") +OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") + +if OPENAI_API_BASE_URL == "": + OPENAI_API_BASE_URL = "https://api.openai.com/v1" + #################################### # WEBUI_VERSION #################################### -WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.42") +WEBUI_VERSION = os.environ.get("WEBUI_VERSION", "v1.0.0-alpha.50") #################################### # WEBUI_AUTH (Required for security) diff --git a/backend/constants.py b/backend/constants.py index 0817445b5..c3fd0dc5f 100644 --- a/backend/constants.py +++ b/backend/constants.py @@ -6,6 +6,7 @@ class MESSAGES(str, Enum): class ERROR_MESSAGES(str, Enum): + def __str__(self) -> str: return super().__str__() @@ -29,8 +30,8 @@ class ERROR_MESSAGES(str, Enum): UNAUTHORIZED = "401 Unauthorized" ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." ACTION_PROHIBITED = ( - "The requested action has been restricted as a security measure." - ) + "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 :/" + API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." MALICIOUS = "Unusual activities detected, please try again in a few minutes." diff --git a/backend/main.py b/backend/main.py index 24bad0c92..0315e5f51 100644 --- a/backend/main.py +++ b/backend/main.py @@ -6,12 +6,15 @@ from fastapi.middleware.cors import CORSMiddleware from starlette.exceptions import HTTPException as StarletteHTTPException from apps.ollama.main import app as ollama_app +from apps.openai.main import app as openai_app + from apps.web.main import app as webui_app import time class SPAStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): try: return await super().get_response(path, scope) @@ -46,5 +49,9 @@ async def check_url(request: Request, call_next): app.mount("/api/v1", webui_app) -app.mount("/ollama/api", WSGIMiddleware(ollama_app)) -app.mount("/", SPAStaticFiles(directory="../build", html=True), name="spa-static-files") +app.mount("/ollama/api", ollama_app) +app.mount("/openai/api", openai_app) + +app.mount("/", + SPAStaticFiles(directory="../build", html=True), + name="spa-static-files") diff --git a/backend/utils/utils.py b/backend/utils/utils.py index f98644f58..be500ce3b 100644 --- a/backend/utils/utils.py +++ b/backend/utils/utils.py @@ -8,9 +8,12 @@ from passlib.context import CryptContext from datetime import datetime, timedelta import requests import jwt - +import logging import config +logging.getLogger("passlib").setLevel(logging.ERROR) + + JWT_SECRET_KEY = config.WEBUI_JWT_SECRET_KEY ALGORITHM = "HS256" diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 000000000..7768741dc Binary files /dev/null and b/bun.lockb differ diff --git a/demo.gif b/demo.gif index 9f4cc6e22..014f40e2b 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/docker-compose.data.yaml b/docker-compose.data.yaml index 57d6fc372..a8f9f77b0 100644 --- a/docker-compose.data.yaml +++ b/docker-compose.data.yaml @@ -3,4 +3,4 @@ version: '3.8' services: ollama: volumes: - - ${OLLAMA_DATA_DIR-./ollama-data}:/root/.ollama \ No newline at end of file + - ${OLLAMA_DATA_DIR-./ollama-data}:/root/.ollama diff --git a/docker-compose.yaml b/docker-compose.yaml index 47263b0ab..7cd1bde0c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,7 +25,7 @@ services: ports: - ${OLLAMA_WEBUI_PORT-3000}:8080 environment: - - "OLLAMA_API_BASE_URL=http://ollama:11434/api" + - 'OLLAMA_API_BASE_URL=http://ollama:11434/api' extra_hosts: - host.docker.internal:host-gateway restart: unless-stopped diff --git a/example.env b/example.env index 9c628b427..74d52223f 100644 --- a/example.env +++ b/example.env @@ -1,12 +1,6 @@ -# If you're serving both the frontend and backend (Recommended) -# Set the public API base URL for seamless communication -PUBLIC_API_BASE_URL='/ollama/api' - -# If you're serving only the frontend (Not recommended and not fully supported) -# Comment above and Uncomment below -# You can use the default value or specify a custom path, e.g., '/api' -# PUBLIC_API_BASE_URL='http://{location.hostname}:11434/api' - # Ollama URL for the backend to connect # The path '/ollama/api' will be redirected to the specified backend URL OLLAMA_API_BASE_URL='http://localhost:11434/api' + +OPENAI_API_BASE_URL='' +OPENAI_API_KEY='' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index af8790a04..a234d4e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", + "dayjs": "^1.11.10", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", "idb": "^7.1.1", @@ -22,12 +23,13 @@ "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-static": "^2.0.3", - "@sveltejs/kit": "^1.20.4", + "@sveltejs/kit": "^1.30.0", "@tailwindcss/typography": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@types/bun": "latest", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", "autoprefixer": "^10.4.16", - "eslint": "^8.28.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", "postcss": "^8.4.31", @@ -429,9 +431,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -452,9 +454,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -469,12 +471,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" }, @@ -496,9 +498,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "node_modules/@jridgewell/gen-mapping": { @@ -783,12 +785,12 @@ } }, "node_modules/@sveltejs/kit": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.26.0.tgz", - "integrity": "sha512-CV/AlTziC05yrz7UjVqEd0pH6+2dnrbmcnHGr2d3jXtmOgzNnlDkXtX8g3BfJ6nntsPD+0jtS2PzhvRHblRz4A==", + "version": "1.30.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz", + "integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==", "hasInstallScript": true, "dependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.1", + "@sveltejs/vite-plugin-svelte": "^2.5.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", "devalue": "^4.3.1", @@ -809,14 +811,14 @@ "node": "^16.14 || >=18" }, "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0-next.0", + "svelte": "^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0", "vite": "^4.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", - "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz", + "integrity": "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==", "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", "debug": "^4.3.4", @@ -830,7 +832,7 @@ "node": "^14.18.0 || >= 16" }, "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0", + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0", "vite": "^4.0.0" } }, @@ -878,6 +880,15 @@ "node": ">=4" } }, + "node_modules/@types/bun": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.0.0.tgz", + "integrity": "sha512-TPI/aImv/fSo0SWlt29wq0tWRqQOWsC4FOXYeUK0Ni6tAS+FqJZ2p7QCGY4hmHaHQeE2KhKJ6Qn9k3kvFfXD3Q==", + "dev": true, + "dependencies": { + "bun-types": "1.0.18" + } + }, "node_modules/@types/cookie": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", @@ -889,9 +900,9 @@ "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==" }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/pug": { @@ -906,22 +917,22 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", + "integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/type-utils": "6.17.0", + "@typescript-eslint/utils": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -947,15 +958,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4" }, "engines": { @@ -975,13 +986,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -992,13 +1003,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz", + "integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/utils": "6.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1019,9 +1030,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1032,16 +1043,17 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" }, @@ -1058,18 +1070,42 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz", + "integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", "semver": "^7.5.4" }, "engines": { @@ -1084,12 +1120,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.17.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1100,6 +1136,12 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "node_modules/acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -1341,6 +1383,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bun-types": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.18.tgz", + "integrity": "sha512-1XZ7AxOF8oO8FZtw1xj006JAKxEjulK3dUhsktZVN95vXBlsf4NIjQxfistVdpt24v3H2I9BwHp+UU+gXSSpAw==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1530,6 +1578,11 @@ "node": ">=4" } }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1688,18 +1741,19 @@ } }, "node_modules/eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -2071,9 +2125,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3025,9 +3079,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -3886,11 +3940,11 @@ } }, "node_modules/vitefu": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", - "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { "vite": { @@ -4116,9 +4170,9 @@ "dev": true }, "@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -4133,9 +4187,9 @@ } }, "@eslint/js": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.51.0.tgz", - "integrity": "sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", + "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", "dev": true }, "@fastify/busboy": { @@ -4144,12 +4198,12 @@ "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==" }, "@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^1.2.1", + "@humanwhocodes/object-schema": "^2.0.1", "debug": "^4.1.1", "minimatch": "^3.0.5" } @@ -4161,9 +4215,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, "@jridgewell/gen-mapping": { @@ -4366,11 +4420,11 @@ "requires": {} }, "@sveltejs/kit": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.26.0.tgz", - "integrity": "sha512-CV/AlTziC05yrz7UjVqEd0pH6+2dnrbmcnHGr2d3jXtmOgzNnlDkXtX8g3BfJ6nntsPD+0jtS2PzhvRHblRz4A==", + "version": "1.30.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.3.tgz", + "integrity": "sha512-0DzVXfU4h+tChFvoc8C61IqErCyskD4ydSIDjpKS2lYlEzIYrtYrY7juSqACFxqcvZAnOEXvSY+zZ8br0+ZMMg==", "requires": { - "@sveltejs/vite-plugin-svelte": "^2.4.1", + "@sveltejs/vite-plugin-svelte": "^2.5.0", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", "devalue": "^4.3.1", @@ -4386,9 +4440,9 @@ } }, "@sveltejs/vite-plugin-svelte": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz", - "integrity": "sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz", + "integrity": "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==", "requires": { "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", "debug": "^4.3.4", @@ -4431,6 +4485,15 @@ } } }, + "@types/bun": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.0.0.tgz", + "integrity": "sha512-TPI/aImv/fSo0SWlt29wq0tWRqQOWsC4FOXYeUK0Ni6tAS+FqJZ2p7QCGY4hmHaHQeE2KhKJ6Qn9k3kvFfXD3Q==", + "dev": true, + "requires": { + "bun-types": "1.0.18" + } + }, "@types/cookie": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.2.tgz", @@ -4442,9 +4505,9 @@ "integrity": "sha512-VeiPZ9MMwXjO32/Xu7+OwflfmeoRwkE/qzndw42gGtgJwZopBnzy2gD//NN1+go1mADzkDcqf/KnFRSjTJ8xJA==" }, "@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "@types/pug": { @@ -4459,22 +4522,22 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" }, "@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.4.tgz", - "integrity": "sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.17.0.tgz", + "integrity": "sha512-Vih/4xLXmY7V490dGwBQJTpIZxH4ZFH6eCVmQ4RFkB+wmaCTDAx4dtgoWwMNGKLkqRY1L6rPqzEbjorRnDo4rQ==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/type-utils": "6.7.4", - "@typescript-eslint/utils": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/type-utils": "6.17.0", + "@typescript-eslint/utils": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -4484,86 +4547,113 @@ } }, "@typescript-eslint/parser": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.4.tgz", - "integrity": "sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.17.0.tgz", + "integrity": "sha512-C4bBaX2orvhK+LlwrY8oWGmSl4WolCfYm513gEccdWZj0CwGadbIADb0FtVEcI+WzUyjyoBj2JRP8g25E6IB8A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.4.tgz", - "integrity": "sha512-SdGqSLUPTXAXi7c3Ob7peAGVnmMoGzZ361VswK2Mqf8UOYcODiYvs8rs5ILqEdfvX1lE7wEZbLyELCW+Yrql1A==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.17.0.tgz", + "integrity": "sha512-RX7a8lwgOi7am0k17NUO0+ZmMOX4PpjLtLRgLmT1d3lBYdWH4ssBUbwdmc5pdRX8rXon8v9x8vaoOSpkHfcXGA==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4" + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0" } }, "@typescript-eslint/type-utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.4.tgz", - "integrity": "sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.17.0.tgz", + "integrity": "sha512-hDXcWmnbtn4P2B37ka3nil3yi3VCQO2QEB9gBiHJmQp5wmyQWqnjA85+ZcE8c4FqnaB6lBwMrPkgd4aBYz3iNg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.7.4", - "@typescript-eslint/utils": "6.7.4", + "@typescript-eslint/typescript-estree": "6.17.0", + "@typescript-eslint/utils": "6.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.4.tgz", - "integrity": "sha512-o9XWK2FLW6eSS/0r/tgjAGsYasLAnOWg7hvZ/dGYSSNjCh+49k5ocPN8OmG5aZcSJ8pclSOyVKP2x03Sj+RrCA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.17.0.tgz", + "integrity": "sha512-qRKs9tvc3a4RBcL/9PXtKSehI/q8wuU9xYJxe97WFxnzH8NWWtcW3ffNS+EWg8uPvIerhjsEZ+rHtDqOCiH57A==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.4.tgz", - "integrity": "sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.17.0.tgz", + "integrity": "sha512-gVQe+SLdNPfjlJn5VNGhlOhrXz4cajwFd5kAgWtZ9dCZf4XJf8xmgCTLIqec7aha3JwgLI2CK6GY1043FRxZwg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/visitor-keys": "6.7.4", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/visitor-keys": "6.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", + "minimatch": "9.0.3", "semver": "^7.5.4", "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + } } }, "@typescript-eslint/utils": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.4.tgz", - "integrity": "sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.17.0.tgz", + "integrity": "sha512-LofsSPjN/ITNkzV47hxas2JCsNCEnGhVvocfyOcLzT9c/tSZE7SfhS/iWtzP1lKNOEfLhRTZz6xqI8N2RzweSQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.4", - "@typescript-eslint/types": "6.7.4", - "@typescript-eslint/typescript-estree": "6.7.4", + "@typescript-eslint/scope-manager": "6.17.0", + "@typescript-eslint/types": "6.17.0", + "@typescript-eslint/typescript-estree": "6.17.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.7.4", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.4.tgz", - "integrity": "sha512-pOW37DUhlTZbvph50x5zZCkFn3xzwkGtNoJHzIM3svpiSkJzwOYr/kVBaXmf+RAQiUDs1AHEZVNPg6UJCJpwRA==", + "version": "6.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.17.0.tgz", + "integrity": "sha512-H6VwB/k3IuIeQOyYczyyKN8wH6ed8EwliaYHLxOIhyF0dYEIsN8+Bk3GE19qafeMKyZJJHP8+O1HiFhFLUNKSg==", "dev": true, "requires": { - "@typescript-eslint/types": "6.7.4", + "@typescript-eslint/types": "6.17.0", "eslint-visitor-keys": "^3.4.1" } }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, "acorn": { "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", @@ -4720,6 +4810,12 @@ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==" }, + "bun-types": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.18.tgz", + "integrity": "sha512-1XZ7AxOF8oO8FZtw1xj006JAKxEjulK3dUhsktZVN95vXBlsf4NIjQxfistVdpt24v3H2I9BwHp+UU+gXSSpAw==", + "dev": true + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4850,6 +4946,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4969,18 +5070,19 @@ "dev": true }, "eslint": { - "version": "8.51.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.51.0.tgz", - "integrity": "sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", + "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.51.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.56.0", + "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -5251,9 +5353,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -5905,9 +6007,9 @@ "requires": {} }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "queue-microtask": { @@ -6430,9 +6532,9 @@ } }, "vitefu": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", - "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", "requires": {} }, "which": { diff --git a/package.json b/package.json index 9c3b6ddfd..853738053 100644 --- a/package.json +++ b/package.json @@ -8,22 +8,23 @@ "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "lint": "npm run eslint", + "lint": "npm run lint:frontend ; npm run lint:types ; npm run lint:backend", + "lint:frontend": "eslint . --fix", "lint:types": "npm run check", - "fmt": "npm run prettier:svelte && npm run prettier", - "eslint": "npx -p eslint@8 -- eslint .", - "prettier:svelte": "npx -p prettier@2 -- prettier --plugin-search-dir . --write .", - "prettier": "npx -p prettier@2 -- prettier --write '**/*.{js,css,md,html,json}'" + "lint:backend": "pylint backend/", + "format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", + "format:backend": "yapf --recursive backend -p -i" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", "@sveltejs/adapter-static": "^2.0.3", - "@sveltejs/kit": "^1.20.4", + "@sveltejs/kit": "^1.30.0", "@tailwindcss/typography": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^6.0.0", - "@typescript-eslint/parser": "^6.0.0", + "@types/bun": "latest", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", "autoprefixer": "^10.4.16", - "eslint": "^8.28.0", + "eslint": "^8.56.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-svelte": "^2.30.0", "postcss": "^8.4.31", @@ -39,6 +40,7 @@ "type": "module", "dependencies": { "@sveltejs/adapter-node": "^1.3.1", + "dayjs": "^1.11.10", "file-saver": "^2.0.5", "highlight.js": "^11.9.0", "idb": "^7.1.1", diff --git a/run-ollama-docker.sh b/run-ollama-docker.sh new file mode 100644 index 000000000..5942fd3c8 --- /dev/null +++ b/run-ollama-docker.sh @@ -0,0 +1,7 @@ +docker rm -f ollama || true +docker pull ollama/ollama +# CPU Only +docker run -d -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama +# GPU Support +# docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama +docker image prune -f \ No newline at end of file diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts index 198ea4184..10eddc1c1 100644 --- a/src/lib/apis/ollama/index.ts +++ b/src/lib/apis/ollama/index.ts @@ -1,12 +1,76 @@ import { OLLAMA_API_BASE_URL } from '$lib/constants'; -export const getOllamaVersion = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string = '' -) => { +export const getOllamaAPIUrl = async (token: string = '') => { let error = null; - const res = await fetch(`${base_url}/version`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/url`, { + 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.OLLAMA_API_BASE_URL; +}; + +export const updateOllamaAPIUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url + }) + }) + .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.OLLAMA_API_BASE_URL; +}; + +export const getOllamaVersion = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/version`, { method: 'GET', headers: { Accept: 'application/json', @@ -35,13 +99,10 @@ export const getOllamaVersion = async ( return res?.version ?? ''; }; -export const getOllamaModels = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string = '' -) => { +export const getOllamaModels = async (token: string = '') => { let error = null; - const res = await fetch(`${base_url}/tags`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/tags`, { method: 'GET', headers: { Accept: 'application/json', @@ -67,18 +128,15 @@ export const getOllamaModels = async ( throw error; } - return res?.models ?? []; + return (res?.models ?? []).sort((a, b) => { + return a.name.localeCompare(b.name); + }); }; -export const generateTitle = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string = '', - model: string, - prompt: string -) => { +export const generateTitle = async (token: string = '', model: string, prompt: string) => { let error = null; - const res = await fetch(`${base_url}/generate`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/generate`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -86,7 +144,7 @@ export const generateTitle = async ( }, 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}`, + prompt: `Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': ${prompt}`, stream: false }) }) @@ -109,14 +167,10 @@ export const generateTitle = async ( return res?.response ?? 'New Chat'; }; -export const generateChatCompletion = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string = '', - body: object -) => { +export const generateChatCompletion = async (token: string = '', body: object) => { let error = null; - const res = await fetch(`${base_url}/chat`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/chat`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -135,15 +189,10 @@ export const generateChatCompletion = async ( return res; }; -export const createModel = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string, - tagName: string, - content: string -) => { +export const createModel = async (token: string, tagName: string, content: string) => { let error = null; - const res = await fetch(`${base_url}/create`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/create`, { method: 'POST', headers: { 'Content-Type': 'text/event-stream', @@ -165,14 +214,10 @@ export const createModel = async ( return res; }; -export const deleteModel = async ( - base_url: string = OLLAMA_API_BASE_URL, - token: string, - tagName: string -) => { +export const deleteModel = async (token: string, tagName: string) => { let error = null; - const res = await fetch(`${base_url}/delete`, { + const res = await fetch(`${OLLAMA_API_BASE_URL}/delete`, { method: 'DELETE', headers: { 'Content-Type': 'text/event-stream', @@ -202,3 +247,27 @@ export const deleteModel = async ( return res; }; + +export const pullModel = async (token: string, tagName: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/pull`, { + method: 'POST', + headers: { + 'Content-Type': 'text/event-stream', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts index 897762684..edebf693c 100644 --- a/src/lib/apis/openai/index.ts +++ b/src/lib/apis/openai/index.ts @@ -1,4 +1,176 @@ -export const getOpenAIModels = async ( +import { OPENAI_API_BASE_URL } from '$lib/constants'; + +export const getOpenAIUrl = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/url`, { + 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.OPENAI_API_BASE_URL; +}; + +export const updateOpenAIUrl = async (token: string = '', url: string) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url + }) + }) + .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.OPENAI_API_BASE_URL; +}; + +export const getOpenAIKey = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/key`, { + 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.OPENAI_API_KEY; +}; + +export const updateOpenAIKey = async (token: string = '', key: string) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + key: key + }) + }) + .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.OPENAI_API_KEY; +}; + +export const getOpenAIModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/models`, { + 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); + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + const models = Array.isArray(res) ? res : res?.data ?? null; + + return models + ? models + .map((model) => ({ name: model.id, external: true })) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }) + : models; +}; + +export const getOpenAIModelsDirect = async ( base_url: string = 'https://api.openai.com/v1', api_key: string = '' ) => { @@ -25,9 +197,35 @@ export const getOpenAIModels = async ( throw error; } - let models = Array.isArray(res) ? res : res?.data ?? null; + const 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)); + .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }); +}; + +export const generateOpenAIChatCompletion = async (token: string = '', body: object) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/chat/completions`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }).catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; }; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts index 3fca8b99a..3faeb8c46 100644 --- a/src/lib/apis/users/index.ts +++ b/src/lib/apis/users/index.ts @@ -84,3 +84,43 @@ export const deleteUserById = async (token: string, userId: string) => { return res; }; + +type UserUpdateForm = { + profile_image_url: string; + email: string; + name: string; + password: string; +}; + +export const updateUserById = async (token: string, userId: string, user: UserUpdateForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + profile_image_url: user.profile_image_url, + email: user.email, + name: user.name, + password: user.password !== '' ? user.password : undefined + }) + }) + .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/components/admin/EditUserModal.svelte b/src/lib/components/admin/EditUserModal.svelte new file mode 100644 index 000000000..09005b30a --- /dev/null +++ b/src/lib/components/admin/EditUserModal.svelte @@ -0,0 +1,172 @@ + + + +
+
+
Edit User
+ +
+
+ +
+
+
{ + submitHandler(); + }} + > +
+
+ User profile +
+ +
+
{selectedUser.name}
+ +
+ Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')} +
+
+
+ +
+ +
+
+
Email
+ +
+ +
+
+ +
+
Name
+ +
+ +
+
+ +
+
New Password
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + diff --git a/src/lib/components/chat/MessageInput/Suggestions.svelte b/src/lib/components/chat/MessageInput/Suggestions.svelte index 58c75fd19..f0d4b8815 100644 --- a/src/lib/components/chat/MessageInput/Suggestions.svelte +++ b/src/lib/components/chat/MessageInput/Suggestions.svelte @@ -7,7 +7,7 @@ {#each suggestionPrompts as prompt, promptIdx}
diff --git a/src/lib/components/chat/Messages/Placeholder.svelte b/src/lib/components/chat/Messages/Placeholder.svelte index 1d709b5f4..59b47ba4b 100644 --- a/src/lib/components/chat/Messages/Placeholder.svelte +++ b/src/lib/components/chat/Messages/Placeholder.svelte @@ -27,7 +27,7 @@ > {#if model in modelfiles} modelfileModels - {/if} - + + {/if} - + + + -
- The field above should be set to '/ollama/api'; - - Click here for help. - +
+ Trouble accessing Ollama? + + Click here for help. + +
- + {/if}
@@ -1103,7 +1017,6 @@ class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" on:click={() => { saveSettings({ - API_BASE_URL: API_BASE_URL === '' ? OLLAMA_API_BASE_URL : API_BASE_URL, system: system !== '' ? system : undefined }); show = false; @@ -1259,7 +1172,7 @@
Pull Progress
{pullProgress ?? 0}% @@ -1494,10 +1407,12 @@
{ - saveSettings({ - OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined, - OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined - }); + updateOpenAIHandler(); + + // saveSettings({ + // OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined, + // OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined + // }); show = false; }} > @@ -1710,6 +1625,64 @@ {:else if selectedTab === 'chats'}
+
+
+
Chat History
+ + +
+ +
+ This setting does not sync across browsers or devices. +
+
+ +
+
{ + if (size === 'sm') { + return 'w-[30rem]'; + } else { + return 'w-[40rem]'; + } + }; + onMount(() => { mounted = true; }); @@ -28,7 +38,9 @@ }} >
{ e.stopPropagation(); diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index b0eb35b21..7ea08f988 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -6,7 +6,7 @@ import { goto, invalidateAll } from '$app/navigation'; import { page } from '$app/stores'; - import { user, chats, showSettings, chatId } from '$lib/stores'; + import { user, chats, settings, showSettings, chatId } from '$lib/stores'; import { onMount } from 'svelte'; import { deleteChatById, getChatList, updateChatById } from '$lib/apis/chats'; @@ -49,6 +49,12 @@ await deleteChatById(localStorage.token, id); await chats.set(await getChatList(localStorage.token)); }; + + const saveSettings = async (updated) => { + await settings.set({ ...$settings, ...updated }); + localStorage.setItem('settings', JSON.stringify($settings)); + location.href = '/'; + };
{/if} -
- + {/each} +
diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 7d67c7a4b..277441971 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,16 +1,10 @@ -import { dev, browser } from '$app/environment'; -import { PUBLIC_API_BASE_URL } from '$env/static/public'; - -export const OLLAMA_API_BASE_URL = dev - ? `http://${location.hostname}:8080/ollama/api` - : PUBLIC_API_BASE_URL === '' - ? browser - ? `http://${location.hostname}:11434/api` - : `http://localhost:11434/api` - : PUBLIC_API_BASE_URL; +import { dev } from '$app/environment'; export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; + export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; +export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`; +export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`; export const WEB_UI_VERSION = 'v1.0.0-alpha-static'; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 344672da5..2029604e0 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -21,7 +21,7 @@ export const splitStream = (splitOn) => { }; export const convertMessagesToHistory = (messages) => { - let history = { + const history = { messages: {}, currentId: null }; @@ -114,7 +114,7 @@ export const checkVersion = (required, current) => { export const findWordIndices = (text) => { const regex = /\[([^\]]+)\]/g; - let matches = []; + const matches = []; let match; while ((match = regex.exec(text)) !== null) { diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index dca6912f5..c264592e8 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -14,7 +14,7 @@ import { getOpenAIModels } from '$lib/apis/openai'; import { user, showSettings, settings, models, modelfiles, prompts } from '$lib/stores'; - import { OLLAMA_API_BASE_URL, REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; + import { REQUIRED_OLLAMA_VERSION, WEBUI_API_BASE_URL } from '$lib/constants'; import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte'; @@ -32,36 +32,28 @@ const getModels = async () => { let models = []; models.push( - ...(await getOllamaModels( - $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, - localStorage.token - ).catch((error) => { + ...(await getOllamaModels(localStorage.token).catch((error) => { toast.error(error); return []; })) ); - // If OpenAI API Key exists - if ($settings.OPENAI_API_KEY) { - const openAIModels = await getOpenAIModels( - $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1', - $settings.OPENAI_API_KEY - ).catch((error) => { - console.log(error); - toast.error(error); - return null; - }); - models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); - } + // $settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1', + // $settings.OPENAI_API_KEY + + const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => { + console.log(error); + return null; + }); + + models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : [])); + return models; }; const setOllamaVersion = async (version: string = '') => { if (version === '') { - version = await getOllamaVersion( - $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, - localStorage.token - ).catch((error) => { + version = await getOllamaVersion(localStorage.token).catch((error) => { return ''; }); } diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 9638861be..86bdbbea9 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -7,7 +7,6 @@ import { page } from '$app/stores'; import { models, modelfiles, user, settings, chats, chatId, config } from '$lib/stores'; - import { OLLAMA_API_BASE_URL } from '$lib/constants'; import { generateChatCompletion, generateTitle } from '$lib/apis/ollama'; import { copyToClipboard, splitStream } from '$lib/utils'; @@ -17,6 +16,7 @@ import ModelSelector from '$lib/components/chat/ModelSelector.svelte'; import Navbar from '$lib/components/layout/Navbar.svelte'; import { createNewChat, getChatList, updateChatById } from '$lib/apis/chats'; + import { generateOpenAIChatCompletion } from '$lib/apis/openai'; let stopResponseFlag = false; let autoScroll = true; @@ -163,36 +163,32 @@ // Scroll down window.scrollTo({ top: document.body.scrollHeight }); - const res = await generateChatCompletion( - $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, - localStorage.token, - { - model: model, - messages: [ - $settings.system - ? { - role: 'system', - content: $settings.system - } - : undefined, - ...messages - ] - .filter((message) => message) - .map((message) => ({ - role: message.role, - content: message.content, - ...(message.files && { - images: message.files - .filter((file) => file.type === 'image') - .map((file) => file.url.slice(file.url.indexOf(',') + 1)) - }) - })), - options: { - ...($settings.options ?? {}) - }, - format: $settings.requestFormat ?? undefined - } - ); + const res = await generateChatCompletion(localStorage.token, { + model: model, + messages: [ + $settings.system + ? { + role: 'system', + content: $settings.system + } + : undefined, + ...messages + ] + .filter((message) => message) + .map((message) => ({ + role: message.role, + content: message.content, + ...(message.files && { + images: message.files + .filter((file) => file.type === 'image') + .map((file) => file.url.slice(file.url.indexOf(',') + 1)) + }) + })), + options: { + ...($settings.options ?? {}) + }, + format: $settings.requestFormat ?? undefined + }); if (res && res.ok) { const reader = res.body @@ -284,11 +280,13 @@ } if ($chatId == _chatId) { - chat = await updateChatById(localStorage.token, _chatId, { - messages: messages, - history: history - }); - await chats.set(await getChatList(localStorage.token)); + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { + messages: messages, + history: history + }); + await chats.set(await getChatList(localStorage.token)); + } } } else { if (res !== null) { @@ -326,188 +324,173 @@ }; const sendPromptOpenAI = async (model, userPrompt, parentId, _chatId) => { - if ($settings.OPENAI_API_KEY) { - if (models) { - let responseMessageId = uuidv4(); + let responseMessageId = uuidv4(); - let responseMessage = { - parentId: parentId, - id: responseMessageId, - childrenIds: [], - role: 'assistant', - content: '', - model: model - }; + let responseMessage = { + parentId: parentId, + id: responseMessageId, + childrenIds: [], + role: 'assistant', + content: '', + model: model + }; - history.messages[responseMessageId] = responseMessage; - history.currentId = responseMessageId; - if (parentId !== null) { - history.messages[parentId].childrenIds = [ - ...history.messages[parentId].childrenIds, - responseMessageId - ]; - } + history.messages[responseMessageId] = responseMessage; + history.currentId = responseMessageId; + if (parentId !== null) { + history.messages[parentId].childrenIds = [ + ...history.messages[parentId].childrenIds, + responseMessageId + ]; + } - window.scrollTo({ top: document.body.scrollHeight }); + window.scrollTo({ top: document.body.scrollHeight }); - const res = await fetch( - `${$settings.OPENAI_API_BASE_URL ?? 'https://api.openai.com/v1'}/chat/completions`, - { - method: 'POST', - headers: { - Authorization: `Bearer ${$settings.OPENAI_API_KEY}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - model: model, - stream: true, - messages: [ - $settings.system - ? { - role: 'system', - content: $settings.system - } - : undefined, - ...messages - ] - .filter((message) => message) - .map((message) => ({ - role: message.role, - ...(message.files - ? { - content: [ - { - type: 'text', - text: message.content - }, - ...message.files - .filter((file) => file.type === 'image') - .map((file) => ({ - type: 'image_url', - image_url: { - url: file.url - } - })) - ] - } - : { content: message.content }) - })), - seed: $settings?.options?.seed ?? undefined, - stop: $settings?.options?.stop ?? undefined, - temperature: $settings?.options?.temperature ?? undefined, - top_p: $settings?.options?.top_p ?? undefined, - num_ctx: $settings?.options?.num_ctx ?? undefined, - frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, - max_tokens: $settings?.options?.num_predict ?? undefined - }) - } - ).catch((err) => { - console.log(err); - return null; - }); + const res = await generateOpenAIChatCompletion(localStorage.token, { + model: model, + stream: true, + messages: [ + $settings.system + ? { + role: 'system', + content: $settings.system + } + : undefined, + ...messages + ] + .filter((message) => message) + .map((message) => ({ + role: message.role, + ...(message.files + ? { + content: [ + { + type: 'text', + text: message.content + }, + ...message.files + .filter((file) => file.type === 'image') + .map((file) => ({ + type: 'image_url', + image_url: { + url: file.url + } + })) + ] + } + : { content: message.content }) + })), + seed: $settings?.options?.seed ?? undefined, + stop: $settings?.options?.stop ?? undefined, + temperature: $settings?.options?.temperature ?? undefined, + top_p: $settings?.options?.top_p ?? undefined, + num_ctx: $settings?.options?.num_ctx ?? undefined, + frequency_penalty: $settings?.options?.repeat_penalty ?? undefined, + max_tokens: $settings?.options?.num_predict ?? undefined + }); - if (res && res.ok) { - const reader = res.body - .pipeThrough(new TextDecoderStream()) - .pipeThrough(splitStream('\n')) - .getReader(); + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); - while (true) { - const { value, done } = await reader.read(); - if (done || stopResponseFlag || _chatId !== $chatId) { - responseMessage.done = true; - messages = messages; - break; - } - - try { - let lines = value.split('\n'); - - for (const line of lines) { - if (line !== '') { - console.log(line); - if (line === 'data: [DONE]') { - responseMessage.done = true; - messages = messages; - } else { - let data = JSON.parse(line.replace(/^data: /, '')); - console.log(data); - - if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { - continue; - } else { - responseMessage.content += data.choices[0].delta.content ?? ''; - messages = messages; - } - } - } - } - } catch (error) { - console.log(error); - } - - if ($settings.notificationEnabled && !document.hasFocus()) { - const notification = new Notification(`OpenAI ${model}`, { - body: responseMessage.content, - icon: '/favicon.png' - }); - } - - if ($settings.responseAutoCopy) { - copyToClipboard(responseMessage.content); - } - - if (autoScroll) { - window.scrollTo({ top: document.body.scrollHeight }); - } - } - - if ($chatId == _chatId) { - chat = await updateChatById(localStorage.token, _chatId, { - messages: messages, - history: history - }); - await chats.set(await getChatList(localStorage.token)); - } - } else { - if (res !== null) { - const error = await res.json(); - console.log(error); - if ('detail' in error) { - toast.error(error.detail); - responseMessage.content = error.detail; - } else { - if ('message' in error.error) { - toast.error(error.error.message); - responseMessage.content = error.error.message; - } else { - toast.error(error.error); - responseMessage.content = error.error; - } - } - } else { - toast.error(`Uh-oh! There was an issue connecting to ${model}.`); - responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; - } - - responseMessage.error = true; - responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; + while (true) { + const { value, done } = await reader.read(); + if (done || stopResponseFlag || _chatId !== $chatId) { responseMessage.done = true; messages = messages; + break; } - stopResponseFlag = false; - await tick(); + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + if (line === 'data: [DONE]') { + responseMessage.done = true; + messages = messages; + } else { + let data = JSON.parse(line.replace(/^data: /, '')); + console.log(data); + + if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { + continue; + } else { + responseMessage.content += data.choices[0].delta.content ?? ''; + messages = messages; + } + } + } + } + } catch (error) { + console.log(error); + } + + if ($settings.notificationEnabled && !document.hasFocus()) { + const notification = new Notification(`OpenAI ${model}`, { + body: responseMessage.content, + icon: '/favicon.png' + }); + } + + if ($settings.responseAutoCopy) { + copyToClipboard(responseMessage.content); + } if (autoScroll) { window.scrollTo({ top: document.body.scrollHeight }); } + } - if (messages.length == 2) { - window.history.replaceState(history.state, '', `/c/${_chatId}`); - await setChatTitle(_chatId, userPrompt); + if ($chatId == _chatId) { + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { + messages: messages, + history: history + }); + await chats.set(await getChatList(localStorage.token)); } } + } else { + if (res !== null) { + const error = await res.json(); + console.log(error); + if ('detail' in error) { + toast.error(error.detail); + responseMessage.content = error.detail; + } else { + if ('message' in error.error) { + toast.error(error.error.message); + responseMessage.content = error.error.message; + } else { + toast.error(error.error); + responseMessage.content = error.error; + } + } + } else { + toast.error(`Uh-oh! There was an issue connecting to ${model}.`); + responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; + } + + responseMessage.error = true; + responseMessage.content = `Uh-oh! There was an issue connecting to ${model}.`; + responseMessage.done = true; + messages = messages; + } + + stopResponseFlag = false; + await tick(); + + if (autoScroll) { + window.scrollTo({ top: document.body.scrollHeight }); + } + + if (messages.length == 2) { + window.history.replaceState(history.state, '', `/c/${_chatId}`); + await setChatTitle(_chatId, userPrompt); } }; @@ -548,20 +531,24 @@ // Create new chat if only one message in messages if (messages.length == 1) { - chat = await createNewChat(localStorage.token, { - id: $chatId, - title: 'New Chat', - models: selectedModels, - system: $settings.system ?? undefined, - options: { - ...($settings.options ?? {}) - }, - messages: messages, - history: history, - timestamp: Date.now() - }); - await chats.set(await getChatList(localStorage.token)); - await chatId.set(chat.id); + if ($settings.saveChatHistory ?? true) { + chat = await createNewChat(localStorage.token, { + id: $chatId, + title: 'New Chat', + models: selectedModels, + system: $settings.system ?? undefined, + options: { + ...($settings.options ?? {}) + }, + messages: messages, + history: history, + timestamp: Date.now() + }); + await chats.set(await getChatList(localStorage.token)); + await chatId.set(chat.id); + } else { + await chatId.set('local'); + } await tick(); } @@ -595,7 +582,6 @@ const generateChatTitle = async (_chatId, userPrompt) => { if ($settings.titleAutoGenerate ?? true) { const title = await generateTitle( - $settings?.API_BASE_URL ?? OLLAMA_API_BASE_URL, localStorage.token, $settings?.titleAutoGenerateModel ?? selectedModels[0], userPrompt @@ -614,8 +600,10 @@ title = _title; } - chat = await updateChatById(localStorage.token, _chatId, { title: _title }); - await chats.set(await getChatList(localStorage.token)); + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { title: _title }); + await chats.set(await getChatList(localStorage.token)); + } }; diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index 1d7ec44f8..92ce7ad2d 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -8,11 +8,15 @@ import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; + import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; let loaded = false; let users = []; + let selectedUser = null; + let signUpEnabled = true; + let showEditUserModal = false; const updateRoleHandler = async (id, role) => { const res = await updateUserRole(localStorage.token, id, role).catch((error) => { @@ -25,6 +29,17 @@ } }; + const editUserPasswordHandler = async (id, password) => { + const res = await deleteUserById(localStorage.token, id).catch((error) => { + toast.error(error); + return null; + }); + if (res) { + users = await getUsers(localStorage.token); + toast.success('Successfully updated'); + } + }; + const deleteUserHandler = async (id) => { const res = await deleteUserById(localStorage.token, id).catch((error) => { toast.error(error); @@ -51,6 +66,17 @@ }); +{#key selectedUser} + { + users = await getUsers(localStorage.token); + }} + /> +{/key} +
@@ -154,7 +180,28 @@ }}>{user.role} - + + +