diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c4a3bb1fa..0a36356f3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,13 +1,17 @@ # Pull Request Checklist -- [ ] **Target branch:** Pull requests should target the `dev` branch. -- [ ] **Description:** Briefly describe the changes in this pull request. +### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request. + +**Before submitting, make sure you've checked the following:** + +- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch. +- [ ] **Description:** Provide a concise description of the changes made in this pull request. - [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. - [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources? - [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? -- [ ] **Testing:** Have you written and run sufficient tests for the changes? -- [ ] **Code Review:** Have you self-reviewed your code and addressed any coding standard issues? -- [ ] **Label title:** Ensure the pull request title is labeled properly using one of the following: +- [ ] **Testing:** Have you written and run sufficient tests for validating the changes? +- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? +- [ ] **Label:** To cleary categorize this pull request, assign a relevant label to the pull request title, using one of the following: - **BREAKING CHANGE**: Significant changes that may affect compatibility - **build**: Changes that affect the build system or external dependencies - **ci**: Changes to our continuous integration processes or workflows @@ -26,7 +30,7 @@ ### Description -- [Briefly describe the changes made in this pull request, including any relevant motivation and impact.] +- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] ### Added @@ -62,3 +66,7 @@ - [Insert any additional context, notes, or explanations for the changes] - [Reference any related issues, commits, or other relevant information] + +### Screenshots or Videos + +- [Attach any relevant screenshots or videos demonstrating the changes] diff --git a/.gitignore b/.gitignore index 55209604a..a54fef595 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ __pycache__/ # C extensions *.so +# Pyodide distribution +static/pyodide/* +!static/pyodide/pyodide-lock.json + # Distribution / packaging .Python build/ diff --git a/.prettierignore b/.prettierignore index bdcce08cc..82c491257 100644 --- a/.prettierignore +++ b/.prettierignore @@ -310,3 +310,7 @@ dist # cypress artifacts cypress/videos cypress/screenshots + + + +/static/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a5cc2bb39..dee049fb4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,9 @@ ARG USE_CUDA_VER=cu121 # IMPORTANT: If you change the embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 ARG USE_RERANKING_MODEL="" +# Override at your own risk - non-root configurations are untested +ARG UID=0 +ARG GID=0 ######## WebUI frontend ######## FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build @@ -32,6 +35,8 @@ ARG USE_OLLAMA ARG USE_CUDA_VER ARG USE_EMBEDDING_MODEL ARG USE_RERANKING_MODEL +ARG UID +ARG GID ## Basis ## ENV ENV=prod \ @@ -76,9 +81,20 @@ ENV HF_HOME="/app/backend/data/cache/embedding/models" WORKDIR /app/backend ENV HOME /root +# Create user and group if not root +RUN if [ $UID -ne 0 ]; then \ + if [ $GID -ne 0 ]; then \ + addgroup --gid $GID app; \ + fi; \ + adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \ + fi + RUN mkdir -p $HOME/.cache/chroma RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id +# Make sure the user has access to the app and root directory +RUN chown -R $UID:$GID /app $HOME + RUN if [ "$USE_OLLAMA" = "true" ]; then \ apt-get update && \ # Install pandoc and netcat @@ -102,7 +118,7 @@ RUN if [ "$USE_OLLAMA" = "true" ]; then \ fi # install python dependencies -COPY ./backend/requirements.txt ./requirements.txt +COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt RUN pip3 install uv && \ if [ "$USE_CUDA" = "true" ]; then \ @@ -125,16 +141,17 @@ RUN pip3 install uv && \ # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx # copy built frontend files -COPY --from=build /app/build /app/build -COPY --from=build /app/CHANGELOG.md /app/CHANGELOG.md -COPY --from=build /app/package.json /app/package.json +COPY --chown=$UID:$GID --from=build /app/build /app/build +COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md +COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json # copy backend files -COPY ./backend . +COPY --chown=$UID:$GID ./backend . EXPOSE 8080 HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1 +USER $UID:$GID CMD [ "bash", "start.sh"] diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py index 1c309439d..4419ccf19 100644 --- a/backend/apps/images/main.py +++ b/backend/apps/images/main.py @@ -397,7 +397,7 @@ def generate_image( user=Depends(get_current_user), ): - width, height = tuple(map(int, app.state.config.IMAGE_SIZE).split("x")) + width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x"))) r = None try: diff --git a/backend/apps/litellm/main.py b/backend/apps/litellm/main.py index 17dcf651e..35cb2bd54 100644 --- a/backend/apps/litellm/main.py +++ b/backend/apps/litellm/main.py @@ -75,6 +75,10 @@ with open(LITELLM_CONFIG_DIR, "r") as file: litellm_config = yaml.safe_load(file) +app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER.value +app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST.value + + app.state.ENABLE = ENABLE_LITELLM app.state.CONFIG = litellm_config app.state.MODEL_CONFIG = MODEL_CONFIG.value.get("litellm", []) @@ -152,10 +156,6 @@ async def shutdown_litellm_background(): background_process = None -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST - - @app.get("/") async def get_status(): return {"status": True} diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py index 282cd0d61..2e9d36bc8 100644 --- a/backend/apps/ollama/main.py +++ b/backend/apps/ollama/main.py @@ -65,8 +65,8 @@ app.add_middleware( app.state.config = AppConfig() -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.MODEL_CONFIG = MODEL_CONFIG.value.get("ollama", []) app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS @@ -126,8 +126,9 @@ async def cancel_ollama_request(request_id: str, user=Depends(get_current_user)) async def fetch_url(url): + timeout = aiohttp.ClientTimeout(total=5) try: - async with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession(timeout=timeout) as session: async with session.get(url) as response: return await response.json() except Exception as e: @@ -190,11 +191,12 @@ async def get_ollama_tags( if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["models"] = list( filter( - lambda model: model["name"] in app.state.MODEL_FILTER_LIST, + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, models["models"], ) ) @@ -1058,11 +1060,12 @@ async def get_openai_models( if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["models"] = list( filter( - lambda model: model["name"] in app.state.MODEL_FILTER_LIST, + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, models["models"], ) ) diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py index e6c1e549c..e0c46e0c7 100644 --- a/backend/apps/openai/main.py +++ b/backend/apps/openai/main.py @@ -21,6 +21,7 @@ from utils.utils import ( ) from config import ( SRC_LOG_LEVELS, + ENABLE_OPENAI_API, OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR, @@ -47,12 +48,15 @@ app.add_middleware( allow_headers=["*"], ) + app.state.config = AppConfig() -app.state.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER -app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST app.state.MODEL_CONFIG = MODEL_CONFIG.value.get("openai", []) + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS @@ -70,6 +74,21 @@ async def check_url(request: Request, call_next): return response +@app.get("/config") +async def get_config(user=Depends(get_admin_user)): + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + +class OpenAIConfigForm(BaseModel): + enable_openai_api: Optional[bool] = None + + +@app.post("/config/update") +async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)): + app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + class UrlsUpdateForm(BaseModel): urls: List[str] @@ -166,11 +185,15 @@ async def speech(request: Request, user=Depends(get_verified_user)): async def fetch_url(url, key): + timeout = aiohttp.ClientTimeout(total=5) try: - headers = {"Authorization": f"Bearer {key}"} - async with aiohttp.ClientSession() as session: - async with session.get(url, headers=headers) as response: - return await response.json() + if key != "": + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession(timeout=timeout) as session: + async with session.get(url, headers=headers) as response: + return await response.json() + else: + return None except Exception as e: # Handle connection error here log.error(f"Connection error: {e}") @@ -202,7 +225,7 @@ async def get_all_models(): if ( len(app.state.config.OPENAI_API_KEYS) == 1 and app.state.config.OPENAI_API_KEYS[0] == "" - ): + ) or not app.state.config.ENABLE_OPENAI_API: models = {"data": []} else: tasks = [ @@ -248,11 +271,11 @@ def add_custom_info_to_model(model: dict): async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_user)): if url_idx == None: models = await get_all_models() - if app.state.ENABLE_MODEL_FILTER: + if app.state.config.ENABLE_MODEL_FILTER: if user.role == "user": models["data"] = list( filter( - lambda model: model["id"] in app.state.MODEL_FILTER_LIST, + lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, models["data"], ) ) diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py index d2c3964ae..ba25f34f6 100644 --- a/backend/apps/rag/main.py +++ b/backend/apps/rag/main.py @@ -433,12 +433,12 @@ async def update_query_settings( form_data: QuerySettingsForm, user=Depends(get_admin_user) ): app.state.config.RAG_TEMPLATE = ( - form_data.template if form_data.template else RAG_TEMPLATE, + form_data.template if form_data.template else RAG_TEMPLATE ) app.state.config.TOP_K = form_data.k if form_data.k else 4 app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0 app.state.config.ENABLE_RAG_HYBRID_SEARCH = ( - form_data.hybrid if form_data.hybrid else False, + form_data.hybrid if form_data.hybrid else False ) return { "status": True, diff --git a/backend/apps/web/routers/users.py b/backend/apps/web/routers/users.py index d87854e89..d77475d8d 100644 --- a/backend/apps/web/routers/users.py +++ b/backend/apps/web/routers/users.py @@ -11,8 +11,9 @@ import logging from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users from apps.web.models.auths import Auths +from apps.web.models.chats import Chats -from utils.utils import get_current_user, get_password_hash, get_admin_user +from utils.utils import get_verified_user, get_password_hash, get_admin_user from constants import ERROR_MESSAGES from config import SRC_LOG_LEVELS @@ -67,6 +68,41 @@ async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin ) +############################ +# GetUserById +############################ + + +class UserResponse(BaseModel): + name: str + profile_image_url: str + + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): + + if user_id.startswith("shared-"): + chat_id = user_id.replace("shared-", "") + chat = Chats.get_chat_by_id(chat_id) + if chat: + user_id = chat.user_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + user = Users.get_user_by_id(user_id) + + if user: + return UserResponse(name=user.name, profile_image_url=user.profile_image_url) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + ############################ # UpdateUserById ############################ diff --git a/backend/config.py b/backend/config.py index c12e0939e..2e94e37bc 100644 --- a/backend/config.py +++ b/backend/config.py @@ -417,6 +417,14 @@ OLLAMA_BASE_URLS = PersistentConfig( # OPENAI_API #################################### + +ENABLE_OPENAI_API = PersistentConfig( + "ENABLE_OPENAI_API", + "openai.enable", + os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true", +) + + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") diff --git a/backend/main.py b/backend/main.py index 2d9c537ec..aae6492cd 100644 --- a/backend/main.py +++ b/backend/main.py @@ -120,6 +120,18 @@ app.state.config.WEBHOOK_URL = WEBHOOK_URL origins = ["*"] +# Custom middleware to add security headers +# class SecurityHeadersMiddleware(BaseHTTPMiddleware): +# async def dispatch(self, request: Request, call_next): +# response: Response = await call_next(request) +# response.headers["Cross-Origin-Opener-Policy"] = "same-origin" +# response.headers["Cross-Origin-Embedder-Policy"] = "require-corp" +# return response + + +# app.add_middleware(SecurityHeadersMiddleware) + + class RAGMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): return_citations = False @@ -280,14 +292,14 @@ class ModelFilterConfigForm(BaseModel): async def update_model_filter_config( form_data: ModelFilterConfigForm, user=Depends(get_admin_user) ): - app.state.config.ENABLE_MODEL_FILTER, form_data.enabled - app.state.config.MODEL_FILTER_LIST, form_data.models + app.state.config.ENABLE_MODEL_FILTER = form_data.enabled + app.state.config.MODEL_FILTER_LIST = form_data.models - ollama_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER - ollama_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST + ollama_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER + ollama_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST - openai_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER - openai_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST + openai_app.state.config.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER + openai_app.state.config.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST litellm_app.state.ENABLE_MODEL_FILTER = app.state.config.ENABLE_MODEL_FILTER litellm_app.state.MODEL_FILTER_LIST = app.state.config.MODEL_FILTER_LIST diff --git a/package-lock.json b/package-lock.json index 8f34cddf3..2a6f8f81e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "open-webui", "version": "0.1.124", "dependencies": { + "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", "bits-ui": "^0.19.7", @@ -22,6 +23,7 @@ "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", + "pyodide": "^0.26.0-alpha.4", "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" @@ -890,6 +892,19 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@pyscript/core": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.32.tgz", + "integrity": "sha512-WQATzPp1ggf871+PukCmTypzScXkEB1EWD/vg5GNxpM96N6rDPqQ13msuA5XvwU01ZVhL8HHSFDLk4IfaXNGWg==", + "dependencies": { + "@ungap/with-resolvers": "^0.1.0", + "basic-devtools": "^0.1.6", + "polyscript": "^0.12.8", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1", + "type-checked-collections": "^0.1.7" + } + }, "node_modules/@rollup/plugin-commonjs": { "version": "25.0.7", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", @@ -1605,8 +1620,12 @@ "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 + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@ungap/with-resolvers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", + "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" }, "node_modules/@vitest/expect": { "version": "1.6.0", @@ -1713,6 +1732,11 @@ "@types/estree": "^1.0.0" } }, + "node_modules/@webreflection/fetch": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", + "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" + }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -2027,6 +2051,11 @@ "dev": true, "optional": true }, + "node_modules/base-64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2047,6 +2076,11 @@ } ] }, + "node_modules/basic-devtools": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", + "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2661,6 +2695,28 @@ "@types/estree": "^1.0.0" } }, + "node_modules/codedent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", + "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", + "dependencies": { + "plain-tag": "^0.1.3" + } + }, + "node_modules/coincident": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", + "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" + }, + "optionalDependencies": { + "ws": "^8.16.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4001,6 +4057,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -4328,6 +4389,11 @@ "node": ">=12.0.0" } }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, "node_modules/htmlparser2": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", @@ -5838,6 +5904,29 @@ "pathe": "^1.1.2" } }, + "node_modules/plain-tag": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", + "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" + }, + "node_modules/polyscript": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.12.8.tgz", + "integrity": "sha512-kcG3W9jU/s1sYjWOTAa2jAh5D2jm3zJRi+glSTsC+lA3D1b/Sd67pEIGpyL9bWNKYSimqAx4se6jAhQjJZ7+jQ==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "@webreflection/fetch": "^0.1.5", + "basic-devtools": "^0.1.6", + "codedent": "^0.1.2", + "coincident": "^1.2.3", + "gc-hook": "^0.3.1", + "html-escaper": "^3.0.3", + "proxy-target": "^3.0.2", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -6151,6 +6240,11 @@ "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", "dev": true }, + "node_modules/proxy-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", + "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -6176,6 +6270,18 @@ "node": ">=6" } }, + "node_modules/pyodide": { + "version": "0.26.0-alpha.4", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.0-alpha.4.tgz", + "integrity": "sha512-Ixuczq99DwhQlE+Bt0RaS6Ln9MHSZOkbU6iN8azwaeorjHtr7ukaxh+FeTxViFrp2y+ITyKgmcobY+JnBPcULw==", + "dependencies": { + "base-64": "^1.0.0", + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/qs": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", @@ -6858,6 +6964,11 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, + "node_modules/sticky-module": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", + "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" + }, "node_modules/stream-composer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", @@ -7520,6 +7631,11 @@ "node": ">=14.14" } }, + "node_modules/to-json-callback": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", + "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -7629,6 +7745,11 @@ "node": ">= 0.8.0" } }, + "node_modules/type-checked-collections": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", + "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -8883,6 +9004,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2b6abdd74..0c542c982 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "version": "0.1.124", "private": true, "scripts": { - "dev": "vite dev --host", - "build": "vite build", + "dev": "npm run pyodide:fetch && vite dev --host", + "build": "npm run pyodide:fetch && vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", @@ -16,7 +16,8 @@ "format:backend": "black . --exclude \"/venv/\"", "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"", "cy:open": "cypress open", - "test:frontend": "vitest" + "test:frontend": "vitest", + "pyodide:fetch": "node scripts/prepare-pyodide.js" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", @@ -47,6 +48,7 @@ }, "type": "module", "dependencies": { + "@pyscript/core": "^0.4.32", "@sveltejs/adapter-node": "^1.3.1", "async": "^3.2.5", "bits-ui": "^0.19.7", @@ -61,6 +63,7 @@ "js-sha256": "^0.10.1", "katex": "^0.16.9", "marked": "^9.1.0", + "pyodide": "^0.26.0-alpha.4", "svelte-sonner": "^0.3.19", "tippy.js": "^6.3.7", "uuid": "^9.0.1" diff --git a/scripts/prepare-pyodide.js b/scripts/prepare-pyodide.js new file mode 100644 index 000000000..3d7aef5aa --- /dev/null +++ b/scripts/prepare-pyodide.js @@ -0,0 +1,38 @@ +const packages = [ + 'requests', + 'beautifulsoup4', + 'numpy', + 'pandas', + 'matplotlib', + 'scikit-learn', + 'scipy', + 'regex' +]; + +import { loadPyodide } from 'pyodide'; +import { writeFile, copyFile, readdir } from 'fs/promises'; + +async function downloadPackages() { + console.log('Setting up pyodide + micropip'); + const pyodide = await loadPyodide({ + packageCacheDir: 'static/pyodide' + }); + await pyodide.loadPackage('micropip'); + const micropip = pyodide.pyimport('micropip'); + console.log('Downloading Pyodide packages:', packages); + await micropip.install(packages); + console.log('Pyodide packages downloaded, freezing into lock file'); + const lockFile = await micropip.freeze(); + await writeFile('static/pyodide/pyodide-lock.json', lockFile); +} + +async function copyPyodide() { + console.log('Copying Pyodide files into static directory'); + // Copy all files from node_modules/pyodide to static/pyodide + for await (const entry of await readdir('node_modules/pyodide')) { + await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`); + } +} + +await downloadPackages(); +await copyPyodide(); diff --git a/src/app.html b/src/app.html index 1616cc668..138fb2829 100644 --- a/src/app.html +++ b/src/app.html @@ -12,6 +12,7 @@ title="Open WebUI" href="/opensearch.xml" /> + {#if code} -
+
{@html lang}
- + +
+ {#if lang === 'python' || checkPythonCode(code)} + {#if executing} +
Running
+ {:else} + + {/if} + {/if} + +
{@html highlightedCode || code}
+ +
+ + {#if executing} +
+
STDOUT/STDERR
+
Running...
+
+ {:else if stdout || stderr || result} +
+
STDOUT/STDERR
+
{stdout || stderr || result}
+
+ {/if}
{/if} diff --git a/src/lib/components/chat/Messages/Name.svelte b/src/lib/components/chat/Messages/Name.svelte index 970fb95aa..6047a08b5 100644 --- a/src/lib/components/chat/Messages/Name.svelte +++ b/src/lib/components/chat/Messages/Name.svelte @@ -1,3 +1,3 @@ -
+
diff --git a/src/lib/components/chat/Messages/Placeholder.svelte b/src/lib/components/chat/Messages/Placeholder.svelte index 5035904d4..dfb6cfb36 100644 --- a/src/lib/components/chat/Messages/Placeholder.svelte +++ b/src/lib/components/chat/Messages/Placeholder.svelte @@ -43,6 +43,7 @@ > {#if model in modelfiles} modelfile {:else} + import { settings } from '$lib/stores'; + import { WEBUI_BASE_URL } from '$lib/constants'; + export let src = '/user.png'; -
- profile +
+ profile
diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte index 65c7aea36..b3e23e3f2 100644 --- a/src/lib/components/chat/Messages/ResponseMessage.svelte +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -332,7 +332,11 @@ {#key message.id} -
+
{:else} - {#each tokens as token} + {#each tokens as token, tokenIdx} {#if token.type === 'code'} @@ -494,7 +499,7 @@ class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500" > {#if siblings.length > 1} -
+
+ +
+ { + updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API); + }} + /> +
- {#if showOpenAI} + {#if ENABLE_OPENAI_API}
{#each OPENAI_API_BASE_URLS as url, idx}
diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte index c92eefbe8..8a7e5257c 100644 --- a/src/lib/components/chat/Settings/Interface.svelte +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -23,6 +23,7 @@ let promptSuggestions = []; let showUsername = false; let chatBubble = true; + let chatDirection: 'LTR' | 'RTL' = 'LTR'; const toggleSplitLargeChunks = async () => { splitLargeChunks = !splitLargeChunks; @@ -76,6 +77,11 @@ } }; + const toggleChangeChatDirection = async () => { + chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR'; + saveSettings({ chatDirection }); + }; + const updateInterfaceHandler = async () => { if ($user.role === 'admin') { promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions); @@ -114,6 +120,7 @@ chatBubble = settings.chatBubble ?? true; fullScreenMode = settings.fullScreenMode ?? false; splitLargeChunks = settings.splitLargeChunks ?? false; + chatDirection = settings.chatDirection ?? 'LTR'; }); @@ -210,27 +217,29 @@
-
-
-
- {$i18n.t('Display the username instead of You in the Chat')} -
+ {#if !$settings.chatBubble} +
+
+
+ {$i18n.t('Display the username instead of You in the Chat')} +
- + +
-
+ {/if}
@@ -255,6 +264,24 @@
+
+
+
{$i18n.t('Chat direction')}
+ + +
+
+
diff --git a/src/lib/components/chat/Settings/Personalization.svelte b/src/lib/components/chat/Settings/Personalization.svelte index ab8f00745..2a339407e 100644 --- a/src/lib/components/chat/Settings/Personalization.svelte +++ b/src/lib/components/chat/Settings/Personalization.svelte @@ -63,7 +63,7 @@
diff --git a/src/lib/components/chat/ShareChatModal.svelte b/src/lib/components/chat/ShareChatModal.svelte index e4c40bbb4..cc8f91623 100644 --- a/src/lib/components/chat/ShareChatModal.svelte +++ b/src/lib/components/chat/ShareChatModal.svelte @@ -128,7 +128,7 @@ {$i18n.t('and create a new shared link.')} {:else} {$i18n.t( - "Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat." + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat." )} {/if}
diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte index 49a31a168..0c4d4e552 100644 --- a/src/lib/components/common/Modal.svelte +++ b/src/lib/components/common/Modal.svelte @@ -18,7 +18,7 @@ } else if (size === 'md') { return 'w-[48rem]'; } else { - return 'w-[50rem]'; + return 'w-[52rem]'; } }; diff --git a/src/lib/components/common/Selector.svelte b/src/lib/components/common/Selector.svelte index 9a56f30bb..00cc2da07 100644 --- a/src/lib/components/common/Selector.svelte +++ b/src/lib/components/common/Selector.svelte @@ -26,7 +26,7 @@ let searchValue = ''; $: filteredItems = searchValue - ? items.filter((item) => item.value.includes(searchValue.toLowerCase())) + ? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase())) : items; diff --git a/src/lib/components/common/Switch.svelte b/src/lib/components/common/Switch.svelte index fe51a2660..0f8f45460 100644 --- a/src/lib/components/common/Switch.svelte +++ b/src/lib/components/common/Switch.svelte @@ -14,7 +14,7 @@ }} class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] transition {state ? ' bg-emerald-600' - : 'bg-gray-200 dark:bg-transparent'} outline outline-gray-100 dark:outline-gray-800" + : 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800" > diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte index fdbf7509a..09b51f293 100644 --- a/src/lib/components/layout/Sidebar.svelte +++ b/src/lib/components/layout/Sidebar.svelte @@ -248,6 +248,7 @@ >
logo { let models = await Promise.all([ - await getOllamaModels(token).catch((error) => { + getOllamaModels(token).catch((error) => { console.log(error); return null; }), - await getOpenAIModels(token).catch((error) => { + getOpenAIModels(token).catch((error) => { console.log(error); return null; }), - await getLiteLLMModels(token).catch((error) => { + getLiteLLMModels(token).catch((error) => { console.log(error); return null; }) diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts new file mode 100644 index 000000000..e463ba8ba --- /dev/null +++ b/src/lib/workers/pyodide.worker.ts @@ -0,0 +1,66 @@ +import { loadPyodide, type PyodideInterface } from 'pyodide'; + +declare global { + interface Window { + stdout: string | null; + stderr: string | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: any; + pyodide: PyodideInterface; + packages: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + } +} + +async function loadPyodideAndPackages(packages: string[] = []) { + self.stdout = null; + self.stderr = null; + self.result = null; + + self.pyodide = await loadPyodide({ + indexURL: '/pyodide/', + stdout: (text) => { + console.log('Python output:', text); + + if (self.stdout) { + self.stdout += `${text}\n`; + } else { + self.stdout = `${text}\n`; + } + }, + stderr: (text) => { + console.log('An error occurred:', text); + if (self.stderr) { + self.stderr += `${text}\n`; + } else { + self.stderr = `${text}\n`; + } + }, + packages: ['micropip'] + }); + + const micropip = self.pyodide.pyimport('micropip'); + + await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json'); + await micropip.install(packages); +} + +self.onmessage = async (event) => { + const { id, code, ...context } = event.data; + + console.log(event.data); + + // The worker copies the context in its own "memory" (an object mapping name to values) + for (const key of Object.keys(context)) { + self[key] = context[key]; + } + + // make sure loading is done + await loadPyodideAndPackages(self.packages); + + self.result = await self.pyodide.runPythonAsync(code); + self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); +}; + +export default {}; diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte index be6691244..449b69eb2 100644 --- a/src/routes/(app)/admin/+page.svelte +++ b/src/routes/(app)/admin/+page.svelte @@ -1,5 +1,5 @@