浏览代码

Merge branch 'dev' into feat/add-i18n

Ased Mammad 1 年之前
父节点
当前提交
7031aa14e8

+ 0 - 1
.dockerignore

@@ -7,7 +7,6 @@ node_modules
 /package
 /package
 .env
 .env
 .env.*
 .env.*
-!.env.example
 vite.config.js.timestamp-*
 vite.config.js.timestamp-*
 vite.config.ts.timestamp-*
 vite.config.ts.timestamp-*
 __pycache__
 __pycache__

+ 2 - 2
.env.example

@@ -1,6 +1,6 @@
 # Ollama URL for the backend to connect
 # 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'
+# The path '/ollama' will be redirected to the specified backend URL
+OLLAMA_BASE_URL='http://localhost:11434'
 
 
 OPENAI_API_BASE_URL=''
 OPENAI_API_BASE_URL=''
 OPENAI_API_KEY=''
 OPENAI_API_KEY=''

+ 12 - 2
.github/workflows/build-release.yml

@@ -19,24 +19,34 @@ jobs:
           echo "No changes to package.json"
           echo "No changes to package.json"
           exit 1
           exit 1
         }
         }
-
+    
     - name: Get version number from package.json
     - name: Get version number from package.json
       id: get_version
       id: get_version
       run: |
       run: |
         VERSION=$(jq -r '.version' package.json)
         VERSION=$(jq -r '.version' package.json)
         echo "::set-output name=version::$VERSION"
         echo "::set-output name=version::$VERSION"
 
 
+    - name: Extract latest CHANGELOG entry
+      id: changelog
+      run: |
+        CHANGELOG_CONTENT=$(awk '/^## \[/{n++} n==1' CHANGELOG.md)
+        echo "CHANGELOG_CONTENT<<EOF" 
+        echo "$CHANGELOG_CONTENT"
+        echo "EOF" 
+        echo "::set-output name=content::${CHANGELOG_CONTENT}"
+
     - name: Create GitHub release
     - name: Create GitHub release
       uses: actions/github-script@v5
       uses: actions/github-script@v5
       with:
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
         script: |
+          const changelog = `${{ steps.changelog.outputs.content }}`;
           const release = await github.rest.repos.createRelease({
           const release = await github.rest.repos.createRelease({
             owner: context.repo.owner,
             owner: context.repo.owner,
             repo: context.repo.repo,
             repo: context.repo.repo,
             tag_name: `v${{ steps.get_version.outputs.version }}`,
             tag_name: `v${{ steps.get_version.outputs.version }}`,
             name: `v${{ steps.get_version.outputs.version }}`,
             name: `v${{ steps.get_version.outputs.version }}`,
-            body: 'Automatically created new release',
+            body: changelog,
           })
           })
           console.log(`Created release ${release.data.html_url}`)
           console.log(`Created release ${release.data.html_url}`)
 
 

+ 32 - 0
CHANGELOG.md

@@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
+## [0.1.110] - 2024-03-06
+
+### Added
+
+- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively.
+
+### Fixed
+
+- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction.
+- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly.
+- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button.
+
+## [0.1.109] - 2024-03-06
+
+### Added
+
+- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278).
+- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064).
+- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050).
+
+### Fixed
+
+- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031).
+- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024).
+- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006).
+- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061).
+
+### Changed
+
+- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004).
+- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details.
+
 ## [0.1.108] - 2024-03-02
 ## [0.1.108] - 2024-03-02
 
 
 ### Added
 ### Added

+ 3 - 1
Dockerfile

@@ -20,7 +20,7 @@ FROM python:3.11-slim-bookworm as base
 ENV ENV=prod
 ENV ENV=prod
 ENV PORT ""
 ENV PORT ""
 
 
-ENV OLLAMA_API_BASE_URL "/ollama/api"
+ENV OLLAMA_BASE_URL "/ollama"
 
 
 ENV OPENAI_API_BASE_URL ""
 ENV OPENAI_API_BASE_URL ""
 ENV OPENAI_API_KEY ""
 ENV OPENAI_API_KEY ""
@@ -53,6 +53,8 @@ WORKDIR /app/backend
 # install python dependencies
 # install python dependencies
 COPY ./backend/requirements.txt ./requirements.txt
 COPY ./backend/requirements.txt ./requirements.txt
 
 
+RUN apt-get update && apt-get install ffmpeg libsm6 libxext6  -y
+
 RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
 RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir
 RUN pip3 install -r requirements.txt --no-cache-dir
 RUN pip3 install -r requirements.txt --no-cache-dir
 
 

+ 3 - 3
README.md

@@ -95,10 +95,10 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open
 
 
 - **If Ollama is on a Different Server**, use this command:
 - **If Ollama is on a Different Server**, use this command:
 
 
-- To connect to Ollama on another server, change the `OLLAMA_API_BASE_URL` to the server's URL:
+- To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL:
 
 
   ```bash
   ```bash
-  docker run -d -p 3000:8080 -e OLLAMA_API_BASE_URL=https://example.com/api -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
+  docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main
   ```
   ```
 
 
 - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
 - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄
@@ -110,7 +110,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
 **Example Docker Command**:
 **Example Docker Command**:
 
 
 ```bash
 ```bash
-docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
+docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
 ```
 ```
 
 
 ### Other Installation Methods
 ### Other Installation Methods

+ 4 - 4
TROUBLESHOOTING.md

@@ -4,7 +4,7 @@
 
 
 The Open 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.
 The Open 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**: The Open 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 Open 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.
+- **How it Works**: The Open 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 Open WebUI backend via `/ollama` 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_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/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.
 - **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.
 
 
@@ -15,7 +15,7 @@ If you're experiencing connection issues, it’s often due to the WebUI docker c
 **Example Docker Command**:
 **Example Docker Command**:
 
 
 ```bash
 ```bash
-docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_URL=http://127.0.0.1:11434/api --name open-webui --restart always ghcr.io/open-webui/open-webui:main
+docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main
 ```
 ```
 
 
 ### General Connection Errors
 ### General Connection Errors
@@ -25,8 +25,8 @@ docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_API_BASE_
 **Troubleshooting Steps**:
 **Troubleshooting Steps**:
 
 
 1. **Verify Ollama URL Format**:
 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).
+   - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups).
    - In the Open WebUI, navigate to "Settings" > "General".
    - In the Open WebUI, navigate to "Settings" > "General".
-   - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]/api` (e.g., `http://localhost:11434/api`), including the `/api` suffix.
+   - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`).
 
 
 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.
 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.

+ 14 - 14
backend/apps/ollama/main.py

@@ -15,7 +15,7 @@ import asyncio
 from apps.web.models.users import Users
 from apps.web.models.users import Users
 from constants import ERROR_MESSAGES
 from constants import ERROR_MESSAGES
 from utils.utils import decode_token, get_current_user, get_admin_user
 from utils.utils import decode_token, get_current_user, get_admin_user
-from config import OLLAMA_BASE_URL, WEBUI_AUTH
+from config import OLLAMA_BASE_URLS
 
 
 from typing import Optional, List, Union
 from typing import Optional, List, Union
 
 
@@ -29,8 +29,7 @@ app.add_middleware(
     allow_headers=["*"],
     allow_headers=["*"],
 )
 )
 
 
-app.state.OLLAMA_BASE_URL = OLLAMA_BASE_URL
-app.state.OLLAMA_BASE_URLS = [OLLAMA_BASE_URL]
+app.state.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS
 app.state.MODELS = {}
 app.state.MODELS = {}
 
 
 
 
@@ -223,7 +222,7 @@ async def pull_model(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/api/pull",
                 url=f"{url}/api/pull",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
                 stream=True,
                 stream=True,
             )
             )
 
 
@@ -295,7 +294,7 @@ async def push_model(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/api/push",
                 url=f"{url}/api/push",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
             )
             )
 
 
             r.raise_for_status()
             r.raise_for_status()
@@ -357,7 +356,7 @@ async def create_model(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/api/create",
                 url=f"{url}/api/create",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
                 stream=True,
                 stream=True,
             )
             )
 
 
@@ -420,7 +419,7 @@ async def copy_model(
         r = requests.request(
         r = requests.request(
             method="POST",
             method="POST",
             url=f"{url}/api/copy",
             url=f"{url}/api/copy",
-            data=form_data.model_dump_json(exclude_none=True),
+            data=form_data.model_dump_json(exclude_none=True).encode(),
         )
         )
         r.raise_for_status()
         r.raise_for_status()
 
 
@@ -467,7 +466,7 @@ async def delete_model(
         r = requests.request(
         r = requests.request(
             method="DELETE",
             method="DELETE",
             url=f"{url}/api/delete",
             url=f"{url}/api/delete",
-            data=form_data.model_dump_json(exclude_none=True),
+            data=form_data.model_dump_json(exclude_none=True).encode(),
         )
         )
         r.raise_for_status()
         r.raise_for_status()
 
 
@@ -507,7 +506,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use
         r = requests.request(
         r = requests.request(
             method="POST",
             method="POST",
             url=f"{url}/api/show",
             url=f"{url}/api/show",
-            data=form_data.model_dump_json(exclude_none=True),
+            data=form_data.model_dump_json(exclude_none=True).encode(),
         )
         )
         r.raise_for_status()
         r.raise_for_status()
 
 
@@ -559,7 +558,7 @@ async def generate_embeddings(
         r = requests.request(
         r = requests.request(
             method="POST",
             method="POST",
             url=f"{url}/api/embeddings",
             url=f"{url}/api/embeddings",
-            data=form_data.model_dump_json(exclude_none=True),
+            data=form_data.model_dump_json(exclude_none=True).encode(),
         )
         )
         r.raise_for_status()
         r.raise_for_status()
 
 
@@ -645,7 +644,7 @@ async def generate_completion(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/api/generate",
                 url=f"{url}/api/generate",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
                 stream=True,
                 stream=True,
             )
             )
 
 
@@ -715,7 +714,7 @@ async def generate_chat_completion(
 
 
     r = None
     r = None
 
 
-    print(form_data.model_dump_json(exclude_none=True))
+    print(form_data.model_dump_json(exclude_none=True).encode())
 
 
     def get_request():
     def get_request():
         nonlocal form_data
         nonlocal form_data
@@ -745,7 +744,7 @@ async def generate_chat_completion(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/api/chat",
                 url=f"{url}/api/chat",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
                 stream=True,
                 stream=True,
             )
             )
 
 
@@ -757,6 +756,7 @@ async def generate_chat_completion(
                 headers=dict(r.headers),
                 headers=dict(r.headers),
             )
             )
         except Exception as e:
         except Exception as e:
+            print(e)
             raise e
             raise e
 
 
     try:
     try:
@@ -844,7 +844,7 @@ async def generate_openai_chat_completion(
             r = requests.request(
             r = requests.request(
                 method="POST",
                 method="POST",
                 url=f"{url}/v1/chat/completions",
                 url=f"{url}/v1/chat/completions",
-                data=form_data.model_dump_json(exclude_none=True),
+                data=form_data.model_dump_json(exclude_none=True).encode(),
                 stream=True,
                 stream=True,
             )
             )
 
 

+ 173 - 83
backend/apps/openai/main.py

@@ -3,7 +3,10 @@ from fastapi.middleware.cors import CORSMiddleware
 from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
 from fastapi.responses import StreamingResponse, JSONResponse, FileResponse
 
 
 import requests
 import requests
+import aiohttp
+import asyncio
 import json
 import json
+
 from pydantic import BaseModel
 from pydantic import BaseModel
 
 
 
 
@@ -15,7 +18,9 @@ from utils.utils import (
     get_verified_user,
     get_verified_user,
     get_admin_user,
     get_admin_user,
 )
 )
-from config import OPENAI_API_BASE_URL, OPENAI_API_KEY, CACHE_DIR
+from config import OPENAI_API_BASE_URLS, OPENAI_API_KEYS, CACHE_DIR
+from typing import List, Optional
+
 
 
 import hashlib
 import hashlib
 from pathlib import Path
 from pathlib import Path
@@ -29,116 +34,207 @@ app.add_middleware(
     allow_headers=["*"],
     allow_headers=["*"],
 )
 )
 
 
-app.state.OPENAI_API_BASE_URL = OPENAI_API_BASE_URL
-app.state.OPENAI_API_KEY = OPENAI_API_KEY
+app.state.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS
+app.state.OPENAI_API_KEYS = OPENAI_API_KEYS
+
+app.state.MODELS = {}
+
 
 
+@app.middleware("http")
+async def check_url(request: Request, call_next):
+    if len(app.state.MODELS) == 0:
+        await get_all_models()
+    else:
+        pass
 
 
-class UrlUpdateForm(BaseModel):
-    url: str
+    response = await call_next(request)
+    return response
 
 
 
 
-class KeyUpdateForm(BaseModel):
-    key: str
+class UrlsUpdateForm(BaseModel):
+    urls: List[str]
 
 
 
 
-@app.get("/url")
-async def get_openai_url(user=Depends(get_admin_user)):
-    return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
+class KeysUpdateForm(BaseModel):
+    keys: List[str]
 
 
 
 
-@app.post("/url/update")
-async def update_openai_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)):
-    app.state.OPENAI_API_BASE_URL = form_data.url
-    return {"OPENAI_API_BASE_URL": app.state.OPENAI_API_BASE_URL}
+@app.get("/urls")
+async def get_openai_urls(user=Depends(get_admin_user)):
+    return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
 
 
 
 
-@app.get("/key")
-async def get_openai_key(user=Depends(get_admin_user)):
-    return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
+@app.post("/urls/update")
+async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)):
+    app.state.OPENAI_API_BASE_URLS = form_data.urls
+    return {"OPENAI_API_BASE_URLS": app.state.OPENAI_API_BASE_URLS}
 
 
 
 
-@app.post("/key/update")
-async def update_openai_key(form_data: KeyUpdateForm, user=Depends(get_admin_user)):
-    app.state.OPENAI_API_KEY = form_data.key
-    return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
+@app.get("/keys")
+async def get_openai_keys(user=Depends(get_admin_user)):
+    return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
+
+
+@app.post("/keys/update")
+async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)):
+    app.state.OPENAI_API_KEYS = form_data.keys
+    return {"OPENAI_API_KEYS": app.state.OPENAI_API_KEYS}
 
 
 
 
 @app.post("/audio/speech")
 @app.post("/audio/speech")
 async def speech(request: Request, user=Depends(get_verified_user)):
 async def speech(request: Request, user=Depends(get_verified_user)):
-    target_url = f"{app.state.OPENAI_API_BASE_URL}/audio/speech"
+    idx = None
+    try:
+        idx = app.state.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1")
+        body = await request.body()
+        name = hashlib.sha256(body).hexdigest()
+
+        SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
+        SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
+        file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
+        file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
+
+        # Check if the file already exists in the cache
+        if file_path.is_file():
+            return FileResponse(file_path)
+
+        headers = {}
+        headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}"
+        headers["Content-Type"] = "application/json"
+
+        try:
+            r = requests.post(
+                url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech",
+                data=body,
+                headers=headers,
+                stream=True,
+            )
 
 
-    if app.state.OPENAI_API_KEY == "":
-        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
+            r.raise_for_status()
 
 
-    body = await request.body()
+            # Save the streaming content to a file
+            with open(file_path, "wb") as f:
+                for chunk in r.iter_content(chunk_size=8192):
+                    f.write(chunk)
 
 
-    name = hashlib.sha256(body).hexdigest()
+            with open(file_body_path, "w") as f:
+                json.dump(json.loads(body.decode("utf-8")), f)
 
 
-    SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/")
-    SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
-    file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3")
-    file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json")
+            # Return the saved file
+            return FileResponse(file_path)
 
 
-    # Check if the file already exists in the cache
-    if file_path.is_file():
-        return FileResponse(file_path)
+        except Exception as e:
+            print(e)
+            error_detail = "Open 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)
+
+    except ValueError:
+        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND)
 
 
-    headers = {}
-    headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
-    headers["Content-Type"] = "application/json"
 
 
+async def fetch_url(url, key):
     try:
     try:
-        print("openai")
-        r = requests.post(
-            url=target_url,
-            data=body,
-            headers=headers,
-            stream=True,
+        headers = {"Authorization": f"Bearer {key}"}
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url, headers=headers) as response:
+                return await response.json()
+    except Exception as e:
+        # Handle connection error here
+        print(f"Connection error: {e}")
+        return None
+
+
+def merge_models_lists(model_lists):
+    merged_list = []
+
+    for idx, models in enumerate(model_lists):
+        merged_list.extend(
+            [
+                {**model, "urlIdx": idx}
+                for model in models
+                if "api.openai.com" not in app.state.OPENAI_API_BASE_URLS[idx]
+                or "gpt" in model["id"]
+            ]
         )
         )
 
 
-        r.raise_for_status()
+    return merged_list
 
 
-        # Save the streaming content to a file
-        with open(file_path, "wb") as f:
-            for chunk in r.iter_content(chunk_size=8192):
-                f.write(chunk)
 
 
-        with open(file_body_path, "w") as f:
-            json.dump(json.loads(body.decode("utf-8")), f)
+async def get_all_models():
+    print("get_all_models")
+    tasks = [
+        fetch_url(f"{url}/models", app.state.OPENAI_API_KEYS[idx])
+        for idx, url in enumerate(app.state.OPENAI_API_BASE_URLS)
+    ]
+    responses = await asyncio.gather(*tasks)
+    responses = list(filter(lambda x: x is not None and "error" not in x, responses))
+    models = {
+        "data": merge_models_lists(
+            list(map(lambda response: response["data"], responses))
+        )
+    }
+    app.state.MODELS = {model["id"]: model for model in models["data"]}
 
 
-        # Return the saved file
-        return FileResponse(file_path)
+    return models
 
 
-    except Exception as e:
-        print(e)
-        error_detail = "Open 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)
+# , user=Depends(get_current_user)
+@app.get("/models")
+@app.get("/models/{url_idx}")
+async def get_models(url_idx: Optional[int] = None):
+    if url_idx == None:
+        return await get_all_models()
+    else:
+        url = app.state.OPENAI_API_BASE_URLS[url_idx]
+        try:
+            r = requests.request(method="GET", url=f"{url}/models")
+            r.raise_for_status()
+
+            response_data = r.json()
+            if "api.openai.com" in url:
+                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 = "Open 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 if r else 500,
+                detail=error_detail,
+            )
 
 
 
 
 @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
 @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
 async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
 async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
-    target_url = f"{app.state.OPENAI_API_BASE_URL}/{path}"
-    print(target_url, app.state.OPENAI_API_KEY)
-
-    if app.state.OPENAI_API_KEY == "":
-        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
+    idx = 0
 
 
     body = await request.body()
     body = await request.body()
-
     # TODO: Remove below after gpt-4-vision fix from Open AI
     # TODO: Remove below after gpt-4-vision fix from Open AI
     # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
     # Try to decode the body of the request from bytes to a UTF-8 string (Require add max_token to fix gpt-4-vision)
     try:
     try:
         body = body.decode("utf-8")
         body = body.decode("utf-8")
         body = json.loads(body)
         body = json.loads(body)
 
 
+        idx = app.state.MODELS[body.get("model")]["urlIdx"]
+
         # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
         # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000
         # This is a workaround until OpenAI fixes the issue with this model
         # This is a workaround until OpenAI fixes the issue with this model
         if body.get("model") == "gpt-4-vision-preview":
         if body.get("model") == "gpt-4-vision-preview":
@@ -158,8 +254,16 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
     except json.JSONDecodeError as e:
     except json.JSONDecodeError as e:
         print("Error loading request body into a dictionary:", e)
         print("Error loading request body into a dictionary:", e)
 
 
+    url = app.state.OPENAI_API_BASE_URLS[idx]
+    key = app.state.OPENAI_API_KEYS[idx]
+
+    target_url = f"{url}/{path}"
+
+    if key == "":
+        raise HTTPException(status_code=401, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
+
     headers = {}
     headers = {}
-    headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
+    headers["Authorization"] = f"Bearer {key}"
     headers["Content-Type"] = "application/json"
     headers["Content-Type"] = "application/json"
 
 
     try:
     try:
@@ -181,21 +285,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)):
                 headers=dict(r.headers),
                 headers=dict(r.headers),
             )
             )
         else:
         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()
             response_data = r.json()
-
-            if "api.openai.com" 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
             return response_data
     except Exception as e:
     except Exception as e:
         print(e)
         print(e)

+ 1 - 1
backend/apps/rag/main.py

@@ -425,7 +425,7 @@ def get_loader(filename: str, file_content_type: str, file_path: str):
     ]
     ]
 
 
     if file_ext == "pdf":
     if file_ext == "pdf":
-        loader = PyPDFLoader(file_path)
+        loader = PyPDFLoader(file_path, extract_images=True)
     elif file_ext == "csv":
     elif file_ext == "csv":
         loader = CSVLoader(file_path)
         loader = CSVLoader(file_path)
     elif file_ext == "rst":
     elif file_ext == "rst":

+ 3 - 3
backend/apps/web/routers/utils.py

@@ -14,7 +14,7 @@ import json
 from utils.utils import get_admin_user
 from utils.utils import get_admin_user
 from utils.misc import calculate_sha256, get_gravatar_url
 from utils.misc import calculate_sha256, get_gravatar_url
 
 
-from config import OLLAMA_API_BASE_URL, DATA_DIR, UPLOAD_DIR
+from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR
 from constants import ERROR_MESSAGES
 from constants import ERROR_MESSAGES
 
 
 
 
@@ -75,7 +75,7 @@ async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024
                     hashed = calculate_sha256(file)
                     hashed = calculate_sha256(file)
                     file.seek(0)
                     file.seek(0)
 
 
-                    url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
+                    url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}"
                     response = requests.post(url, data=file)
                     response = requests.post(url, data=file)
 
 
                     if response.ok:
                     if response.ok:
@@ -147,7 +147,7 @@ def upload(file: UploadFile = File(...)):
                     hashed = calculate_sha256(f)
                     hashed = calculate_sha256(f)
                     f.seek(0)
                     f.seek(0)
 
 
-                    url = f"{OLLAMA_API_BASE_URL}/blobs/sha256:{hashed}"
+                    url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}"
                     response = requests.post(url, data=f)
                     response = requests.post(url, data=f)
 
 
                     if response.ok:
                     if response.ok:

+ 27 - 6
backend/config.py

@@ -200,27 +200,32 @@ if not os.path.exists(LITELLM_CONFIG_PATH):
 
 
 
 
 ####################################
 ####################################
-# OLLAMA_API_BASE_URL
+# OLLAMA_BASE_URL
 ####################################
 ####################################
 
 
 OLLAMA_API_BASE_URL = os.environ.get(
 OLLAMA_API_BASE_URL = os.environ.get(
     "OLLAMA_API_BASE_URL", "http://localhost:11434/api"
     "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"
+OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
 
 
+if ENV == "prod":
+    if OLLAMA_BASE_URL == "/ollama":
+        OLLAMA_BASE_URL = "http://host.docker.internal:11434"
 
 
-OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "")
 
 
-if OLLAMA_BASE_URL == "":
+if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "":
     OLLAMA_BASE_URL = (
     OLLAMA_BASE_URL = (
         OLLAMA_API_BASE_URL[:-4]
         OLLAMA_API_BASE_URL[:-4]
         if OLLAMA_API_BASE_URL.endswith("/api")
         if OLLAMA_API_BASE_URL.endswith("/api")
         else OLLAMA_API_BASE_URL
         else OLLAMA_API_BASE_URL
     )
     )
 
 
+OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "")
+OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL
+
+OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")]
+
 
 
 ####################################
 ####################################
 # OPENAI_API
 # OPENAI_API
@@ -229,9 +234,25 @@ if OLLAMA_BASE_URL == "":
 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
 OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
 OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
 OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "")
 
 
+if OPENAI_API_KEY == "":
+    OPENAI_API_KEY = "none"
+
 if OPENAI_API_BASE_URL == "":
 if OPENAI_API_BASE_URL == "":
     OPENAI_API_BASE_URL = "https://api.openai.com/v1"
     OPENAI_API_BASE_URL = "https://api.openai.com/v1"
 
 
+OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "")
+OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY
+
+OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")]
+
+
+OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "")
+OPENAI_API_BASE_URLS = (
+    OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL
+)
+
+OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URL.split(";")]
+
 
 
 ####################################
 ####################################
 # WEBUI
 # WEBUI

+ 2 - 0
backend/constants.py

@@ -41,6 +41,7 @@ class ERROR_MESSAGES(str, Enum):
     NOT_FOUND = "We could not find what you're looking for :/"
     NOT_FOUND = "We could not find what you're looking for :/"
     USER_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."
     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."
     MALICIOUS = "Unusual activities detected, please try again in a few minutes."
 
 
     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance."
@@ -50,3 +51,4 @@ class ERROR_MESSAGES(str, Enum):
     RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
     RATE_LIMIT_EXCEEDED = "API rate limit exceeded"
 
 
     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found"
+    OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found"

+ 3 - 0
backend/requirements.txt

@@ -35,6 +35,9 @@ openpyxl
 pyxlsb
 pyxlsb
 xlrd
 xlrd
 
 
+opencv-python-headless
+rapidocr-onnxruntime
+
 faster-whisper
 faster-whisper
 
 
 PyJWT
 PyJWT

+ 2 - 2
docker-compose.yaml

@@ -14,7 +14,7 @@ services:
     build:
     build:
       context: .
       context: .
       args:
       args:
-        OLLAMA_API_BASE_URL: '/ollama/api'
+        OLLAMA_BASE_URL: '/ollama'
       dockerfile: Dockerfile
       dockerfile: Dockerfile
     image: ghcr.io/open-webui/open-webui:main
     image: ghcr.io/open-webui/open-webui:main
     container_name: open-webui
     container_name: open-webui
@@ -25,7 +25,7 @@ services:
     ports:
     ports:
       - ${OPEN_WEBUI_PORT-3000}:8080
       - ${OPEN_WEBUI_PORT-3000}:8080
     environment:
     environment:
-      - 'OLLAMA_API_BASE_URL=http://ollama:11434/api'
+      - 'OLLAMA_BASE_URL=http://ollama:11434'
       - 'WEBUI_SECRET_KEY='
       - 'WEBUI_SECRET_KEY='
     extra_hosts:
     extra_hosts:
       - host.docker.internal:host-gateway
       - host.docker.internal:host-gateway

+ 1 - 1
kubernetes/helm/templates/webui-deployment.yaml

@@ -40,7 +40,7 @@ spec:
         - name: data
         - name: data
           mountPath: /app/backend/data
           mountPath: /app/backend/data
         env:
         env:
-        - name: OLLAMA_API_BASE_URL
+        - name: OLLAMA_BASE_URL
           value: {{ include "ollama.url" . | quote }}
           value: {{ include "ollama.url" . | quote }}
         tty: true
         tty: true
       {{- with .Values.webui.nodeSelector }}
       {{- with .Values.webui.nodeSelector }}

+ 2 - 2
kubernetes/manifest/base/webui-deployment.yaml

@@ -26,8 +26,8 @@ spec:
             cpu: "1000m"
             cpu: "1000m"
             memory: "1Gi"
             memory: "1Gi"
         env:
         env:
-        - name: OLLAMA_API_BASE_URL
-          value: "http://ollama-service.open-webui.svc.cluster.local:11434/api"
+        - name: OLLAMA_BASE_URL
+          value: "http://ollama-service.open-webui.svc.cluster.local:11434"
         tty: true
         tty: true
         volumeMounts:
         volumeMounts:
         - name: webui-volume
         - name: webui-volume

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
 	"name": "open-webui",
 	"name": "open-webui",
-	"version": "0.1.108",
+	"version": "0.1.110",
 	"private": true,
 	"private": true,
 	"scripts": {
 	"scripts": {
 		"dev": "vite dev --host",
 		"dev": "vite dev --host",

+ 4 - 0
src/app.css

@@ -43,6 +43,10 @@ ol > li {
 	font-weight: 400;
 	font-weight: 400;
 }
 }
 
 
+li p {
+	display: inline;
+}
+
 ::-webkit-scrollbar-thumb {
 ::-webkit-scrollbar-thumb {
 	--tw-border-opacity: 1;
 	--tw-border-opacity: 1;
 	background-color: rgba(217, 217, 227, 0.8);
 	background-color: rgba(217, 217, 227, 0.8);

+ 14 - 14
src/lib/apis/openai/index.ts

@@ -1,9 +1,9 @@
 import { OPENAI_API_BASE_URL } from '$lib/constants';
 import { OPENAI_API_BASE_URL } from '$lib/constants';
 
 
-export const getOpenAIUrl = async (token: string = '') => {
+export const getOpenAIUrls = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${OPENAI_API_BASE_URL}/url`, {
+	const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -29,13 +29,13 @@ export const getOpenAIUrl = async (token: string = '') => {
 		throw error;
 		throw error;
 	}
 	}
 
 
-	return res.OPENAI_API_BASE_URL;
+	return res.OPENAI_API_BASE_URLS;
 };
 };
 
 
-export const updateOpenAIUrl = async (token: string = '', url: string) => {
+export const updateOpenAIUrls = async (token: string = '', urls: string[]) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${OPENAI_API_BASE_URL}/url/update`, {
+	const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -43,7 +43,7 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
 			...(token && { authorization: `Bearer ${token}` })
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
-			url: url
+			urls: urls
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {
@@ -64,13 +64,13 @@ export const updateOpenAIUrl = async (token: string = '', url: string) => {
 		throw error;
 		throw error;
 	}
 	}
 
 
-	return res.OPENAI_API_BASE_URL;
+	return res.OPENAI_API_BASE_URLS;
 };
 };
 
 
-export const getOpenAIKey = async (token: string = '') => {
+export const getOpenAIKeys = async (token: string = '') => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${OPENAI_API_BASE_URL}/key`, {
+	const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, {
 		method: 'GET',
 		method: 'GET',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -96,13 +96,13 @@ export const getOpenAIKey = async (token: string = '') => {
 		throw error;
 		throw error;
 	}
 	}
 
 
-	return res.OPENAI_API_KEY;
+	return res.OPENAI_API_KEYS;
 };
 };
 
 
-export const updateOpenAIKey = async (token: string = '', key: string) => {
+export const updateOpenAIKeys = async (token: string = '', keys: string[]) => {
 	let error = null;
 	let error = null;
 
 
-	const res = await fetch(`${OPENAI_API_BASE_URL}/key/update`, {
+	const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, {
 		method: 'POST',
 		method: 'POST',
 		headers: {
 		headers: {
 			Accept: 'application/json',
 			Accept: 'application/json',
@@ -110,7 +110,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
 			...(token && { authorization: `Bearer ${token}` })
 			...(token && { authorization: `Bearer ${token}` })
 		},
 		},
 		body: JSON.stringify({
 		body: JSON.stringify({
-			key: key
+			keys: keys
 		})
 		})
 	})
 	})
 		.then(async (res) => {
 		.then(async (res) => {
@@ -131,7 +131,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => {
 		throw error;
 		throw error;
 	}
 	}
 
 
-	return res.OPENAI_API_KEY;
+	return res.OPENAI_API_KEYS;
 };
 };
 
 
 export const getOpenAIModels = async (token: string = '') => {
 export const getOpenAIModels = async (token: string = '') => {

+ 117 - 72
src/lib/components/chat/Messages.svelte

@@ -225,33 +225,80 @@
 		}, 100);
 		}, 100);
 	};
 	};
 
 
-	// TODO: change delete behaviour
-	// const deleteMessageAndDescendants = async (messageId: string) => {
-	// 	if (history.messages[messageId]) {
-	// 		history.messages[messageId].deleted = true;
+	const messageDeleteHandler = async (messageId) => {
+		const messageToDelete = history.messages[messageId];
+		const messageParentId = messageToDelete.parentId;
+		const messageChildrenIds = messageToDelete.childrenIds ?? [];
+		const hasSibling = messageChildrenIds.some(
+			(childId) => history.messages[childId]?.childrenIds?.length > 0
+		);
+		messageChildrenIds.forEach((childId) => {
+			const child = history.messages[childId];
+			if (child && child.childrenIds) {
+				if (child.childrenIds.length === 0 && !hasSibling) {
+					// if last prompt/response pair
+					history.messages[messageParentId].childrenIds = [];
+					history.currentId = messageParentId;
+				} else {
+					child.childrenIds.forEach((grandChildId) => {
+						if (history.messages[grandChildId]) {
+							history.messages[grandChildId].parentId = messageParentId;
+							history.messages[messageParentId].childrenIds.push(grandChildId);
+						}
+					});
+				}
+			}
+			// remove response
+			history.messages[messageParentId].childrenIds = history.messages[
+				messageParentId
+			].childrenIds.filter((id) => id !== childId);
+		});
+		// remove prompt
+		history.messages[messageParentId].childrenIds = history.messages[
+			messageParentId
+		].childrenIds.filter((id) => id !== messageId);
+		await updateChatById(localStorage.token, chatId, {
+			messages: messages,
+			history: history
+		});
+	};
+
+	// const messageDeleteHandler = async (messageId) => {
+	// 	const message = history.messages[messageId];
+	// 	const parentId = message.parentId;
+	// 	const childrenIds = message.childrenIds ?? [];
+	// 	const grandchildrenIds = [];
 
 
-	// 		for (const childId of history.messages[messageId].childrenIds) {
-	// 			await deleteMessageAndDescendants(childId);
+	// 	// Iterate through childrenIds to find grandchildrenIds
+	// 	for (const childId of childrenIds) {
+	// 		const childMessage = history.messages[childId];
+	// 		const grandChildrenIds = childMessage.childrenIds ?? [];
+
+	// 		for (const grandchildId of grandchildrenIds) {
+	// 			const childMessage = history.messages[grandchildId];
+	// 			childMessage.parentId = parentId;
 	// 		}
 	// 		}
+	// 		grandchildrenIds.push(...grandChildrenIds);
 	// 	}
 	// 	}
-	// };
-
-	// const triggerDeleteMessageRecursive = async (messageId: string) => {
-	// 	await deleteMessageAndDescendants(messageId);
-	// 	await updateChatById(localStorage.token, chatId, { history });
-	// 	await chats.set(await getChatList(localStorage.token));
-	// };
 
 
-	const messageDeleteHandler = async (messageId) => {
-		if (history.messages[messageId]) {
-			history.messages[messageId].deleted = true;
+	// 	history.messages[parentId].childrenIds.push(...grandchildrenIds);
+	// 	history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
+	// 		(id) => id !== messageId
+	// 	);
+
+	// 	// Select latest message
+	// 	let currentMessageId = grandchildrenIds.at(-1);
+	// 	if (currentMessageId) {
+	// 		let messageChildrenIds = history.messages[currentMessageId].childrenIds;
+	// 		while (messageChildrenIds.length !== 0) {
+	// 			currentMessageId = messageChildrenIds.at(-1);
+	// 			messageChildrenIds = history.messages[currentMessageId].childrenIds;
+	// 		}
+	// 		history.currentId = currentMessageId;
+	// 	}
 
 
-			for (const childId of history.messages[messageId].childrenIds) {
-				history.messages[childId].deleted = true;
-			}
-		}
-		await updateChatById(localStorage.token, chatId, { history });
-	};
+	// 	await updateChatById(localStorage.token, chatId, { messages, history });
+	// };
 </script>
 </script>
 
 
 {#if messages.length == 0}
 {#if messages.length == 0}
@@ -260,57 +307,55 @@
 	<div class=" pb-10">
 	<div class=" pb-10">
 		{#key chatId}
 		{#key chatId}
 			{#each messages as message, messageIdx}
 			{#each messages as message, messageIdx}
-				{#if !message.deleted}
-					<div class=" w-full">
-						<div
-							class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
-								? 'max-w-full'
-								: 'max-w-3xl'} mx-auto rounded-lg group"
-						>
-							{#if message.role === 'user'}
-								<UserMessage
-									on:delete={() => messageDeleteHandler(message.id)}
-									user={$user}
-									{message}
-									isFirstMessage={messageIdx === 0}
-									siblings={message.parentId !== null
-										? history.messages[message.parentId]?.childrenIds ?? []
-										: Object.values(history.messages)
-												.filter((message) => message.parentId === null)
-												.map((message) => message.id) ?? []}
-									{confirmEditMessage}
-									{showPreviousMessage}
-									{showNextMessage}
-									{copyToClipboard}
-								/>
-							{:else}
-								<ResponseMessage
-									{message}
-									modelfiles={selectedModelfiles}
-									siblings={history.messages[message.parentId]?.childrenIds ?? []}
-									isLastMessage={messageIdx + 1 === messages.length}
-									{confirmEditResponseMessage}
-									{showPreviousMessage}
-									{showNextMessage}
-									{rateMessage}
-									{copyToClipboard}
-									{continueGeneration}
-									{regenerateResponse}
-									on:save={async (e) => {
-										console.log('save', e);
-
-										const message = e.detail;
-										history.messages[message.id] = message;
-										await updateChatById(localStorage.token, chatId, {
-											messages: messages,
-											history: history
-										});
-									}}
-								/>
-							{/if}
-						</div>
+				<div class=" w-full">
+					<div
+						class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
+							? 'max-w-full'
+							: 'max-w-3xl'} mx-auto rounded-lg group"
+					>
+						{#if message.role === 'user'}
+							<UserMessage
+								on:delete={() => messageDeleteHandler(message.id)}
+								user={$user}
+								{message}
+								isFirstMessage={messageIdx === 0}
+								siblings={message.parentId !== null
+									? history.messages[message.parentId]?.childrenIds ?? []
+									: Object.values(history.messages)
+											.filter((message) => message.parentId === null)
+											.map((message) => message.id) ?? []}
+								{confirmEditMessage}
+								{showPreviousMessage}
+								{showNextMessage}
+								{copyToClipboard}
+							/>
+						{:else}
+							<ResponseMessage
+								{message}
+								modelfiles={selectedModelfiles}
+								siblings={history.messages[message.parentId]?.childrenIds ?? []}
+								isLastMessage={messageIdx + 1 === messages.length}
+								{confirmEditResponseMessage}
+								{showPreviousMessage}
+								{showNextMessage}
+								{rateMessage}
+								{copyToClipboard}
+								{continueGeneration}
+								{regenerateResponse}
+								on:save={async (e) => {
+									console.log('save', e);
+
+									const message = e.detail;
+									history.messages[message.id] = message;
+									await updateChatById(localStorage.token, chatId, {
+										messages: messages,
+										history: history
+									});
+								}}
+							/>
+						{/if}
 					</div>
 					</div>
-				{/if}
+				</div>
 			{/each}
 			{/each}
 
 
 			{#if bottomPadding}
 			{#if bottomPadding}

+ 246 - 220
src/lib/components/chat/Messages/ResponseMessage.svelte

@@ -24,6 +24,7 @@
 	import CodeBlock from './CodeBlock.svelte';
 	import CodeBlock from './CodeBlock.svelte';
 	import Image from '$lib/components/common/Image.svelte';
 	import Image from '$lib/components/common/Image.svelte';
 	import { WEBUI_BASE_URL } from '$lib/constants';
 	import { WEBUI_BASE_URL } from '$lib/constants';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 
 	export let modelfiles = [];
 	export let modelfiles = [];
 	export let message;
 	export let message;
@@ -346,6 +347,7 @@
 									class=" bg-transparent outline-none w-full resize-none"
 									class=" bg-transparent outline-none w-full resize-none"
 									bind:value={editedContent}
 									bind:value={editedContent}
 									on:input={(e) => {
 									on:input={(e) => {
+										e.target.style.height = '';
 										e.target.style.height = `${e.target.scrollHeight}px`;
 										e.target.style.height = `${e.target.scrollHeight}px`;
 									}}
 									}}
 								/>
 								/>
@@ -464,145 +466,15 @@
 											</div>
 											</div>
 										{/if}
 										{/if}
 
 
-										<button
-											class="{isLastMessage
-												? 'visible'
-												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
-											on:click={() => {
-												editMessageHandler();
-											}}
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
-												/>
-											</svg>
-										</button>
-
-										<button
-											class="{isLastMessage
-												? 'visible'
-												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
-											on:click={() => {
-												copyToClipboard(message.content);
-											}}
-										>
-											<svg
-												xmlns="http://www.w3.org/2000/svg"
-												fill="none"
-												viewBox="0 0 24 24"
-												stroke-width="1.5"
-												stroke="currentColor"
-												class="w-4 h-4"
-											>
-												<path
-													stroke-linecap="round"
-													stroke-linejoin="round"
-													d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
-												/>
-											</svg>
-										</button>
-
-										<button
-											class="{isLastMessage
-												? 'visible'
-												: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
-												? 'bg-gray-100 dark:bg-gray-800'
-												: ''} dark:hover:text-white hover:text-black transition"
-											on:click={() => {
-												rateMessage(message.id, 1);
-											}}
-										>
-											<svg
-												stroke="currentColor"
-												fill="none"
-												stroke-width="2"
-												viewBox="0 0 24 24"
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												class="w-4 h-4"
-												xmlns="http://www.w3.org/2000/svg"
-												><path
-													d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
-												/></svg
-											>
-										</button>
-										<button
-											class="{isLastMessage
-												? 'visible'
-												: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
-												? 'bg-gray-100 dark:bg-gray-800'
-												: ''} dark:hover:text-white hover:text-black transition"
-											on:click={() => {
-												rateMessage(message.id, -1);
-											}}
-										>
-											<svg
-												stroke="currentColor"
-												fill="none"
-												stroke-width="2"
-												viewBox="0 0 24 24"
-												stroke-linecap="round"
-												stroke-linejoin="round"
-												class="w-4 h-4"
-												xmlns="http://www.w3.org/2000/svg"
-												><path
-													d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
-												/></svg
+										<Tooltip content="Edit" placement="bottom">
+											<button
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+												on:click={() => {
+													editMessageHandler();
+												}}
 											>
 											>
-										</button>
-
-										<button
-											id="speak-button-{message.id}"
-											class="{isLastMessage
-												? 'visible'
-												: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
-											on:click={() => {
-												if (!loadingSpeech) {
-													toggleSpeakMessage(message);
-												}
-											}}
-										>
-											{#if loadingSpeech}
-												<svg
-													class=" w-4 h-4"
-													fill="currentColor"
-													viewBox="0 0 24 24"
-													xmlns="http://www.w3.org/2000/svg"
-													><style>
-														.spinner_S1WN {
-															animation: spinner_MGfb 0.8s linear infinite;
-															animation-delay: -0.8s;
-														}
-														.spinner_Km9P {
-															animation-delay: -0.65s;
-														}
-														.spinner_JApP {
-															animation-delay: -0.5s;
-														}
-														@keyframes spinner_MGfb {
-															93.75%,
-															100% {
-																opacity: 0.2;
-															}
-														}
-													</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
-														class="spinner_S1WN spinner_Km9P"
-														cx="12"
-														cy="12"
-														r="3"
-													/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
-												>
-											{:else if speaking}
 												<svg
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													xmlns="http://www.w3.org/2000/svg"
 													fill="none"
 													fill="none"
@@ -614,10 +486,21 @@
 													<path
 													<path
 														stroke-linecap="round"
 														stroke-linecap="round"
 														stroke-linejoin="round"
 														stroke-linejoin="round"
-														d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
+														d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
 													/>
 													/>
 												</svg>
 												</svg>
-											{:else}
+											</button>
+										</Tooltip>
+
+										<Tooltip content="Copy" placement="bottom">
+											<button
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
+												on:click={() => {
+													copyToClipboard(message.content);
+												}}
+											>
 												<svg
 												<svg
 													xmlns="http://www.w3.org/2000/svg"
 													xmlns="http://www.w3.org/2000/svg"
 													fill="none"
 													fill="none"
@@ -629,24 +512,79 @@
 													<path
 													<path
 														stroke-linecap="round"
 														stroke-linecap="round"
 														stroke-linejoin="round"
 														stroke-linejoin="round"
-														d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
+														d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
 													/>
 													/>
 												</svg>
 												</svg>
-											{/if}
-										</button>
+											</button>
+										</Tooltip>
 
 
-										{#if $config.images}
+										<Tooltip content="Good Response" placement="bottom">
+											<button
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
+													? 'bg-gray-100 dark:bg-gray-800'
+													: ''} dark:hover:text-white hover:text-black transition"
+												on:click={() => {
+													rateMessage(message.id, 1);
+												}}
+											>
+												<svg
+													stroke="currentColor"
+													fill="none"
+													stroke-width="2"
+													viewBox="0 0 24 24"
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													class="w-4 h-4"
+													xmlns="http://www.w3.org/2000/svg"
+													><path
+														d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
+													/></svg
+												>
+											</button>
+										</Tooltip>
+
+										<Tooltip content="Bad Response" placement="bottom">
 											<button
 											<button
+												class="{isLastMessage
+													? 'visible'
+													: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
+													? 'bg-gray-100 dark:bg-gray-800'
+													: ''} dark:hover:text-white hover:text-black transition"
+												on:click={() => {
+													rateMessage(message.id, -1);
+												}}
+											>
+												<svg
+													stroke="currentColor"
+													fill="none"
+													stroke-width="2"
+													viewBox="0 0 24 24"
+													stroke-linecap="round"
+													stroke-linejoin="round"
+													class="w-4 h-4"
+													xmlns="http://www.w3.org/2000/svg"
+													><path
+														d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
+													/></svg
+												>
+											</button>
+										</Tooltip>
+
+										<Tooltip content="Read Aloud" placement="bottom">
+											<button
+												id="speak-button-{message.id}"
 												class="{isLastMessage
 												class="{isLastMessage
 													? 'visible'
 													? 'visible'
 													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
 													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
 												on:click={() => {
 												on:click={() => {
-													if (!generatingImage) {
-														generateImage(message);
+													if (!loadingSpeech) {
+														toggleSpeakMessage(message);
 													}
 													}
 												}}
 												}}
 											>
 											>
-												{#if generatingImage}
+												{#if loadingSpeech}
 													<svg
 													<svg
 														class=" w-4 h-4"
 														class=" w-4 h-4"
 														fill="currentColor"
 														fill="currentColor"
@@ -681,6 +619,21 @@
 															r="3"
 															r="3"
 														/></svg
 														/></svg
 													>
 													>
+												{:else if speaking}
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														fill="none"
+														viewBox="0 0 24 24"
+														stroke-width="1.5"
+														stroke="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															stroke-linecap="round"
+															stroke-linejoin="round"
+															d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
+														/>
+													</svg>
 												{:else}
 												{:else}
 													<svg
 													<svg
 														xmlns="http://www.w3.org/2000/svg"
 														xmlns="http://www.w3.org/2000/svg"
@@ -693,93 +646,166 @@
 														<path
 														<path
 															stroke-linecap="round"
 															stroke-linecap="round"
 															stroke-linejoin="round"
 															stroke-linejoin="round"
-															d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
+															d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
 														/>
 														/>
 													</svg>
 													</svg>
 												{/if}
 												{/if}
 											</button>
 											</button>
+										</Tooltip>
+
+										{#if $config.images}
+											<Tooltip content="Generate Image" placement="bottom">
+												<button
+													class="{isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
+													on:click={() => {
+														if (!generatingImage) {
+															generateImage(message);
+														}
+													}}
+												>
+													{#if generatingImage}
+														<svg
+															class=" w-4 h-4"
+															fill="currentColor"
+															viewBox="0 0 24 24"
+															xmlns="http://www.w3.org/2000/svg"
+															><style>
+																.spinner_S1WN {
+																	animation: spinner_MGfb 0.8s linear infinite;
+																	animation-delay: -0.8s;
+																}
+																.spinner_Km9P {
+																	animation-delay: -0.65s;
+																}
+																.spinner_JApP {
+																	animation-delay: -0.5s;
+																}
+																@keyframes spinner_MGfb {
+																	93.75%,
+																	100% {
+																		opacity: 0.2;
+																	}
+																}
+															</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
+																class="spinner_S1WN spinner_Km9P"
+																cx="12"
+																cy="12"
+																r="3"
+															/><circle
+																class="spinner_S1WN spinner_JApP"
+																cx="20"
+																cy="12"
+																r="3"
+															/></svg
+														>
+													{:else}
+														<svg
+															xmlns="http://www.w3.org/2000/svg"
+															fill="none"
+															viewBox="0 0 24 24"
+															stroke-width="1.5"
+															stroke="currentColor"
+															class="w-4 h-4"
+														>
+															<path
+																stroke-linecap="round"
+																stroke-linejoin="round"
+																d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
+															/>
+														</svg>
+													{/if}
+												</button>
+											</Tooltip>
 										{/if}
 										{/if}
 
 
 										{#if message.info}
 										{#if message.info}
-											<button
-												class=" {isLastMessage
-													? 'visible'
-													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
-												on:click={() => {
-													console.log(message);
-												}}
-												id="info-{message.id}"
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													fill="none"
-													viewBox="0 0 24 24"
-													stroke-width="1.5"
-													stroke="currentColor"
-													class="w-4 h-4"
+											<Tooltip content="Generation Info" placement="bottom">
+												<button
+													class=" {isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
+													on:click={() => {
+														console.log(message);
+													}}
+													id="info-{message.id}"
 												>
 												>
-													<path
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
-													/>
-												</svg>
-											</button>
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														fill="none"
+														viewBox="0 0 24 24"
+														stroke-width="1.5"
+														stroke="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															stroke-linecap="round"
+															stroke-linejoin="round"
+															d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
+														/>
+													</svg>
+												</button>
+											</Tooltip>
 										{/if}
 										{/if}
 
 
 										{#if isLastMessage}
 										{#if isLastMessage}
-											<button
-												type="button"
-												class="{isLastMessage
-													? 'visible'
-													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
-												on:click={() => {
-													continueGeneration();
-												}}
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													fill="none"
-													viewBox="0 0 24 24"
-													stroke-width="1.5"
-													stroke="currentColor"
-													class="w-4 h-4"
+											<Tooltip content="Continue Response" placement="bottom">
+												<button
+													type="button"
+													class="{isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
+													on:click={() => {
+														continueGeneration();
+													}}
 												>
 												>
-													<path
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
-													/>
-													<path
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
-													/>
-												</svg>
-											</button>
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														fill="none"
+														viewBox="0 0 24 24"
+														stroke-width="1.5"
+														stroke="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															stroke-linecap="round"
+															stroke-linejoin="round"
+															d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
+														/>
+														<path
+															stroke-linecap="round"
+															stroke-linejoin="round"
+															d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
+														/>
+													</svg>
+												</button>
+											</Tooltip>
 
 
-											<button
-												type="button"
-												class="{isLastMessage
-													? 'visible'
-													: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
-												on:click={regenerateResponse}
-											>
-												<svg
-													xmlns="http://www.w3.org/2000/svg"
-													fill="none"
-													viewBox="0 0 24 24"
-													stroke-width="1.5"
-													stroke="currentColor"
-													class="w-4 h-4"
+											<Tooltip content="Regenerate" placement="bottom">
+												<button
+													type="button"
+													class="{isLastMessage
+														? 'visible'
+														: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
+													on:click={regenerateResponse}
 												>
 												>
-													<path
-														stroke-linecap="round"
-														stroke-linejoin="round"
-														d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
-													/>
-												</svg>
-											</button>
+													<svg
+														xmlns="http://www.w3.org/2000/svg"
+														fill="none"
+														viewBox="0 0 24 24"
+														stroke-width="1.5"
+														stroke="currentColor"
+														class="w-4 h-4"
+													>
+														<path
+															stroke-linecap="round"
+															stroke-linejoin="round"
+															d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
+														/>
+													</svg>
+												</button>
+											</Tooltip>
 										{/if}
 										{/if}
 									</div>
 									</div>
 								{/if}
 								{/if}

+ 54 - 46
src/lib/components/chat/Messages/UserMessage.svelte

@@ -5,6 +5,7 @@
 	import Name from './Name.svelte';
 	import Name from './Name.svelte';
 	import ProfileImage from './ProfileImage.svelte';
 	import ProfileImage from './ProfileImage.svelte';
 	import { modelfiles, settings } from '$lib/stores';
 	import { modelfiles, settings } from '$lib/stores';
+	import Tooltip from '$lib/components/common/Tooltip.svelte';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
 
 
@@ -171,7 +172,8 @@
 						class=" bg-transparent outline-none w-full resize-none"
 						class=" bg-transparent outline-none w-full resize-none"
 						bind:value={editedContent}
 						bind:value={editedContent}
 						on:input={(e) => {
 						on:input={(e) => {
-							messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
+							e.target.style.height = '';
+							e.target.style.height = `${e.target.scrollHeight}px`;
 						}}
 						}}
 					/>
 					/>
 
 
@@ -248,55 +250,35 @@
 							</div>
 							</div>
 						{/if}
 						{/if}
 
 
-						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
-							on:click={() => {
-								editMessageHandler();
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								fill="none"
-								viewBox="0 0 24 24"
-								stroke-width="1.5"
-								stroke="currentColor"
-								class="w-4 h-4"
-							>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
-								/>
-							</svg>
-						</button>
-
-						<button
-							class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
-							on:click={() => {
-								copyToClipboard(message.content);
-							}}
-						>
-							<svg
-								xmlns="http://www.w3.org/2000/svg"
-								fill="none"
-								viewBox="0 0 24 24"
-								stroke-width="1.5"
-								stroke="currentColor"
-								class="w-4 h-4"
+						<Tooltip content="Edit" placement="bottom">
+							<button
+								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
+								on:click={() => {
+									editMessageHandler();
+								}}
 							>
 							>
-								<path
-									stroke-linecap="round"
-									stroke-linejoin="round"
-									d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
-								/>
-							</svg>
-						</button>
+								<svg
+									xmlns="http://www.w3.org/2000/svg"
+									fill="none"
+									viewBox="0 0 24 24"
+									stroke-width="1.5"
+									stroke="currentColor"
+									class="w-4 h-4"
+								>
+									<path
+										stroke-linecap="round"
+										stroke-linejoin="round"
+										d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
+									/>
+								</svg>
+							</button>
+						</Tooltip>
 
 
-						{#if !isFirstMessage}
+						<Tooltip content="Copy" placement="bottom">
 							<button
 							<button
 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 								class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
 								on:click={() => {
 								on:click={() => {
-									deleteMessageHandler();
+									copyToClipboard(message.content);
 								}}
 								}}
 							>
 							>
 								<svg
 								<svg
@@ -310,10 +292,36 @@
 									<path
 									<path
 										stroke-linecap="round"
 										stroke-linecap="round"
 										stroke-linejoin="round"
 										stroke-linejoin="round"
-										d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+										d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
 									/>
 									/>
 								</svg>
 								</svg>
 							</button>
 							</button>
+						</Tooltip>
+
+						{#if !isFirstMessage}
+							<Tooltip content="Delete" placement="bottom">
+								<button
+									class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
+									on:click={() => {
+										deleteMessageHandler();
+									}}
+								>
+									<svg
+										xmlns="http://www.w3.org/2000/svg"
+										fill="none"
+										viewBox="0 0 24 24"
+										stroke-width="1.5"
+										stroke="currentColor"
+										class="w-4 h-4"
+									>
+										<path
+											stroke-linecap="round"
+											stroke-linejoin="round"
+											d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
+										/>
+									</svg>
+								</button>
+							</Tooltip>
 						{/if}
 						{/if}
 					</div>
 					</div>
 				</div>
 				</div>

+ 79 - 35
src/lib/components/chat/Settings/Connections.svelte

@@ -4,7 +4,12 @@
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
-	import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
+	import {
+		getOpenAIKeys,
+		getOpenAIUrls,
+		updateOpenAIKeys,
+		updateOpenAIUrls
+	} from '$lib/apis/openai';
 	import { toast } from 'svelte-sonner';
 	import { toast } from 'svelte-sonner';
 
 
 	const i18n = getContext('i18n');
 	const i18n = getContext('i18n');
@@ -18,12 +23,14 @@
 	let OPENAI_API_KEY = '';
 	let OPENAI_API_KEY = '';
 	let OPENAI_API_BASE_URL = '';
 	let OPENAI_API_BASE_URL = '';
 
 
+	let OPENAI_API_KEYS = [''];
+	let OPENAI_API_BASE_URLS = [''];
+
 	let showOpenAI = false;
 	let showOpenAI = false;
-	let showLiteLLM = false;
 
 
 	const updateOpenAIHandler = async () => {
 	const updateOpenAIHandler = async () => {
-		OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
-		OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
+		OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
+		OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
 
 
 		await models.set(await getModels());
 		await models.set(await getModels());
 	};
 	};
@@ -45,8 +52,8 @@
 	onMount(async () => {
 	onMount(async () => {
 		if ($user.role === 'admin') {
 		if ($user.role === 'admin') {
 			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
 			OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
-			OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
-			OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
+			OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
+			OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
 		}
 		}
 	});
 	});
 </script>
 </script>
@@ -73,37 +80,74 @@
 				</div>
 				</div>
 
 
 				{#if showOpenAI}
 				{#if showOpenAI}
-					<div>
-						<div class=" mb-2.5 text-sm font-medium">{$i18n.t('API Key')}</div>
-						<div class="flex w-full">
-							<div class="flex-1">
-								<input
-									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-									placeholder={$i18n.t('Enter OpenAI API Key')}
-									bind:value={OPENAI_API_KEY}
-									autocomplete="off"
-								/>
-							</div>
-						</div>
-					</div>
+					<div class="flex flex-col gap-1">
+						{#each OPENAI_API_BASE_URLS as url, idx}
+							<div class="flex w-full gap-2">
+								<div class="flex-1">
+									<input
+										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										placeholder={$i18n.t('API Base URL')}
+										bind:value={url}
+										autocomplete="off"
+									/>
+								</div>
 
 
-					<div>
-						<div class=" mb-2.5 text-sm font-medium">{$i18n.t('API Base URL')}</div>
-						<div class="flex w-full">
-							<div class="flex-1">
-								<input
-									class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
-									placeholder="Enter OpenAI API Base URL"
-									bind:value={OPENAI_API_BASE_URL}
-									autocomplete="off"
-								/>
+								<div class="flex-1">
+									<input
+										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
+										placeholder={$i18n.t('API Key')}
+										bind:value={OPENAI_API_KEYS[idx]}
+										autocomplete="off"
+									/>
+								</div>
+								<div class="self-center flex items-center">
+									{#if idx === 0}
+										<button
+											class="px-1"
+											on:click={() => {
+												OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
+												OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
+											}}
+											type="button"
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path
+													d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
+												/>
+											</svg>
+										</button>
+									{:else}
+										<button
+											class="px-1"
+											on:click={() => {
+												OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
+													(url, urlIdx) => idx !== urlIdx
+												);
+												OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
+											}}
+											type="button"
+										>
+											<svg
+												xmlns="http://www.w3.org/2000/svg"
+												viewBox="0 0 16 16"
+												fill="currentColor"
+												class="w-4 h-4"
+											>
+												<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
+											</svg>
+										</button>
+									{/if}
+								</div>
 							</div>
 							</div>
-						</div>
-						<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
-							WebUI will make requests to <span class=" text-gray-200"
-								>'{OPENAI_API_BASE_URL}/chat'</span
-							>
-						</div>
+							<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
+								WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span>
+							</div>
+						{/each}
 					</div>
 					</div>
 				{/if}
 				{/if}
 			</div>
 			</div>

+ 3 - 3
src/lib/components/chat/Settings/Models.svelte

@@ -56,7 +56,7 @@
 	let modelUploadMode = 'file';
 	let modelUploadMode = 'file';
 	let modelInputFile = '';
 	let modelInputFile = '';
 	let modelFileUrl = '';
 	let modelFileUrl = '';
-	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`;
+	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
 	let modelFileDigest = '';
 	let modelFileDigest = '';
 	let uploadProgress = null;
 	let uploadProgress = null;
 
 
@@ -517,7 +517,7 @@
 									{#if !deleteModelTag}
 									{#if !deleteModelTag}
 										<option value="" disabled selected>Select a model</option>
 										<option value="" disabled selected>Select a model</option>
 									{/if}
 									{/if}
-									{#each $models.filter((m) => m.size != null) as model}
+									{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
 										<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 										<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
 											>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
 											>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
 										>
 										>
@@ -599,7 +599,7 @@
 												on:change={() => {
 												on:change={() => {
 													console.log(modelInputFile);
 													console.log(modelInputFile);
 												}}
 												}}
-												accept=".gguf"
+												accept=".gguf,.safetensors"
 												required
 												required
 												hidden
 												hidden
 											/>
 											/>

+ 3 - 1
src/lib/components/documents/AddDocModal.svelte

@@ -140,7 +140,9 @@
 						<button
 						<button
 							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
 							class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
 							type="button"
 							type="button"
-							on:click={uploadDocInputElement.click}
+							on:click={() => {
+								uploadDocInputElement.click();
+							}}
 						>
 						>
 							{#if inputFiles}
 							{#if inputFiles}
 								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
 								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.

+ 0 - 5
src/lib/constants.ts

@@ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [
 // This feature, akin to $env/static/private, exclusively incorporates environment variables
 // This feature, akin to $env/static/private, exclusively incorporates environment variables
 // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
 // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
 // Consequently, these variables can be securely exposed to client-side code.
 // Consequently, these variables can be securely exposed to client-side code.
-
-// Example of the .env configuration:
-// OLLAMA_API_BASE_URL="http://localhost:11434/api"
-// # Public
-// PUBLIC_API_BASE_URL=$OLLAMA_API_BASE_URL

+ 0 - 3
src/routes/(app)/+layout.svelte

@@ -99,14 +99,11 @@
 					if (localDBChats.length === 0) {
 					if (localDBChats.length === 0) {
 						await deleteDB('Chats');
 						await deleteDB('Chats');
 					}
 					}
-
-					console.log('localdb', localDBChats);
 				}
 				}
 
 
 				console.log(DB);
 				console.log(DB);
 			} catch (error) {
 			} catch (error) {
 				// IndexedDB Not Found
 				// IndexedDB Not Found
-				console.log('IDB Not Found');
 			}
 			}
 
 
 			console.log();
 			console.log();

+ 2 - 2
src/routes/(app)/+page.svelte

@@ -344,7 +344,7 @@
 						content: $settings.system
 						content: $settings.system
 				  }
 				  }
 				: undefined,
 				: undefined,
-			...messages.filter((message) => !message.deleted)
+			...messages
 		]
 		]
 			.filter((message) => message)
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
 			.map((message, idx, arr) => ({
@@ -558,7 +558,7 @@
 								content: $settings.system
 								content: $settings.system
 						  }
 						  }
 						: undefined,
 						: undefined,
-					...messages.filter((message) => !message.deleted)
+					...messages
 				]
 				]
 					.filter((message) => message)
 					.filter((message) => message)
 					.map((message, idx, arr) => ({
 					.map((message, idx, arr) => ({

+ 2 - 2
src/routes/(app)/c/[id]/+page.svelte

@@ -354,7 +354,7 @@
 						content: $settings.system
 						content: $settings.system
 				  }
 				  }
 				: undefined,
 				: undefined,
-			...messages.filter((message) => !message.deleted)
+			...messages
 		]
 		]
 			.filter((message) => message)
 			.filter((message) => message)
 			.map((message, idx, arr) => ({
 			.map((message, idx, arr) => ({
@@ -568,7 +568,7 @@
 								content: $settings.system
 								content: $settings.system
 						  }
 						  }
 						: undefined,
 						: undefined,
-					...messages.filter((message) => !message.deleted)
+					...messages
 				]
 				]
 					.filter((message) => message)
 					.filter((message) => message)
 					.map((message, idx, arr) => ({
 					.map((message, idx, arr) => ({