forked from open-webui/open-webui
		
	Revert "Merge Updates & Dockerfile improvements" (#3)
This reverts commit 9763d885be.
			
			
This commit is contained in:
		
							parent
							
								
									9763d885be
								
							
						
					
					
						commit
						099b1d066b
					
				
					 155 changed files with 4795 additions and 14501 deletions
				
			
		
							
								
								
									
										34
									
								
								.github/workflows/format-backend.yaml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										34
									
								
								.github/workflows/format-backend.yaml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,39 +1,27 @@ | |||
| name: Python CI | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - dev | ||||
|     branches: ['main'] | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - dev | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     name: 'Format Backend' | ||||
|     env: | ||||
|       PUBLIC_API_BASE_URL: '' | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         python-version: [3.11] | ||||
| 
 | ||||
|         node-version: | ||||
|           - latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
| 
 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: ${{ matrix.python-version }} | ||||
| 
 | ||||
|       - name: Use Python | ||||
|         uses: actions/setup-python@v4 | ||||
|       - name: Use Bun | ||||
|         uses: oven-sh/setup-bun@v1 | ||||
|       - name: Install dependencies | ||||
|         run: | | ||||
|           python -m pip install --upgrade pip | ||||
|           pip install black | ||||
| 
 | ||||
|           pip install yapf | ||||
|       - name: Format backend | ||||
|         run: npm run format:backend | ||||
| 
 | ||||
|       - name: Check for changes after format | ||||
|         run: git diff --exit-code | ||||
|         run: bun run format:backend | ||||
|  |  | |||
							
								
								
									
										42
									
								
								.github/workflows/format-build-frontend.yaml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								.github/workflows/format-build-frontend.yaml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,36 +1,22 @@ | |||
| name: Frontend Build | ||||
| 
 | ||||
| name: Bun CI | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - dev | ||||
|     branches: ['main'] | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - dev | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     name: 'Format & Build Frontend' | ||||
|     env: | ||||
|       PUBLIC_API_BASE_URL: '' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout Repository | ||||
|         uses: actions/checkout@v4 | ||||
| 
 | ||||
|       - name: Setup Node.js | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '20' # Or specify any other version you want to use | ||||
| 
 | ||||
|       - name: Install Dependencies | ||||
|         run: npm install | ||||
| 
 | ||||
|       - name: Format Frontend | ||||
|         run: npm run format | ||||
| 
 | ||||
|       - name: Check for Changes After Format | ||||
|         run: git diff --exit-code | ||||
| 
 | ||||
|       - name: Build Frontend | ||||
|         run: npm run build | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Use Bun | ||||
|         uses: oven-sh/setup-bun@v1 | ||||
|       - run: bun --version | ||||
|       - name: Install frontend dependencies | ||||
|         run: bun install | ||||
|       - name: Format frontend | ||||
|         run: bun run format | ||||
|       - name: Build frontend | ||||
|         run: bun run build | ||||
|  |  | |||
							
								
								
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -166,7 +166,7 @@ cython_debug/ | |||
| #  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore | ||||
| #  and can be added to the global gitignore or merged into this file.  For a more nuclear | ||||
| #  option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||||
| .idea/ | ||||
| #.idea/ | ||||
| 
 | ||||
| # Logs | ||||
| logs | ||||
|  |  | |||
							
								
								
									
										80
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										80
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,86 +5,6 @@ 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/), | ||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||
| 
 | ||||
| ## [0.1.116] - 2024-03-31 | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations. | ||||
| - **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding. | ||||
| - **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams. | ||||
| - **🌐 Localization**: Korean translation (I18n) now available. | ||||
| - **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage. | ||||
| - **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging. | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue. | ||||
| - **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags. | ||||
| - **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue. | ||||
| - **✅ Spelling**: Corrected various spelling issues for improved readability. | ||||
| 
 | ||||
| ## [0.1.115] - 2024-03-24 | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature. | ||||
| - **🛑 Cancel Model Download**: Added the ability to cancel model downloads. | ||||
| - **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI. | ||||
| - **🌟 Updated Light Theme**: Updated the light theme for a fresh look. | ||||
| - **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch. | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality. | ||||
| 
 | ||||
| ### Changed | ||||
| 
 | ||||
| - **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings. | ||||
| 
 | ||||
| ## [0.1.114] - 2024-03-20 | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL. | ||||
| - **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy. | ||||
| - **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others. | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields. | ||||
| - **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding. | ||||
| 
 | ||||
| ### Changed | ||||
| 
 | ||||
| - **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization. | ||||
| 
 | ||||
| ## [0.1.113] - 2024-03-18 | ||||
| 
 | ||||
| ### Added | ||||
| 
 | ||||
| - 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization). | ||||
| - 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience. | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices. | ||||
| - **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning. | ||||
| - **RAG Encoding Issue**: Fixed encoding problem in RAG. | ||||
| - **npm Audit Fix**: Addressed npm audit findings. | ||||
| - **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px. | ||||
| 
 | ||||
| ### Changed | ||||
| 
 | ||||
| - 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation. | ||||
| 
 | ||||
| ## [0.1.112] - 2024-03-15 | ||||
| 
 | ||||
| ### Fixed | ||||
| 
 | ||||
| - 🗨️ Resolved chat malfunction after image generation. | ||||
| - 🎨 Fixed various RAG issues. | ||||
| - 🧪 Rectified experimental broken GGUF upload logic. | ||||
| 
 | ||||
| ## [0.1.111] - 2024-03-10 | ||||
| 
 | ||||
| ### Added | ||||
|  |  | |||
							
								
								
									
										15
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								Dockerfile
									
										
									
									
									
								
							|  | @ -2,8 +2,6 @@ | |||
| # Initialize device type args | ||||
| # use build args in the docker build commmand with --build-arg="BUILDARG=true" | ||||
| ARG USE_CUDA=false | ||||
| ARG USE_CUDA_VER=cu121 | ||||
| ARG USE_EMBEDDING_MODEL=all-MiniLM-L6-v2 | ||||
| ARG USE_MPS=false | ||||
| ARG INCLUDE_OLLAMA=false | ||||
| 
 | ||||
|  | @ -30,9 +28,8 @@ RUN npm run build | |||
| ######## WebUI backend ######## | ||||
| FROM python:3.11-slim-bookworm as base | ||||
| 
 | ||||
| # Use args | ||||
| ARG USE_CUDA | ||||
| ARG USE_CUDA_VER | ||||
| ARG USE_EMBEDDING_MODEL | ||||
| ARG USE_MPS | ||||
| ARG INCLUDE_OLLAMA | ||||
| 
 | ||||
|  | @ -42,9 +39,7 @@ ENV ENV=prod \ | |||
|     # pass build args to the build | ||||
|     INCLUDE_OLLAMA_DOCKER=${INCLUDE_OLLAMA} \ | ||||
|     USE_MPS_DOCKER=${USE_MPS} \ | ||||
|     USE_CUDA_DOCKER=${USE_CUDA} \ | ||||
|     USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \ | ||||
|     USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} | ||||
|     USE_CUDA_DOCKER=${USE_CUDA} | ||||
| 
 | ||||
| ## Basis URL Config ## | ||||
| ENV OLLAMA_BASE_URL="/ollama" \ | ||||
|  | @ -66,7 +61,7 @@ ENV WHISPER_MODEL="base" \ | |||
| # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard  | ||||
| # for better performance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) | ||||
| # IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. | ||||
| ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \ | ||||
| ENV RAG_EMBEDDING_MODEL="all-MiniLM-L6-v2" \ | ||||
|     RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \ | ||||
|     SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" \ | ||||
|     # device type for whisper tts and embbeding models - "cpu" (default) or "mps" (apple silicon) - choosing this right can lead to better performance | ||||
|  | @ -83,10 +78,8 @@ WORKDIR /app/backend | |||
| COPY ./backend/requirements.txt ./requirements.txt | ||||
| 
 | ||||
| RUN if [ "$USE_CUDA" = "true" ]; then \ | ||||
|         pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ | ||||
|         pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu117 --no-cache-dir && \ | ||||
|         pip3 install -r requirements.txt --no-cache-dir; \ | ||||
|         python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='cpu', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" && \ | ||||
|         python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device='cpu')"; \ | ||||
|     elif [ "$USE_MPS" = "true" ]; then \ | ||||
|         pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ | ||||
|         pip3 install -r requirements.txt --no-cache-dir && \ | ||||
|  |  | |||
							
								
								
									
										2
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								Makefile
									
										
									
									
									
								
							|  | @ -8,8 +8,6 @@ remove: | |||
| 
 | ||||
| start: | ||||
| 	@docker-compose start | ||||
| startAndBuild:  | ||||
| 	docker-compose up -d --build | ||||
| 
 | ||||
| stop: | ||||
| 	@docker-compose stop | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ | |||
| [](https://discord.gg/5rJgQTnV4s) | ||||
| [](https://github.com/sponsors/tjbck) | ||||
| 
 | ||||
| Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). | ||||
| User-friendly WebUI for LLMs, supported LLM runners include Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
|  | @ -79,8 +79,6 @@ Open WebUI is an extensible, feature-rich, and user-friendly self-hosted WebUI d | |||
| 
 | ||||
| - 🔒 **Backend Reverse Proxy Support**: Bolster security through direct communication between Open WebUI backend and Ollama. This key feature eliminates the need to expose Ollama over LAN. Requests made to the '/ollama/api' route from the web UI are seamlessly redirected to Ollama from the backend, enhancing overall system security. | ||||
| 
 | ||||
| - 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors! | ||||
| 
 | ||||
| - 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates and new features. | ||||
| 
 | ||||
| ## 🔗 Also Check Out Open WebUI Community! | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import os | ||||
| import logging | ||||
| from fastapi import ( | ||||
|     FastAPI, | ||||
|     Request, | ||||
|  | @ -22,24 +21,11 @@ from utils.utils import ( | |||
| ) | ||||
| from utils.misc import calculate_sha256 | ||||
| 
 | ||||
| from config import ( | ||||
|     SRC_LOG_LEVELS, | ||||
|     CACHE_DIR, | ||||
|     UPLOAD_DIR, | ||||
|     WHISPER_MODEL, | ||||
|     WHISPER_MODEL_DIR, | ||||
|     DEVICE_TYPE, | ||||
| ) | ||||
| from config import CACHE_DIR, UPLOAD_DIR, WHISPER_MODEL, WHISPER_MODEL_DIR, DEVICE_TYPE | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["AUDIO"]) | ||||
| 
 | ||||
| whisper_device_type = DEVICE_TYPE | ||||
| 
 | ||||
| if whisper_device_type != "cuda": | ||||
| if DEVICE_TYPE != "cuda": | ||||
|     whisper_device_type = "cpu" | ||||
| 
 | ||||
| log.info(f"whisper_device_type: {whisper_device_type}") | ||||
| 
 | ||||
| app = FastAPI() | ||||
| app.add_middleware( | ||||
|  | @ -56,7 +42,7 @@ def transcribe( | |||
|     file: UploadFile = File(...), | ||||
|     user=Depends(get_current_user), | ||||
| ): | ||||
|     log.info(f"file.content_type: {file.content_type}") | ||||
|     print(file.content_type) | ||||
| 
 | ||||
|     if file.content_type not in ["audio/mpeg", "audio/wav"]: | ||||
|         raise HTTPException( | ||||
|  | @ -80,7 +66,7 @@ def transcribe( | |||
|         ) | ||||
| 
 | ||||
|         segments, info = model.transcribe(file_path, beam_size=5) | ||||
|         log.info( | ||||
|         print( | ||||
|             "Detected language '%s' with probability %f" | ||||
|             % (info.language, info.language_probability) | ||||
|         ) | ||||
|  | @ -90,7 +76,7 @@ def transcribe( | |||
|         return {"text": transcript.strip()} | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
| 
 | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_400_BAD_REQUEST, | ||||
|  |  | |||
|  | @ -18,8 +18,6 @@ from utils.utils import ( | |||
|     get_current_user, | ||||
|     get_admin_user, | ||||
| ) | ||||
| 
 | ||||
| from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image | ||||
| from utils.misc import calculate_sha256 | ||||
| from typing import Optional | ||||
| from pydantic import BaseModel | ||||
|  | @ -27,14 +25,10 @@ from pathlib import Path | |||
| import uuid | ||||
| import base64 | ||||
| import json | ||||
| import logging | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL | ||||
| from config import CACHE_DIR, AUTOMATIC1111_BASE_URL | ||||
| 
 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["IMAGES"]) | ||||
| 
 | ||||
| IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/") | ||||
| IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) | ||||
| 
 | ||||
|  | @ -55,8 +49,6 @@ app.state.MODEL = "" | |||
| 
 | ||||
| 
 | ||||
| app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL | ||||
| app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL | ||||
| 
 | ||||
| 
 | ||||
| app.state.IMAGE_SIZE = "512x512" | ||||
| app.state.IMAGE_STEPS = 50 | ||||
|  | @ -79,48 +71,32 @@ async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user | |||
|     return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED} | ||||
| 
 | ||||
| 
 | ||||
| class EngineUrlUpdateForm(BaseModel): | ||||
|     AUTOMATIC1111_BASE_URL: Optional[str] = None | ||||
|     COMFYUI_BASE_URL: Optional[str] = None | ||||
| class UrlUpdateForm(BaseModel): | ||||
|     url: str | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/url") | ||||
| async def get_engine_url(user=Depends(get_admin_user)): | ||||
|     return { | ||||
|         "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL, | ||||
|         "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL, | ||||
|     } | ||||
| async def get_automatic1111_url(user=Depends(get_admin_user)): | ||||
|     return {"AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL} | ||||
| 
 | ||||
| 
 | ||||
| @app.post("/url/update") | ||||
| async def update_engine_url( | ||||
|     form_data: EngineUrlUpdateForm, user=Depends(get_admin_user) | ||||
| async def update_automatic1111_url( | ||||
|     form_data: UrlUpdateForm, user=Depends(get_admin_user) | ||||
| ): | ||||
| 
 | ||||
|     if form_data.AUTOMATIC1111_BASE_URL == None: | ||||
|     if form_data.url == "": | ||||
|         app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL | ||||
|     else: | ||||
|         url = form_data.AUTOMATIC1111_BASE_URL.strip("/") | ||||
|         url = form_data.url.strip("/") | ||||
|         try: | ||||
|             r = requests.head(url) | ||||
|             app.state.AUTOMATIC1111_BASE_URL = url | ||||
|         except Exception as e: | ||||
|             raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) | ||||
| 
 | ||||
|     if form_data.COMFYUI_BASE_URL == None: | ||||
|         app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL | ||||
|     else: | ||||
|         url = form_data.COMFYUI_BASE_URL.strip("/") | ||||
| 
 | ||||
|         try: | ||||
|             r = requests.head(url) | ||||
|             app.state.COMFYUI_BASE_URL = url | ||||
|         except Exception as e: | ||||
|             raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) | ||||
| 
 | ||||
|     return { | ||||
|         "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL, | ||||
|         "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL, | ||||
|         "status": True, | ||||
|     } | ||||
| 
 | ||||
|  | @ -210,18 +186,6 @@ def get_models(user=Depends(get_current_user)): | |||
|                 {"id": "dall-e-2", "name": "DALL·E 2"}, | ||||
|                 {"id": "dall-e-3", "name": "DALL·E 3"}, | ||||
|             ] | ||||
|         elif app.state.ENGINE == "comfyui": | ||||
| 
 | ||||
|             r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info") | ||||
|             info = r.json() | ||||
| 
 | ||||
|             return list( | ||||
|                 map( | ||||
|                     lambda model: {"id": model, "name": model}, | ||||
|                     info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0], | ||||
|                 ) | ||||
|             ) | ||||
| 
 | ||||
|         else: | ||||
|             r = requests.get( | ||||
|                 url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models" | ||||
|  | @ -243,8 +207,6 @@ async def get_default_model(user=Depends(get_admin_user)): | |||
|     try: | ||||
|         if app.state.ENGINE == "openai": | ||||
|             return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"} | ||||
|         elif app.state.ENGINE == "comfyui": | ||||
|             return {"model": app.state.MODEL if app.state.MODEL else ""} | ||||
|         else: | ||||
|             r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") | ||||
|             options = r.json() | ||||
|  | @ -259,12 +221,10 @@ class UpdateModelForm(BaseModel): | |||
| 
 | ||||
| 
 | ||||
| def set_model_handler(model: str): | ||||
| 
 | ||||
|     if app.state.ENGINE == "openai": | ||||
|         app.state.MODEL = model | ||||
|         return app.state.MODEL | ||||
|     if app.state.ENGINE == "comfyui": | ||||
|         app.state.MODEL = model | ||||
|         return app.state.MODEL | ||||
|     else: | ||||
|         r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options") | ||||
|         options = r.json() | ||||
|  | @ -308,24 +268,7 @@ def save_b64_image(b64_str): | |||
| 
 | ||||
|         return image_id | ||||
|     except Exception as e: | ||||
|         log.error(f"Error saving image: {e}") | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| def save_url_image(url): | ||||
|     image_id = str(uuid.uuid4()) | ||||
|     file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png") | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.get(url) | ||||
|         r.raise_for_status() | ||||
| 
 | ||||
|         with open(file_path, "wb") as image_file: | ||||
|             image_file.write(r.content) | ||||
| 
 | ||||
|         return image_id | ||||
|     except Exception as e: | ||||
|         log.exception(f"Error saving image: {e}") | ||||
|         print(f"Error saving image: {e}") | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
|  | @ -335,8 +278,6 @@ def generate_image( | |||
|     user=Depends(get_current_user), | ||||
| ): | ||||
| 
 | ||||
|     width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x"))) | ||||
| 
 | ||||
|     r = None | ||||
|     try: | ||||
|         if app.state.ENGINE == "openai": | ||||
|  | @ -352,7 +293,6 @@ def generate_image( | |||
|                 "size": form_data.size if form_data.size else app.state.IMAGE_SIZE, | ||||
|                 "response_format": "b64_json", | ||||
|             } | ||||
| 
 | ||||
|             r = requests.post( | ||||
|                 url=f"https://api.openai.com/v1/images/generations", | ||||
|                 json=data, | ||||
|  | @ -360,6 +300,7 @@ def generate_image( | |||
|             ) | ||||
| 
 | ||||
|             r.raise_for_status() | ||||
| 
 | ||||
|             res = r.json() | ||||
| 
 | ||||
|             images = [] | ||||
|  | @ -374,47 +315,12 @@ def generate_image( | |||
| 
 | ||||
|             return images | ||||
| 
 | ||||
|         elif app.state.ENGINE == "comfyui": | ||||
| 
 | ||||
|             data = { | ||||
|                 "prompt": form_data.prompt, | ||||
|                 "width": width, | ||||
|                 "height": height, | ||||
|                 "n": form_data.n, | ||||
|             } | ||||
| 
 | ||||
|             if app.state.IMAGE_STEPS != None: | ||||
|                 data["steps"] = app.state.IMAGE_STEPS | ||||
| 
 | ||||
|             if form_data.negative_prompt != None: | ||||
|                 data["negative_prompt"] = form_data.negative_prompt | ||||
| 
 | ||||
|             data = ImageGenerationPayload(**data) | ||||
| 
 | ||||
|             res = comfyui_generate_image( | ||||
|                 app.state.MODEL, | ||||
|                 data, | ||||
|                 user.id, | ||||
|                 app.state.COMFYUI_BASE_URL, | ||||
|             ) | ||||
|             log.debug(f"res: {res}") | ||||
| 
 | ||||
|             images = [] | ||||
| 
 | ||||
|             for image in res["data"]: | ||||
|                 image_id = save_url_image(image["url"]) | ||||
|                 images.append({"url": f"/cache/image/generations/{image_id}.png"}) | ||||
|                 file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json") | ||||
| 
 | ||||
|                 with open(file_body_path, "w") as f: | ||||
|                     json.dump(data.model_dump(exclude_none=True), f) | ||||
| 
 | ||||
|             log.debug(f"images: {images}") | ||||
|             return images | ||||
|         else: | ||||
|             if form_data.model: | ||||
|                 set_model_handler(form_data.model) | ||||
| 
 | ||||
|             width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x"))) | ||||
| 
 | ||||
|             data = { | ||||
|                 "prompt": form_data.prompt, | ||||
|                 "batch_size": form_data.n, | ||||
|  | @ -435,7 +341,7 @@ def generate_image( | |||
| 
 | ||||
|             res = r.json() | ||||
| 
 | ||||
|             log.debug(f"res: {res}") | ||||
|             print(res) | ||||
| 
 | ||||
|             images = [] | ||||
| 
 | ||||
|  | @ -450,10 +356,7 @@ def generate_image( | |||
|             return images | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         error = e | ||||
| 
 | ||||
|         if r != None: | ||||
|             data = r.json() | ||||
|             if "error" in data: | ||||
|                 error = data["error"]["message"] | ||||
|         raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) | ||||
|         print(e) | ||||
|         if r: | ||||
|             print(r.json()) | ||||
|         raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) | ||||
|  |  | |||
|  | @ -1,234 +0,0 @@ | |||
| import websocket  # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) | ||||
| import uuid | ||||
| import json | ||||
| import urllib.request | ||||
| import urllib.parse | ||||
| import random | ||||
| import logging | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["COMFYUI"]) | ||||
| 
 | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
| from typing import Optional | ||||
| 
 | ||||
| COMFYUI_DEFAULT_PROMPT = """ | ||||
| { | ||||
|   "3": { | ||||
|     "inputs": { | ||||
|       "seed": 0, | ||||
|       "steps": 20, | ||||
|       "cfg": 8, | ||||
|       "sampler_name": "euler", | ||||
|       "scheduler": "normal", | ||||
|       "denoise": 1, | ||||
|       "model": [ | ||||
|         "4", | ||||
|         0 | ||||
|       ], | ||||
|       "positive": [ | ||||
|         "6", | ||||
|         0 | ||||
|       ], | ||||
|       "negative": [ | ||||
|         "7", | ||||
|         0 | ||||
|       ], | ||||
|       "latent_image": [ | ||||
|         "5", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "KSampler", | ||||
|     "_meta": { | ||||
|       "title": "KSampler" | ||||
|     } | ||||
|   }, | ||||
|   "4": { | ||||
|     "inputs": { | ||||
|       "ckpt_name": "model.safetensors" | ||||
|     }, | ||||
|     "class_type": "CheckpointLoaderSimple", | ||||
|     "_meta": { | ||||
|       "title": "Load Checkpoint" | ||||
|     } | ||||
|   }, | ||||
|   "5": { | ||||
|     "inputs": { | ||||
|       "width": 512, | ||||
|       "height": 512, | ||||
|       "batch_size": 1 | ||||
|     }, | ||||
|     "class_type": "EmptyLatentImage", | ||||
|     "_meta": { | ||||
|       "title": "Empty Latent Image" | ||||
|     } | ||||
|   }, | ||||
|   "6": { | ||||
|     "inputs": { | ||||
|       "text": "Prompt", | ||||
|       "clip": [ | ||||
|         "4", | ||||
|         1 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "CLIPTextEncode", | ||||
|     "_meta": { | ||||
|       "title": "CLIP Text Encode (Prompt)" | ||||
|     } | ||||
|   }, | ||||
|   "7": { | ||||
|     "inputs": { | ||||
|       "text": "Negative Prompt", | ||||
|       "clip": [ | ||||
|         "4", | ||||
|         1 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "CLIPTextEncode", | ||||
|     "_meta": { | ||||
|       "title": "CLIP Text Encode (Prompt)" | ||||
|     } | ||||
|   }, | ||||
|   "8": { | ||||
|     "inputs": { | ||||
|       "samples": [ | ||||
|         "3", | ||||
|         0 | ||||
|       ], | ||||
|       "vae": [ | ||||
|         "4", | ||||
|         2 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "VAEDecode", | ||||
|     "_meta": { | ||||
|       "title": "VAE Decode" | ||||
|     } | ||||
|   }, | ||||
|   "9": { | ||||
|     "inputs": { | ||||
|       "filename_prefix": "ComfyUI", | ||||
|       "images": [ | ||||
|         "8", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "SaveImage", | ||||
|     "_meta": { | ||||
|       "title": "Save Image" | ||||
|     } | ||||
|   } | ||||
| } | ||||
| """ | ||||
| 
 | ||||
| 
 | ||||
| def queue_prompt(prompt, client_id, base_url): | ||||
|     log.info("queue_prompt") | ||||
|     p = {"prompt": prompt, "client_id": client_id} | ||||
|     data = json.dumps(p).encode("utf-8") | ||||
|     req = urllib.request.Request(f"{base_url}/prompt", data=data) | ||||
|     return json.loads(urllib.request.urlopen(req).read()) | ||||
| 
 | ||||
| 
 | ||||
| def get_image(filename, subfolder, folder_type, base_url): | ||||
|     log.info("get_image") | ||||
|     data = {"filename": filename, "subfolder": subfolder, "type": folder_type} | ||||
|     url_values = urllib.parse.urlencode(data) | ||||
|     with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response: | ||||
|         return response.read() | ||||
| 
 | ||||
| 
 | ||||
| def get_image_url(filename, subfolder, folder_type, base_url): | ||||
|     log.info("get_image") | ||||
|     data = {"filename": filename, "subfolder": subfolder, "type": folder_type} | ||||
|     url_values = urllib.parse.urlencode(data) | ||||
|     return f"{base_url}/view?{url_values}" | ||||
| 
 | ||||
| 
 | ||||
| def get_history(prompt_id, base_url): | ||||
|     log.info("get_history") | ||||
|     with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response: | ||||
|         return json.loads(response.read()) | ||||
| 
 | ||||
| 
 | ||||
| def get_images(ws, prompt, client_id, base_url): | ||||
|     prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"] | ||||
|     output_images = [] | ||||
|     while True: | ||||
|         out = ws.recv() | ||||
|         if isinstance(out, str): | ||||
|             message = json.loads(out) | ||||
|             if message["type"] == "executing": | ||||
|                 data = message["data"] | ||||
|                 if data["node"] is None and data["prompt_id"] == prompt_id: | ||||
|                     break  # Execution is done | ||||
|         else: | ||||
|             continue  # previews are binary data | ||||
| 
 | ||||
|     history = get_history(prompt_id, base_url)[prompt_id] | ||||
|     for o in history["outputs"]: | ||||
|         for node_id in history["outputs"]: | ||||
|             node_output = history["outputs"][node_id] | ||||
|             if "images" in node_output: | ||||
|                 for image in node_output["images"]: | ||||
|                     url = get_image_url( | ||||
|                         image["filename"], image["subfolder"], image["type"], base_url | ||||
|                     ) | ||||
|                     output_images.append({"url": url}) | ||||
|     return {"data": output_images} | ||||
| 
 | ||||
| 
 | ||||
| class ImageGenerationPayload(BaseModel): | ||||
|     prompt: str | ||||
|     negative_prompt: Optional[str] = "" | ||||
|     steps: Optional[int] = None | ||||
|     seed: Optional[int] = None | ||||
|     width: int | ||||
|     height: int | ||||
|     n: int = 1 | ||||
| 
 | ||||
| 
 | ||||
| def comfyui_generate_image( | ||||
|     model: str, payload: ImageGenerationPayload, client_id, base_url | ||||
| ): | ||||
|     host = base_url.replace("http://", "").replace("https://", "") | ||||
| 
 | ||||
|     comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT) | ||||
| 
 | ||||
|     comfyui_prompt["4"]["inputs"]["ckpt_name"] = model | ||||
|     comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n | ||||
|     comfyui_prompt["5"]["inputs"]["width"] = payload.width | ||||
|     comfyui_prompt["5"]["inputs"]["height"] = payload.height | ||||
| 
 | ||||
|     # set the text prompt for our positive CLIPTextEncode | ||||
|     comfyui_prompt["6"]["inputs"]["text"] = payload.prompt | ||||
|     comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt | ||||
| 
 | ||||
|     if payload.steps: | ||||
|         comfyui_prompt["3"]["inputs"]["steps"] = payload.steps | ||||
| 
 | ||||
|     comfyui_prompt["3"]["inputs"]["seed"] = ( | ||||
|         payload.seed if payload.seed else random.randint(0, 18446744073709551614) | ||||
|     ) | ||||
| 
 | ||||
|     try: | ||||
|         ws = websocket.WebSocket() | ||||
|         ws.connect(f"ws://{host}/ws?clientId={client_id}") | ||||
|         log.info("WebSocket connection established.") | ||||
|     except Exception as e: | ||||
|         log.exception(f"Failed to connect to WebSocket server: {e}") | ||||
|         return None | ||||
| 
 | ||||
|     try: | ||||
|         images = get_images(ws, comfyui_prompt, client_id, base_url) | ||||
|     except Exception as e: | ||||
|         log.exception(f"Error while receiving images: {e}") | ||||
|         images = None | ||||
| 
 | ||||
|     ws.close() | ||||
| 
 | ||||
|     return images | ||||
|  | @ -1,27 +1,10 @@ | |||
| import logging | ||||
| 
 | ||||
| from litellm.proxy.proxy_server import ProxyConfig, initialize | ||||
| from litellm.proxy.proxy_server import app | ||||
| 
 | ||||
| from fastapi import FastAPI, Request, Depends, status, Response | ||||
| from fastapi import FastAPI, Request, Depends, status | ||||
| from fastapi.responses import JSONResponse | ||||
| 
 | ||||
| from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint | ||||
| from starlette.responses import StreamingResponse | ||||
| import json | ||||
| 
 | ||||
| from utils.utils import get_http_authorization_cred, get_current_user | ||||
| from config import SRC_LOG_LEVELS, ENV | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["LITELLM"]) | ||||
| 
 | ||||
| 
 | ||||
| from config import ( | ||||
|     MODEL_FILTER_ENABLED, | ||||
|     MODEL_FILTER_LIST, | ||||
| ) | ||||
| 
 | ||||
| from config import ENV | ||||
| 
 | ||||
| proxy_config = ProxyConfig() | ||||
| 
 | ||||
|  | @ -43,58 +26,16 @@ async def on_startup(): | |||
|     await startup() | ||||
| 
 | ||||
| 
 | ||||
| app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED | ||||
| app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST | ||||
| 
 | ||||
| 
 | ||||
| @app.middleware("http") | ||||
| async def auth_middleware(request: Request, call_next): | ||||
|     auth_header = request.headers.get("Authorization", "") | ||||
|     request.state.user = None | ||||
| 
 | ||||
|     if ENV != "dev": | ||||
|         try: | ||||
|             user = get_current_user(get_http_authorization_cred(auth_header)) | ||||
|         log.debug(f"user: {user}") | ||||
|         request.state.user = user | ||||
|             print(user) | ||||
|         except Exception as e: | ||||
|             return JSONResponse(status_code=400, content={"detail": str(e)}) | ||||
| 
 | ||||
|     response = await call_next(request) | ||||
|     return response | ||||
| 
 | ||||
| 
 | ||||
| class ModifyModelsResponseMiddleware(BaseHTTPMiddleware): | ||||
|     async def dispatch( | ||||
|         self, request: Request, call_next: RequestResponseEndpoint | ||||
|     ) -> Response: | ||||
| 
 | ||||
|         response = await call_next(request) | ||||
|         user = request.state.user | ||||
| 
 | ||||
|         if "/models" in request.url.path: | ||||
|             if isinstance(response, StreamingResponse): | ||||
|                 # Read the content of the streaming response | ||||
|                 body = b"" | ||||
|                 async for chunk in response.body_iterator: | ||||
|                     body += chunk | ||||
| 
 | ||||
|                 data = json.loads(body.decode("utf-8")) | ||||
| 
 | ||||
|                 if app.state.MODEL_FILTER_ENABLED: | ||||
|                     if user and user.role == "user": | ||||
|                         data["data"] = list( | ||||
|                             filter( | ||||
|                                 lambda model: model["id"] | ||||
|                                 in app.state.MODEL_FILTER_LIST, | ||||
|                                 data["data"], | ||||
|                             ) | ||||
|                         ) | ||||
| 
 | ||||
|                 # Modified Flag | ||||
|                 data["modified"] = True | ||||
|                 return JSONResponse(content=data) | ||||
| 
 | ||||
|         return response | ||||
| 
 | ||||
| 
 | ||||
| app.add_middleware(ModifyModelsResponseMiddleware) | ||||
|  |  | |||
|  | @ -1,49 +1,24 @@ | |||
| from fastapi import ( | ||||
|     FastAPI, | ||||
|     Request, | ||||
|     Response, | ||||
|     HTTPException, | ||||
|     Depends, | ||||
|     status, | ||||
|     UploadFile, | ||||
|     File, | ||||
|     BackgroundTasks, | ||||
| ) | ||||
| from fastapi import FastAPI, Request, Response, HTTPException, Depends, status | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| from fastapi.responses import StreamingResponse | ||||
| from fastapi.concurrency import run_in_threadpool | ||||
| 
 | ||||
| from pydantic import BaseModel, ConfigDict | ||||
| 
 | ||||
| import os | ||||
| import copy | ||||
| import random | ||||
| import requests | ||||
| import json | ||||
| import uuid | ||||
| import aiohttp | ||||
| import asyncio | ||||
| import logging | ||||
| from urllib.parse import urlparse | ||||
| from typing import Optional, List, Union | ||||
| 
 | ||||
| 
 | ||||
| from apps.web.models.users import Users | ||||
| from constants import ERROR_MESSAGES | ||||
| from utils.utils import decode_token, get_current_user, get_admin_user | ||||
| from config import OLLAMA_BASE_URLS, MODEL_FILTER_ENABLED, MODEL_FILTER_LIST | ||||
| 
 | ||||
| from typing import Optional, List, Union | ||||
| 
 | ||||
| from config import ( | ||||
|     SRC_LOG_LEVELS, | ||||
|     OLLAMA_BASE_URLS, | ||||
|     MODEL_FILTER_ENABLED, | ||||
|     MODEL_FILTER_LIST, | ||||
|     UPLOAD_DIR, | ||||
| ) | ||||
| from utils.misc import calculate_sha256 | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["OLLAMA"]) | ||||
| 
 | ||||
| app = FastAPI() | ||||
| app.add_middleware( | ||||
|  | @ -94,7 +69,7 @@ class UrlUpdateForm(BaseModel): | |||
| async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): | ||||
|     app.state.OLLAMA_BASE_URLS = form_data.urls | ||||
| 
 | ||||
|     log.info(f"app.state.OLLAMA_BASE_URLS: {app.state.OLLAMA_BASE_URLS}") | ||||
|     print(app.state.OLLAMA_BASE_URLS) | ||||
|     return {"OLLAMA_BASE_URLS": app.state.OLLAMA_BASE_URLS} | ||||
| 
 | ||||
| 
 | ||||
|  | @ -115,7 +90,7 @@ async def fetch_url(url): | |||
|                 return await response.json() | ||||
|     except Exception as e: | ||||
|         # Handle connection error here | ||||
|         log.error(f"Connection error: {e}") | ||||
|         print(f"Connection error: {e}") | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
|  | @ -123,7 +98,6 @@ def merge_models_lists(model_lists): | |||
|     merged_models = {} | ||||
| 
 | ||||
|     for idx, model_list in enumerate(model_lists): | ||||
|         if model_list is not None: | ||||
|         for model in model_list: | ||||
|             digest = model["digest"] | ||||
|             if digest not in merged_models: | ||||
|  | @ -139,16 +113,16 @@ def merge_models_lists(model_lists): | |||
| 
 | ||||
| 
 | ||||
| async def get_all_models(): | ||||
|     log.info("get_all_models()") | ||||
|     print("get_all_models") | ||||
|     tasks = [fetch_url(f"{url}/api/tags") for url in app.state.OLLAMA_BASE_URLS] | ||||
|     responses = await asyncio.gather(*tasks) | ||||
|     responses = list(filter(lambda x: x is not None, responses)) | ||||
| 
 | ||||
|     models = { | ||||
|         "models": merge_models_lists( | ||||
|             map(lambda response: response["models"] if response else None, responses) | ||||
|             map(lambda response: response["models"], responses) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     app.state.MODELS = {model["model"]: model for model in models["models"]} | ||||
| 
 | ||||
|     return models | ||||
|  | @ -180,7 +154,7 @@ async def get_ollama_tags( | |||
| 
 | ||||
|             return r.json() | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             error_detail = "Open WebUI: Server Connection Error" | ||||
|             if r is not None: | ||||
|                 try: | ||||
|  | @ -207,17 +181,11 @@ async def get_ollama_versions(url_idx: Optional[int] = None): | |||
|         responses = await asyncio.gather(*tasks) | ||||
|         responses = list(filter(lambda x: x is not None, responses)) | ||||
| 
 | ||||
|         if len(responses) > 0: | ||||
|         lowest_version = min( | ||||
|             responses, key=lambda x: tuple(map(int, x["version"].split("."))) | ||||
|         ) | ||||
| 
 | ||||
|         return {"version": lowest_version["version"]} | ||||
|         else: | ||||
|             raise HTTPException( | ||||
|                 status_code=500, | ||||
|                 detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND, | ||||
|             ) | ||||
|     else: | ||||
|         url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|         try: | ||||
|  | @ -226,7 +194,7 @@ async def get_ollama_versions(url_idx: Optional[int] = None): | |||
| 
 | ||||
|             return r.json() | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             error_detail = "Open WebUI: Server Connection Error" | ||||
|             if r is not None: | ||||
|                 try: | ||||
|  | @ -252,33 +220,18 @@ async def pull_model( | |||
|     form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user) | ||||
| ): | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|     def get_request(): | ||||
|         nonlocal url | ||||
|         nonlocal r | ||||
| 
 | ||||
|         request_id = str(uuid.uuid4()) | ||||
|         try: | ||||
|             REQUEST_POOL.append(request_id) | ||||
| 
 | ||||
|             def stream_content(): | ||||
|                 try: | ||||
|                     yield json.dumps({"id": request_id, "done": False}) + "\n" | ||||
| 
 | ||||
|                 for chunk in r.iter_content(chunk_size=8192): | ||||
|                         if request_id in REQUEST_POOL: | ||||
|                     yield chunk | ||||
|                         else: | ||||
|                             log.warning("User: canceled request") | ||||
|                             break | ||||
|                 finally: | ||||
|                     if hasattr(r, "close"): | ||||
|                         r.close() | ||||
|                         if request_id in REQUEST_POOL: | ||||
|                             REQUEST_POOL.remove(request_id) | ||||
| 
 | ||||
|             r = requests.request( | ||||
|                 method="POST", | ||||
|  | @ -299,9 +252,8 @@ async def pull_model( | |||
| 
 | ||||
|     try: | ||||
|         return await run_in_threadpool(get_request) | ||||
| 
 | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -340,7 +292,7 @@ async def push_model( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.debug(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|  | @ -372,7 +324,7 @@ async def push_model( | |||
|     try: | ||||
|         return await run_in_threadpool(get_request) | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -400,9 +352,9 @@ class CreateModelForm(BaseModel): | |||
| async def create_model( | ||||
|     form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user) | ||||
| ): | ||||
|     log.debug(f"form_data: {form_data}") | ||||
|     print(form_data) | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|  | @ -424,7 +376,7 @@ async def create_model( | |||
| 
 | ||||
|             r.raise_for_status() | ||||
| 
 | ||||
|             log.debug(f"r: {r}") | ||||
|             print(r) | ||||
| 
 | ||||
|             return StreamingResponse( | ||||
|                 stream_content(), | ||||
|  | @ -437,7 +389,7 @@ async def create_model( | |||
|     try: | ||||
|         return await run_in_threadpool(get_request) | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -475,7 +427,7 @@ async def copy_model( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.request( | ||||
|  | @ -485,11 +437,11 @@ async def copy_model( | |||
|         ) | ||||
|         r.raise_for_status() | ||||
| 
 | ||||
|         log.debug(f"r.text: {r.text}") | ||||
|         print(r.text) | ||||
| 
 | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -522,7 +474,7 @@ async def delete_model( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.request( | ||||
|  | @ -532,11 +484,11 @@ async def delete_model( | |||
|         ) | ||||
|         r.raise_for_status() | ||||
| 
 | ||||
|         log.debug(f"r.text: {r.text}") | ||||
|         print(r.text) | ||||
| 
 | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -562,7 +514,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use | |||
| 
 | ||||
|     url_idx = random.choice(app.state.MODELS[form_data.name]["urls"]) | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.request( | ||||
|  | @ -574,7 +526,7 @@ async def show_model_info(form_data: ModelNameForm, user=Depends(get_current_use | |||
| 
 | ||||
|         return r.json() | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -614,7 +566,7 @@ async def generate_embeddings( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.request( | ||||
|  | @ -626,7 +578,7 @@ async def generate_embeddings( | |||
| 
 | ||||
|         return r.json() | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -670,11 +622,11 @@ async def generate_completion( | |||
|         else: | ||||
|             raise HTTPException( | ||||
|                 status_code=400, | ||||
|                 detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), | ||||
|                 detail="error_detail", | ||||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|  | @ -695,7 +647,7 @@ async def generate_completion( | |||
|                         if request_id in REQUEST_POOL: | ||||
|                             yield chunk | ||||
|                         else: | ||||
|                             log.warning("User: canceled request") | ||||
|                             print("User: canceled request") | ||||
|                             break | ||||
|                 finally: | ||||
|                     if hasattr(r, "close"): | ||||
|  | @ -750,7 +702,7 @@ class GenerateChatCompletionForm(BaseModel): | |||
|     format: Optional[str] = None | ||||
|     options: Optional[dict] = None | ||||
|     template: Optional[str] = None | ||||
|     stream: Optional[bool] = None | ||||
|     stream: Optional[bool] = True | ||||
|     keep_alive: Optional[Union[int, str]] = None | ||||
| 
 | ||||
| 
 | ||||
|  | @ -772,15 +724,11 @@ async def generate_chat_completion( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|     log.debug( | ||||
|         "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format( | ||||
|             form_data.model_dump_json(exclude_none=True).encode() | ||||
|         ) | ||||
|     ) | ||||
|     print(form_data.model_dump_json(exclude_none=True).encode()) | ||||
| 
 | ||||
|     def get_request(): | ||||
|         nonlocal form_data | ||||
|  | @ -799,7 +747,7 @@ async def generate_chat_completion( | |||
|                         if request_id in REQUEST_POOL: | ||||
|                             yield chunk | ||||
|                         else: | ||||
|                             log.warning("User: canceled request") | ||||
|                             print("User: canceled request") | ||||
|                             break | ||||
|                 finally: | ||||
|                     if hasattr(r, "close"): | ||||
|  | @ -822,7 +770,7 @@ async def generate_chat_completion( | |||
|                 headers=dict(r.headers), | ||||
|             ) | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             raise e | ||||
| 
 | ||||
|     try: | ||||
|  | @ -876,7 +824,7 @@ async def generate_openai_chat_completion( | |||
|             ) | ||||
| 
 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
|     log.info(f"url: {url}") | ||||
|     print(url) | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|  | @ -899,7 +847,7 @@ async def generate_openai_chat_completion( | |||
|                         if request_id in REQUEST_POOL: | ||||
|                             yield chunk | ||||
|                         else: | ||||
|                             log.warning("User: canceled request") | ||||
|                             print("User: canceled request") | ||||
|                             break | ||||
|                 finally: | ||||
|                     if hasattr(r, "close"): | ||||
|  | @ -942,220 +890,6 @@ async def generate_openai_chat_completion( | |||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| class UrlForm(BaseModel): | ||||
|     url: str | ||||
| 
 | ||||
| 
 | ||||
| class UploadBlobForm(BaseModel): | ||||
|     filename: str | ||||
| 
 | ||||
| 
 | ||||
| def parse_huggingface_url(hf_url): | ||||
|     try: | ||||
|         # Parse the URL | ||||
|         parsed_url = urlparse(hf_url) | ||||
| 
 | ||||
|         # Get the path and split it into components | ||||
|         path_components = parsed_url.path.split("/") | ||||
| 
 | ||||
|         # Extract the desired output | ||||
|         user_repo = "/".join(path_components[1:3]) | ||||
|         model_file = path_components[-1] | ||||
| 
 | ||||
|         return model_file | ||||
|     except ValueError: | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| async def download_file_stream( | ||||
|     ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024 | ||||
| ): | ||||
|     done = False | ||||
| 
 | ||||
|     if os.path.exists(file_path): | ||||
|         current_size = os.path.getsize(file_path) | ||||
|     else: | ||||
|         current_size = 0 | ||||
| 
 | ||||
|     headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} | ||||
| 
 | ||||
|     timeout = aiohttp.ClientTimeout(total=600)  # Set the timeout | ||||
| 
 | ||||
|     async with aiohttp.ClientSession(timeout=timeout) as session: | ||||
|         async with session.get(file_url, headers=headers) as response: | ||||
|             total_size = int(response.headers.get("content-length", 0)) + current_size | ||||
| 
 | ||||
|             with open(file_path, "ab+") as file: | ||||
|                 async for data in response.content.iter_chunked(chunk_size): | ||||
|                     current_size += len(data) | ||||
|                     file.write(data) | ||||
| 
 | ||||
|                     done = current_size == total_size | ||||
|                     progress = round((current_size / total_size) * 100, 2) | ||||
| 
 | ||||
|                     yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' | ||||
| 
 | ||||
|                 if done: | ||||
|                     file.seek(0) | ||||
|                     hashed = calculate_sha256(file) | ||||
|                     file.seek(0) | ||||
| 
 | ||||
|                     url = f"{ollama_url}/api/blobs/sha256:{hashed}" | ||||
|                     response = requests.post(url, data=file) | ||||
| 
 | ||||
|                     if response.ok: | ||||
|                         res = { | ||||
|                             "done": done, | ||||
|                             "blob": f"sha256:{hashed}", | ||||
|                             "name": file_name, | ||||
|                         } | ||||
|                         os.remove(file_path) | ||||
| 
 | ||||
|                         yield f"data: {json.dumps(res)}\n\n" | ||||
|                     else: | ||||
|                         raise "Ollama: Could not create blob, Please try again." | ||||
| 
 | ||||
| 
 | ||||
| # def number_generator(): | ||||
| #     for i in range(1, 101): | ||||
| #         yield f"data: {i}\n" | ||||
| 
 | ||||
| 
 | ||||
| # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" | ||||
| @app.post("/models/download") | ||||
| @app.post("/models/download/{url_idx}") | ||||
| async def download_model( | ||||
|     form_data: UrlForm, | ||||
|     url_idx: Optional[int] = None, | ||||
| ): | ||||
| 
 | ||||
|     allowed_hosts = ["https://huggingface.co/", "https://github.com/"] | ||||
| 
 | ||||
|     if not any(form_data.url.startswith(host) for host in allowed_hosts): | ||||
|         raise HTTPException( | ||||
|             status_code=400, | ||||
|             detail="Invalid file_url. Only URLs from allowed hosts are permitted.", | ||||
|         ) | ||||
| 
 | ||||
|     if url_idx == None: | ||||
|         url_idx = 0 | ||||
|     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
| 
 | ||||
|     file_name = parse_huggingface_url(form_data.url) | ||||
| 
 | ||||
|     if file_name: | ||||
|         file_path = f"{UPLOAD_DIR}/{file_name}" | ||||
| 
 | ||||
|         return StreamingResponse( | ||||
|             download_file_stream(url, form_data.url, file_path, file_name), | ||||
|         ) | ||||
|     else: | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| @app.post("/models/upload") | ||||
| @app.post("/models/upload/{url_idx}") | ||||
| def upload_model(file: UploadFile = File(...), url_idx: Optional[int] = None): | ||||
|     if url_idx == None: | ||||
|         url_idx = 0 | ||||
|     ollama_url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
| 
 | ||||
|     file_path = f"{UPLOAD_DIR}/{file.filename}" | ||||
| 
 | ||||
|     # Save file in chunks | ||||
|     with open(file_path, "wb+") as f: | ||||
|         for chunk in file.file: | ||||
|             f.write(chunk) | ||||
| 
 | ||||
|     def file_process_stream(): | ||||
|         nonlocal ollama_url | ||||
|         total_size = os.path.getsize(file_path) | ||||
|         chunk_size = 1024 * 1024 | ||||
|         try: | ||||
|             with open(file_path, "rb") as f: | ||||
|                 total = 0 | ||||
|                 done = False | ||||
| 
 | ||||
|                 while not done: | ||||
|                     chunk = f.read(chunk_size) | ||||
|                     if not chunk: | ||||
|                         done = True | ||||
|                         continue | ||||
| 
 | ||||
|                     total += len(chunk) | ||||
|                     progress = round((total / total_size) * 100, 2) | ||||
| 
 | ||||
|                     res = { | ||||
|                         "progress": progress, | ||||
|                         "total": total_size, | ||||
|                         "completed": total, | ||||
|                     } | ||||
|                     yield f"data: {json.dumps(res)}\n\n" | ||||
| 
 | ||||
|                 if done: | ||||
|                     f.seek(0) | ||||
|                     hashed = calculate_sha256(f) | ||||
|                     f.seek(0) | ||||
| 
 | ||||
|                     url = f"{ollama_url}/api/blobs/sha256:{hashed}" | ||||
|                     response = requests.post(url, data=f) | ||||
| 
 | ||||
|                     if response.ok: | ||||
|                         res = { | ||||
|                             "done": done, | ||||
|                             "blob": f"sha256:{hashed}", | ||||
|                             "name": file.filename, | ||||
|                         } | ||||
|                         os.remove(file_path) | ||||
|                         yield f"data: {json.dumps(res)}\n\n" | ||||
|                     else: | ||||
|                         raise Exception( | ||||
|                             "Ollama: Could not create blob, Please try again." | ||||
|                         ) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             res = {"error": str(e)} | ||||
|             yield f"data: {json.dumps(res)}\n\n" | ||||
| 
 | ||||
|     return StreamingResponse(file_process_stream(), media_type="text/event-stream") | ||||
| 
 | ||||
| 
 | ||||
| # async def upload_model(file: UploadFile = File(), url_idx: Optional[int] = None): | ||||
| #     if url_idx == None: | ||||
| #         url_idx = 0 | ||||
| #     url = app.state.OLLAMA_BASE_URLS[url_idx] | ||||
| 
 | ||||
| #     file_location = os.path.join(UPLOAD_DIR, file.filename) | ||||
| #     total_size = file.size | ||||
| 
 | ||||
| #     async def file_upload_generator(file): | ||||
| #         print(file) | ||||
| #         try: | ||||
| #             async with aiofiles.open(file_location, "wb") as f: | ||||
| #                 completed_size = 0 | ||||
| #                 while True: | ||||
| #                     chunk = await file.read(1024*1024) | ||||
| #                     if not chunk: | ||||
| #                         break | ||||
| #                     await f.write(chunk) | ||||
| #                     completed_size += len(chunk) | ||||
| #                     progress = (completed_size / total_size) * 100 | ||||
| 
 | ||||
| #                     print(progress) | ||||
| #                     yield f'data: {json.dumps({"status": "uploading", "percentage": progress, "total": total_size, "completed": completed_size, "done": False})}\n' | ||||
| #         except Exception as e: | ||||
| #             print(e) | ||||
| #             yield f"data: {json.dumps({'status': 'error', 'message': str(e)})}\n" | ||||
| #         finally: | ||||
| #             await file.close() | ||||
| #             print("done") | ||||
| #             yield f'data: {json.dumps({"status": "completed", "percentage": 100, "total": total_size, "completed": completed_size, "done": True})}\n' | ||||
| 
 | ||||
| #     return StreamingResponse( | ||||
| #         file_upload_generator(copy.deepcopy(file)), media_type="text/event-stream" | ||||
| #     ) | ||||
| 
 | ||||
| 
 | ||||
| @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) | ||||
| async def deprecated_proxy(path: str, request: Request, user=Depends(get_current_user)): | ||||
|     url = app.state.OLLAMA_BASE_URLS[0] | ||||
|  | @ -1206,7 +940,7 @@ async def deprecated_proxy(path: str, request: Request, user=Depends(get_current | |||
|                         if request_id in REQUEST_POOL: | ||||
|                             yield chunk | ||||
|                         else: | ||||
|                             log.warning("User: canceled request") | ||||
|                             print("User: canceled request") | ||||
|                             break | ||||
|                 finally: | ||||
|                     if hasattr(r, "close"): | ||||
|  |  | |||
|  | @ -6,7 +6,6 @@ import requests | |||
| import aiohttp | ||||
| import asyncio | ||||
| import json | ||||
| import logging | ||||
| 
 | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
|  | @ -20,7 +19,6 @@ from utils.utils import ( | |||
|     get_admin_user, | ||||
| ) | ||||
| from config import ( | ||||
|     SRC_LOG_LEVELS, | ||||
|     OPENAI_API_BASE_URLS, | ||||
|     OPENAI_API_KEYS, | ||||
|     CACHE_DIR, | ||||
|  | @ -33,9 +31,6 @@ from typing import List, Optional | |||
| import hashlib | ||||
| from pathlib import Path | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["OPENAI"]) | ||||
| 
 | ||||
| app = FastAPI() | ||||
| app.add_middleware( | ||||
|     CORSMiddleware, | ||||
|  | @ -116,7 +111,6 @@ async def speech(request: Request, user=Depends(get_verified_user)): | |||
|         headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEYS[idx]}" | ||||
|         headers["Content-Type"] = "application/json" | ||||
| 
 | ||||
|         r = None | ||||
|         try: | ||||
|             r = requests.post( | ||||
|                 url=f"{app.state.OPENAI_API_BASE_URLS[idx]}/audio/speech", | ||||
|  | @ -139,7 +133,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): | |||
|             return FileResponse(file_path) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             error_detail = "Open WebUI: Server Connection Error" | ||||
|             if r is not None: | ||||
|                 try: | ||||
|  | @ -149,9 +143,7 @@ async def speech(request: Request, user=Depends(get_verified_user)): | |||
|                 except: | ||||
|                     error_detail = f"External: {e}" | ||||
| 
 | ||||
|             raise HTTPException( | ||||
|                 status_code=r.status_code if r else 500, detail=error_detail | ||||
|             ) | ||||
|             raise HTTPException(status_code=r.status_code, detail=error_detail) | ||||
| 
 | ||||
|     except ValueError: | ||||
|         raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) | ||||
|  | @ -165,7 +157,7 @@ async def fetch_url(url, key): | |||
|                 return await response.json() | ||||
|     except Exception as e: | ||||
|         # Handle connection error here | ||||
|         log.error(f"Connection error: {e}") | ||||
|         print(f"Connection error: {e}") | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
|  | @ -173,7 +165,6 @@ def merge_models_lists(model_lists): | |||
|     merged_list = [] | ||||
| 
 | ||||
|     for idx, models in enumerate(model_lists): | ||||
|         if models is not None and "error" not in models: | ||||
|         merged_list.extend( | ||||
|             [ | ||||
|                 {**model, "urlIdx": idx} | ||||
|  | @ -187,7 +178,7 @@ def merge_models_lists(model_lists): | |||
| 
 | ||||
| 
 | ||||
| async def get_all_models(): | ||||
|     log.info("get_all_models()") | ||||
|     print("get_all_models") | ||||
| 
 | ||||
|     if len(app.state.OPENAI_API_KEYS) == 1 and app.state.OPENAI_API_KEYS[0] == "": | ||||
|         models = {"data": []} | ||||
|  | @ -196,24 +187,15 @@ async def get_all_models(): | |||
|             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"] | ||||
|                             if response and "data" in response | ||||
|                             else None | ||||
|                         ), | ||||
|                         responses, | ||||
|                     ) | ||||
|                 ) | ||||
|                 list(map(lambda response: response["data"], responses)) | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         log.info(f"models: {models}") | ||||
|         app.state.MODELS = {model["id"]: model for model in models["data"]} | ||||
| 
 | ||||
|         return models | ||||
|  | @ -236,9 +218,6 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use | |||
|         return models | ||||
|     else: | ||||
|         url = app.state.OPENAI_API_BASE_URLS[url_idx] | ||||
| 
 | ||||
|         r = None | ||||
| 
 | ||||
|         try: | ||||
|             r = requests.request(method="GET", url=f"{url}/models") | ||||
|             r.raise_for_status() | ||||
|  | @ -251,7 +230,7 @@ async def get_models(url_idx: Optional[int] = None, user=Depends(get_current_use | |||
| 
 | ||||
|             return response_data | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             error_detail = "Open WebUI: Server Connection Error" | ||||
|             if r is not None: | ||||
|                 try: | ||||
|  | @ -285,7 +264,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | |||
|         if body.get("model") == "gpt-4-vision-preview": | ||||
|             if "max_tokens" not in body: | ||||
|                 body["max_tokens"] = 4000 | ||||
|             log.debug("Modified body_dict:", body) | ||||
|             print("Modified body_dict:", body) | ||||
| 
 | ||||
|         # Fix for ChatGPT calls failing because the num_ctx key is in body | ||||
|         if "num_ctx" in body: | ||||
|  | @ -297,7 +276,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | |||
|         # Convert the modified body back to JSON | ||||
|         body = json.dumps(body) | ||||
|     except json.JSONDecodeError as e: | ||||
|         log.error("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] | ||||
|  | @ -311,8 +290,6 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | |||
|     headers["Authorization"] = f"Bearer {key}" | ||||
|     headers["Content-Type"] = "application/json" | ||||
| 
 | ||||
|     r = None | ||||
| 
 | ||||
|     try: | ||||
|         r = requests.request( | ||||
|             method=request.method, | ||||
|  | @ -335,7 +312,7 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | |||
|             response_data = r.json() | ||||
|             return response_data | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         error_detail = "Open WebUI: Server Connection Error" | ||||
|         if r is not None: | ||||
|             try: | ||||
|  | @ -345,6 +322,4 @@ async def proxy(path: str, request: Request, user=Depends(get_verified_user)): | |||
|             except: | ||||
|                 error_detail = f"External: {e}" | ||||
| 
 | ||||
|         raise HTTPException( | ||||
|             status_code=r.status_code if r else 500, detail=error_detail | ||||
|         ) | ||||
|         raise HTTPException(status_code=r.status_code, detail=error_detail) | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ from fastapi import ( | |||
|     Form, | ||||
| ) | ||||
| from fastapi.middleware.cors import CORSMiddleware | ||||
| import os, shutil, logging | ||||
| import os, shutil | ||||
| 
 | ||||
| from pathlib import Path | ||||
| from typing import List | ||||
|  | @ -21,7 +21,6 @@ from langchain_community.document_loaders import ( | |||
|     TextLoader, | ||||
|     PyPDFLoader, | ||||
|     CSVLoader, | ||||
|     BSHTMLLoader, | ||||
|     Docx2txtLoader, | ||||
|     UnstructuredEPubLoader, | ||||
|     UnstructuredWordDocumentLoader, | ||||
|  | @ -55,7 +54,6 @@ from utils.misc import ( | |||
| ) | ||||
| from utils.utils import get_current_user, get_admin_user | ||||
| from config import ( | ||||
|     SRC_LOG_LEVELS, | ||||
|     UPLOAD_DIR, | ||||
|     DOCS_DIR, | ||||
|     RAG_EMBEDDING_MODEL, | ||||
|  | @ -68,9 +66,6 @@ from config import ( | |||
| 
 | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["RAG"]) | ||||
| 
 | ||||
| # | ||||
| # if RAG_EMBEDDING_MODEL: | ||||
| #    sentence_transformer_ef = SentenceTransformer( | ||||
|  | @ -116,6 +111,39 @@ class StoreWebForm(CollectionNameForm): | |||
|     url: str | ||||
| 
 | ||||
| 
 | ||||
| def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: | ||||
|     text_splitter = RecursiveCharacterTextSplitter( | ||||
|         chunk_size=app.state.CHUNK_SIZE, chunk_overlap=app.state.CHUNK_OVERLAP | ||||
|     ) | ||||
|     docs = text_splitter.split_documents(data) | ||||
| 
 | ||||
|     texts = [doc.page_content for doc in docs] | ||||
|     metadatas = [doc.metadata for doc in docs] | ||||
| 
 | ||||
|     try: | ||||
|         if overwrite: | ||||
|             for collection in CHROMA_CLIENT.list_collections(): | ||||
|                 if collection_name == collection.name: | ||||
|                     print(f"deleting existing collection {collection_name}") | ||||
|                     CHROMA_CLIENT.delete_collection(name=collection_name) | ||||
| 
 | ||||
|         collection = CHROMA_CLIENT.create_collection( | ||||
|             name=collection_name, | ||||
|             embedding_function=app.state.sentence_transformer_ef, | ||||
|         ) | ||||
| 
 | ||||
|         collection.add( | ||||
|             documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts] | ||||
|         ) | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         print(e) | ||||
|         if e.__class__.__name__ == "UniqueConstraintError": | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/") | ||||
| async def get_status(): | ||||
|     return { | ||||
|  | @ -245,7 +273,7 @@ def query_doc_handler( | |||
|             embedding_function=app.state.sentence_transformer_ef, | ||||
|         ) | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_400_BAD_REQUEST, | ||||
|             detail=ERROR_MESSAGES.DEFAULT(e), | ||||
|  | @ -289,69 +317,13 @@ def store_web(form_data: StoreWebForm, user=Depends(get_current_user)): | |||
|             "filename": form_data.url, | ||||
|         } | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_400_BAD_REQUEST, | ||||
|             detail=ERROR_MESSAGES.DEFAULT(e), | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| def store_data_in_vector_db(data, collection_name, overwrite: bool = False) -> bool: | ||||
| 
 | ||||
|     text_splitter = RecursiveCharacterTextSplitter( | ||||
|         chunk_size=app.state.CHUNK_SIZE, | ||||
|         chunk_overlap=app.state.CHUNK_OVERLAP, | ||||
|         add_start_index=True, | ||||
|     ) | ||||
|     docs = text_splitter.split_documents(data) | ||||
| 
 | ||||
|     if len(docs) > 0: | ||||
|         return store_docs_in_vector_db(docs, collection_name, overwrite), None | ||||
|     else: | ||||
|         raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) | ||||
| 
 | ||||
| 
 | ||||
| def store_text_in_vector_db( | ||||
|     text, metadata, collection_name, overwrite: bool = False | ||||
| ) -> bool: | ||||
|     text_splitter = RecursiveCharacterTextSplitter( | ||||
|         chunk_size=app.state.CHUNK_SIZE, | ||||
|         chunk_overlap=app.state.CHUNK_OVERLAP, | ||||
|         add_start_index=True, | ||||
|     ) | ||||
|     docs = text_splitter.create_documents([text], metadatas=[metadata]) | ||||
|     return store_docs_in_vector_db(docs, collection_name, overwrite) | ||||
| 
 | ||||
| 
 | ||||
| def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> bool: | ||||
| 
 | ||||
|     texts = [doc.page_content for doc in docs] | ||||
|     metadatas = [doc.metadata for doc in docs] | ||||
| 
 | ||||
|     try: | ||||
|         if overwrite: | ||||
|             for collection in CHROMA_CLIENT.list_collections(): | ||||
|                 if collection_name == collection.name: | ||||
|                     log.info(f"deleting existing collection {collection_name}") | ||||
|                     CHROMA_CLIENT.delete_collection(name=collection_name) | ||||
| 
 | ||||
|         collection = CHROMA_CLIENT.create_collection( | ||||
|             name=collection_name, | ||||
|             embedding_function=app.state.sentence_transformer_ef, | ||||
|         ) | ||||
| 
 | ||||
|         collection.add( | ||||
|             documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts] | ||||
|         ) | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         if e.__class__.__name__ == "UniqueConstraintError": | ||||
|             return True | ||||
| 
 | ||||
|         return False | ||||
| 
 | ||||
| 
 | ||||
| def get_loader(filename: str, file_content_type: str, file_path: str): | ||||
|     file_ext = filename.split(".")[-1].lower() | ||||
|     known_type = True | ||||
|  | @ -409,8 +381,6 @@ def get_loader(filename: str, file_content_type: str, file_path: str): | |||
|         loader = UnstructuredRSTLoader(file_path, mode="elements") | ||||
|     elif file_ext == "xml": | ||||
|         loader = UnstructuredXMLLoader(file_path) | ||||
|     elif file_ext in ["htm", "html"]: | ||||
|         loader = BSHTMLLoader(file_path, open_encoding="unicode_escape") | ||||
|     elif file_ext == "md": | ||||
|         loader = UnstructuredMarkdownLoader(file_path) | ||||
|     elif file_content_type == "application/epub+zip": | ||||
|  | @ -429,9 +399,9 @@ def get_loader(filename: str, file_content_type: str, file_path: str): | |||
|     elif file_ext in known_source_ext or ( | ||||
|         file_content_type and file_content_type.find("text/") >= 0 | ||||
|     ): | ||||
|         loader = TextLoader(file_path, autodetect_encoding=True) | ||||
|         loader = TextLoader(file_path) | ||||
|     else: | ||||
|         loader = TextLoader(file_path, autodetect_encoding=True) | ||||
|         loader = TextLoader(file_path) | ||||
|         known_type = False | ||||
| 
 | ||||
|     return loader, known_type | ||||
|  | @ -445,7 +415,7 @@ def store_doc( | |||
| ): | ||||
|     # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" | ||||
| 
 | ||||
|     log.info(f"file.content_type: {file.content_type}") | ||||
|     print(file.content_type) | ||||
|     try: | ||||
|         filename = file.filename | ||||
|         file_path = f"{UPLOAD_DIR}/{filename}" | ||||
|  | @ -461,8 +431,6 @@ def store_doc( | |||
| 
 | ||||
|         loader, known_type = get_loader(file.filename, file.content_type, file_path) | ||||
|         data = loader.load() | ||||
| 
 | ||||
|         try: | ||||
|         result = store_data_in_vector_db(data, collection_name) | ||||
| 
 | ||||
|         if result: | ||||
|  | @ -472,13 +440,13 @@ def store_doc( | |||
|                 "filename": filename, | ||||
|                 "known_type": known_type, | ||||
|             } | ||||
|         except Exception as e: | ||||
|         else: | ||||
|             raise HTTPException( | ||||
|                 status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||||
|                 detail=e, | ||||
|                 detail=ERROR_MESSAGES.DEFAULT(), | ||||
|             ) | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         if "No pandoc was found" in str(e): | ||||
|             raise HTTPException( | ||||
|                 status_code=status.HTTP_400_BAD_REQUEST, | ||||
|  | @ -491,37 +459,6 @@ def store_doc( | |||
|             ) | ||||
| 
 | ||||
| 
 | ||||
| class TextRAGForm(BaseModel): | ||||
|     name: str | ||||
|     content: str | ||||
|     collection_name: Optional[str] = None | ||||
| 
 | ||||
| 
 | ||||
| @app.post("/text") | ||||
| def store_text( | ||||
|     form_data: TextRAGForm, | ||||
|     user=Depends(get_current_user), | ||||
| ): | ||||
| 
 | ||||
|     collection_name = form_data.collection_name | ||||
|     if collection_name == None: | ||||
|         collection_name = calculate_sha256_string(form_data.content) | ||||
| 
 | ||||
|     result = store_text_in_vector_db( | ||||
|         form_data.content, | ||||
|         metadata={"name": form_data.name, "created_by": user.id}, | ||||
|         collection_name=collection_name, | ||||
|     ) | ||||
| 
 | ||||
|     if result: | ||||
|         return {"status": True, "collection_name": collection_name} | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||||
|             detail=ERROR_MESSAGES.DEFAULT(), | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/scan") | ||||
| def scan_docs_dir(user=Depends(get_admin_user)): | ||||
|     for path in Path(DOCS_DIR).rglob("./**/*"): | ||||
|  | @ -540,7 +477,6 @@ def scan_docs_dir(user=Depends(get_admin_user)): | |||
|                 ) | ||||
|                 data = loader.load() | ||||
| 
 | ||||
|                 try: | ||||
|                 result = store_data_in_vector_db(data, collection_name) | ||||
| 
 | ||||
|                 if result: | ||||
|  | @ -573,12 +509,9 @@ def scan_docs_dir(user=Depends(get_admin_user)): | |||
|                                 } | ||||
|                             ), | ||||
|                         ) | ||||
|                 except Exception as e: | ||||
|                     log.exception(e) | ||||
|                     pass | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
| 
 | ||||
|     return True | ||||
| 
 | ||||
|  | @ -599,11 +532,11 @@ def reset(user=Depends(get_admin_user)) -> bool: | |||
|             elif os.path.isdir(file_path): | ||||
|                 shutil.rmtree(file_path) | ||||
|         except Exception as e: | ||||
|             log.error("Failed to delete %s. Reason: %s" % (file_path, e)) | ||||
|             print("Failed to delete %s. Reason: %s" % (file_path, e)) | ||||
| 
 | ||||
|     try: | ||||
|         CHROMA_CLIENT.reset() | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
| 
 | ||||
|     return True | ||||
|  |  | |||
|  | @ -1,11 +1,7 @@ | |||
| import re | ||||
| import logging | ||||
| from typing import List | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS, CHROMA_CLIENT | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["RAG"]) | ||||
| from config import CHROMA_CLIENT | ||||
| 
 | ||||
| 
 | ||||
| def query_doc(collection_name: str, query: str, k: int, embedding_function): | ||||
|  | @ -95,13 +91,14 @@ def query_collection( | |||
| 
 | ||||
| 
 | ||||
| def rag_template(template: str, context: str, query: str): | ||||
|     template = template.replace("[context]", context) | ||||
|     template = template.replace("[query]", query) | ||||
|     template = re.sub(r"\[context\]", context, template) | ||||
|     template = re.sub(r"\[query\]", query, template) | ||||
| 
 | ||||
|     return template | ||||
| 
 | ||||
| 
 | ||||
| def rag_messages(docs, messages, template, k, embedding_function): | ||||
|     log.debug(f"docs: {docs}") | ||||
|     print(docs) | ||||
| 
 | ||||
|     last_user_message_idx = None | ||||
|     for i in range(len(messages) - 1, -1, -1): | ||||
|  | @ -141,8 +138,6 @@ def rag_messages(docs, messages, template, k, embedding_function): | |||
|                     k=k, | ||||
|                     embedding_function=embedding_function, | ||||
|                 ) | ||||
|             elif doc["type"] == "text": | ||||
|                 context = doc["content"] | ||||
|             else: | ||||
|                 context = query_doc( | ||||
|                     collection_name=doc["collection_name"], | ||||
|  | @ -151,13 +146,11 @@ def rag_messages(docs, messages, template, k, embedding_function): | |||
|                     embedding_function=embedding_function, | ||||
|                 ) | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             context = None | ||||
| 
 | ||||
|         relevant_contexts.append(context) | ||||
| 
 | ||||
|     log.debug(f"relevant_contexts: {relevant_contexts}") | ||||
| 
 | ||||
|     context_string = "" | ||||
|     for context in relevant_contexts: | ||||
|         if context: | ||||
|  |  | |||
|  | @ -1,16 +1,13 @@ | |||
| from peewee import * | ||||
| from config import SRC_LOG_LEVELS, DATA_DIR | ||||
| from config import DATA_DIR | ||||
| import os | ||||
| import logging | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["DB"]) | ||||
| 
 | ||||
| # Check if the file exists | ||||
| if os.path.exists(f"{DATA_DIR}/ollama.db"): | ||||
|     # Rename the file | ||||
|     os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db") | ||||
|     log.info("File renamed successfully.") | ||||
|     print("File renamed successfully.") | ||||
| else: | ||||
|     pass | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,6 @@ from config import ( | |||
|     DEFAULT_USER_ROLE, | ||||
|     ENABLE_SIGNUP, | ||||
|     USER_PERMISSIONS, | ||||
|     WEBHOOK_URL, | ||||
| ) | ||||
| 
 | ||||
| app = FastAPI() | ||||
|  | @ -33,7 +32,6 @@ app.state.DEFAULT_MODELS = DEFAULT_MODELS | |||
| app.state.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS | ||||
| app.state.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE | ||||
| app.state.USER_PERMISSIONS = USER_PERMISSIONS | ||||
| app.state.WEBHOOK_URL = WEBHOOK_URL | ||||
| 
 | ||||
| 
 | ||||
| app.add_middleware( | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ from pydantic import BaseModel | |||
| from typing import List, Union, Optional | ||||
| import time | ||||
| import uuid | ||||
| import logging | ||||
| from peewee import * | ||||
| 
 | ||||
| from apps.web.models.users import UserModel, Users | ||||
|  | @ -10,11 +9,6 @@ from utils.utils import verify_password | |||
| 
 | ||||
| from apps.web.internal.db import DB | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MODELS"]) | ||||
| 
 | ||||
| #################### | ||||
| # DB MODEL | ||||
| #################### | ||||
|  | @ -92,7 +86,7 @@ class AuthsTable: | |||
|     def insert_new_auth( | ||||
|         self, email: str, password: str, name: str, role: str = "pending" | ||||
|     ) -> Optional[UserModel]: | ||||
|         log.info("insert_new_auth") | ||||
|         print("insert_new_auth") | ||||
| 
 | ||||
|         id = str(uuid.uuid4()) | ||||
| 
 | ||||
|  | @ -109,7 +103,7 @@ class AuthsTable: | |||
|             return None | ||||
| 
 | ||||
|     def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: | ||||
|         log.info(f"authenticate_user: {email}") | ||||
|         print("authenticate_user", email) | ||||
|         try: | ||||
|             auth = Auth.get(Auth.email == email, Auth.active == True) | ||||
|             if auth: | ||||
|  |  | |||
|  | @ -95,6 +95,20 @@ class ChatTable: | |||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: | ||||
|         try: | ||||
|             query = Chat.update( | ||||
|                 chat=json.dumps(chat), | ||||
|                 title=chat["title"] if "title" in chat else "New Chat", | ||||
|                 timestamp=int(time.time()), | ||||
|             ).where(Chat.id == id) | ||||
|             query.execute() | ||||
| 
 | ||||
|             chat = Chat.get(Chat.id == id) | ||||
|             return ChatModel(**model_to_dict(chat)) | ||||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def get_chat_lists_by_user_id( | ||||
|         self, user_id: str, skip: int = 0, limit: int = 50 | ||||
|     ) -> List[ChatModel]: | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ from peewee import * | |||
| from playhouse.shortcuts import model_to_dict | ||||
| from typing import List, Union, Optional | ||||
| import time | ||||
| import logging | ||||
| 
 | ||||
| from utils.utils import decode_token | ||||
| from utils.misc import get_gravatar_url | ||||
|  | @ -12,11 +11,6 @@ from apps.web.internal.db import DB | |||
| 
 | ||||
| import json | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MODELS"]) | ||||
| 
 | ||||
| #################### | ||||
| # Documents DB Schema | ||||
| #################### | ||||
|  | @ -124,7 +118,7 @@ class DocumentsTable: | |||
|             doc = Document.get(Document.name == form_data.name) | ||||
|             return DocumentModel(**model_to_dict(doc)) | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             return None | ||||
| 
 | ||||
|     def update_doc_content_by_name( | ||||
|  | @ -144,7 +138,7 @@ class DocumentsTable: | |||
|             doc = Document.get(Document.name == name) | ||||
|             return DocumentModel(**model_to_dict(doc)) | ||||
|         except Exception as e: | ||||
|             log.exception(e) | ||||
|             print(e) | ||||
|             return None | ||||
| 
 | ||||
|     def delete_doc_by_name(self, name: str) -> bool: | ||||
|  |  | |||
|  | @ -64,8 +64,8 @@ class ModelfilesTable: | |||
|         self.db.create_tables([Modelfile]) | ||||
| 
 | ||||
|     def insert_new_modelfile( | ||||
|         self, user_id: str, form_data: ModelfileForm | ||||
|     ) -> Optional[ModelfileModel]: | ||||
|             self, user_id: str, | ||||
|             form_data: ModelfileForm) -> Optional[ModelfileModel]: | ||||
|         if "tagName" in form_data.modelfile: | ||||
|             modelfile = ModelfileModel( | ||||
|                 **{ | ||||
|  | @ -73,8 +73,7 @@ class ModelfilesTable: | |||
|                     "tag_name": form_data.modelfile["tagName"], | ||||
|                     "modelfile": json.dumps(form_data.modelfile), | ||||
|                     "timestamp": int(time.time()), | ||||
|                 } | ||||
|             ) | ||||
|                 }) | ||||
| 
 | ||||
|             try: | ||||
|                 result = Modelfile.create(**modelfile.model_dump()) | ||||
|  | @ -88,28 +87,29 @@ class ModelfilesTable: | |||
|         else: | ||||
|             return None | ||||
| 
 | ||||
|     def get_modelfile_by_tag_name(self, tag_name: str) -> Optional[ModelfileModel]: | ||||
|     def get_modelfile_by_tag_name(self, | ||||
|                                   tag_name: str) -> Optional[ModelfileModel]: | ||||
|         try: | ||||
|             modelfile = Modelfile.get(Modelfile.tag_name == tag_name) | ||||
|             return ModelfileModel(**model_to_dict(modelfile)) | ||||
|         except: | ||||
|             return None | ||||
| 
 | ||||
|     def get_modelfiles(self, skip: int = 0, limit: int = 50) -> List[ModelfileResponse]: | ||||
|     def get_modelfiles(self, | ||||
|                        skip: int = 0, | ||||
|                        limit: int = 50) -> List[ModelfileResponse]: | ||||
|         return [ | ||||
|             ModelfileResponse( | ||||
|                 **{ | ||||
|                     **model_to_dict(modelfile), | ||||
|                     "modelfile": json.loads(modelfile.modelfile), | ||||
|                 } | ||||
|             ) | ||||
|             for modelfile in Modelfile.select() | ||||
|                     "modelfile": | ||||
|                     json.loads(modelfile.modelfile), | ||||
|                 }) for modelfile in Modelfile.select() | ||||
|             # .limit(limit).offset(skip) | ||||
|         ] | ||||
| 
 | ||||
|     def update_modelfile_by_tag_name( | ||||
|         self, tag_name: str, modelfile: dict | ||||
|     ) -> Optional[ModelfileModel]: | ||||
|             self, tag_name: str, modelfile: dict) -> Optional[ModelfileModel]: | ||||
|         try: | ||||
|             query = Modelfile.update( | ||||
|                 modelfile=json.dumps(modelfile), | ||||
|  |  | |||
|  | @ -52,9 +52,8 @@ class PromptsTable: | |||
|         self.db = db | ||||
|         self.db.create_tables([Prompt]) | ||||
| 
 | ||||
|     def insert_new_prompt( | ||||
|         self, user_id: str, form_data: PromptForm | ||||
|     ) -> Optional[PromptModel]: | ||||
|     def insert_new_prompt(self, user_id: str, | ||||
|                           form_data: PromptForm) -> Optional[PromptModel]: | ||||
|         prompt = PromptModel( | ||||
|             **{ | ||||
|                 "user_id": user_id, | ||||
|  | @ -62,8 +61,7 @@ class PromptsTable: | |||
|                 "title": form_data.title, | ||||
|                 "content": form_data.content, | ||||
|                 "timestamp": int(time.time()), | ||||
|             } | ||||
|         ) | ||||
|             }) | ||||
| 
 | ||||
|         try: | ||||
|             result = Prompt.create(**prompt.model_dump()) | ||||
|  | @ -83,14 +81,13 @@ class PromptsTable: | |||
| 
 | ||||
|     def get_prompts(self) -> List[PromptModel]: | ||||
|         return [ | ||||
|             PromptModel(**model_to_dict(prompt)) | ||||
|             for prompt in Prompt.select() | ||||
|             PromptModel(**model_to_dict(prompt)) for prompt in Prompt.select() | ||||
|             # .limit(limit).offset(skip) | ||||
|         ] | ||||
| 
 | ||||
|     def update_prompt_by_command( | ||||
|         self, command: str, form_data: PromptForm | ||||
|     ) -> Optional[PromptModel]: | ||||
|             self, command: str, | ||||
|             form_data: PromptForm) -> Optional[PromptModel]: | ||||
|         try: | ||||
|             query = Prompt.update( | ||||
|                 title=form_data.title, | ||||
|  |  | |||
|  | @ -6,15 +6,9 @@ from playhouse.shortcuts import model_to_dict | |||
| import json | ||||
| import uuid | ||||
| import time | ||||
| import logging | ||||
| 
 | ||||
| from apps.web.internal.db import DB | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MODELS"]) | ||||
| 
 | ||||
| #################### | ||||
| # Tag DB Schema | ||||
| #################### | ||||
|  | @ -179,7 +173,7 @@ class TagTable: | |||
|                 (ChatIdTag.tag_name == tag_name) & (ChatIdTag.user_id == user_id) | ||||
|             ) | ||||
|             res = query.execute()  # Remove the rows, return number of rows removed. | ||||
|             log.debug(f"res: {res}") | ||||
|             print(res) | ||||
| 
 | ||||
|             tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) | ||||
|             if tag_count == 0: | ||||
|  | @ -191,7 +185,7 @@ class TagTable: | |||
| 
 | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             log.error(f"delete_tag: {e}") | ||||
|             print("delete_tag", e) | ||||
|             return False | ||||
| 
 | ||||
|     def delete_tag_by_tag_name_and_chat_id_and_user_id( | ||||
|  | @ -204,7 +198,7 @@ class TagTable: | |||
|                 & (ChatIdTag.user_id == user_id) | ||||
|             ) | ||||
|             res = query.execute()  # Remove the rows, return number of rows removed. | ||||
|             log.debug(f"res: {res}") | ||||
|             print(res) | ||||
| 
 | ||||
|             tag_count = self.count_chat_ids_by_tag_name_and_user_id(tag_name, user_id) | ||||
|             if tag_count == 0: | ||||
|  | @ -216,7 +210,7 @@ class TagTable: | |||
| 
 | ||||
|             return True | ||||
|         except Exception as e: | ||||
|             log.error(f"delete_tag: {e}") | ||||
|             print("delete_tag", e) | ||||
|             return False | ||||
| 
 | ||||
|     def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool: | ||||
|  |  | |||
|  | @ -27,8 +27,7 @@ from utils.utils import ( | |||
|     create_token, | ||||
| ) | ||||
| from utils.misc import parse_duration, validate_email_format | ||||
| from utils.webhook import post_webhook | ||||
| from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
|  | @ -156,17 +155,6 @@ async def signup(request: Request, form_data: SignupForm): | |||
|             ) | ||||
|             # response.set_cookie(key='token', value=token, httponly=True) | ||||
| 
 | ||||
|             if request.app.state.WEBHOOK_URL: | ||||
|                 post_webhook( | ||||
|                     request.app.state.WEBHOOK_URL, | ||||
|                     WEBHOOK_MESSAGES.USER_SIGNUP(user.name), | ||||
|                     { | ||||
|                         "action": "signup", | ||||
|                         "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), | ||||
|                         "user": user.model_dump_json(exclude_none=True), | ||||
|                     }, | ||||
|                 ) | ||||
| 
 | ||||
|             return { | ||||
|                 "token": token, | ||||
|                 "token_type": "Bearer", | ||||
|  |  | |||
|  | @ -5,7 +5,6 @@ from utils.utils import get_current_user, get_admin_user | |||
| from fastapi import APIRouter | ||||
| from pydantic import BaseModel | ||||
| import json | ||||
| import logging | ||||
| 
 | ||||
| from apps.web.models.users import Users | ||||
| from apps.web.models.chats import ( | ||||
|  | @ -28,11 +27,6 @@ from apps.web.models.tags import ( | |||
| 
 | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MODELS"]) | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
| ############################ | ||||
|  | @ -84,7 +78,7 @@ async def create_new_chat(form_data: ChatForm, user=Depends(get_current_user)): | |||
|         chat = Chats.insert_new_chat(user.id, form_data) | ||||
|         return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() | ||||
|         ) | ||||
|  | @ -101,7 +95,7 @@ async def get_all_tags(user=Depends(get_current_user)): | |||
|         tags = Tags.get_tags_by_user_id(user.id) | ||||
|         return tags | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() | ||||
|         ) | ||||
|  |  | |||
|  | @ -10,12 +10,7 @@ import uuid | |||
| 
 | ||||
| from apps.web.models.users import Users | ||||
| 
 | ||||
| from utils.utils import ( | ||||
|     get_password_hash, | ||||
|     get_current_user, | ||||
|     get_admin_user, | ||||
|     create_token, | ||||
| ) | ||||
| from utils.utils import get_password_hash, get_current_user, get_admin_user, create_token | ||||
| from utils.misc import get_gravatar_url, validate_email_format | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
|  | @ -48,6 +43,7 @@ async def set_global_default_models( | |||
|     return request.app.state.DEFAULT_MODELS | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/default/suggestions", response_model=List[PromptSuggestion]) | ||||
| async def set_global_default_suggestions( | ||||
|     request: Request, | ||||
|  |  | |||
|  | @ -24,9 +24,9 @@ router = APIRouter() | |||
| 
 | ||||
| 
 | ||||
| @router.get("/", response_model=List[ModelfileResponse]) | ||||
| async def get_modelfiles( | ||||
|     skip: int = 0, limit: int = 50, user=Depends(get_current_user) | ||||
| ): | ||||
| async def get_modelfiles(skip: int = 0, | ||||
|                          limit: int = 50, | ||||
|                          user=Depends(get_current_user)): | ||||
|     return Modelfiles.get_modelfiles(skip, limit) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -36,16 +36,17 @@ async def get_modelfiles( | |||
| 
 | ||||
| 
 | ||||
| @router.post("/create", response_model=Optional[ModelfileResponse]) | ||||
| async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_user)): | ||||
| async def create_new_modelfile(form_data: ModelfileForm, | ||||
|                                user=Depends(get_admin_user)): | ||||
|     modelfile = Modelfiles.insert_new_modelfile(user.id, form_data) | ||||
| 
 | ||||
|     if modelfile: | ||||
|         return ModelfileResponse( | ||||
|             **{ | ||||
|                 **modelfile.model_dump(), | ||||
|                 "modelfile": json.loads(modelfile.modelfile), | ||||
|             } | ||||
|         ) | ||||
|                 "modelfile": | ||||
|                 json.loads(modelfile.modelfile), | ||||
|             }) | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|  | @ -59,18 +60,17 @@ async def create_new_modelfile(form_data: ModelfileForm, user=Depends(get_admin_ | |||
| 
 | ||||
| 
 | ||||
| @router.post("/", response_model=Optional[ModelfileResponse]) | ||||
| async def get_modelfile_by_tag_name( | ||||
|     form_data: ModelfileTagNameForm, user=Depends(get_current_user) | ||||
| ): | ||||
| async def get_modelfile_by_tag_name(form_data: ModelfileTagNameForm, | ||||
|                                     user=Depends(get_current_user)): | ||||
|     modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) | ||||
| 
 | ||||
|     if modelfile: | ||||
|         return ModelfileResponse( | ||||
|             **{ | ||||
|                 **modelfile.model_dump(), | ||||
|                 "modelfile": json.loads(modelfile.modelfile), | ||||
|             } | ||||
|         ) | ||||
|                 "modelfile": | ||||
|                 json.loads(modelfile.modelfile), | ||||
|             }) | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|  | @ -84,9 +84,8 @@ async def get_modelfile_by_tag_name( | |||
| 
 | ||||
| 
 | ||||
| @router.post("/update", response_model=Optional[ModelfileResponse]) | ||||
| async def update_modelfile_by_tag_name( | ||||
|     form_data: ModelfileUpdateForm, user=Depends(get_admin_user) | ||||
| ): | ||||
| async def update_modelfile_by_tag_name(form_data: ModelfileUpdateForm, | ||||
|                                        user=Depends(get_admin_user)): | ||||
|     modelfile = Modelfiles.get_modelfile_by_tag_name(form_data.tag_name) | ||||
|     if modelfile: | ||||
|         updated_modelfile = { | ||||
|  | @ -95,15 +94,14 @@ async def update_modelfile_by_tag_name( | |||
|         } | ||||
| 
 | ||||
|         modelfile = Modelfiles.update_modelfile_by_tag_name( | ||||
|             form_data.tag_name, updated_modelfile | ||||
|         ) | ||||
|             form_data.tag_name, updated_modelfile) | ||||
| 
 | ||||
|         return ModelfileResponse( | ||||
|             **{ | ||||
|                 **modelfile.model_dump(), | ||||
|                 "modelfile": json.loads(modelfile.modelfile), | ||||
|             } | ||||
|         ) | ||||
|                 "modelfile": | ||||
|                 json.loads(modelfile.modelfile), | ||||
|             }) | ||||
|     else: | ||||
|         raise HTTPException( | ||||
|             status_code=status.HTTP_401_UNAUTHORIZED, | ||||
|  | @ -117,8 +115,7 @@ async def update_modelfile_by_tag_name( | |||
| 
 | ||||
| 
 | ||||
| @router.delete("/delete", response_model=bool) | ||||
| async def delete_modelfile_by_tag_name( | ||||
|     form_data: ModelfileTagNameForm, user=Depends(get_admin_user) | ||||
| ): | ||||
| async def delete_modelfile_by_tag_name(form_data: ModelfileTagNameForm, | ||||
|                                        user=Depends(get_admin_user)): | ||||
|     result = Modelfiles.delete_modelfile_by_tag_name(form_data.tag_name) | ||||
|     return result | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ from fastapi import APIRouter | |||
| from pydantic import BaseModel | ||||
| import time | ||||
| import uuid | ||||
| import logging | ||||
| 
 | ||||
| from apps.web.models.users import UserModel, UserUpdateForm, UserRoleUpdateForm, Users | ||||
| from apps.web.models.auths import Auths | ||||
|  | @ -15,11 +14,6 @@ from apps.web.models.auths import Auths | |||
| from utils.utils import get_current_user, get_password_hash, get_admin_user | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MODELS"]) | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
| ############################ | ||||
|  | @ -89,7 +83,7 @@ async def update_user_by_id( | |||
| 
 | ||||
|         if form_data.password: | ||||
|             hashed = get_password_hash(form_data.password) | ||||
|             log.debug(f"hashed: {hashed}") | ||||
|             print(hashed) | ||||
|             Auths.update_user_password_by_id(user_id, hashed) | ||||
| 
 | ||||
|         Auths.update_email_by_id(user_id, form_data.email.lower()) | ||||
|  |  | |||
|  | @ -21,6 +21,155 @@ from constants import ERROR_MESSAGES | |||
| router = APIRouter() | ||||
| 
 | ||||
| 
 | ||||
| class UploadBlobForm(BaseModel): | ||||
|     filename: str | ||||
| 
 | ||||
| 
 | ||||
| from urllib.parse import urlparse | ||||
| 
 | ||||
| 
 | ||||
| def parse_huggingface_url(hf_url): | ||||
|     try: | ||||
|         # Parse the URL | ||||
|         parsed_url = urlparse(hf_url) | ||||
| 
 | ||||
|         # Get the path and split it into components | ||||
|         path_components = parsed_url.path.split("/") | ||||
| 
 | ||||
|         # Extract the desired output | ||||
|         user_repo = "/".join(path_components[1:3]) | ||||
|         model_file = path_components[-1] | ||||
| 
 | ||||
|         return model_file | ||||
|     except ValueError: | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| async def download_file_stream(url, file_path, file_name, chunk_size=1024 * 1024): | ||||
|     done = False | ||||
| 
 | ||||
|     if os.path.exists(file_path): | ||||
|         current_size = os.path.getsize(file_path) | ||||
|     else: | ||||
|         current_size = 0 | ||||
| 
 | ||||
|     headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} | ||||
| 
 | ||||
|     timeout = aiohttp.ClientTimeout(total=600)  # Set the timeout | ||||
| 
 | ||||
|     async with aiohttp.ClientSession(timeout=timeout) as session: | ||||
|         async with session.get(url, headers=headers) as response: | ||||
|             total_size = int(response.headers.get("content-length", 0)) + current_size | ||||
| 
 | ||||
|             with open(file_path, "ab+") as file: | ||||
|                 async for data in response.content.iter_chunked(chunk_size): | ||||
|                     current_size += len(data) | ||||
|                     file.write(data) | ||||
| 
 | ||||
|                     done = current_size == total_size | ||||
|                     progress = round((current_size / total_size) * 100, 2) | ||||
|                     yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' | ||||
| 
 | ||||
|                 if done: | ||||
|                     file.seek(0) | ||||
|                     hashed = calculate_sha256(file) | ||||
|                     file.seek(0) | ||||
| 
 | ||||
|                     url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" | ||||
|                     response = requests.post(url, data=file) | ||||
| 
 | ||||
|                     if response.ok: | ||||
|                         res = { | ||||
|                             "done": done, | ||||
|                             "blob": f"sha256:{hashed}", | ||||
|                             "name": file_name, | ||||
|                         } | ||||
|                         os.remove(file_path) | ||||
| 
 | ||||
|                         yield f"data: {json.dumps(res)}\n\n" | ||||
|                     else: | ||||
|                         raise "Ollama: Could not create blob, Please try again." | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/download") | ||||
| async def download( | ||||
|     url: str, | ||||
| ): | ||||
|     # url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" | ||||
|     file_name = parse_huggingface_url(url) | ||||
| 
 | ||||
|     if file_name: | ||||
|         file_path = f"{UPLOAD_DIR}/{file_name}" | ||||
| 
 | ||||
|         return StreamingResponse( | ||||
|             download_file_stream(url, file_path, file_name), | ||||
|             media_type="text/event-stream", | ||||
|         ) | ||||
|     else: | ||||
|         return None | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/upload") | ||||
| def upload(file: UploadFile = File(...)): | ||||
|     file_path = f"{UPLOAD_DIR}/{file.filename}" | ||||
| 
 | ||||
|     # Save file in chunks | ||||
|     with open(file_path, "wb+") as f: | ||||
|         for chunk in file.file: | ||||
|             f.write(chunk) | ||||
| 
 | ||||
|     def file_process_stream(): | ||||
|         total_size = os.path.getsize(file_path) | ||||
|         chunk_size = 1024 * 1024 | ||||
|         try: | ||||
|             with open(file_path, "rb") as f: | ||||
|                 total = 0 | ||||
|                 done = False | ||||
| 
 | ||||
|                 while not done: | ||||
|                     chunk = f.read(chunk_size) | ||||
|                     if not chunk: | ||||
|                         done = True | ||||
|                         continue | ||||
| 
 | ||||
|                     total += len(chunk) | ||||
|                     progress = round((total / total_size) * 100, 2) | ||||
| 
 | ||||
|                     res = { | ||||
|                         "progress": progress, | ||||
|                         "total": total_size, | ||||
|                         "completed": total, | ||||
|                     } | ||||
|                     yield f"data: {json.dumps(res)}\n\n" | ||||
| 
 | ||||
|                 if done: | ||||
|                     f.seek(0) | ||||
|                     hashed = calculate_sha256(f) | ||||
|                     f.seek(0) | ||||
| 
 | ||||
|                     url = f"{OLLAMA_BASE_URLS[0]}/blobs/sha256:{hashed}" | ||||
|                     response = requests.post(url, data=f) | ||||
| 
 | ||||
|                     if response.ok: | ||||
|                         res = { | ||||
|                             "done": done, | ||||
|                             "blob": f"sha256:{hashed}", | ||||
|                             "name": file.filename, | ||||
|                         } | ||||
|                         os.remove(file_path) | ||||
|                         yield f"data: {json.dumps(res)}\n\n" | ||||
|                     else: | ||||
|                         raise Exception( | ||||
|                             "Ollama: Could not create blob, Please try again." | ||||
|                         ) | ||||
| 
 | ||||
|         except Exception as e: | ||||
|             res = {"error": str(e)} | ||||
|             yield f"data: {json.dumps(res)}\n\n" | ||||
| 
 | ||||
|     return StreamingResponse(file_process_stream(), media_type="text/event-stream") | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/gravatar") | ||||
| async def get_gravatar( | ||||
|     email: str, | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| import os | ||||
| import sys | ||||
| import logging | ||||
| import chromadb | ||||
| from chromadb import Settings | ||||
| from base64 import b64encode | ||||
|  | @ -23,10 +21,9 @@ try: | |||
| 
 | ||||
|     load_dotenv(find_dotenv("../.env")) | ||||
| except ImportError: | ||||
|     log.warning("dotenv not installed, skipping...") | ||||
|     print("dotenv not installed, skipping...") | ||||
| 
 | ||||
| WEBUI_NAME = "Open WebUI" | ||||
| WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" | ||||
| shutil.copyfile("../build/favicon.png", "./static/favicon.png") | ||||
| 
 | ||||
| #################################### | ||||
|  | @ -103,47 +100,6 @@ for version in soup.find_all("h2"): | |||
| CHANGELOG = changelog_json | ||||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
| # LOGGING | ||||
| #################################### | ||||
| log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] | ||||
| 
 | ||||
| GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper() | ||||
| if GLOBAL_LOG_LEVEL in log_levels: | ||||
|     logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) | ||||
| else: | ||||
|     GLOBAL_LOG_LEVEL = "INFO" | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") | ||||
| 
 | ||||
| log_sources = [ | ||||
|     "AUDIO", | ||||
|     "COMFYUI", | ||||
|     "CONFIG", | ||||
|     "DB", | ||||
|     "IMAGES", | ||||
|     "LITELLM", | ||||
|     "MAIN", | ||||
|     "MODELS", | ||||
|     "OLLAMA", | ||||
|     "OPENAI", | ||||
|     "RAG", | ||||
|     "WEBHOOK", | ||||
| ] | ||||
| 
 | ||||
| SRC_LOG_LEVELS = {} | ||||
| 
 | ||||
| for source in log_sources: | ||||
|     log_env_var = source + "_LOG_LEVEL" | ||||
|     SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper() | ||||
|     if SRC_LOG_LEVELS[source] not in log_levels: | ||||
|         SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL | ||||
|     log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}") | ||||
| 
 | ||||
| log.setLevel(SRC_LOG_LEVELS["CONFIG"]) | ||||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
| # CUSTOM_NAME | ||||
| #################################### | ||||
|  | @ -155,7 +111,7 @@ if CUSTOM_NAME: | |||
|         data = r.json() | ||||
|         if r.ok: | ||||
|             if "logo" in data: | ||||
|                 WEBUI_FAVICON_URL = url = ( | ||||
|                 url = ( | ||||
|                     f"https://api.openwebui.com{data['logo']}" | ||||
|                     if data["logo"][0] == "/" | ||||
|                     else data["logo"] | ||||
|  | @ -169,7 +125,7 @@ if CUSTOM_NAME: | |||
| 
 | ||||
|             WEBUI_NAME = data["name"] | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         print(e) | ||||
|         pass | ||||
| 
 | ||||
| 
 | ||||
|  | @ -238,9 +194,9 @@ def create_config_file(file_path): | |||
| LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml" | ||||
| 
 | ||||
| if not os.path.exists(LITELLM_CONFIG_PATH): | ||||
|     log.info("Config file doesn't exist. Creating...") | ||||
|     print("Config file doesn't exist. Creating...") | ||||
|     create_config_file(LITELLM_CONFIG_PATH) | ||||
|     log.info("Config file created successfully.") | ||||
|     print("Config file created successfully.") | ||||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
|  | @ -253,7 +209,7 @@ OLLAMA_API_BASE_URL = os.environ.get( | |||
| 
 | ||||
| OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | ||||
| INCLUDE_OLLAMA = os.environ.get("INCLUDE_OLLAMA_ENV", "false") | ||||
| K8S_FLAG = os.environ.get("K8S_FLAG", "") | ||||
| 
 | ||||
| 
 | ||||
| if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||
|     OLLAMA_BASE_URL = ( | ||||
|  | @ -271,9 +227,6 @@ if ENV == "prod": | |||
|         else:     | ||||
|             OLLAMA_BASE_URL = "http://host.docker.internal:11434" | ||||
| 
 | ||||
|     elif K8S_FLAG: | ||||
|         OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" | ||||
| 
 | ||||
| 
 | ||||
| OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") | ||||
| OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL | ||||
|  | @ -303,10 +256,8 @@ OPENAI_API_BASE_URLS = ( | |||
|     OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL | ||||
| ) | ||||
| 
 | ||||
| OPENAI_API_BASE_URLS = [ | ||||
|     url.strip() if url != "" else "https://api.openai.com/v1" | ||||
|     for url in OPENAI_API_BASE_URLS.split(";") | ||||
| ] | ||||
| OPENAI_API_BASE_URLS = [url.strip() for url in OPENAI_API_BASE_URLS.split(";")] | ||||
| 
 | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI | ||||
|  | @ -343,19 +294,13 @@ DEFAULT_PROMPT_SUGGESTIONS = ( | |||
| 
 | ||||
| 
 | ||||
| DEFAULT_USER_ROLE = os.getenv("DEFAULT_USER_ROLE", "pending") | ||||
| 
 | ||||
| USER_PERMISSIONS_CHAT_DELETION = ( | ||||
|     os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true" | ||||
| ) | ||||
| 
 | ||||
| USER_PERMISSIONS = {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}} | ||||
| USER_PERMISSIONS = {"chat": {"deletion": True}} | ||||
| 
 | ||||
| 
 | ||||
| MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", "False").lower() == "true" | ||||
| MODEL_FILTER_ENABLED = os.environ.get("MODEL_FILTER_ENABLED", False) | ||||
| MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "") | ||||
| MODEL_FILTER_LIST = [model.strip() for model in MODEL_FILTER_LIST.split(";")] | ||||
| 
 | ||||
| WEBHOOK_URL = os.environ.get("WEBHOOK_URL", "") | ||||
| 
 | ||||
| #################################### | ||||
| # WEBUI_VERSION | ||||
|  | @ -440,4 +385,3 @@ WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models" | |||
| #################################### | ||||
| 
 | ||||
| AUTOMATIC1111_BASE_URL = os.getenv("AUTOMATIC1111_BASE_URL", "") | ||||
| COMFYUI_BASE_URL = os.getenv("COMFYUI_BASE_URL", "") | ||||
|  |  | |||
|  | @ -5,13 +5,6 @@ class MESSAGES(str, Enum): | |||
|     DEFAULT = lambda msg="": f"{msg if msg else ''}" | ||||
| 
 | ||||
| 
 | ||||
| class WEBHOOK_MESSAGES(str, Enum): | ||||
|     DEFAULT = lambda msg="": f"{msg if msg else ''}" | ||||
|     USER_SIGNUP = lambda username="": ( | ||||
|         f"New user signed up: {username}" if username else "New user signed up" | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| class ERROR_MESSAGES(str, Enum): | ||||
|     def __str__(self) -> str: | ||||
|         return super().__str__() | ||||
|  | @ -53,12 +46,9 @@ class ERROR_MESSAGES(str, Enum): | |||
| 
 | ||||
|     PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." | ||||
|     INCORRECT_FORMAT = ( | ||||
|         lambda err="": f"Invalid format. Please use the correct format{err}" | ||||
|         lambda err="": f"Invalid format. Please use the correct format{err if err else ''}" | ||||
|     ) | ||||
|     RATE_LIMIT_EXCEEDED = "API rate limit exceeded" | ||||
| 
 | ||||
|     MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" | ||||
|     OPENAI_NOT_FOUND = lambda name="": f"OpenAI API was not found" | ||||
|     OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" | ||||
| 
 | ||||
|     EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." | ||||
|  |  | |||
|  | @ -1,22 +1,33 @@ | |||
| { | ||||
| 	"version": 0, | ||||
|     "version": "0.0.1", | ||||
|     "ui": { | ||||
| 		"default_locale": "en-US", | ||||
|         "prompt_suggestions": [ | ||||
|             { | ||||
| 				"title": ["Help me study", "vocabulary for a college entrance exam"], | ||||
|                 "title": [ | ||||
|                     "Help me study", | ||||
|                     "vocabulary for a college entrance exam" | ||||
|                 ], | ||||
|                 "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." | ||||
|             }, | ||||
|             { | ||||
| 				"title": ["Give me ideas", "for what to do with my kids' art"], | ||||
|                 "title": [ | ||||
|                     "Give me ideas", | ||||
|                     "for what to do with my kids' art" | ||||
|                 ], | ||||
|                 "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." | ||||
|             }, | ||||
|             { | ||||
| 				"title": ["Tell me a fun fact", "about the Roman Empire"], | ||||
|                 "title": [ | ||||
|                     "Tell me a fun fact", | ||||
|                     "about the Roman Empire" | ||||
|                 ], | ||||
|                 "content": "Tell me a random fun fact about the Roman Empire" | ||||
|             }, | ||||
|             { | ||||
| 				"title": ["Show me a code snippet", "of a website's sticky header"], | ||||
|                 "title": [ | ||||
|                     "Show me a code snippet", | ||||
|                     "of a website's sticky header" | ||||
|                 ], | ||||
|                 "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." | ||||
|             } | ||||
|         ] | ||||
|  |  | |||
|  | @ -4,7 +4,6 @@ import markdown | |||
| import time | ||||
| import os | ||||
| import sys | ||||
| import logging | ||||
| import requests | ||||
| 
 | ||||
| from fastapi import FastAPI, Request, Depends, status | ||||
|  | @ -32,7 +31,6 @@ from utils.utils import get_admin_user | |||
| from apps.rag.utils import rag_messages | ||||
| 
 | ||||
| from config import ( | ||||
|     CONFIG_DATA, | ||||
|     WEBUI_NAME, | ||||
|     ENV, | ||||
|     VERSION, | ||||
|  | @ -40,16 +38,9 @@ from config import ( | |||
|     FRONTEND_BUILD_DIR, | ||||
|     MODEL_FILTER_ENABLED, | ||||
|     MODEL_FILTER_LIST, | ||||
|     GLOBAL_LOG_LEVEL, | ||||
|     SRC_LOG_LEVELS, | ||||
|     WEBHOOK_URL, | ||||
| ) | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["MAIN"]) | ||||
| 
 | ||||
| 
 | ||||
| class SPAStaticFiles(StaticFiles): | ||||
|     async def get_response(self, path: str, scope): | ||||
|  | @ -67,9 +58,6 @@ app = FastAPI(docs_url="/docs" if ENV == "dev" else None, redoc_url=None) | |||
| app.state.MODEL_FILTER_ENABLED = MODEL_FILTER_ENABLED | ||||
| app.state.MODEL_FILTER_LIST = MODEL_FILTER_LIST | ||||
| 
 | ||||
| app.state.WEBHOOK_URL = WEBHOOK_URL | ||||
| 
 | ||||
| 
 | ||||
| origins = ["*"] | ||||
| 
 | ||||
| 
 | ||||
|  | @ -78,7 +66,7 @@ class RAGMiddleware(BaseHTTPMiddleware): | |||
|         if request.method == "POST" and ( | ||||
|             "/api/chat" in request.url.path or "/chat/completions" in request.url.path | ||||
|         ): | ||||
|             log.debug(f"request.url.path: {request.url.path}") | ||||
|             print(request.url.path) | ||||
| 
 | ||||
|             # Read the original request body | ||||
|             body = await request.body() | ||||
|  | @ -90,6 +78,7 @@ class RAGMiddleware(BaseHTTPMiddleware): | |||
|             # Example: Add a new key-value pair or modify existing ones | ||||
|             # data["modified"] = True  # Example modification | ||||
|             if "docs" in data: | ||||
| 
 | ||||
|                 data = {**data} | ||||
|                 data["messages"] = rag_messages( | ||||
|                     data["docs"], | ||||
|  | @ -100,7 +89,7 @@ class RAGMiddleware(BaseHTTPMiddleware): | |||
|                 ) | ||||
|                 del data["docs"] | ||||
| 
 | ||||
|                 log.debug(f"data['messages']: {data['messages']}") | ||||
|                 print(data["messages"]) | ||||
| 
 | ||||
|             modified_body_bytes = json.dumps(data).encode("utf-8") | ||||
| 
 | ||||
|  | @ -164,18 +153,11 @@ app.mount("/rag/api/v1", rag_app) | |||
| 
 | ||||
| @app.get("/api/config") | ||||
| async def get_app_config(): | ||||
|     # Checking and Handling the Absence of 'ui' in CONFIG_DATA | ||||
| 
 | ||||
|     default_locale = "en-US" | ||||
|     if "ui" in CONFIG_DATA: | ||||
|         default_locale = CONFIG_DATA["ui"].get("default_locale", "en-US") | ||||
| 
 | ||||
|     # The Rest of the Function Now Uses the Variables Defined Above | ||||
|     return { | ||||
|         "status": True, | ||||
|         "name": WEBUI_NAME, | ||||
|         "version": VERSION, | ||||
|         "default_locale": default_locale, | ||||
|         "images": images_app.state.ENABLED, | ||||
|         "default_models": webui_app.state.DEFAULT_MODELS, | ||||
|         "default_prompt_suggestions": webui_app.state.DEFAULT_PROMPT_SUGGESTIONS, | ||||
|  | @ -196,9 +178,10 @@ class ModelFilterConfigForm(BaseModel): | |||
| 
 | ||||
| 
 | ||||
| @app.post("/api/config/model/filter") | ||||
| async def update_model_filter_config( | ||||
| async def get_model_filter_config( | ||||
|     form_data: ModelFilterConfigForm, user=Depends(get_admin_user) | ||||
| ): | ||||
| 
 | ||||
|     app.state.MODEL_FILTER_ENABLED = form_data.enabled | ||||
|     app.state.MODEL_FILTER_LIST = form_data.models | ||||
| 
 | ||||
|  | @ -208,39 +191,15 @@ async def update_model_filter_config( | |||
|     openai_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED | ||||
|     openai_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST | ||||
| 
 | ||||
|     litellm_app.state.MODEL_FILTER_ENABLED = app.state.MODEL_FILTER_ENABLED | ||||
|     litellm_app.state.MODEL_FILTER_LIST = app.state.MODEL_FILTER_LIST | ||||
| 
 | ||||
|     return { | ||||
|         "enabled": app.state.MODEL_FILTER_ENABLED, | ||||
|         "models": app.state.MODEL_FILTER_LIST, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/api/webhook") | ||||
| async def get_webhook_url(user=Depends(get_admin_user)): | ||||
|     return { | ||||
|         "url": app.state.WEBHOOK_URL, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| class UrlForm(BaseModel): | ||||
|     url: str | ||||
| 
 | ||||
| 
 | ||||
| @app.post("/api/webhook") | ||||
| async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): | ||||
|     app.state.WEBHOOK_URL = form_data.url | ||||
| 
 | ||||
|     webui_app.state.WEBHOOK_URL = app.state.WEBHOOK_URL | ||||
| 
 | ||||
|     return { | ||||
|         "url": app.state.WEBHOOK_URL, | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/api/version") | ||||
| async def get_app_config(): | ||||
| 
 | ||||
|     return { | ||||
|         "version": VERSION, | ||||
|     } | ||||
|  | @ -248,7 +207,7 @@ async def get_app_config(): | |||
| 
 | ||||
| @app.get("/api/changelog") | ||||
| async def get_app_changelog(): | ||||
|     return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5} | ||||
|     return CHANGELOG | ||||
| 
 | ||||
| 
 | ||||
| @app.get("/api/version/updates") | ||||
|  |  | |||
|  | @ -45,4 +45,3 @@ PyJWT | |||
| pyjwt[crypto] | ||||
| 
 | ||||
| black | ||||
| langfuse | ||||
|  |  | |||
|  | @ -28,9 +28,4 @@ if [ "$INCLUDE_OLLAMA" = "true" ]; then | |||
|     ollama serve & | ||||
| fi | ||||
| 
 | ||||
| if [ "$USE_CUDA_DOCKER" = "true" ]; then | ||||
|     echo "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries." | ||||
|     export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib/python3.11/site-packages/torch/lib:/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib" | ||||
| fi | ||||
| 
 | ||||
| WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' | ||||
|  |  | |||
|  | @ -1,54 +0,0 @@ | |||
| import json | ||||
| import requests | ||||
| import logging | ||||
| 
 | ||||
| from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
| log.setLevel(SRC_LOG_LEVELS["WEBHOOK"]) | ||||
| 
 | ||||
| 
 | ||||
| def post_webhook(url: str, message: str, event_data: dict) -> bool: | ||||
|     try: | ||||
|         payload = {} | ||||
| 
 | ||||
|         # Slack and Google Chat Webhooks | ||||
|         if "https://hooks.slack.com" in url or "https://chat.googleapis.com" in url: | ||||
|             payload["text"] = message | ||||
|         # Discord Webhooks | ||||
|         elif "https://discord.com/api/webhooks" in url: | ||||
|             payload["content"] = message | ||||
|         # Microsoft Teams Webhooks | ||||
|         elif "webhook.office.com" in url: | ||||
|             action = event_data.get("action", "undefined") | ||||
|             facts = [ | ||||
|                 {"name": name, "value": value} | ||||
|                 for name, value in json.loads(event_data.get("user", {})).items() | ||||
|             ] | ||||
|             payload = { | ||||
|                 "@type": "MessageCard", | ||||
|                 "@context": "http://schema.org/extensions", | ||||
|                 "themeColor": "0076D7", | ||||
|                 "summary": message, | ||||
|                 "sections": [ | ||||
|                     { | ||||
|                         "activityTitle": message, | ||||
|                         "activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}", | ||||
|                         "activityImage": WEBUI_FAVICON_URL, | ||||
|                         "facts": facts, | ||||
|                         "markdown": True, | ||||
|                     } | ||||
|                 ], | ||||
|             } | ||||
|         # Default Payload | ||||
|         else: | ||||
|             payload = {**event_data} | ||||
| 
 | ||||
|         log.debug(f"payload: {payload}") | ||||
|         r = requests.post(url, json=payload) | ||||
|         r.raise_for_status() | ||||
|         log.debug(f"r.text: {r.text}") | ||||
|         return True | ||||
|     except Exception as e: | ||||
|         log.exception(e) | ||||
|         return False | ||||
							
								
								
									
										
											BIN
										
									
								
								demo.gif
									
										
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								demo.gif
									
										
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 5 MiB After Width: | Height: | Size: 6.1 MiB | 
|  | @ -50,18 +50,6 @@ We welcome pull requests. Before submitting one, please: | |||
| 
 | ||||
| Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI. | ||||
| 
 | ||||
| ### 🌐 Translations and Internationalization | ||||
| 
 | ||||
| Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project. | ||||
| 
 | ||||
| We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes][http://www.lingoes.net/en/translator/langcode.htm] to find the appropriate code for a specific language. | ||||
| 
 | ||||
| To add a new language: | ||||
| 
 | ||||
| - Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`. | ||||
| - Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object. | ||||
| - Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`. | ||||
| 
 | ||||
| ### 🤔 Questions & Feedback | ||||
| 
 | ||||
| Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help! | ||||
|  |  | |||
|  | @ -1,38 +0,0 @@ | |||
| // i18next-parser.config.ts
 | ||||
| import { getLanguages } from './src/lib/i18n/index.ts'; | ||||
| 
 | ||||
| const getLangCodes = async () => { | ||||
| 	const languages = await getLanguages(); | ||||
| 	return languages.map((l) => l.code); | ||||
| }; | ||||
| 
 | ||||
| export default { | ||||
| 	contextSeparator: '_', | ||||
| 	createOldCatalogs: false, | ||||
| 	defaultNamespace: 'translation', | ||||
| 	defaultValue: '', | ||||
| 	indentation: 2, | ||||
| 	keepRemoved: false, | ||||
| 	keySeparator: false, | ||||
| 	lexers: { | ||||
| 		svelte: ['JavascriptLexer'], | ||||
| 		js: ['JavascriptLexer'], | ||||
| 		ts: ['JavascriptLexer'], | ||||
| 
 | ||||
| 		default: ['JavascriptLexer'] | ||||
| 	}, | ||||
| 	lineEnding: 'auto', | ||||
| 	locales: await getLangCodes(), | ||||
| 	namespaceSeparator: false, | ||||
| 	output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json', | ||||
| 	pluralSeparator: '_', | ||||
| 	input: 'src/**/*.{js,svelte}', | ||||
| 	sort: true, | ||||
| 	verbose: true, | ||||
| 	failOnWarnings: false, | ||||
| 	failOnUpdate: false, | ||||
| 	customValueTemplate: null, | ||||
| 	resetDefaultValueLocale: null, | ||||
| 	i18nextOptions: null, | ||||
| 	yamlOptions: null | ||||
| }; | ||||
|  | @ -88,7 +88,7 @@ spec: | |||
|       resources: | ||||
|         requests: | ||||
|           storage: {{ .Values.ollama.persistence.size | quote }} | ||||
|       storageClassName: {{ .Values.ollama.persistence.storageClass }} | ||||
|       storageClass: {{ .Values.ollama.persistence.storageClass }} | ||||
|       {{- with .Values.ollama.persistence.selector }} | ||||
|       selector: | ||||
|         {{- toYaml . | nindent 8 }} | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ spec: | |||
|   resources: | ||||
|     requests: | ||||
|       storage: {{ .Values.webui.persistence.size }} | ||||
|   storageClassName: {{ .Values.webui.persistence.storageClass }} | ||||
|   storageClass: {{ .Values.webui.persistence.storageClass }} | ||||
|   {{- with .Values.webui.persistence.selector }} | ||||
|   selector: | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|  |  | |||
|  | @ -4,9 +4,6 @@ metadata: | |||
|   name: {{ include "open-webui.name" . }} | ||||
|   labels: | ||||
|     {{- include "open-webui.labels" . | nindent 4 }} | ||||
|     {{- with .Values.webui.service.labels }} | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|     {{- end }} | ||||
|   {{- with .Values.webui.service.annotations }} | ||||
|   annotations: | ||||
|     {{- toYaml . | nindent 4 }} | ||||
|  | @ -14,16 +11,14 @@ metadata: | |||
| spec: | ||||
|   selector: | ||||
|     {{- include "open-webui.selectorLabels" . | nindent 4 }} | ||||
|   type: {{ .Values.webui.service.type | default "ClusterIP" }} | ||||
| {{- with .Values.webui.service }} | ||||
|   type: {{ .type }} | ||||
|   ports: | ||||
|   - protocol: TCP | ||||
|     name: http | ||||
|     port: {{ .Values.webui.service.port }} | ||||
|     port: {{ .port }} | ||||
|     targetPort: http | ||||
|     {{- if .Values.webui.service.nodePort }} | ||||
|     nodePort: {{ .Values.webui.service.nodePort | int }} | ||||
|     {{- if .nodePort }} | ||||
|     nodePort: {{ .nodePort | int }} | ||||
|     {{- end }} | ||||
|   {{- if .Values.webui.service.loadBalancerClass }} | ||||
|   loadBalancerClass: {{ .Values.webui.service.loadBalancerClass | quote }} | ||||
|   {{- end }} | ||||
| 
 | ||||
| {{- end }} | ||||
|  |  | |||
|  | @ -70,5 +70,3 @@ webui: | |||
|     port: 80 | ||||
|     containerPort: 8080 | ||||
|     nodePort: "" | ||||
|     labels: {} | ||||
|     loadBalancerClass: ""  | ||||
|  |  | |||
|  | @ -35,4 +35,4 @@ spec: | |||
|       volumes: | ||||
|       - name: webui-volume | ||||
|         persistentVolumeClaim: | ||||
|           claimName: open-webui-pvc           | ||||
|           claimName: ollama-webui-pvc           | ||||
|  | @ -2,8 +2,8 @@ apiVersion: v1 | |||
| kind: PersistentVolumeClaim | ||||
| metadata: | ||||
|   labels: | ||||
|     app: open-webui | ||||
|   name: open-webui-pvc | ||||
|     app: ollama-webui | ||||
|   name: ollama-webui-pvc | ||||
|   namespace: open-webui | ||||
| spec: | ||||
|   accessModes: ["ReadWriteOnce"] | ||||
|  |  | |||
							
								
								
									
										6053
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										6053
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
										
									
									
									
								
							|  | @ -1,6 +1,6 @@ | |||
| { | ||||
| 	"name": "open-webui", | ||||
| 	"version": "0.1.116", | ||||
| 	"version": "0.1.111", | ||||
| 	"private": true, | ||||
| 	"scripts": { | ||||
| 		"dev": "vite dev --host", | ||||
|  | @ -13,8 +13,7 @@ | |||
| 		"lint:types": "npm run check", | ||||
| 		"lint:backend": "pylint backend/", | ||||
| 		"format": "prettier --plugin-search-dir --write '**/*.{js,ts,svelte,css,md,html,json}'", | ||||
| 		"format:backend": "black . --exclude \"/venv/\"", | ||||
| 		"i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write 'src/lib/i18n/**/*.{js,json}'" | ||||
| 		"format:backend": "yapf --recursive backend -p -i" | ||||
| 	}, | ||||
| 	"devDependencies": { | ||||
| 		"@sveltejs/adapter-auto": "^2.0.0", | ||||
|  | @ -28,7 +27,6 @@ | |||
| 		"eslint": "^8.56.0", | ||||
| 		"eslint-config-prettier": "^8.5.0", | ||||
| 		"eslint-plugin-svelte": "^2.30.0", | ||||
| 		"i18next-parser": "^8.13.0", | ||||
| 		"postcss": "^8.4.31", | ||||
| 		"prettier": "^2.8.0", | ||||
| 		"prettier-plugin-svelte": "^2.10.1", | ||||
|  | @ -44,13 +42,9 @@ | |||
| 	"dependencies": { | ||||
| 		"@sveltejs/adapter-node": "^1.3.1", | ||||
| 		"async": "^3.2.5", | ||||
| 		"bits-ui": "^0.19.7", | ||||
| 		"dayjs": "^1.11.10", | ||||
| 		"file-saver": "^2.0.5", | ||||
| 		"highlight.js": "^11.9.0", | ||||
| 		"i18next": "^23.10.0", | ||||
| 		"i18next-browser-languagedetector": "^7.2.0", | ||||
| 		"i18next-resources-to-backend": "^1.2.0", | ||||
| 		"idb": "^7.1.1", | ||||
| 		"js-sha256": "^0.10.1", | ||||
| 		"katex": "^0.16.9", | ||||
|  |  | |||
|  | @ -78,7 +78,3 @@ select { | |||
| 	/* for Chrome */ | ||||
| 	-webkit-appearance: none; | ||||
| } | ||||
| 
 | ||||
| .katex-mathml { | ||||
| 	display: none; | ||||
| } | ||||
|  |  | |||
							
								
								
									
										25
									
								
								src/app.html
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								src/app.html
									
										
									
									
									
								
							|  | @ -8,39 +8,18 @@ | |||
| 		<meta name="robots" content="noindex,nofollow" /> | ||||
| 		<script> | ||||
| 			// On page load or when changing themes, best to add inline in `head` to avoid FOUC | ||||
| 			(() => { | ||||
| 				if (localStorage?.theme && localStorage?.theme.includes('oled')) { | ||||
| 					document.documentElement.style.setProperty('--color-gray-900', '#000000'); | ||||
| 					document.documentElement.style.setProperty('--color-gray-950', '#000000'); | ||||
| 					document.documentElement.classList.add('dark'); | ||||
| 				} else if ( | ||||
| 			if ( | ||||
| 				localStorage.theme === 'light' || | ||||
| 				(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) | ||||
| 			) { | ||||
| 				document.documentElement.classList.add('light'); | ||||
| 				} else if (localStorage.theme && localStorage.theme !== 'system') { | ||||
| 			} else if (localStorage.theme) { | ||||
| 				localStorage.theme.split(' ').forEach((e) => { | ||||
| 					document.documentElement.classList.add(e); | ||||
| 				}); | ||||
| 				} else if (localStorage.theme && localStorage.theme === 'system') { | ||||
| 					systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; | ||||
| 					document.documentElement.classList.add(systemTheme ? 'dark' : 'light'); | ||||
| 			} else { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 			} | ||||
| 
 | ||||
| 				window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => { | ||||
| 					if (localStorage.theme === 'system') { | ||||
| 						if (e.matches) { | ||||
| 							document.documentElement.classList.add('dark'); | ||||
| 							document.documentElement.classList.remove('light'); | ||||
| 						} else { | ||||
| 							document.documentElement.classList.add('light'); | ||||
| 							document.documentElement.classList.remove('dark'); | ||||
| 						} | ||||
| 					} | ||||
| 				}); | ||||
| 			})(); | ||||
| 		</script> | ||||
| 
 | ||||
| 		%sveltekit.head% | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ export const updateOpenAIKey = async (token: string = '', key: string) => { | |||
| 	return res.OPENAI_API_KEY; | ||||
| }; | ||||
| 
 | ||||
| export const getImageGenerationEngineUrls = async (token: string = '') => { | ||||
| export const getAUTOMATIC1111Url = async (token: string = '') => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${IMAGES_API_BASE_URL}/url`, { | ||||
|  | @ -168,10 +168,10 @@ export const getImageGenerationEngineUrls = async (token: string = '') => { | |||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| 	return res.AUTOMATIC1111_BASE_URL; | ||||
| }; | ||||
| 
 | ||||
| export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => { | ||||
| export const updateAUTOMATIC1111Url = async (token: string = '', url: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, { | ||||
|  | @ -182,7 +182,7 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls: | |||
| 			...(token && { authorization: `Bearer ${token}` }) | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			...urls | ||||
| 			url: url | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
|  | @ -203,7 +203,7 @@ export const updateImageGenerationEngineUrls = async (token: string = '', urls: | |||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res; | ||||
| 	return res.AUTOMATIC1111_BASE_URL; | ||||
| }; | ||||
| 
 | ||||
| export const getImageSize = async (token: string = '') => { | ||||
|  |  | |||
|  | @ -139,60 +139,3 @@ export const updateModelFilterConfig = async ( | |||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const getWebhookUrl = async (token: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { | ||||
| 		method: 'GET', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		} | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			console.log(err); | ||||
| 			error = err; | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res.url; | ||||
| }; | ||||
| 
 | ||||
| export const updateWebhookUrl = async (token: string, url: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			url: url | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			console.log(err); | ||||
| 			error = err; | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res.url; | ||||
| }; | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ export const getLiteLLMModels = async (token: string = '') => { | |||
| 					id: model.id, | ||||
| 					name: model.name ?? model.id, | ||||
| 					external: true, | ||||
| 					source: 'LiteLLM' | ||||
| 					source: 'litellm' | ||||
| 				})) | ||||
| 				.sort((a, b) => { | ||||
| 					return a.name.localeCompare(b.name); | ||||
|  |  | |||
|  | @ -271,7 +271,7 @@ export const generateChatCompletion = async (token: string = '', body: object) = | |||
| 	return [res, controller]; | ||||
| }; | ||||
| 
 | ||||
| export const cancelOllamaRequest = async (token: string = '', requestId: string) => { | ||||
| export const cancelChatCompletion = async (token: string = '', requestId: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch(`${OLLAMA_API_BASE_URL}/cancel/${requestId}`, { | ||||
|  | @ -390,73 +390,6 @@ export const pullModel = async (token: string, tagName: string, urlIdx: string | | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const downloadModel = async ( | ||||
| 	token: string, | ||||
| 	download_url: string, | ||||
| 	urlIdx: string | null = null | ||||
| ) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const res = await fetch( | ||||
| 		`${OLLAMA_API_BASE_URL}/models/download${urlIdx !== null ? `/${urlIdx}` : ''}`, | ||||
| 		{ | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				Accept: 'application/json', | ||||
| 				'Content-Type': 'application/json', | ||||
| 				Authorization: `Bearer ${token}` | ||||
| 			}, | ||||
| 			body: JSON.stringify({ | ||||
| 				url: download_url | ||||
| 			}) | ||||
| 		} | ||||
| 	).catch((err) => { | ||||
| 		console.log(err); | ||||
| 		error = err; | ||||
| 
 | ||||
| 		if ('detail' in err) { | ||||
| 			error = err.detail; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	}); | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const uploadModel = async (token: string, file: File, urlIdx: string | null = null) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const formData = new FormData(); | ||||
| 	formData.append('file', file); | ||||
| 
 | ||||
| 	const res = await fetch( | ||||
| 		`${OLLAMA_API_BASE_URL}/models/upload${urlIdx !== null ? `/${urlIdx}` : ''}`, | ||||
| 		{ | ||||
| 			method: 'POST', | ||||
| 			headers: { | ||||
| 				Authorization: `Bearer ${token}` | ||||
| 			}, | ||||
| 			body: formData | ||||
| 		} | ||||
| 	).catch((err) => { | ||||
| 		console.log(err); | ||||
| 		error = err; | ||||
| 
 | ||||
| 		if ('detail' in err) { | ||||
| 			error = err.detail; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	}); | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| // export const pullModel = async (token: string, tagName: string) => {
 | ||||
| // 	return await fetch(`${OLLAMA_API_BASE_URL}/pull`, {
 | ||||
| // 		method: 'POST',
 | ||||
|  |  | |||
|  | @ -263,53 +263,3 @@ export const synthesizeOpenAISpeech = async ( | |||
| 
 | ||||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const generateTitle = async ( | ||||
| 	token: string = '', | ||||
| 	template: string, | ||||
| 	model: string, | ||||
| 	prompt: string, | ||||
| 	url: string = OPENAI_API_BASE_URL | ||||
| ) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	template = template.replace(/{{prompt}}/g, prompt); | ||||
| 
 | ||||
| 	console.log(template); | ||||
| 
 | ||||
| 	const res = await fetch(`${url}/chat/completions`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			Accept: 'application/json', | ||||
| 			'Content-Type': 'application/json', | ||||
| 			Authorization: `Bearer ${token}` | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			model: model, | ||||
| 			messages: [ | ||||
| 				{ | ||||
| 					role: 'user', | ||||
| 					content: template | ||||
| 				} | ||||
| 			], | ||||
| 			stream: false | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.json(); | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			console.log(err); | ||||
| 			if ('detail' in err) { | ||||
| 				error = err.detail; | ||||
| 			} | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	if (error) { | ||||
| 		throw error; | ||||
| 	} | ||||
| 
 | ||||
| 	return res?.choices[0]?.message?.content ?? 'New Chat'; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,13 +1,8 @@ | |||
| <script> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	const i18n = getContext('i18n'); | ||||
| </script> | ||||
| 
 | ||||
| <div class="  text-center text-6xl mb-3">📄</div> | ||||
| <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div> | ||||
| <div class="text-center dark:text-white text-2xl font-semibold z-50">Add Files</div> | ||||
| 
 | ||||
| <slot | ||||
| 	><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> | ||||
| 		{$i18n.t('Drop any files here to add to the conversation')} | ||||
| 		Drop any files here to add to the conversation | ||||
| 	</div> | ||||
| </slot> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <script lang="ts"> | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { Confetti } from 'svelte-confetti'; | ||||
| 
 | ||||
| 	import { WEBUI_NAME, config } from '$lib/stores'; | ||||
|  | @ -9,8 +9,6 @@ | |||
| 
 | ||||
| 	import Modal from './common/Modal.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	let changelog = null; | ||||
|  | @ -25,8 +23,7 @@ | |||
| 	<div class="px-5 py-4 dark:text-gray-300"> | ||||
| 		<div class="flex justify-between items-start"> | ||||
| 			<div class="text-xl font-bold"> | ||||
| 				{$i18n.t('What’s New in')} | ||||
| 				{$WEBUI_NAME} | ||||
| 				What’s New in {$WEBUI_NAME} | ||||
| 				<Confetti x={[-1, -0.25]} y={[0, 0.5]} /> | ||||
| 			</div> | ||||
| 			<button | ||||
|  | @ -48,7 +45,7 @@ | |||
| 			</button> | ||||
| 		</div> | ||||
| 		<div class="flex items-center mt-1"> | ||||
| 			<div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div> | ||||
| 			<div class="text-sm dark:text-gray-200">Release Notes</div> | ||||
| 			<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> | ||||
| 			<div class="text-sm dark:text-gray-200"> | ||||
| 				v{WEBUI_VERSION} | ||||
|  | @ -111,7 +108,7 @@ | |||
| 				}} | ||||
| 				class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			> | ||||
| 				<span class="relative">{$i18n.t("Okay, Let's Go!")}</span> | ||||
| 				<span class="relative">Okay, Let's Go!</span> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  |  | |||
|  | @ -2,12 +2,11 @@ | |||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import dayjs from 'dayjs'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	import { updateUserById } from '$lib/apis/users'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let show = false; | ||||
|  | @ -43,7 +42,7 @@ | |||
| <Modal size="sm" bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Edit User</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -85,8 +84,7 @@ | |||
| 							<div class=" self-center capitalize font-semibold">{selectedUser.name}</div> | ||||
| 
 | ||||
| 							<div class="text-xs text-gray-500"> | ||||
| 								{$i18n.t('Created at')} | ||||
| 								{dayjs(selectedUser.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))} | ||||
| 								Created at {dayjs(selectedUser.timestamp * 1000).format('MMMM DD, YYYY')} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | @ -95,7 +93,7 @@ | |||
| 
 | ||||
| 					<div class=" flex flex-col space-y-1.5"> | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">Email</div> | ||||
| 
 | ||||
| 							<div class="flex-1"> | ||||
| 								<input | ||||
|  | @ -110,7 +108,7 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">Name</div> | ||||
| 
 | ||||
| 							<div class="flex-1"> | ||||
| 								<input | ||||
|  | @ -124,7 +122,7 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">New Password</div> | ||||
| 
 | ||||
| 							<div class="flex-1"> | ||||
| 								<input | ||||
|  | @ -142,7 +140,7 @@ | |||
| 							class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 							type="submit" | ||||
| 						> | ||||
| 							{$i18n.t('Save')} | ||||
| 							Save | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</form> | ||||
|  |  | |||
|  | @ -1,8 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 	import { downloadDatabase } from '$lib/apis/utils'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 
 | ||||
|  | @ -19,10 +17,10 @@ | |||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div> | ||||
| 			<div class=" mb-2 text-sm font-medium">Database</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> --> | ||||
| 				<!-- <div class=" self-center text-xs font-medium">Allow Chat Deletion</div> --> | ||||
| 
 | ||||
| 				<button | ||||
| 					class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
|  | @ -48,7 +46,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div> | ||||
| 					<div class=" self-center text-sm font-medium">Download Database</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| <script lang="ts"> | ||||
| 	import { getWebhookUrl, updateWebhookUrl } from '$lib/apis'; | ||||
| 	import { | ||||
| 		getDefaultUserRole, | ||||
| 		getJWTExpiresDuration, | ||||
|  | @ -8,17 +7,13 @@ | |||
| 		updateDefaultUserRole, | ||||
| 		updateJWTExpiresDuration | ||||
| 	} from '$lib/apis/auths'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 	let signUpEnabled = true; | ||||
| 	let defaultUserRole = 'pending'; | ||||
| 	let JWTExpiresIn = ''; | ||||
| 
 | ||||
| 	let webhookUrl = ''; | ||||
| 
 | ||||
| 	const toggleSignUpEnabled = async () => { | ||||
| 		signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token); | ||||
| 	}; | ||||
|  | @ -31,32 +26,27 @@ | |||
| 		JWTExpiresIn = await updateJWTExpiresDuration(localStorage.token, duration); | ||||
| 	}; | ||||
| 
 | ||||
| 	const updateWebhookUrlHandler = async () => { | ||||
| 		webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl); | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		signUpEnabled = await getSignUpEnabledStatus(localStorage.token); | ||||
| 		defaultUserRole = await getDefaultUserRole(localStorage.token); | ||||
| 		JWTExpiresIn = await getJWTExpiresDuration(localStorage.token); | ||||
| 		webhookUrl = await getWebhookUrl(localStorage.token); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <form | ||||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={() => { | ||||
| 		// console.log('submit'); | ||||
| 		updateJWTExpiresDurationHandler(JWTExpiresIn); | ||||
| 		updateWebhookUrlHandler(); | ||||
| 		saveHandler(); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | ||||
| 			<div class=" mb-2 text-sm font-medium">General Settings</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Enable New Sign Ups</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -76,7 +66,7 @@ | |||
| 								d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Enabled')}</span> | ||||
| 						<span class="ml-2 self-center">Enabled</span> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -91,25 +81,25 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Disabled')}</span> | ||||
| 						<span class="ml-2 self-center">Disabled</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Default User Role</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={defaultUserRole} | ||||
| 						placeholder="Select a theme" | ||||
| 						on:change={(e) => { | ||||
| 							updateDefaultUserRoleHandler(e.target.value); | ||||
| 						}} | ||||
| 					> | ||||
| 						<option value="pending">{$i18n.t('pending')}</option> | ||||
| 						<option value="user">{$i18n.t('user')}</option> | ||||
| 						<option value="admin">{$i18n.t('admin')}</option> | ||||
| 						<option value="pending">Pending</option> | ||||
| 						<option value="user">User</option> | ||||
| 						<option value="admin">Admin</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | @ -118,24 +108,7 @@ | |||
| 
 | ||||
| 			<div class=" w-full justify-between"> | ||||
| 				<div class="flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="flex mt-2 space-x-2"> | ||||
| 					<input | ||||
| 						class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 						type="text" | ||||
| 						placeholder={`https://example.com/webhook`} | ||||
| 						bind:value={webhookUrl} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<hr class=" dark:border-gray-700 my-3" /> | ||||
| 
 | ||||
| 			<div class=" w-full justify-between"> | ||||
| 				<div class="flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div> | ||||
| 					<div class=" self-center text-xs font-medium">JWT Expiration</div> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="flex mt-2 space-x-2"> | ||||
|  | @ -148,9 +121,8 @@ | |||
| 				</div> | ||||
| 
 | ||||
| 				<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 					{$i18n.t('Valid time units:')} | ||||
| 					<span class=" text-gray-300 font-medium" | ||||
| 						>{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span | ||||
| 					Valid time units: <span class=" text-gray-300 font-medium" | ||||
| 						>'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.</span | ||||
| 					> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  | @ -162,7 +134,7 @@ | |||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -2,11 +2,8 @@ | |||
| 	import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis'; | ||||
| 	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; | ||||
| 	import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; | ||||
| 
 | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { models } from '$lib/stores'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 
 | ||||
|  | @ -42,10 +39,10 @@ | |||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div> | ||||
| 			<div class=" mb-2 text-sm font-medium">User Permissions</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Allow Chat Deletion</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -65,7 +62,7 @@ | |||
| 								d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Allow')}</span> | ||||
| 						<span class="ml-2 self-center">Allow</span> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -80,7 +77,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span> | ||||
| 						<span class="ml-2 self-center">Don't Allow</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | @ -92,21 +89,21 @@ | |||
| 			<div> | ||||
| 				<div class="mb-2"> | ||||
| 					<div class="flex justify-between items-center text-xs"> | ||||
| 						<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div> | ||||
| 						<div class=" text-sm font-medium">Manage Models</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class=" space-y-3"> | ||||
| 					<div> | ||||
| 						<div class="flex justify-between items-center text-xs"> | ||||
| 							<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div> | ||||
| 							<div class=" text-xs font-medium">Model Whitelisting</div> | ||||
| 
 | ||||
| 							<button | ||||
| 								class=" text-xs font-medium text-gray-500" | ||||
| 								type="button" | ||||
| 								on:click={() => { | ||||
| 									whitelistEnabled = !whitelistEnabled; | ||||
| 								}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button | ||||
| 								}}>{whitelistEnabled ? 'On' : 'Off'}</button | ||||
| 							> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | @ -122,7 +119,7 @@ | |||
| 												bind:value={modelId} | ||||
| 												placeholder="Select a model" | ||||
| 											> | ||||
| 												<option value="" disabled selected>{$i18n.t('Select a model')}</option> | ||||
| 												<option value="" disabled selected>Select a model</option> | ||||
| 												{#each $models.filter((model) => model.id) as model} | ||||
| 													<option value={model.id} class="bg-gray-100 dark:bg-gray-700" | ||||
| 														>{model.name}</option | ||||
|  | @ -177,8 +174,7 @@ | |||
| 
 | ||||
| 							<div class="flex justify-end items-center text-xs mt-1.5 text-right"> | ||||
| 								<div class=" text-xs font-medium"> | ||||
| 									{whitelistModels.length} | ||||
| 									{$i18n.t('Model(s) Whitelisted')} | ||||
| 									{whitelistModels.length} Model(s) Whitelisted | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
|  | @ -193,7 +189,7 @@ | |||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -1,13 +1,10 @@ | |||
| <script> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 	import Database from './Settings/Database.svelte'; | ||||
| 
 | ||||
| 	import General from './Settings/General.svelte'; | ||||
| 	import Users from './Settings/Users.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	let selectedTab = 'general'; | ||||
|  | @ -16,7 +13,7 @@ | |||
| <Modal bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Admin Settings')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Admin Settings</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -64,7 +61,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('General')}</div> | ||||
| 					<div class=" self-center">General</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
|  | @ -88,7 +85,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Users')}</div> | ||||
| 					<div class=" self-center">Users</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
|  | @ -116,7 +113,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Database')}</div> | ||||
| 					<div class=" self-center">Database</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div class="flex-1 md:min-h-[380px]"> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
| 	import { onMount, tick } from 'svelte'; | ||||
| 	import { settings } from '$lib/stores'; | ||||
| 	import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; | ||||
| 
 | ||||
|  | @ -14,8 +14,6 @@ | |||
| 	import { transcribeAudio } from '$lib/apis/audio'; | ||||
| 	import Tooltip from '../common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let submitPrompt: Function; | ||||
| 	export let stopResponse: Function; | ||||
| 
 | ||||
|  | @ -211,11 +209,11 @@ | |||
| 					// Event triggered when an error occurs | ||||
| 					speechRecognition.onerror = function (event) { | ||||
| 						console.log(event); | ||||
| 						toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error })); | ||||
| 						toast.error(`Speech recognition error: ${event.error}`); | ||||
| 						isRecording = false; | ||||
| 					}; | ||||
| 				} else { | ||||
| 					toast.error($i18n.t('SpeechRecognition API is not supported in this browser.')); | ||||
| 					toast.error('SpeechRecognition API is not supported in this browser.'); | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | @ -335,15 +333,12 @@ | |||
| 						uploadDoc(file); | ||||
| 					} else { | ||||
| 						toast.error( | ||||
| 							$i18n.t( | ||||
| 								`Unknown File Type '{{file_type}}', but accepting and treating as plain text`, | ||||
| 								{ file_type: file['type'] } | ||||
| 							) | ||||
| 							`Unknown File Type '${file['type']}', but accepting and treating as plain text` | ||||
| 						); | ||||
| 						uploadDoc(file); | ||||
| 					} | ||||
| 				} else { | ||||
| 					toast.error($i18n.t(`File not found.`)); | ||||
| 					toast.error(`File not found.`); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
|  | @ -482,16 +477,13 @@ | |||
| 								filesInputElement.value = ''; | ||||
| 							} else { | ||||
| 								toast.error( | ||||
| 									$i18n.t( | ||||
| 										`Unknown File Type '{{file_type}}', but accepting and treating as plain text`, | ||||
| 										{ file_type: file['type'] } | ||||
| 									) | ||||
| 									`Unknown File Type '${file['type']}', but accepting and treating as plain text` | ||||
| 								); | ||||
| 								uploadDoc(file); | ||||
| 								filesInputElement.value = ''; | ||||
| 							} | ||||
| 						} else { | ||||
| 							toast.error($i18n.t(`File not found.`)); | ||||
| 							toast.error(`File not found.`); | ||||
| 						} | ||||
| 					}} | ||||
| 				/> | ||||
|  | @ -578,7 +570,7 @@ | |||
| 													{file.name} | ||||
| 												</div> | ||||
| 
 | ||||
| 												<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div> | ||||
| 												<div class=" text-gray-500 text-sm">Document</div> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									{:else if file.type === 'collection'} | ||||
|  | @ -606,7 +598,7 @@ | |||
| 													{file?.title ?? `#${file.name}`} | ||||
| 												</div> | ||||
| 
 | ||||
| 												<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div> | ||||
| 												<div class=" text-gray-500 text-sm">Collection</div> | ||||
| 											</div> | ||||
| 										</div> | ||||
| 									{/if} | ||||
|  | @ -640,7 +632,7 @@ | |||
| 					<div class=" flex"> | ||||
| 						{#if fileUploadEnabled} | ||||
| 							<div class=" self-end mb-2 ml-1"> | ||||
| 								<Tooltip content={$i18n.t('Upload files')}> | ||||
| 								<Tooltip content="Upload files"> | ||||
| 									<button | ||||
| 										class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5" | ||||
| 										type="button" | ||||
|  | @ -672,8 +664,8 @@ | |||
| 							placeholder={chatInputPlaceholder !== '' | ||||
| 								? chatInputPlaceholder | ||||
| 								: isRecording | ||||
| 								? $i18n.t('Listening...') | ||||
| 								: $i18n.t('Send a Message')} | ||||
| 								? 'Listening...' | ||||
| 								: 'Send a message'} | ||||
| 							bind:value={prompt} | ||||
| 							on:keypress={(e) => { | ||||
| 								if (e.keyCode == 13 && !e.shiftKey) { | ||||
|  | @ -812,7 +804,7 @@ | |||
| 
 | ||||
| 						<div class="self-end mb-2 flex space-x-1 mr-1"> | ||||
| 							{#if messages.length == 0 || messages.at(-1).done == true} | ||||
| 								<Tooltip content={$i18n.t('Record voice')}> | ||||
| 								<Tooltip content="Record voice"> | ||||
| 									{#if speechRecognitionEnabled} | ||||
| 										<button | ||||
| 											id="voice-input-button" | ||||
|  | @ -881,7 +873,7 @@ | |||
| 									{/if} | ||||
| 								</Tooltip> | ||||
| 
 | ||||
| 								<Tooltip content={$i18n.t('Send message')}> | ||||
| 								<Tooltip content="Send message"> | ||||
| 									<button | ||||
| 										class="{prompt !== '' | ||||
| 											? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' | ||||
|  | @ -927,7 +919,7 @@ | |||
| 				</form> | ||||
| 
 | ||||
| 				<div class="mt-1.5 text-xs text-gray-500 text-center"> | ||||
| 					{$i18n.t('LLMs can make mistakes. Verify important information.')} | ||||
| 					LLMs can make mistakes. Verify important information. | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -3,11 +3,9 @@ | |||
| 
 | ||||
| 	import { documents } from '$lib/stores'; | ||||
| 	import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils'; | ||||
| 	import { tick, getContext } from 'svelte'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let prompt = ''; | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | @ -119,7 +117,7 @@ | |||
| 									{doc?.title ?? `#${doc.name}`} | ||||
| 								</div> | ||||
| 
 | ||||
| 								<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div> | ||||
| 								<div class=" text-xs text-gray-600 line-clamp-1">Collection</div> | ||||
| 							{:else} | ||||
| 								<div class=" font-medium text-black line-clamp-1"> | ||||
| 									#{doc.name} ({doc.filename}) | ||||
|  | @ -142,9 +140,7 @@ | |||
| 									confirmSelectWeb(url); | ||||
| 								} else { | ||||
| 									toast.error( | ||||
| 										$i18n.t( | ||||
| 										'Oops! Looks like the URL is invalid. Please double-check and try again.' | ||||
| 										) | ||||
| 									); | ||||
| 								} | ||||
| 							}} | ||||
|  | @ -153,7 +149,7 @@ | |||
| 								{prompt.split(' ')?.at(0)?.substring(1)} | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div> | ||||
| 							<div class=" text-xs text-gray-600 line-clamp-1">Web</div> | ||||
| 						</button> | ||||
| 					{/if} | ||||
| 				</div> | ||||
|  |  | |||
|  | @ -2,11 +2,9 @@ | |||
| 	import { generatePrompt } from '$lib/apis/ollama'; | ||||
| 	import { models } from '$lib/stores'; | ||||
| 	import { splitStream } from '$lib/utils'; | ||||
| 	import { tick, getContext } from 'svelte'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let prompt = ''; | ||||
| 	export let user = null; | ||||
| 
 | ||||
|  | @ -43,7 +41,7 @@ | |||
| 		user = JSON.parse(JSON.stringify(model.name)); | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name }); | ||||
| 		chatInputPlaceholder = `'${model.name}' is thinking...`; | ||||
| 
 | ||||
| 		const chatInputElement = document.getElementById('chat-textarea'); | ||||
| 
 | ||||
|  | @ -115,9 +113,7 @@ | |||
| 					toast.error(error.error); | ||||
| 				} | ||||
| 			} else { | ||||
| 				toast.error( | ||||
| 					$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' }) | ||||
| 				); | ||||
| 				toast.error(`Uh-oh! There was an issue connecting to Ollama.`); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,9 @@ | |||
| <script lang="ts"> | ||||
| 	import { prompts } from '$lib/stores'; | ||||
| 	import { findWordIndices } from '$lib/utils'; | ||||
| 	import { tick, getContext } from 'svelte'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let prompt = ''; | ||||
| 	let selectedCommandIdx = 0; | ||||
| 	let filteredPromptCommands = []; | ||||
|  | @ -31,7 +29,7 @@ | |||
| 
 | ||||
| 		if (command.content.includes('{{CLIPBOARD}}')) { | ||||
| 			const clipboardText = await navigator.clipboard.readText().catch((err) => { | ||||
| 				toast.error($i18n.t('Failed to read clipboard contents')); | ||||
| 				toast.error('Failed to read clipboard contents'); | ||||
| 				return '{{CLIPBOARD}}'; | ||||
| 			}); | ||||
| 
 | ||||
|  | @ -115,9 +113,8 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="line-clamp-1"> | ||||
| 						{$i18n.t( | ||||
| 							'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.' | ||||
| 						)} | ||||
| 						Tip: Update multiple variable slots consecutively by pressing the tab key in the chat | ||||
| 						input after each replacement. | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
| 	import { v4 as uuidv4 } from 'uuid'; | ||||
| 
 | ||||
| 	import { chats, config, modelfiles, settings, user } from '$lib/stores'; | ||||
| 	import { tick, getContext } from 'svelte'; | ||||
| 	import { tick } from 'svelte'; | ||||
| 
 | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { getChatList, updateChatById } from '$lib/apis/chats'; | ||||
|  | @ -13,8 +13,6 @@ | |||
| 	import Spinner from '../common/Spinner.svelte'; | ||||
| 	import { imageGenerations } from '$lib/apis/images'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let chatId = ''; | ||||
| 	export let sendPrompt: Function; | ||||
| 	export let continueGeneration: Function; | ||||
|  | @ -69,7 +67,7 @@ | |||
| 		navigator.clipboard.writeText(text).then( | ||||
| 			function () { | ||||
| 				console.log('Async: Copying to clipboard was successful!'); | ||||
| 				toast.success($i18n.t('Copying to clipboard was successful!')); | ||||
| 				toast.success('Copying to clipboard was successful!'); | ||||
| 			}, | ||||
| 			function (err) { | ||||
| 				console.error('Async: Could not copy text: ', err); | ||||
|  |  | |||
|  | @ -1,9 +1,7 @@ | |||
| <script lang="ts"> | ||||
| 	import { WEBUI_BASE_URL } from '$lib/constants'; | ||||
| 	import { user } from '$lib/stores'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	export let models = []; | ||||
| 	export let modelfiles = []; | ||||
|  | @ -33,7 +31,7 @@ | |||
| 							<img | ||||
| 								src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`} | ||||
| 								alt="modelfile" | ||||
| 								class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none" | ||||
| 								class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none" | ||||
| 								draggable="false" | ||||
| 							/> | ||||
| 						{:else} | ||||
|  | @ -41,7 +39,7 @@ | |||
| 								src={models.length === 1 | ||||
| 									? `${WEBUI_BASE_URL}/static/favicon.png` | ||||
| 									: `${WEBUI_BASE_URL}/static/favicon.png`} | ||||
| 								class=" size-12 rounded-full border-[1px] border-gray-200 dark:border-none" | ||||
| 								class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none" | ||||
| 								alt="logo" | ||||
| 								draggable="false" | ||||
| 							/> | ||||
|  | @ -66,9 +64,9 @@ | |||
| 					</div> | ||||
| 				{/if} | ||||
| 			{:else} | ||||
| 				<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div> | ||||
| 				<div class=" line-clamp-1">Hello, {$user.name}</div> | ||||
| 
 | ||||
| 				<div>{$i18n.t('How can I help you today?')}</div> | ||||
| 				<div>How can I help you today?</div> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	</div> | ||||
|  |  | |||
|  | @ -8,9 +8,7 @@ | |||
| 
 | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount, tick } from 'svelte'; | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
|  | @ -318,7 +316,7 @@ | |||
| 
 | ||||
| 				{#if message.timestamp} | ||||
| 					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> | ||||
| 						{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))} | ||||
| 						{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} | ||||
| 					</span> | ||||
| 				{/if} | ||||
| 			</Name> | ||||
|  | @ -362,7 +360,7 @@ | |||
| 											editMessageConfirmHandler(); | ||||
| 										}} | ||||
| 									> | ||||
| 										{$i18n.t('Save')} | ||||
| 										Save | ||||
| 									</button> | ||||
| 
 | ||||
| 									<button | ||||
|  | @ -371,7 +369,7 @@ | |||
| 											cancelEditMessage(); | ||||
| 										}} | ||||
| 									> | ||||
| 										{$i18n.t('Cancel')} | ||||
| 										Cancel | ||||
| 									</button> | ||||
| 								</div> | ||||
| 							</div> | ||||
|  | @ -422,7 +420,7 @@ | |||
| 										class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500" | ||||
| 									> | ||||
| 										{#if siblings.length > 1} | ||||
| 											<div class="flex self-center min-w-fit -mt-1"> | ||||
| 											<div class="flex self-center min-w-fit"> | ||||
| 												<button | ||||
| 													class="self-center dark:hover:text-white hover:text-black transition" | ||||
| 													on:click={() => { | ||||
|  |  | |||
|  | @ -1,14 +1,12 @@ | |||
| <script lang="ts"> | ||||
| 	import dayjs from 'dayjs'; | ||||
| 
 | ||||
| 	import { tick, createEventDispatcher, getContext } from 'svelte'; | ||||
| 	import { tick, createEventDispatcher } from 'svelte'; | ||||
| 	import Name from './Name.svelte'; | ||||
| 	import ProfileImage from './ProfileImage.svelte'; | ||||
| 	import { modelfiles, settings } from '$lib/stores'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let user; | ||||
|  | @ -67,18 +65,17 @@ | |||
| 					{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)} | ||||
| 						{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title} | ||||
| 					{:else} | ||||
| 						{$i18n.t('You')} | ||||
| 						<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span> | ||||
| 						You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span> | ||||
| 					{/if} | ||||
| 				{:else if $settings.showUsername} | ||||
| 					{user.name} | ||||
| 				{:else} | ||||
| 					{$i18n.t('You')} | ||||
| 					You | ||||
| 				{/if} | ||||
| 
 | ||||
| 				{#if message.timestamp} | ||||
| 					<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium"> | ||||
| 						{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))} | ||||
| 						{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')} | ||||
| 					</span> | ||||
| 				{/if} | ||||
| 			</Name> | ||||
|  | @ -126,7 +123,7 @@ | |||
| 											{file.name} | ||||
| 										</div> | ||||
| 
 | ||||
| 										<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div> | ||||
| 										<div class=" text-gray-500 text-sm">Document</div> | ||||
| 									</div> | ||||
| 								</button> | ||||
| 							{:else if file.type === 'collection'} | ||||
|  | @ -155,7 +152,7 @@ | |||
| 											{file?.title ?? `#${file.name}`} | ||||
| 										</div> | ||||
| 
 | ||||
| 										<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div> | ||||
| 										<div class=" text-gray-500 text-sm">Collection</div> | ||||
| 									</div> | ||||
| 								</button> | ||||
| 							{/if} | ||||
|  | @ -184,7 +181,7 @@ | |||
| 								editMessageConfirmHandler(); | ||||
| 							}} | ||||
| 						> | ||||
| 							{$i18n.t('Save & Submit')} | ||||
| 							Save & Submit | ||||
| 						</button> | ||||
| 
 | ||||
| 						<button | ||||
|  | @ -193,7 +190,7 @@ | |||
| 								cancelEditMessage(); | ||||
| 							}} | ||||
| 						> | ||||
| 							{$i18n.t('Cancel')} | ||||
| 							Cancel | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  | @ -203,7 +200,7 @@ | |||
| 
 | ||||
| 					<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500"> | ||||
| 						{#if siblings.length > 1} | ||||
| 							<div class="flex self-center -mt-1"> | ||||
| 							<div class="flex self-center"> | ||||
| 								<button | ||||
| 									class="self-center dark:hover:text-white hover:text-black transition" | ||||
| 									on:click={() => { | ||||
|  |  | |||
|  | @ -1,14 +1,8 @@ | |||
| <script lang="ts"> | ||||
| 	import { Collapsible } from 'bits-ui'; | ||||
| 
 | ||||
| 	import { setDefaultModels } from '$lib/apis/configs'; | ||||
| 	import { models, showSettings, settings, user } from '$lib/stores'; | ||||
| 	import { onMount, tick, getContext } from 'svelte'; | ||||
| 	import { onMount, tick } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import Selector from './ModelSelector/Selector.svelte'; | ||||
| 	import Tooltip from '../common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let selectedModels = ['']; | ||||
| 	export let disabled = false; | ||||
|  | @ -16,7 +10,7 @@ | |||
| 	const saveDefaultModel = async () => { | ||||
| 		const hasEmptyModel = selectedModels.filter((it) => it === ''); | ||||
| 		if (hasEmptyModel.length) { | ||||
| 			toast.error($i18n.t('Choose a model before saving...')); | ||||
| 			toast.error('Choose a model before saving...'); | ||||
| 			return; | ||||
| 		} | ||||
| 		settings.set({ ...$settings, models: selectedModels }); | ||||
|  | @ -26,7 +20,7 @@ | |||
| 			console.log('setting default models globally'); | ||||
| 			await setDefaultModels(localStorage.token, selectedModels.join(',')); | ||||
| 		} | ||||
| 		toast.success($i18n.t('Default model updated')); | ||||
| 		toast.success('Default model updated'); | ||||
| 	}; | ||||
| 
 | ||||
| 	$: if (selectedModels.length > 0 && $models.length > 0) { | ||||
|  | @ -36,30 +30,34 @@ | |||
| 	} | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col mt-0.5 w-full"> | ||||
| <div class="flex flex-col my-2"> | ||||
| 	{#each selectedModels as selectedModel, selectedModelIdx} | ||||
| 		<div class="flex w-full"> | ||||
| 			<div class="overflow-hidden w-full"> | ||||
| 				<div class="mr-0.5 max-w-full"> | ||||
| 					<Selector | ||||
| 						placeholder={$i18n.t('Select a model')} | ||||
| 						items={$models | ||||
| 							.filter((model) => model.name !== 'hr') | ||||
| 							.map((model) => ({ | ||||
| 								value: model.id, | ||||
| 								label: model.name, | ||||
| 								info: model | ||||
| 							}))} | ||||
| 		<div class="flex"> | ||||
| 			<select | ||||
| 				id="models" | ||||
| 				class="outline-none bg-transparent text-lg font-semibold rounded-lg block w-full placeholder-gray-400" | ||||
| 				bind:value={selectedModel} | ||||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 				{disabled} | ||||
| 			> | ||||
| 				<option class=" text-gray-700" value="" selected disabled>Select a model</option> | ||||
| 
 | ||||
| 				{#each $models as model} | ||||
| 					{#if model.name === 'hr'} | ||||
| 						<hr /> | ||||
| 					{:else} | ||||
| 						<option value={model.id} class="text-gray-700 text-lg" | ||||
| 							>{model.name + | ||||
| 								`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option | ||||
| 						> | ||||
| 					{/if} | ||||
| 				{/each} | ||||
| 			</select> | ||||
| 
 | ||||
| 			{#if selectedModelIdx === 0} | ||||
| 				<div class="  self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600"> | ||||
| 					<Tooltip content="Add Model"> | ||||
| 				<button | ||||
| 							class=" " | ||||
| 					class="  self-center {selectedModelIdx === 0 | ||||
| 						? 'mr-3' | ||||
| 						: 'mr-7'} disabled:text-gray-600 disabled:hover:text-gray-600" | ||||
| 					{disabled} | ||||
| 					on:click={() => { | ||||
| 						selectedModels = [...selectedModels, '']; | ||||
|  | @ -76,12 +74,12 @@ | |||
| 						<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" /> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 					</Tooltip> | ||||
| 				</div> | ||||
| 			{:else} | ||||
| 				<div class="  self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2"> | ||||
| 					<Tooltip content="Remove Model"> | ||||
| 				<button | ||||
| 					class="  self-center disabled:text-gray-600 disabled:hover:text-gray-600 {selectedModelIdx === | ||||
| 					0 | ||||
| 						? 'mr-3' | ||||
| 						: 'mr-7'}" | ||||
| 					{disabled} | ||||
| 					on:click={() => { | ||||
| 						selectedModels.splice(selectedModelIdx, 1); | ||||
|  | @ -99,13 +97,41 @@ | |||
| 						<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" /> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 					</Tooltip> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 
 | ||||
| 			{#if selectedModelIdx === 0} | ||||
| 				<button | ||||
| 					class=" self-center dark:hover:text-gray-300" | ||||
| 					id="open-settings-button" | ||||
| 					on:click={async () => { | ||||
| 						await showSettings.set(!$showSettings); | ||||
| 					}} | ||||
| 				> | ||||
| 					<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="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" | ||||
| 						/> | ||||
| 						<path | ||||
| 							stroke-linecap="round" | ||||
| 							stroke-linejoin="round" | ||||
| 							d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			{/if} | ||||
| 		</div> | ||||
| 	{/each} | ||||
| </div> | ||||
| 
 | ||||
| <div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500"> | ||||
| 	<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> | ||||
| <div class="text-left mt-1.5 text-xs text-gray-500"> | ||||
| 	<button on:click={saveDefaultModel}> Set as default</button> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,389 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { Select } from 'bits-ui'; | ||||
| 
 | ||||
| 	import { flyAndScale } from '$lib/utils/transitions'; | ||||
| 	import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; | ||||
| 
 | ||||
| 	import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; | ||||
| 	import Check from '$lib/components/icons/Check.svelte'; | ||||
| 	import Search from '$lib/components/icons/Search.svelte'; | ||||
| 
 | ||||
| 	import { cancelOllamaRequest, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama'; | ||||
| 
 | ||||
| 	import { user, MODEL_DOWNLOAD_POOL, models } from '$lib/stores'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { capitalizeFirstLetter, getModels, splitStream } from '$lib/utils'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let value = ''; | ||||
| 	export let placeholder = 'Select a model'; | ||||
| 	export let searchEnabled = true; | ||||
| 	export let searchPlaceholder = 'Search a model'; | ||||
| 
 | ||||
| 	export let items = [{ value: 'mango', label: 'Mango' }]; | ||||
| 
 | ||||
| 	let searchValue = ''; | ||||
| 	let ollamaVersion = null; | ||||
| 
 | ||||
| 	$: filteredItems = searchValue | ||||
| 		? items.filter((item) => item.value.includes(searchValue.toLowerCase())) | ||||
| 		: items; | ||||
| 
 | ||||
| 	const pullModelHandler = async () => { | ||||
| 		const sanitizedModelTag = searchValue.trim(); | ||||
| 
 | ||||
| 		console.log($MODEL_DOWNLOAD_POOL); | ||||
| 		if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) { | ||||
| 			toast.error( | ||||
| 				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, { | ||||
| 					modelTag: sanitizedModelTag | ||||
| 				}) | ||||
| 			); | ||||
| 			return; | ||||
| 		} | ||||
| 		if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) { | ||||
| 			toast.error( | ||||
| 				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.') | ||||
| 			); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		const res = await pullModel(localStorage.token, sanitizedModelTag, '0').catch((error) => { | ||||
| 			toast.error(error); | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 			const reader = res.body | ||||
| 				.pipeThrough(new TextDecoderStream()) | ||||
| 				.pipeThrough(splitStream('\n')) | ||||
| 				.getReader(); | ||||
| 
 | ||||
| 			while (true) { | ||||
| 				try { | ||||
| 					const { value, done } = await reader.read(); | ||||
| 					if (done) break; | ||||
| 
 | ||||
| 					let lines = value.split('\n'); | ||||
| 
 | ||||
| 					for (const line of lines) { | ||||
| 						if (line !== '') { | ||||
| 							let data = JSON.parse(line); | ||||
| 							console.log(data); | ||||
| 							if (data.error) { | ||||
| 								throw data.error; | ||||
| 							} | ||||
| 							if (data.detail) { | ||||
| 								throw data.detail; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.id) { | ||||
| 								MODEL_DOWNLOAD_POOL.set({ | ||||
| 									...$MODEL_DOWNLOAD_POOL, | ||||
| 									[sanitizedModelTag]: { | ||||
| 										...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], | ||||
| 										requestId: data.id, | ||||
| 										reader, | ||||
| 										done: false | ||||
| 									} | ||||
| 								}); | ||||
| 								console.log(data); | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.status) { | ||||
| 								if (data.digest) { | ||||
| 									let downloadProgress = 0; | ||||
| 									if (data.completed) { | ||||
| 										downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; | ||||
| 									} else { | ||||
| 										downloadProgress = 100; | ||||
| 									} | ||||
| 
 | ||||
| 									MODEL_DOWNLOAD_POOL.set({ | ||||
| 										...$MODEL_DOWNLOAD_POOL, | ||||
| 										[sanitizedModelTag]: { | ||||
| 											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], | ||||
| 											pullProgress: downloadProgress, | ||||
| 											digest: data.digest | ||||
| 										} | ||||
| 									}); | ||||
| 								} else { | ||||
| 									toast.success(data.status); | ||||
| 
 | ||||
| 									MODEL_DOWNLOAD_POOL.set({ | ||||
| 										...$MODEL_DOWNLOAD_POOL, | ||||
| 										[sanitizedModelTag]: { | ||||
| 											...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], | ||||
| 											done: data.status === 'success' | ||||
| 										} | ||||
| 									}); | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				} catch (error) { | ||||
| 					console.log(error); | ||||
| 					if (typeof error !== 'string') { | ||||
| 						error = error.message; | ||||
| 					} | ||||
| 
 | ||||
| 					toast.error(error); | ||||
| 					// opts.callback({ success: false, error, modelName: opts.modelName }); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) { | ||||
| 				toast.success( | ||||
| 					$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { | ||||
| 						modelName: sanitizedModelTag | ||||
| 					}) | ||||
| 				); | ||||
| 
 | ||||
| 				models.set(await getModels(localStorage.token)); | ||||
| 			} else { | ||||
| 				toast.error('Download canceled'); | ||||
| 			} | ||||
| 
 | ||||
| 			delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag]; | ||||
| 
 | ||||
| 			MODEL_DOWNLOAD_POOL.set({ | ||||
| 				...$MODEL_DOWNLOAD_POOL | ||||
| 			}); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); | ||||
| 	}); | ||||
| 
 | ||||
| 	const cancelModelPullHandler = async (model: string) => { | ||||
| 		const { reader, requestId } = $MODEL_DOWNLOAD_POOL[model]; | ||||
| 		if (reader) { | ||||
| 			await reader.cancel(); | ||||
| 
 | ||||
| 			await cancelOllamaRequest(localStorage.token, requestId); | ||||
| 			delete $MODEL_DOWNLOAD_POOL[model]; | ||||
| 			MODEL_DOWNLOAD_POOL.set({ | ||||
| 				...$MODEL_DOWNLOAD_POOL | ||||
| 			}); | ||||
| 			await deleteModel(localStorage.token, model); | ||||
| 			toast.success(`${model} download has been canceled`); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <Select.Root | ||||
| 	{items} | ||||
| 	onOpenChange={async () => { | ||||
| 		searchValue = ''; | ||||
| 		window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); | ||||
| 	}} | ||||
| 	selected={items.find((item) => item.value === value) ?? ''} | ||||
| 	onSelectedChange={(selectedItem) => { | ||||
| 		value = selectedItem.value; | ||||
| 	}} | ||||
| > | ||||
| 	<Select.Trigger class="relative w-full" aria-label={placeholder}> | ||||
| 		<Select.Value | ||||
| 			class="flex text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400  focus:outline-none" | ||||
| 			{placeholder} | ||||
| 		/> | ||||
| 		<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> | ||||
| 	</Select.Trigger> | ||||
| 	<Select.Content | ||||
| 		class=" z-40 w-full rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none" | ||||
| 		transition={flyAndScale} | ||||
| 		sideOffset={4} | ||||
| 	> | ||||
| 		<slot> | ||||
| 			{#if searchEnabled} | ||||
| 				<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3"> | ||||
| 					<Search className="size-4" strokeWidth="2.5" /> | ||||
| 
 | ||||
| 					<input | ||||
| 						id="model-search-input" | ||||
| 						bind:value={searchValue} | ||||
| 						class="w-full text-sm bg-transparent outline-none" | ||||
| 						placeholder={searchPlaceholder} | ||||
| 					/> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<hr class="border-gray-100 dark:border-gray-800" /> | ||||
| 			{/if} | ||||
| 
 | ||||
| 			<div class="px-3 my-2 max-h-72 overflow-y-auto"> | ||||
| 				{#each filteredItems as item} | ||||
| 					<Select.Item | ||||
| 						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm  text-gray-700 dark:text-gray-100  outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted" | ||||
| 						value={item.value} | ||||
| 						label={item.label} | ||||
| 					> | ||||
| 						<div class="flex items-center gap-2"> | ||||
| 							<div class="line-clamp-1"> | ||||
| 								{item.label} | ||||
| 
 | ||||
| 								<span class=" text-xs font-medium text-gray-600 dark:text-gray-400" | ||||
| 									>{item.info?.details?.parameter_size ?? ''}</span | ||||
| 								> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<!-- {JSON.stringify(item.info)} --> | ||||
| 
 | ||||
| 							{#if item.info.external} | ||||
| 								<Tooltip content={item.info?.source ?? 'External'}> | ||||
| 									<div class=" mr-2"> | ||||
| 										<svg | ||||
| 											xmlns="http://www.w3.org/2000/svg" | ||||
| 											viewBox="0 0 16 16" | ||||
| 											fill="currentColor" | ||||
| 											class="size-3" | ||||
| 										> | ||||
| 											<path | ||||
| 												fill-rule="evenodd" | ||||
| 												d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" | ||||
| 												clip-rule="evenodd" | ||||
| 											/> | ||||
| 											<path | ||||
| 												fill-rule="evenodd" | ||||
| 												d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" | ||||
| 												clip-rule="evenodd" | ||||
| 											/> | ||||
| 										</svg> | ||||
| 									</div> | ||||
| 								</Tooltip> | ||||
| 							{:else} | ||||
| 								<Tooltip | ||||
| 									content={`${ | ||||
| 										item.info?.details?.quantization_level | ||||
| 											? item.info?.details?.quantization_level + ' ' | ||||
| 											: '' | ||||
| 									}${item.info.size ? `(${(item.info.size / 1024 ** 3).toFixed(1)}GB)` : ''}`} | ||||
| 								> | ||||
| 									<div class=" mr-2"> | ||||
| 										<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.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" | ||||
| 											/> | ||||
| 										</svg> | ||||
| 									</div> | ||||
| 								</Tooltip> | ||||
| 							{/if} | ||||
| 						</div> | ||||
| 
 | ||||
| 						{#if value === item.value} | ||||
| 							<div class="ml-auto"> | ||||
| 								<Check /> | ||||
| 							</div> | ||||
| 						{/if} | ||||
| 					</Select.Item> | ||||
| 				{:else} | ||||
| 					<div> | ||||
| 						<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100"> | ||||
| 							No results found | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 
 | ||||
| 				{#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'} | ||||
| 					<button | ||||
| 						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted" | ||||
| 						on:click={() => { | ||||
| 							pullModelHandler(); | ||||
| 						}} | ||||
| 					> | ||||
| 						Pull "{searchValue}" from Ollama.com | ||||
| 					</button> | ||||
| 				{/if} | ||||
| 
 | ||||
| 				{#each Object.keys($MODEL_DOWNLOAD_POOL) as model} | ||||
| 					<div | ||||
| 						class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted" | ||||
| 					> | ||||
| 						<div class="flex"> | ||||
| 							<div class="-ml-2 mr-2.5 translate-y-0.5"> | ||||
| 								<svg | ||||
| 									class="size-4" | ||||
| 									viewBox="0 0 24 24" | ||||
| 									fill="currentColor" | ||||
| 									xmlns="http://www.w3.org/2000/svg" | ||||
| 									><style> | ||||
| 										.spinner_ajPY { | ||||
| 											transform-origin: center; | ||||
| 											animation: spinner_AtaB 0.75s infinite linear; | ||||
| 										} | ||||
| 										@keyframes spinner_AtaB { | ||||
| 											100% { | ||||
| 												transform: rotate(360deg); | ||||
| 											} | ||||
| 										} | ||||
| 									</style><path | ||||
| 										d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" | ||||
| 										opacity=".25" | ||||
| 									/><path | ||||
| 										d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" | ||||
| 										class="spinner_ajPY" | ||||
| 									/></svg | ||||
| 								> | ||||
| 							</div> | ||||
| 
 | ||||
| 							<div class="flex flex-col self-start"> | ||||
| 								<div class="line-clamp-1"> | ||||
| 									Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model] | ||||
| 										? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)` | ||||
| 										: ''} | ||||
| 								</div> | ||||
| 
 | ||||
| 								{#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest} | ||||
| 									<div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1"> | ||||
| 										{$MODEL_DOWNLOAD_POOL[model].digest} | ||||
| 									</div> | ||||
| 								{/if} | ||||
| 							</div> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div class="mr-2 translate-y-0.5"> | ||||
| 							<Tooltip content="Cancel"> | ||||
| 								<button | ||||
| 									class="text-gray-800 dark:text-gray-100" | ||||
| 									on:click={() => { | ||||
| 										cancelModelPullHandler(model); | ||||
| 									}} | ||||
| 								> | ||||
| 									<svg | ||||
| 										class="w-4 h-4 text-gray-800 dark:text-white" | ||||
| 										aria-hidden="true" | ||||
| 										xmlns="http://www.w3.org/2000/svg" | ||||
| 										width="24" | ||||
| 										height="24" | ||||
| 										fill="currentColor" | ||||
| 										viewBox="0 0 24 24" | ||||
| 									> | ||||
| 										<path | ||||
| 											stroke="currentColor" | ||||
| 											stroke-linecap="round" | ||||
| 											stroke-linejoin="round" | ||||
| 											stroke-width="2" | ||||
| 											d="M6 18 17.94 6M18 18 6.06 6" | ||||
| 										/> | ||||
| 									</svg> | ||||
| 								</button> | ||||
| 							</Tooltip> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</slot> | ||||
| 	</Select.Content> | ||||
| </Select.Root> | ||||
|  | @ -4,9 +4,7 @@ | |||
| 	import { WEBUI_VERSION } from '$lib/constants'; | ||||
| 	import { WEBUI_NAME, config, showChangelog } from '$lib/stores'; | ||||
| 	import { compareVersion } from '$lib/utils'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	let ollamaVersion = ''; | ||||
| 
 | ||||
|  | @ -45,8 +43,7 @@ | |||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center"> | ||||
| 				<div> | ||||
| 					{$WEBUI_NAME} | ||||
| 					{$i18n.t('Version')} | ||||
| 					{$WEBUI_NAME} Version | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			<div class="flex w-full justify-between items-center"> | ||||
|  | @ -59,10 +56,10 @@ | |||
| 							target="_blank" | ||||
| 						> | ||||
| 							{updateAvailable === null | ||||
| 								? $i18n.t('Checking for updates...') | ||||
| 								? 'Checking for updates...' | ||||
| 								: updateAvailable | ||||
| 								? `(v${version.latest} ${$i18n.t('available!')})` | ||||
| 								: $i18n.t('(latest)')} | ||||
| 								? `(v${version.latest} available!)` | ||||
| 								: '(latest)'} | ||||
| 						</a> | ||||
| 					</div> | ||||
| 
 | ||||
|  | @ -72,7 +69,7 @@ | |||
| 							showChangelog.set(true); | ||||
| 						}} | ||||
| 					> | ||||
| 						<div>{$i18n.t("See what's new")}</div> | ||||
| 						<div>See what's new</div> | ||||
| 					</button> | ||||
| 				</div> | ||||
| 
 | ||||
|  | @ -82,7 +79,7 @@ | |||
| 						checkForVersionUpdates(); | ||||
| 					}} | ||||
| 				> | ||||
| 					{$i18n.t('Check for updates')} | ||||
| 					Check for updates | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | @ -91,7 +88,7 @@ | |||
| 			<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Ollama Version</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> | ||||
| 						{ollamaVersion ?? 'N/A'} | ||||
|  | @ -126,8 +123,7 @@ | |||
| 		</div> | ||||
| 
 | ||||
| 		<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 			{$i18n.t('Created by')} | ||||
| 			<a | ||||
| 			Created by <a | ||||
| 				class=" text-gray-500 dark:text-gray-300 font-medium" | ||||
| 				href="https://github.com/tjbck" | ||||
| 				target="_blank">Timothy J. Baek</a | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	import { user } from '$lib/stores'; | ||||
| 	import { updateUserProfile } from '$lib/apis/auths'; | ||||
|  | @ -9,8 +9,6 @@ | |||
| 	import { getGravatarUrl } from '$lib/apis/utils'; | ||||
| 	import { copyToClipboard } from '$lib/utils'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 
 | ||||
| 	let profileImageUrl = ''; | ||||
|  | @ -40,7 +38,7 @@ | |||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<input | ||||
| 			id="profile-image-input" | ||||
| 			bind:this={profileImageInputElement} | ||||
|  | @ -103,7 +101,7 @@ | |||
| 			}} | ||||
| 		/> | ||||
| 
 | ||||
| 		<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div> | ||||
| 		<div class=" mb-2.5 text-sm font-medium">Profile</div> | ||||
| 
 | ||||
| 		<div class="flex space-x-5"> | ||||
| 			<div class="flex flex-col"> | ||||
|  | @ -145,13 +143,13 @@ | |||
| 						const url = await getGravatarUrl($user.email); | ||||
| 
 | ||||
| 						profileImageUrl = url; | ||||
| 					}}>{$i18n.t('Use Gravatar')}</button | ||||
| 					}}>Use Gravatar</button | ||||
| 				> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="flex-1"> | ||||
| 				<div class="flex flex-col w-full"> | ||||
| 					<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> | ||||
| 					<div class=" mb-1 text-xs text-gray-500">Name</div> | ||||
| 
 | ||||
| 					<div class="flex-1"> | ||||
| 						<input | ||||
|  | @ -172,7 +170,7 @@ | |||
| 
 | ||||
| 		<div class=" w-full justify-between"> | ||||
| 			<div class="flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">JWT Token</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="flex mt-2"> | ||||
|  | @ -282,7 +280,7 @@ | |||
| 				} | ||||
| 			}} | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,10 +1,7 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { updateUserPassword } from '$lib/apis/auths'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	let show = false; | ||||
| 	let currentPassword = ''; | ||||
| 	let newPassword = ''; | ||||
|  | @ -20,7 +17,7 @@ | |||
| 			); | ||||
| 
 | ||||
| 			if (res) { | ||||
| 				toast.success($i18n.t('Successfully updated.')); | ||||
| 				toast.success('Successfully updated.'); | ||||
| 			} | ||||
| 
 | ||||
| 			currentPassword = ''; | ||||
|  | @ -43,20 +40,20 @@ | |||
| 	}} | ||||
| > | ||||
| 	<div class="flex justify-between items-center text-sm"> | ||||
| 		<div class="  font-medium">{$i18n.t('Change Password')}</div> | ||||
| 		<div class="  font-medium">Change Password</div> | ||||
| 		<button | ||||
| 			class=" text-xs font-medium text-gray-500" | ||||
| 			type="button" | ||||
| 			on:click={() => { | ||||
| 				show = !show; | ||||
| 			}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||
| 			}}>{show ? 'Hide' : 'Show'}</button | ||||
| 		> | ||||
| 	</div> | ||||
| 
 | ||||
| 	{#if show} | ||||
| 		<div class=" py-2.5 space-y-1.5"> | ||||
| 			<div class="flex flex-col w-full"> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">Current Password</div> | ||||
| 
 | ||||
| 				<div class="flex-1"> | ||||
| 					<input | ||||
|  | @ -70,7 +67,7 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div class="flex flex-col w-full"> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">New Password</div> | ||||
| 
 | ||||
| 				<div class="flex-1"> | ||||
| 					<input | ||||
|  | @ -84,7 +81,7 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div class="flex flex-col w-full"> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div> | ||||
| 				<div class=" mb-1 text-xs text-gray-500">Confirm Password</div> | ||||
| 
 | ||||
| 				<div class="flex-1"> | ||||
| 					<input | ||||
|  | @ -102,7 +99,7 @@ | |||
| 			<button | ||||
| 				class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium" | ||||
| 			> | ||||
| 				{$i18n.t('Update password')} | ||||
| 				Update password | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	{/if} | ||||
|  |  | |||
|  | @ -1,10 +1,8 @@ | |||
| <script lang="ts"> | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import AdvancedParams from './Advanced/AdvancedParams.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	import AdvancedParams from './Advanced/AdvancedParams.svelte'; | ||||
| 	export let saveSettings: Function; | ||||
| 
 | ||||
| 	// Advanced | ||||
|  | @ -57,14 +55,14 @@ | |||
| 
 | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div> | ||||
| 		<div class=" text-sm font-medium">Parameters</div> | ||||
| 
 | ||||
| 		<AdvancedParams bind:options /> | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div class=" py-1 w-full justify-between"> | ||||
| 			<div class="flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Keep Alive</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -74,9 +72,9 @@ | |||
| 					}} | ||||
| 				> | ||||
| 					{#if keepAlive === null} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 						<span class="ml-2 self-center"> Default </span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Custom')}</span> | ||||
| 						<span class="ml-2 self-center"> Custom </span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | @ -86,7 +84,7 @@ | |||
| 					<input | ||||
| 						class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 						type="text" | ||||
| 						placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")} | ||||
| 						placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`} | ||||
| 						bind:value={keepAlive} | ||||
| 					/> | ||||
| 				</div> | ||||
|  | @ -95,7 +93,7 @@ | |||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" py-1 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div> | ||||
| 				<div class=" self-center text-sm font-medium">Request Mode</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -104,7 +102,7 @@ | |||
| 					}} | ||||
| 				> | ||||
| 					{#if requestFormat === ''} | ||||
| 						<span class="ml-2 self-center"> {$i18n.t('Default')} </span> | ||||
| 						<span class="ml-2 self-center"> Default </span> | ||||
| 					{:else if requestFormat === 'json'} | ||||
| 						<!-- <svg | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -116,7 +114,7 @@ | |||
|                                 d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" | ||||
|                             /> | ||||
|                         </svg> --> | ||||
| 						<span class="ml-2 self-center">{$i18n.t('JSON')}</span> | ||||
| 						<span class="ml-2 self-center"> JSON </span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | @ -149,7 +147,7 @@ | |||
| 				dispatch('save'); | ||||
| 			}} | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,8 +1,4 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let options = { | ||||
| 		// Advanced | ||||
| 		seed: 0, | ||||
|  | @ -24,7 +20,7 @@ | |||
| <div class=" space-y-3 text-xs"> | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Seed</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
|  | @ -40,12 +36,12 @@ | |||
| 
 | ||||
| 	<div> | ||||
| 		<div class=" py-0.5 flex w-full justify-between"> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div> | ||||
| 			<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div> | ||||
| 			<div class=" flex-1 self-center"> | ||||
| 				<input | ||||
| 					class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 					type="text" | ||||
| 					placeholder={$i18n.t('Enter stop sequence')} | ||||
| 					placeholder="Enter Stop Sequence" | ||||
| 					bind:value={options.stop} | ||||
| 					autocomplete="off" | ||||
| 				/> | ||||
|  | @ -55,7 +51,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Temperature</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -65,9 +61,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.temperature === ''} | ||||
| 					<span class="ml-2 self-center"> {$i18n.t('Default')} </span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center"> {$i18n.t('Custom')} </span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -101,7 +97,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -111,9 +107,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -147,7 +143,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Eta</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -157,9 +153,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_eta === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -193,7 +189,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Mirostat Tau</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -203,9 +199,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.mirostat_tau === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Custom')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -239,7 +235,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Top K</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -249,9 +245,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_k === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -285,7 +281,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Top P</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -295,9 +291,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.top_p === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -331,7 +327,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Penalty</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -341,9 +337,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_penalty === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -377,7 +373,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Repeat Last N</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -387,9 +383,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.repeat_last_n === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -423,7 +419,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Tfs Z</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -433,9 +429,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.tfs_z === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -469,7 +465,7 @@ | |||
| 
 | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Context Length</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -479,9 +475,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_ctx === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  | @ -514,7 +510,7 @@ | |||
| 	</div> | ||||
| 	<div class=" py-0.5 w-full justify-between"> | ||||
| 		<div class="flex w-full justify-between"> | ||||
| 			<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div> | ||||
| 			<div class=" self-center text-xs font-medium">Max Tokens</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -524,9 +520,9 @@ | |||
| 				}} | ||||
| 			> | ||||
| 				{#if options.num_predict === ''} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Default </span> | ||||
| 				{:else} | ||||
| 					<span class="ml-2 self-center">{$i18n.t('Default')}</span> | ||||
| 					<span class="ml-2 self-center"> Custom </span> | ||||
| 				{/if} | ||||
| 			</button> | ||||
| 		</div> | ||||
|  |  | |||
|  | @ -1,10 +1,8 @@ | |||
| <script lang="ts"> | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 
 | ||||
| 	// Audio | ||||
|  | @ -103,36 +101,32 @@ | |||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div> | ||||
| 			<div class=" mb-1 text-sm font-medium">STT Settings</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Speech-to-Text Engine</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||
| 						class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={STTEngine} | ||||
| 						placeholder="Select a mode" | ||||
| 						on:change={(e) => { | ||||
| 							if (e.target.value !== '') { | ||||
| 								navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) { | ||||
| 									toast.error( | ||||
| 										$i18n.t(`Permission denied when accessing microphone: {{error}}`, { | ||||
| 											error: err | ||||
| 										}) | ||||
| 									); | ||||
| 									toast.error(`Permission denied when accessing microphone: ${err}`); | ||||
| 									STTEngine = ''; | ||||
| 								}); | ||||
| 							} | ||||
| 						}} | ||||
| 					> | ||||
| 						<option value="">{$i18n.t('Default (Web API)')}</option> | ||||
| 						<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option> | ||||
| 						<option value="">Default (Web API)</option> | ||||
| 						<option value="whisper-local">Whisper (Local)</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Conversation Mode</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -142,17 +136,15 @@ | |||
| 					type="button" | ||||
| 				> | ||||
| 					{#if conversationMode === true} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 						<span class="ml-2 self-center">On</span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium"> | ||||
| 					{$i18n.t('Auto-send input after 3 sec.')} | ||||
| 				</div> | ||||
| 				<div class=" self-center text-xs font-medium">Auto-send input after 3 sec.</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -162,22 +154,22 @@ | |||
| 					type="button" | ||||
| 				> | ||||
| 					{#if speechAutoSend === true} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 						<span class="ml-2 self-center">On</span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div> | ||||
| 			<div class=" mb-1 text-sm font-medium">TTS Settings</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Text-to-Speech Engine</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||
| 						class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={TTSEngine} | ||||
| 						placeholder="Select a mode" | ||||
| 						on:change={(e) => { | ||||
|  | @ -190,14 +182,14 @@ | |||
| 							} | ||||
| 						}} | ||||
| 					> | ||||
| 						<option value="">{$i18n.t('Default (Web API)')}</option> | ||||
| 						<option value="openai">{$i18n.t('Open AI')}</option> | ||||
| 						<option value="">Default (Web API)</option> | ||||
| 						<option value="openai">Open AI</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Auto-playback response</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -207,9 +199,9 @@ | |||
| 					type="button" | ||||
| 				> | ||||
| 					{#if responseAutoPlayback === true} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 						<span class="ml-2 self-center">On</span> | ||||
| 					{:else} | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
|  | @ -219,7 +211,7 @@ | |||
| 
 | ||||
| 		{#if TTSEngine === ''} | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Voice</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1"> | ||||
| 						<select | ||||
|  | @ -227,7 +219,7 @@ | |||
| 							bind:value={speaker} | ||||
| 							placeholder="Select a voice" | ||||
| 						> | ||||
| 							<option value="" selected>{$i18n.t('Default')}</option> | ||||
| 							<option value="" selected>Default</option> | ||||
| 							{#each voices.filter((v) => v.localService === true) as voice} | ||||
| 								<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option | ||||
| 								> | ||||
|  | @ -238,7 +230,7 @@ | |||
| 			</div> | ||||
| 		{:else if TTSEngine === 'openai'} | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Voice</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1"> | ||||
| 						<select | ||||
|  | @ -262,7 +254,7 @@ | |||
| 			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
| 	import fileSaver from 'file-saver'; | ||||
| 	const { saveAs } = fileSaver; | ||||
| 
 | ||||
| 	import { resetVectorDB } from '$lib/apis/rag'; | ||||
| 	import { chats, user } from '$lib/stores'; | ||||
| 
 | ||||
| 	import { | ||||
|  | @ -12,12 +13,10 @@ | |||
| 		getChatList | ||||
| 	} from '$lib/apis/chats'; | ||||
| 	import { getImportOrigin, convertOpenAIChats } from '$lib/utils'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 	// Chats | ||||
| 	let saveChatHistory = true; | ||||
|  | @ -100,13 +99,13 @@ | |||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]"> | ||||
| <div class="flex flex-col h-full justify-between space-y-3 text-sm"> | ||||
| 	<div class=" space-y-2"> | ||||
| 		<div | ||||
| 			class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition" | ||||
| 		> | ||||
| 			<div class="flex w-full justify-between"> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div> | ||||
| 				<div class=" self-center text-sm font-medium">Chat History</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -130,7 +129,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center"> {$i18n.t('On')} </span> | ||||
| 						<span class="ml-2 self-center"> On </span> | ||||
| 					{:else} | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -148,13 +147,13 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 
 | ||||
| 						<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 						<span class="ml-2 self-center">Off</span> | ||||
| 					{/if} | ||||
| 				</button> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="text-xs text-left w-full font-medium mt-0.5"> | ||||
| 				{$i18n.t('This setting does not sync across browsers or devices.')} | ||||
| 				This setting does not sync across browsers or devices. | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
|  | @ -189,7 +188,7 @@ | |||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div> | ||||
| 				<div class=" self-center text-sm font-medium">Import Chats</div> | ||||
| 			</button> | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
|  | @ -211,7 +210,7 @@ | |||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div> | ||||
| 				<div class=" self-center text-sm font-medium">Export Chats</div> | ||||
| 			</button> | ||||
| 		</div> | ||||
| 
 | ||||
|  | @ -233,7 +232,7 @@ | |||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 					<span>{$i18n.t('Are you sure?')}</span> | ||||
| 					<span>Are you sure?</span> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="flex space-x-1.5 items-center"> | ||||
|  | @ -297,7 +296,7 @@ | |||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div> | ||||
| 				<div class=" self-center text-sm font-medium">Delete Chats</div> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 
 | ||||
|  | @ -325,9 +324,39 @@ | |||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium"> | ||||
| 					{$i18n.t('Export All Chats (All Users)')} | ||||
| 				<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div> | ||||
| 			</button> | ||||
| 
 | ||||
| 			<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					const res = resetVectorDB(localStorage.token).catch((error) => { | ||||
| 						toast.error(error); | ||||
| 						return null; | ||||
| 					}); | ||||
| 
 | ||||
| 					if (res) { | ||||
| 						toast.success('Success'); | ||||
| 					} | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 16 16" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">Reset Vector Storage</div> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 	</div> | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 	import { models, user } from '$lib/stores'; | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama'; | ||||
|  | @ -12,8 +12,6 @@ | |||
| 	} from '$lib/apis/openai'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	// External | ||||
|  | @ -44,7 +42,7 @@ | |||
| 		}); | ||||
| 
 | ||||
| 		if (ollamaVersion) { | ||||
| 			toast.success($i18n.t('Server connection verified')); | ||||
| 			toast.success('Server connection verified'); | ||||
| 			await models.set(await getModels()); | ||||
| 		} | ||||
| 	}; | ||||
|  | @ -65,17 +63,17 @@ | |||
| 		dispatch('save'); | ||||
| 	}} | ||||
| > | ||||
| 	<div class="  pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3"> | ||||
| 	<div class="  pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3"> | ||||
| 		<div class=" space-y-3"> | ||||
| 			<div class="mt-2 space-y-2 pr-1.5"> | ||||
| 				<div class="flex justify-between items-center text-sm"> | ||||
| 					<div class="  font-medium">{$i18n.t('OpenAI API')}</div> | ||||
| 					<div class="  font-medium">OpenAI API</div> | ||||
| 					<button | ||||
| 						class=" text-xs font-medium text-gray-500" | ||||
| 						type="button" | ||||
| 						on:click={() => { | ||||
| 							showOpenAI = !showOpenAI; | ||||
| 						}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||
| 						}}>{showOpenAI ? 'Hide' : 'Show'}</button | ||||
| 					> | ||||
| 				</div> | ||||
| 
 | ||||
|  | @ -86,7 +84,7 @@ | |||
| 								<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')} | ||||
| 										placeholder="API Base URL" | ||||
| 										bind:value={url} | ||||
| 										autocomplete="off" | ||||
| 									/> | ||||
|  | @ -95,7 +93,7 @@ | |||
| 								<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')} | ||||
| 										placeholder="API Key" | ||||
| 										bind:value={OPENAI_API_KEYS[idx]} | ||||
| 										autocomplete="off" | ||||
| 									/> | ||||
|  | @ -145,8 +143,7 @@ | |||
| 								</div> | ||||
| 							</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 								{$i18n.t('WebUI will make requests to')} | ||||
| 								<span class=" text-gray-200">'{url}/models'</span> | ||||
| 								WebUI will make requests to <span class=" text-gray-200">'{url}/models'</span> | ||||
| 							</div> | ||||
| 						{/each} | ||||
| 					</div> | ||||
|  | @ -157,7 +154,7 @@ | |||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div> | ||||
| 			<div class="flex w-full gap-1.5"> | ||||
| 				<div class="flex-1 flex flex-col gap-2"> | ||||
| 					{#each OLLAMA_BASE_URLS as url, idx} | ||||
|  | @ -236,13 +233,13 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				{$i18n.t('Trouble accessing Ollama?')} | ||||
| 				Trouble accessing Ollama? | ||||
| 				<a | ||||
| 					class=" text-gray-300 font-medium underline" | ||||
| 					class=" text-gray-300 font-medium" | ||||
| 					href="https://github.com/open-webui/open-webui#troubleshooting" | ||||
| 					target="_blank" | ||||
| 				> | ||||
| 					{$i18n.t('Click here for help.')} | ||||
| 					Click here for help. | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		</div> | ||||
|  | @ -253,7 +250,7 @@ | |||
| 			class="  px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -1,12 +1,9 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import { getLanguages } from '$lib/i18n'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	import { models, user, theme } from '$lib/stores'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 	import { models, user } from '$lib/stores'; | ||||
| 
 | ||||
| 	import AdvancedParams from './Advanced/AdvancedParams.svelte'; | ||||
| 
 | ||||
|  | @ -14,11 +11,8 @@ | |||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	// General | ||||
| 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark']; | ||||
| 	let selectedTheme = 'system'; | ||||
| 
 | ||||
| 	let languages = []; | ||||
| 	let lang = $i18n.language; | ||||
| 	let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light']; | ||||
| 	let theme = 'dark'; | ||||
| 	let notificationEnabled = false; | ||||
| 	let system = ''; | ||||
| 
 | ||||
|  | @ -69,11 +63,9 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	onMount(async () => { | ||||
| 		selectedTheme = localStorage.theme ?? 'system'; | ||||
| 
 | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 		languages = await getLanguages(); | ||||
| 
 | ||||
| 		theme = localStorage.theme ?? 'dark'; | ||||
| 		notificationEnabled = settings.notificationEnabled ?? false; | ||||
| 		system = settings.system ?? ''; | ||||
| 
 | ||||
|  | @ -89,103 +81,77 @@ | |||
| 		options = { ...options, ...settings.options }; | ||||
| 		options.stop = (settings?.options?.stop ?? []).join(','); | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| 	const applyTheme = (_theme: string) => { | ||||
| 		let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme; | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class="  pr-1.5 overflow-y-scroll max-h-[20.5rem]"> | ||||
| 		<div class=""> | ||||
| 			<div class=" mb-1 text-sm font-medium">WebUI Settings</div> | ||||
| 
 | ||||
| 		if (_theme === 'system') { | ||||
| 			themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; | ||||
| 		} | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">Theme</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<div class=" absolute right-16"> | ||||
| 						{#if theme === 'dark'} | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 20 20" | ||||
| 								fill="currentColor" | ||||
| 								class="w-4 h-4" | ||||
| 							> | ||||
| 								<path | ||||
| 									fill-rule="evenodd" | ||||
| 									d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" | ||||
| 									clip-rule="evenodd" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						{:else if theme === 'light'} | ||||
| 							<svg | ||||
| 								xmlns="http://www.w3.org/2000/svg" | ||||
| 								viewBox="0 0 20 20" | ||||
| 								fill="currentColor" | ||||
| 								class="w-4 h-4 self-center" | ||||
| 							> | ||||
| 								<path | ||||
| 									d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" | ||||
| 								/> | ||||
| 							</svg> | ||||
| 						{/if} | ||||
| 					</div> | ||||
| 
 | ||||
| 		if (themeToApply === 'dark' && !_theme.includes('oled')) { | ||||
| 			document.documentElement.style.setProperty('--color-gray-900', '#171717'); | ||||
| 			document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d'); | ||||
| 		} | ||||
| 					<select | ||||
| 						class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={theme} | ||||
| 						placeholder="Select a theme" | ||||
| 						on:change={(e) => { | ||||
| 							localStorage.theme = theme; | ||||
| 
 | ||||
| 							themes | ||||
| 			.filter((e) => e !== themeToApply) | ||||
| 								.filter((e) => e !== theme) | ||||
| 								.forEach((e) => { | ||||
| 									e.split(' ').forEach((e) => { | ||||
| 										document.documentElement.classList.remove(e); | ||||
| 									}); | ||||
| 								}); | ||||
| 
 | ||||
| 		themeToApply.split(' ').forEach((e) => { | ||||
| 							theme.split(' ').forEach((e) => { | ||||
| 								document.documentElement.classList.add(e); | ||||
| 							}); | ||||
| 
 | ||||
| 		console.log(_theme); | ||||
| 	}; | ||||
| 
 | ||||
| 	const themeChangeHandler = (_theme: string) => { | ||||
| 		theme.set(_theme); | ||||
| 		localStorage.setItem('theme', _theme); | ||||
| 		if (_theme.includes('oled')) { | ||||
| 			document.documentElement.style.setProperty('--color-gray-900', '#000000'); | ||||
| 			document.documentElement.style.setProperty('--color-gray-950', '#000000'); | ||||
| 			document.documentElement.classList.add('dark'); | ||||
| 		} | ||||
| 		applyTheme(_theme); | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class="  pr-1.5 overflow-y-scroll max-h-[22rem]"> | ||||
| 		<div class=""> | ||||
| 			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div> | ||||
| 
 | ||||
| 			<div class="flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={selectedTheme} | ||||
| 						placeholder="Select a theme" | ||||
| 						on:change={() => themeChangeHandler(selectedTheme)} | ||||
| 					> | ||||
| 						<option value="system">⚙️ {$i18n.t('System')}</option> | ||||
| 						<option value="dark">🌑 {$i18n.t('Dark')}</option> | ||||
| 						<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option> | ||||
| 						<option value="light">☀️ {$i18n.t('Light')}</option> | ||||
| 						<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option> | ||||
| 						<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class=" flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={lang} | ||||
| 						placeholder="Select a language" | ||||
| 						on:change={(e) => { | ||||
| 							$i18n.changeLanguage(lang); | ||||
| 							console.log(theme); | ||||
| 						}} | ||||
| 					> | ||||
| 						{#each languages as language} | ||||
| 							<option value={language['code']}>{language['title']}</option> | ||||
| 						{/each} | ||||
| 						<option value="dark">Dark</option> | ||||
| 						<option value="light">Light</option> | ||||
| 						<option value="rose-pine dark">Rosé Pine</option> | ||||
| 						<option value="rose-pine-dawn light">Rosé Pine Dawn</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{#if $i18n.language === 'en-US'} | ||||
| 				<div class="mb-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 					Couldn't find your language? | ||||
| 					<a | ||||
| 						class=" text-gray-300 font-medium underline" | ||||
| 						href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization" | ||||
| 						target="_blank" | ||||
| 					> | ||||
| 						Help us translate Open WebUI! | ||||
| 					</a> | ||||
| 				</div> | ||||
| 			{/if} | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div> | ||||
| 					<div class=" self-center text-xs font-medium">Notification</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -195,9 +161,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if notificationEnabled === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -207,7 +173,7 @@ | |||
| 		<hr class=" dark:border-gray-700 my-3" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div> | ||||
| 			<div class=" my-2.5 text-sm font-medium">System Prompt</div> | ||||
| 			<textarea | ||||
| 				bind:value={system} | ||||
| 				class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" | ||||
|  | @ -217,13 +183,13 @@ | |||
| 
 | ||||
| 		<div class="mt-2 space-y-3 pr-1.5"> | ||||
| 			<div class="flex justify-between items-center text-sm"> | ||||
| 				<div class="  font-medium">{$i18n.t('Advanced Parameters')}</div> | ||||
| 				<div class="  font-medium">Advanced Parameters</div> | ||||
| 				<button | ||||
| 					class=" text-xs font-medium text-gray-500" | ||||
| 					type="button" | ||||
| 					on:click={() => { | ||||
| 						showAdvanced = !showAdvanced; | ||||
| 					}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||
| 					}}>{showAdvanced ? 'Hide' : 'Show'}</button | ||||
| 				> | ||||
| 			</div> | ||||
| 
 | ||||
|  | @ -233,7 +199,7 @@ | |||
| 
 | ||||
| 				<div class=" py-1 w-full justify-between"> | ||||
| 					<div class="flex w-full justify-between"> | ||||
| 						<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div> | ||||
| 						<div class=" self-center text-xs font-medium">Keep Alive</div> | ||||
| 
 | ||||
| 						<button | ||||
| 							class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -243,9 +209,9 @@ | |||
| 							}} | ||||
| 						> | ||||
| 							{#if keepAlive === null} | ||||
| 								<span class="ml-2 self-center"> {$i18n.t('Default')} </span> | ||||
| 								<span class="ml-2 self-center"> Default </span> | ||||
| 							{:else} | ||||
| 								<span class="ml-2 self-center"> {$i18n.t('Custom')} </span> | ||||
| 								<span class="ml-2 self-center"> Custom </span> | ||||
| 							{/if} | ||||
| 						</button> | ||||
| 					</div> | ||||
|  | @ -255,7 +221,7 @@ | |||
| 							<input | ||||
| 								class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 								type="text" | ||||
| 								placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")} | ||||
| 								placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`} | ||||
| 								bind:value={keepAlive} | ||||
| 							/> | ||||
| 						</div> | ||||
|  | @ -264,7 +230,7 @@ | |||
| 
 | ||||
| 				<div> | ||||
| 					<div class=" py-1 flex w-full justify-between"> | ||||
| 						<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div> | ||||
| 						<div class=" self-center text-sm font-medium">Request Mode</div> | ||||
| 
 | ||||
| 						<button | ||||
| 							class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -273,7 +239,7 @@ | |||
| 							}} | ||||
| 						> | ||||
| 							{#if requestFormat === ''} | ||||
| 								<span class="ml-2 self-center"> {$i18n.t('Default')} </span> | ||||
| 								<span class="ml-2 self-center"> Default </span> | ||||
| 							{:else if requestFormat === 'json'} | ||||
| 								<!-- <svg | ||||
|                             xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -285,7 +251,7 @@ | |||
|                                 d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" | ||||
|                             /> | ||||
|                         </svg> --> | ||||
| 								<span class="ml-2 self-center"> {$i18n.t('JSON')} </span> | ||||
| 								<span class="ml-2 self-center"> JSON </span> | ||||
| 							{/if} | ||||
| 						</button> | ||||
| 					</div> | ||||
|  | @ -320,7 +286,7 @@ | |||
| 				dispatch('save'); | ||||
| 			}} | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,17 +1,17 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	import { config, user } from '$lib/stores'; | ||||
| 	import { | ||||
| 		getAUTOMATIC1111Url, | ||||
| 		getImageGenerationModels, | ||||
| 		getDefaultImageGenerationModel, | ||||
| 		updateDefaultImageGenerationModel, | ||||
| 		getImageSize, | ||||
| 		getImageGenerationConfig, | ||||
| 		updateImageGenerationConfig, | ||||
| 		getImageGenerationEngineUrls, | ||||
| 		updateImageGenerationEngineUrls, | ||||
| 		updateAUTOMATIC1111Url, | ||||
| 		updateImageSize, | ||||
| 		getImageSteps, | ||||
| 		updateImageSteps, | ||||
|  | @ -21,8 +21,6 @@ | |||
| 	import { getBackendConfig } from '$lib/apis'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 
 | ||||
| 	let loading = false; | ||||
|  | @ -31,8 +29,6 @@ | |||
| 	let enableImageGeneration = false; | ||||
| 
 | ||||
| 	let AUTOMATIC1111_BASE_URL = ''; | ||||
| 	let COMFYUI_BASE_URL = ''; | ||||
| 
 | ||||
| 	let OPENAI_API_KEY = ''; | ||||
| 
 | ||||
| 	let selectedModel = ''; | ||||
|  | @ -51,47 +47,24 @@ | |||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
| 	const updateUrlHandler = async () => { | ||||
| 		if (imageGenerationEngine === 'comfyui') { | ||||
| 			const res = await updateImageGenerationEngineUrls(localStorage.token, { | ||||
| 				COMFYUI_BASE_URL: COMFYUI_BASE_URL | ||||
| 			}).catch((error) => { | ||||
| 	const updateAUTOMATIC1111UrlHandler = async () => { | ||||
| 		const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch( | ||||
| 			(error) => { | ||||
| 				toast.error(error); | ||||
| 
 | ||||
| 				console.log(error); | ||||
| 				return null; | ||||
| 			}); | ||||
| 			} | ||||
| 		); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 				COMFYUI_BASE_URL = res.COMFYUI_BASE_URL; | ||||
| 			AUTOMATIC1111_BASE_URL = res; | ||||
| 
 | ||||
| 			await getModels(); | ||||
| 
 | ||||
| 			if (models) { | ||||
| 					toast.success($i18n.t('Server connection verified')); | ||||
| 				toast.success('Server connection verified'); | ||||
| 			} | ||||
| 		} else { | ||||
| 				({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token)); | ||||
| 			} | ||||
| 		} else { | ||||
| 			const res = await updateImageGenerationEngineUrls(localStorage.token, { | ||||
| 				AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL | ||||
| 			}).catch((error) => { | ||||
| 				toast.error(error); | ||||
| 				return null; | ||||
| 			}); | ||||
| 
 | ||||
| 			if (res) { | ||||
| 				AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL; | ||||
| 
 | ||||
| 				await getModels(); | ||||
| 
 | ||||
| 				if (models) { | ||||
| 					toast.success($i18n.t('Server connection verified')); | ||||
| 				} | ||||
| 			} else { | ||||
| 				({ AUTOMATIC1111_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token)); | ||||
| 			} | ||||
| 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token); | ||||
| 		} | ||||
| 	}; | ||||
| 	const updateImageGeneration = async () => { | ||||
|  | @ -126,11 +99,7 @@ | |||
| 				imageGenerationEngine = res.engine; | ||||
| 				enableImageGeneration = res.enabled; | ||||
| 			} | ||||
| 			const URLS = await getImageGenerationEngineUrls(localStorage.token); | ||||
| 
 | ||||
| 			AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL; | ||||
| 			COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL; | ||||
| 
 | ||||
| 			AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token); | ||||
| 			OPENAI_API_KEY = await getOpenAIKey(localStorage.token); | ||||
| 
 | ||||
| 			imageSize = await getImageSize(localStorage.token); | ||||
|  | @ -147,13 +116,11 @@ | |||
| 	class="flex flex-col h-full justify-between space-y-3 text-sm" | ||||
| 	on:submit|preventDefault={async () => { | ||||
| 		loading = true; | ||||
| 
 | ||||
| 		if (imageGenerationEngine === 'openai') { | ||||
| 		await updateOpenAIKey(localStorage.token, OPENAI_API_KEY); | ||||
| 		} | ||||
| 
 | ||||
| 		await updateDefaultImageGenerationModel(localStorage.token, selectedModel); | ||||
| 
 | ||||
| 		await updateDefaultImageGenerationModel(localStorage.token, selectedModel); | ||||
| 		await updateImageSize(localStorage.token, imageSize).catch((error) => { | ||||
| 			toast.error(error); | ||||
| 			return null; | ||||
|  | @ -167,45 +134,39 @@ | |||
| 		loading = false; | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[20.5rem]"> | ||||
| 		<div> | ||||
| 			<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div> | ||||
| 			<div class=" mb-1 text-sm font-medium">Image Settings</div> | ||||
| 
 | ||||
| 			<div class=" py-0.5 flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div> | ||||
| 				<div class=" self-center text-xs font-medium">Image Generation Engine</div> | ||||
| 				<div class="flex items-center relative"> | ||||
| 					<select | ||||
| 						class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" | ||||
| 						bind:value={imageGenerationEngine} | ||||
| 						placeholder={$i18n.t('Select a mode')} | ||||
| 						placeholder="Select a mode" | ||||
| 						on:change={async () => { | ||||
| 							await updateImageGeneration(); | ||||
| 						}} | ||||
| 					> | ||||
| 						<option value="">{$i18n.t('Default (Automatic1111)')}</option> | ||||
| 						<option value="comfyui">{$i18n.t('ComfyUI')}</option> | ||||
| 						<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option> | ||||
| 						<option value="">Default (Automatic1111)</option> | ||||
| 						<option value="openai">Open AI (Dall-E)</option> | ||||
| 					</select> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium"> | ||||
| 						{$i18n.t('Image Generation (Experimental)')} | ||||
| 					</div> | ||||
| 					<div class=" self-center text-xs font-medium">Image Generation (Experimental)</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
| 						on:click={() => { | ||||
| 							if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') { | ||||
| 								toast.error($i18n.t('AUTOMATIC1111 Base URL is required.')); | ||||
| 								enableImageGeneration = false; | ||||
| 							} else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') { | ||||
| 								toast.error($i18n.t('ComfyUI Base URL is required.')); | ||||
| 								toast.error('AUTOMATIC1111 Base URL is required.'); | ||||
| 								enableImageGeneration = false; | ||||
| 							} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') { | ||||
| 								toast.error($i18n.t('OpenAI API Key is required.')); | ||||
| 								toast.error('OpenAI API Key is required.'); | ||||
| 								enableImageGeneration = false; | ||||
| 							} else { | ||||
| 								enableImageGeneration = !enableImageGeneration; | ||||
|  | @ -216,9 +177,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if enableImageGeneration === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -227,20 +188,22 @@ | |||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		{#if imageGenerationEngine === ''} | ||||
| 			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">AUTOMATIC1111 Base URL</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<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('Enter URL (e.g. http://127.0.0.1:7860/)')} | ||||
| 						placeholder="Enter URL (e.g. http://127.0.0.1:7860/)" | ||||
| 						bind:value={AUTOMATIC1111_BASE_URL} | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" | ||||
| 					class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded-lg transition" | ||||
| 					type="button" | ||||
| 					on:click={() => { | ||||
| 						updateUrlHandler(); | ||||
| 						// updateOllamaAPIUrlHandler(); | ||||
| 
 | ||||
| 						updateAUTOMATIC1111UrlHandler(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<svg | ||||
|  | @ -259,53 +222,22 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 				{$i18n.t('Include `--api` flag when running stable-diffusion-webui')} | ||||
| 				Include `--api` flag when running stable-diffusion-webui | ||||
| 				<a | ||||
| 					class=" text-gray-300 font-medium" | ||||
| 					href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734" | ||||
| 					target="_blank" | ||||
| 				> | ||||
| 					{$i18n.t('(e.g. `sh webui.sh --api`)')} | ||||
| 					(e.g. `sh webui.sh --api`) | ||||
| 				</a> | ||||
| 			</div> | ||||
| 		{:else if imageGenerationEngine === 'comfyui'} | ||||
| 			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<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('Enter URL (e.g. http://127.0.0.1:7860/)')} | ||||
| 						bind:value={COMFYUI_BASE_URL} | ||||
| 					/> | ||||
| 				</div> | ||||
| 				<button | ||||
| 					class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" | ||||
| 					type="button" | ||||
| 					on:click={() => { | ||||
| 						updateUrlHandler(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 20 20" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 		{:else if imageGenerationEngine === 'openai'} | ||||
| 			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<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('Enter API Key')} | ||||
| 						placeholder="Enter API Key" | ||||
| 						bind:value={OPENAI_API_KEY} | ||||
| 					/> | ||||
| 				</div> | ||||
|  | @ -316,17 +248,16 @@ | |||
| 			<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Default Model</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1 mr-2"> | ||||
| 						<select | ||||
| 							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 							bind:value={selectedModel} | ||||
| 							placeholder={$i18n.t('Select a model')} | ||||
| 							required | ||||
| 							placeholder="Select a model" | ||||
| 						> | ||||
| 							{#if !selectedModel} | ||||
| 								<option value="" disabled selected>{$i18n.t('Select a model')}</option> | ||||
| 								<option value="" disabled selected>Select a model</option> | ||||
| 							{/if} | ||||
| 							{#each models ?? [] as model} | ||||
| 								<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> | ||||
|  | @ -337,12 +268,12 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Image Size</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1 mr-2"> | ||||
| 						<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('Enter Image Size (e.g. 512x512)')} | ||||
| 							placeholder="Enter Image Size (e.g. 512x512)" | ||||
| 							bind:value={imageSize} | ||||
| 						/> | ||||
| 					</div> | ||||
|  | @ -350,12 +281,12 @@ | |||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Set Steps</div> | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class="flex-1 mr-2"> | ||||
| 						<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('Enter Number of Steps (e.g. 50)')} | ||||
| 							placeholder="Enter Number of Steps (e.g. 50)" | ||||
| 							bind:value={steps} | ||||
| 						/> | ||||
| 					</div> | ||||
|  | @ -372,7 +303,7 @@ | |||
| 			type="submit" | ||||
| 			disabled={loading} | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 
 | ||||
| 			{#if loading} | ||||
| 				<div class="ml-2 self-center"> | ||||
|  |  | |||
|  | @ -1,20 +1,17 @@ | |||
| <script lang="ts"> | ||||
| 	import { getBackendConfig } from '$lib/apis'; | ||||
| 	import { setDefaultPromptSuggestions } from '$lib/apis/configs'; | ||||
| 	import { config, models, settings, user } from '$lib/stores'; | ||||
| 	import { createEventDispatcher, onMount, getContext } from 'svelte'; | ||||
| 	import { config, models, user } from '$lib/stores'; | ||||
| 	import { createEventDispatcher, onMount } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveSettings: Function; | ||||
| 
 | ||||
| 	// Addons | ||||
| 	let titleAutoGenerate = true; | ||||
| 	let responseAutoCopy = false; | ||||
| 	let titleAutoGenerateModel = ''; | ||||
| 	let titleAutoGenerateModelExternal = ''; | ||||
| 	let fullScreenMode = false; | ||||
| 	let titleGenerationPrompt = ''; | ||||
| 
 | ||||
|  | @ -34,12 +31,7 @@ | |||
| 
 | ||||
| 	const toggleTitleAutoGenerate = async () => { | ||||
| 		titleAutoGenerate = !titleAutoGenerate; | ||||
| 		saveSettings({ | ||||
| 			title: { | ||||
| 				...$settings.title, | ||||
| 				auto: titleAutoGenerate | ||||
| 			} | ||||
| 		}); | ||||
| 		saveSettings({ titleAutoGenerate: titleAutoGenerate }); | ||||
| 	}; | ||||
| 
 | ||||
| 	const toggleResponseAutoCopy = async () => { | ||||
|  | @ -71,13 +63,8 @@ | |||
| 		} | ||||
| 
 | ||||
| 		saveSettings({ | ||||
| 			title: { | ||||
| 				...$settings.title, | ||||
| 				model: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined, | ||||
| 				modelExternal: | ||||
| 					titleAutoGenerateModelExternal !== '' ? titleAutoGenerateModelExternal : undefined, | ||||
| 				prompt: titleGenerationPrompt ? titleGenerationPrompt : undefined | ||||
| 			} | ||||
| 			titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined, | ||||
| 			titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined | ||||
| 		}); | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -88,18 +75,14 @@ | |||
| 
 | ||||
| 		let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); | ||||
| 
 | ||||
| 		titleAutoGenerate = settings?.title?.auto ?? true; | ||||
| 		titleAutoGenerateModel = settings?.title?.model ?? ''; | ||||
| 		titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? ''; | ||||
| 		titleGenerationPrompt = | ||||
| 			settings?.title?.prompt ?? | ||||
| 			$i18n.t( | ||||
| 				"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':" | ||||
| 			) + ' {{prompt}}'; | ||||
| 
 | ||||
| 		titleAutoGenerate = settings.titleAutoGenerate ?? true; | ||||
| 		responseAutoCopy = settings.responseAutoCopy ?? false; | ||||
| 		showUsername = settings.showUsername ?? false; | ||||
| 		fullScreenMode = settings.fullScreenMode ?? false; | ||||
| 		titleAutoGenerateModel = settings.titleAutoGenerateModel ?? ''; | ||||
| 		titleGenerationPrompt = | ||||
| 			settings.titleGenerationPrompt ?? | ||||
| 			`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`; | ||||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -110,13 +93,13 @@ | |||
| 		dispatch('save'); | ||||
| 	}} | ||||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div> | ||||
| 			<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div> | ||||
| 					<div class=" self-center text-xs font-medium">Title Auto-Generation</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -126,9 +109,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if titleAutoGenerate === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -136,9 +119,7 @@ | |||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium"> | ||||
| 						{$i18n.t('Response AutoCopy to Clipboard')} | ||||
| 					</div> | ||||
| 					<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -148,9 +129,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if responseAutoCopy === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -158,7 +139,7 @@ | |||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div> | ||||
| 					<div class=" self-center text-xs font-medium">Full Screen Mode</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -168,9 +149,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if fullScreenMode === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -179,7 +160,7 @@ | |||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium"> | ||||
| 						{$i18n.t('Display the username instead of You in the Chat')} | ||||
| 						Display the username instead of "You" in the Chat | ||||
| 					</div> | ||||
| 
 | ||||
| 					<button | ||||
|  | @ -190,9 +171,9 @@ | |||
| 						type="button" | ||||
| 					> | ||||
| 						{#if showUsername === true} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('On')}</span> | ||||
| 							<span class="ml-2 self-center">On</span> | ||||
| 						{:else} | ||||
| 							<span class="ml-2 self-center">{$i18n.t('Off')}</span> | ||||
| 							<span class="ml-2 self-center">Off</span> | ||||
| 						{/if} | ||||
| 					</button> | ||||
| 				</div> | ||||
|  | @ -202,16 +183,15 @@ | |||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation Model')}</div> | ||||
| 			<div class="flex w-full gap-2 pr-2"> | ||||
| 				<div class="flex-1"> | ||||
| 					<div class=" text-xs mb-1">Local Models</div> | ||||
| 			<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div> | ||||
| 			<div class="flex w-full"> | ||||
| 				<div class="flex-1 mr-2"> | ||||
| 					<select | ||||
| 						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 						bind:value={titleAutoGenerateModel} | ||||
| 						placeholder={$i18n.t('Select a model')} | ||||
| 						placeholder="Select a model" | ||||
| 					> | ||||
| 						<option value="" selected>{$i18n.t('Current Model')}</option> | ||||
| 						<option value="" selected>Current Model</option> | ||||
| 						{#each $models as model} | ||||
| 							{#if model.size != null} | ||||
| 								<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> | ||||
|  | @ -221,28 +201,9 @@ | |||
| 						{/each} | ||||
| 					</select> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<div class="flex-1"> | ||||
| 					<div class=" text-xs mb-1">External Models</div> | ||||
| 					<select | ||||
| 						class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 						bind:value={titleAutoGenerateModelExternal} | ||||
| 						placeholder={$i18n.t('Select a model')} | ||||
| 					> | ||||
| 						<option value="" selected>{$i18n.t('Current Model')}</option> | ||||
| 						{#each $models as model} | ||||
| 							{#if model.name !== 'hr'} | ||||
| 								<option value={model.name} class="bg-gray-100 dark:bg-gray-700"> | ||||
| 									{model.name} | ||||
| 								</option> | ||||
| 							{/if} | ||||
| 						{/each} | ||||
| 					</select> | ||||
| 			</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div class="mt-3 mr-2"> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">Title Generation Prompt</div> | ||||
| 				<textarea | ||||
| 					bind:value={titleGenerationPrompt} | ||||
| 					class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" | ||||
|  | @ -256,9 +217,7 @@ | |||
| 
 | ||||
| 			<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 				<div class="flex w-full justify-between mb-2"> | ||||
| 					<div class=" self-center text-sm font-semibold"> | ||||
| 						{$i18n.t('Default Prompt Suggestions')} | ||||
| 					</div> | ||||
| 					<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -331,19 +290,19 @@ | |||
| 
 | ||||
| 				{#if promptSuggestions.length > 0} | ||||
| 					<div class="text-xs text-left w-full mt-2"> | ||||
| 						{$i18n.t('Adjusting these settings will apply changes universally to all users.')} | ||||
| 						Adjusting these settings will apply changes universally to all users. | ||||
| 					</div> | ||||
| 				{/if} | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="flex justify-end text-sm font-medium"> | ||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
| 		<button | ||||
| 			class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -5,22 +5,17 @@ | |||
| 	import { | ||||
| 		createModel, | ||||
| 		deleteModel, | ||||
| 		downloadModel, | ||||
| 		getOllamaUrls, | ||||
| 		getOllamaVersion, | ||||
| 		pullModel, | ||||
| 		cancelOllamaRequest, | ||||
| 		uploadModel | ||||
| 		pullModel | ||||
| 	} from '$lib/apis/ollama'; | ||||
| 	import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; | ||||
| 	import { WEBUI_NAME, models, user } from '$lib/stores'; | ||||
| 	import { splitStream } from '$lib/utils'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { addLiteLLMModel, deleteLiteLLMModel, getLiteLLMModelInfo } from '$lib/apis/litellm'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let getModels: Function; | ||||
| 
 | ||||
| 	let showLiteLLM = false; | ||||
|  | @ -63,13 +58,11 @@ | |||
| 	let pullProgress = null; | ||||
| 
 | ||||
| 	let modelUploadMode = 'file'; | ||||
| 	let modelInputFile: File[] | null = null; | ||||
| 	let modelInputFile = ''; | ||||
| 	let modelFileUrl = ''; | ||||
| 	let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`; | ||||
| 	let modelFileDigest = ''; | ||||
| 
 | ||||
| 	let uploadProgress = null; | ||||
| 	let uploadMessage = ''; | ||||
| 
 | ||||
| 	let deleteModelTag = ''; | ||||
| 
 | ||||
|  | @ -141,17 +134,11 @@ | |||
| 	const pullModelHandler = async () => { | ||||
| 		const sanitizedModelTag = modelTag.trim(); | ||||
| 		if (modelDownloadStatus[sanitizedModelTag]) { | ||||
| 			toast.error( | ||||
| 				$i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, { | ||||
| 					modelTag: sanitizedModelTag | ||||
| 				}) | ||||
| 			); | ||||
| 			toast.error(`Model '${sanitizedModelTag}' is already in queue for downloading.`); | ||||
| 			return; | ||||
| 		} | ||||
| 		if (Object.keys(modelDownloadStatus).length === 3) { | ||||
| 			toast.error( | ||||
| 				$i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.') | ||||
| 			); | ||||
| 			toast.error('Maximum of 3 models can be downloaded simultaneously. Please try again later.'); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
|  | @ -164,17 +151,15 @@ | |||
| 				// Remove the downloaded model | ||||
| 				delete modelDownloadStatus[modelName]; | ||||
| 
 | ||||
| 				modelDownloadStatus = { ...modelDownloadStatus }; | ||||
| 				console.log(data); | ||||
| 
 | ||||
| 				if (!data.success) { | ||||
| 					toast.error(data.error); | ||||
| 				} else { | ||||
| 					toast.success( | ||||
| 						$i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }) | ||||
| 					); | ||||
| 					toast.success(`Model '${modelName}' has been successfully downloaded.`); | ||||
| 
 | ||||
| 					const notification = new Notification($WEBUI_NAME, { | ||||
| 						body: $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { modelName }), | ||||
| 						body: `Model '${modelName}' has been successfully downloaded.`, | ||||
| 						icon: `${WEBUI_BASE_URL}/static/favicon.png` | ||||
| 					}); | ||||
| 
 | ||||
|  | @ -189,32 +174,35 @@ | |||
| 
 | ||||
| 	const uploadModelHandler = async () => { | ||||
| 		modelTransferring = true; | ||||
| 		uploadProgress = 0; | ||||
| 
 | ||||
| 		let uploaded = false; | ||||
| 		let fileResponse = null; | ||||
| 		let name = ''; | ||||
| 
 | ||||
| 		if (modelUploadMode === 'file') { | ||||
| 			const file = modelInputFile ? modelInputFile[0] : null; | ||||
| 			const file = modelInputFile[0]; | ||||
| 			const formData = new FormData(); | ||||
| 			formData.append('file', file); | ||||
| 
 | ||||
| 			if (file) { | ||||
| 				uploadMessage = 'Uploading...'; | ||||
| 
 | ||||
| 				fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch( | ||||
| 					(error) => { | ||||
| 						toast.error(error); | ||||
| 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/upload`, { | ||||
| 				method: 'POST', | ||||
| 				headers: { | ||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 				}, | ||||
| 				body: formData | ||||
| 			}).catch((error) => { | ||||
| 				console.log(error); | ||||
| 				return null; | ||||
| 					} | ||||
| 				); | ||||
| 			} | ||||
| 			}); | ||||
| 		} else { | ||||
| 			uploadProgress = 0; | ||||
| 			fileResponse = await downloadModel( | ||||
| 				localStorage.token, | ||||
| 				modelFileUrl, | ||||
| 				selectedOllamaUrlIdx | ||||
| 			).catch((error) => { | ||||
| 				toast.error(error); | ||||
| 			fileResponse = await fetch(`${WEBUI_API_BASE_URL}/utils/download?url=${modelFileUrl}`, { | ||||
| 				method: 'GET', | ||||
| 				headers: { | ||||
| 					...($user && { Authorization: `Bearer ${localStorage.token}` }) | ||||
| 				} | ||||
| 			}).catch((error) => { | ||||
| 				console.log(error); | ||||
| 				return null; | ||||
| 			}); | ||||
| 		} | ||||
|  | @ -237,9 +225,6 @@ | |||
| 							let data = JSON.parse(line.replace(/^data: /, '')); | ||||
| 
 | ||||
| 							if (data.progress) { | ||||
| 								if (uploadMessage) { | ||||
| 									uploadMessage = ''; | ||||
| 								} | ||||
| 								uploadProgress = data.progress; | ||||
| 							} | ||||
| 
 | ||||
|  | @ -258,9 +243,6 @@ | |||
| 					console.log(error); | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			const error = await fileResponse?.json(); | ||||
| 			toast.error(error?.detail ?? error); | ||||
| 		} | ||||
| 
 | ||||
| 		if (uploaded) { | ||||
|  | @ -326,11 +308,7 @@ | |||
| 		} | ||||
| 
 | ||||
| 		modelFileUrl = ''; | ||||
| 
 | ||||
| 		if (modelUploadInputElement) { | ||||
| 			modelUploadInputElement.value = ''; | ||||
| 		} | ||||
| 		modelInputFile = null; | ||||
| 		modelInputFile = ''; | ||||
| 		modelTransferring = false; | ||||
| 		uploadProgress = null; | ||||
| 
 | ||||
|  | @ -345,7 +323,7 @@ | |||
| 		); | ||||
| 
 | ||||
| 		if (res) { | ||||
| 			toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag })); | ||||
| 			toast.success(`Deleted ${deleteModelTag}`); | ||||
| 		} | ||||
| 
 | ||||
| 		deleteModelTag = ''; | ||||
|  | @ -376,24 +354,12 @@ | |||
| 					for (const line of lines) { | ||||
| 						if (line !== '') { | ||||
| 							let data = JSON.parse(line); | ||||
| 							console.log(data); | ||||
| 							if (data.error) { | ||||
| 								throw data.error; | ||||
| 							} | ||||
| 							if (data.detail) { | ||||
| 								throw data.detail; | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.id) { | ||||
| 								modelDownloadStatus[opts.modelName] = { | ||||
| 									...modelDownloadStatus[opts.modelName], | ||||
| 									requestId: data.id, | ||||
| 									reader, | ||||
| 									done: false | ||||
| 								}; | ||||
| 								console.log(data); | ||||
| 							} | ||||
| 
 | ||||
| 							if (data.status) { | ||||
| 								if (data.digest) { | ||||
| 									let downloadProgress = 0; | ||||
|  | @ -403,17 +369,11 @@ | |||
| 										downloadProgress = 100; | ||||
| 									} | ||||
| 									modelDownloadStatus[opts.modelName] = { | ||||
| 										...modelDownloadStatus[opts.modelName], | ||||
| 										pullProgress: downloadProgress, | ||||
| 										digest: data.digest | ||||
| 									}; | ||||
| 								} else { | ||||
| 									toast.success(data.status); | ||||
| 
 | ||||
| 									modelDownloadStatus[opts.modelName] = { | ||||
| 										...modelDownloadStatus[opts.modelName], | ||||
| 										done: data.status === 'success' | ||||
| 									}; | ||||
| 								} | ||||
| 							} | ||||
| 						} | ||||
|  | @ -426,14 +386,7 @@ | |||
| 					opts.callback({ success: false, error, modelName: opts.modelName }); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			console.log(modelDownloadStatus[opts.modelName]); | ||||
| 
 | ||||
| 			if (modelDownloadStatus[opts.modelName].done) { | ||||
| 			opts.callback({ success: true, modelName: opts.modelName }); | ||||
| 			} else { | ||||
| 				opts.callback({ success: false, error: 'Download canceled', modelName: opts.modelName }); | ||||
| 			} | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -457,7 +410,7 @@ | |||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 			toast.error($i18n.t(`Model {{modelName}} already exists.`, { modelName: liteLLMModelName })); | ||||
| 			toast.error(`Model ${liteLLMModelName} already exists.`); | ||||
| 		} | ||||
| 
 | ||||
| 		liteLLMModelName = ''; | ||||
|  | @ -503,25 +456,13 @@ | |||
| 		ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); | ||||
| 		liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token); | ||||
| 	}); | ||||
| 
 | ||||
| 	const cancelModelPullHandler = async (model: string) => { | ||||
| 		const { reader, requestId } = modelDownloadStatus[model]; | ||||
| 		if (reader) { | ||||
| 			await reader.cancel(); | ||||
| 
 | ||||
| 			await cancelOllamaRequest(localStorage.token, requestId); | ||||
| 			delete modelDownloadStatus[model]; | ||||
| 			await deleteModel(localStorage.token, model); | ||||
| 			toast.success(`${model} download has been canceled`); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-col h-full justify-between text-sm"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[24rem]"> | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]"> | ||||
| 		{#if ollamaVersion} | ||||
| 			<div class="space-y-2 pr-1.5"> | ||||
| 				<div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> | ||||
| 				<div class="text-sm font-medium">Manage Ollama Models</div> | ||||
| 
 | ||||
| 				{#if OLLAMA_URLS.length > 0} | ||||
| 					<div class="flex gap-2"> | ||||
|  | @ -529,7 +470,7 @@ | |||
| 							<select | ||||
| 								class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 								bind:value={selectedOllamaUrlIdx} | ||||
| 								placeholder={$i18n.t('Select an Ollama instance')} | ||||
| 								placeholder="Select an Ollama instance" | ||||
| 							> | ||||
| 								{#each OLLAMA_URLS as url, idx} | ||||
| 									<option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> | ||||
|  | @ -572,14 +513,12 @@ | |||
| 
 | ||||
| 				<div class="space-y-2"> | ||||
| 					<div> | ||||
| 						<div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div> | ||||
| 						<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div> | ||||
| 						<div class="flex w-full"> | ||||
| 							<div class="flex-1 mr-2"> | ||||
| 								<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('Enter model tag (e.g. {{modelTag}})', { | ||||
| 										modelTag: 'mistral:7b' | ||||
| 									})} | ||||
| 									placeholder="Enter model tag (e.g. mistral:7b)" | ||||
| 									bind:value={modelTag} | ||||
| 								/> | ||||
| 							</div> | ||||
|  | @ -634,84 +573,47 @@ | |||
| 							</button> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div> | ||||
| 							<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 							{$i18n.t('To access the available model names for downloading,')} | ||||
| 							<a | ||||
| 								To access the available model names for downloading, <a | ||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium underline" | ||||
| 									href="https://ollama.com/library" | ||||
| 								target="_blank">{$i18n.t('click here.')}</a | ||||
| 									target="_blank">click here.</a | ||||
| 								> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 
 | ||||
| 						{#if Object.keys(modelDownloadStatus).length > 0} | ||||
| 							{#each Object.keys(modelDownloadStatus) as model} | ||||
| 								{#if 'pullProgress' in modelDownloadStatus[model]} | ||||
| 								<div class="flex flex-col"> | ||||
| 									<div class="font-medium mb-1">{model}</div> | ||||
| 									<div class=""> | ||||
| 											<div class="flex flex-row justify-between space-x-4 pr-2"> | ||||
| 												<div class=" flex-1"> | ||||
| 										<div | ||||
| 											class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" | ||||
| 														style="width: {Math.max( | ||||
| 															15, | ||||
| 															modelDownloadStatus[model].pullProgress ?? 0 | ||||
| 														)}%" | ||||
| 											style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%" | ||||
| 										> | ||||
| 											{modelDownloadStatus[model].pullProgress ?? 0}% | ||||
| 										</div> | ||||
| 												</div> | ||||
| 
 | ||||
| 												<Tooltip content="Cancel"> | ||||
| 													<button | ||||
| 														class="text-gray-800 dark:text-gray-100" | ||||
| 														on:click={() => { | ||||
| 															cancelModelPullHandler(model); | ||||
| 														}} | ||||
| 													> | ||||
| 														<svg | ||||
| 															class="w-4 h-4 text-gray-800 dark:text-white" | ||||
| 															aria-hidden="true" | ||||
| 															xmlns="http://www.w3.org/2000/svg" | ||||
| 															width="24" | ||||
| 															height="24" | ||||
| 															fill="currentColor" | ||||
| 															viewBox="0 0 24 24" | ||||
| 														> | ||||
| 															<path | ||||
| 																stroke="currentColor" | ||||
| 																stroke-linecap="round" | ||||
| 																stroke-linejoin="round" | ||||
| 																stroke-width="2" | ||||
| 																d="M6 18 17.94 6M18 18 6.06 6" | ||||
| 															/> | ||||
| 														</svg> | ||||
| 													</button> | ||||
| 												</Tooltip> | ||||
| 											</div> | ||||
| 											{#if 'digest' in modelDownloadStatus[model]} | ||||
| 										<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||
| 											{modelDownloadStatus[model].digest} | ||||
| 										</div> | ||||
| 											{/if} | ||||
| 									</div> | ||||
| 								</div> | ||||
| 								{/if} | ||||
| 							{/each} | ||||
| 						{/if} | ||||
| 					</div> | ||||
| 
 | ||||
| 					<div> | ||||
| 						<div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div> | ||||
| 						<div class=" mb-2 text-sm font-medium">Delete a model</div> | ||||
| 						<div class="flex w-full"> | ||||
| 							<div class="flex-1 mr-2"> | ||||
| 								<select | ||||
| 									class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 									bind:value={deleteModelTag} | ||||
| 									placeholder={$i18n.t('Select a model')} | ||||
| 									placeholder="Select a model" | ||||
| 								> | ||||
| 									{#if !deleteModelTag} | ||||
| 										<option value="" disabled selected>{$i18n.t('Select a model')}</option> | ||||
| 										<option value="" disabled selected>Select a model</option> | ||||
| 									{/if} | ||||
| 									{#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" | ||||
|  | @ -744,13 +646,13 @@ | |||
| 
 | ||||
| 					<div class="pt-1"> | ||||
| 						<div class="flex justify-between items-center text-xs"> | ||||
| 							<div class=" text-sm font-medium">{$i18n.t('Experimental')}</div> | ||||
| 							<div class=" text-sm font-medium">Experimental</div> | ||||
| 							<button | ||||
| 								class=" text-xs font-medium text-gray-500" | ||||
| 								type="button" | ||||
| 								on:click={() => { | ||||
| 									showExperimentalOllama = !showExperimentalOllama; | ||||
| 								}}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||
| 								}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button | ||||
| 							> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | @ -762,7 +664,7 @@ | |||
| 							}} | ||||
| 						> | ||||
| 							<div class=" mb-2 flex w-full justify-between"> | ||||
| 								<div class="  text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div> | ||||
| 								<div class="  text-sm font-medium">Upload a GGUF model</div> | ||||
| 
 | ||||
| 								<button | ||||
| 									class="p-1 px-3 text-xs flex rounded transition" | ||||
|  | @ -776,9 +678,9 @@ | |||
| 									type="button" | ||||
| 								> | ||||
| 									{#if modelUploadMode === 'file'} | ||||
| 										<span class="ml-2 self-center">{$i18n.t('File Mode')}</span> | ||||
| 										<span class="ml-2 self-center">File Mode</span> | ||||
| 									{:else} | ||||
| 										<span class="ml-2 self-center">{$i18n.t('URL Mode')}</span> | ||||
| 										<span class="ml-2 self-center">URL Mode</span> | ||||
| 									{/if} | ||||
| 								</button> | ||||
| 							</div> | ||||
|  | @ -802,7 +704,7 @@ | |||
| 
 | ||||
| 											<button | ||||
| 												type="button" | ||||
| 												class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850" | ||||
| 												class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850" | ||||
| 												on:click={() => { | ||||
| 													modelUploadInputElement.click(); | ||||
| 												}} | ||||
|  | @ -810,21 +712,21 @@ | |||
| 												{#if modelInputFile && modelInputFile.length > 0} | ||||
| 													{modelInputFile[0].name} | ||||
| 												{:else} | ||||
| 													{$i18n.t('Click here to select')} | ||||
| 													Click here to select | ||||
| 												{/if} | ||||
| 											</button> | ||||
| 										</div> | ||||
| 									{:else} | ||||
| 										<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> | ||||
| 											<input | ||||
| 												class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== | ||||
| 												class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== | ||||
| 												'' | ||||
| 													? 'mr-2' | ||||
| 													: ''}" | ||||
| 												type="url" | ||||
| 												required | ||||
| 												bind:value={modelFileUrl} | ||||
| 												placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')} | ||||
| 												placeholder="Type Hugging Face Resolve (Download) URL" | ||||
| 											/> | ||||
| 										</div> | ||||
| 									{/if} | ||||
|  | @ -832,7 +734,7 @@ | |||
| 
 | ||||
| 								{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 									<button | ||||
| 										class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition" | ||||
| 										class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" | ||||
| 										type="submit" | ||||
| 										disabled={modelTransferring} | ||||
| 									> | ||||
|  | @ -884,43 +786,26 @@ | |||
| 							{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} | ||||
| 								<div> | ||||
| 									<div> | ||||
| 										<div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div> | ||||
| 										<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> | ||||
| 										<textarea | ||||
| 											bind:value={modelFileContent} | ||||
| 											class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" | ||||
| 											class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||
| 											rows="6" | ||||
| 										/> | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							{/if} | ||||
| 							<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 								{$i18n.t('To access the GGUF models available for downloading,')} | ||||
| 								<a | ||||
| 								To access the GGUF models available for downloading, <a | ||||
| 									class=" text-gray-500 dark:text-gray-300 font-medium underline" | ||||
| 									href="https://huggingface.co/models?search=gguf" | ||||
| 									target="_blank">{$i18n.t('click here.')}</a | ||||
| 									target="_blank">click here.</a | ||||
| 								> | ||||
| 							</div> | ||||
| 
 | ||||
| 							{#if uploadMessage} | ||||
| 							{#if uploadProgress !== null} | ||||
| 								<div class="mt-2"> | ||||
| 									<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> | ||||
| 
 | ||||
| 									<div class="w-full rounded-full dark:bg-gray-800"> | ||||
| 										<div | ||||
| 											class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" | ||||
| 											style="width: 100%" | ||||
| 										> | ||||
| 											{uploadMessage} | ||||
| 										</div> | ||||
| 									</div> | ||||
| 									<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> | ||||
| 										{modelFileDigest} | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							{:else if uploadProgress !== null} | ||||
| 								<div class="mt-2"> | ||||
| 									<div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> | ||||
| 									<div class=" mb-2 text-xs">Upload Progress</div> | ||||
| 
 | ||||
| 									<div class="w-full rounded-full dark:bg-gray-800"> | ||||
| 										<div | ||||
|  | @ -947,13 +832,13 @@ | |||
| 				<div> | ||||
| 					<div class="mb-2"> | ||||
| 						<div class="flex justify-between items-center text-xs"> | ||||
| 							<div class=" text-sm font-medium">{$i18n.t('Manage LiteLLM Models')}</div> | ||||
| 							<div class=" text-sm font-medium">Manage LiteLLM Models</div> | ||||
| 							<button | ||||
| 								class=" text-xs font-medium text-gray-500" | ||||
| 								type="button" | ||||
| 								on:click={() => { | ||||
| 									showLiteLLM = !showLiteLLM; | ||||
| 								}}>{showLiteLLM ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||
| 								}}>{showLiteLLM ? 'Hide' : 'Show'}</button | ||||
| 							> | ||||
| 						</div> | ||||
| 					</div> | ||||
|  | @ -961,16 +846,14 @@ | |||
| 					{#if showLiteLLM} | ||||
| 						<div> | ||||
| 							<div class="flex justify-between items-center text-xs"> | ||||
| 								<div class=" text-sm font-medium">{$i18n.t('Add a model')}</div> | ||||
| 								<div class=" text-sm font-medium">Add a model</div> | ||||
| 								<button | ||||
| 									class=" text-xs font-medium text-gray-500" | ||||
| 									type="button" | ||||
| 									on:click={() => { | ||||
| 										showLiteLLMParams = !showLiteLLMParams; | ||||
| 									}} | ||||
| 									>{showLiteLLMParams | ||||
| 										? $i18n.t('Hide Additional Params') | ||||
| 										: $i18n.t('Show Additional Params')}</button | ||||
| 									>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button | ||||
| 								> | ||||
| 							</div> | ||||
| 						</div> | ||||
|  | @ -980,7 +863,7 @@ | |||
| 								<div class="flex-1 mr-2"> | ||||
| 									<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('Enter LiteLLM Model (litellm_params.model)')} | ||||
| 										placeholder="Enter LiteLLM Model (litellm_params.model)" | ||||
| 										bind:value={liteLLMModel} | ||||
| 										autocomplete="off" | ||||
| 									/> | ||||
|  | @ -1007,7 +890,7 @@ | |||
| 
 | ||||
| 							{#if showLiteLLMParams} | ||||
| 								<div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Model Name')}</div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">Model Name</div> | ||||
| 									<div class="flex w-full"> | ||||
| 										<div class="flex-1"> | ||||
| 											<input | ||||
|  | @ -1021,14 +904,12 @@ | |||
| 								</div> | ||||
| 
 | ||||
| 								<div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Base URL')}</div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">API Base URL</div> | ||||
| 									<div class="flex w-full"> | ||||
| 										<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( | ||||
| 													'Enter LiteLLM API Base URL (litellm_params.api_base)' | ||||
| 												)} | ||||
| 												placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)" | ||||
| 												bind:value={liteLLMAPIBase} | ||||
| 												autocomplete="off" | ||||
| 											/> | ||||
|  | @ -1037,12 +918,12 @@ | |||
| 								</div> | ||||
| 
 | ||||
| 								<div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">{$i18n.t('API Key')}</div> | ||||
| 									<div class=" mb-1.5 text-sm font-medium">API Key</div> | ||||
| 									<div class="flex w-full"> | ||||
| 										<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('Enter LiteLLM API Key (litellm_params.api_key)')} | ||||
| 												placeholder="Enter LiteLLM API Key (litellm_params.api_key)" | ||||
| 												bind:value={liteLLMAPIKey} | ||||
| 												autocomplete="off" | ||||
| 											/> | ||||
|  | @ -1051,12 +932,12 @@ | |||
| 								</div> | ||||
| 
 | ||||
| 								<div> | ||||
| 									<div class="mb-1.5 text-sm font-medium">{$i18n.t('API RPM')}</div> | ||||
| 									<div class="mb-1.5 text-sm font-medium">API RPM</div> | ||||
| 									<div class="flex w-full"> | ||||
| 										<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('Enter LiteLLM API RPM (litellm_params.rpm)')} | ||||
| 												placeholder="Enter LiteLLM API RPM (litellm_params.rpm)" | ||||
| 												bind:value={liteLLMRPM} | ||||
| 												autocomplete="off" | ||||
| 											/> | ||||
|  | @ -1065,12 +946,12 @@ | |||
| 								</div> | ||||
| 
 | ||||
| 								<div> | ||||
| 									<div class="mb-1.5 text-sm font-medium">{$i18n.t('Max Tokens')}</div> | ||||
| 									<div class="mb-1.5 text-sm font-medium">Max Tokens</div> | ||||
| 									<div class="flex w-full"> | ||||
| 										<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('Enter Max Tokens (litellm_params.max_tokens)')} | ||||
| 												placeholder="Enter Max Tokens (litellm_params.max_tokens)" | ||||
| 												bind:value={liteLLMMaxTokens} | ||||
| 												type="number" | ||||
| 												min="1" | ||||
|  | @ -1083,27 +964,27 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<div class="mb-2 text-xs text-gray-400 dark:text-gray-500"> | ||||
| 							{$i18n.t('Not sure what to add?')} | ||||
| 							Not sure what to add? | ||||
| 							<a | ||||
| 								class=" text-gray-300 font-medium underline" | ||||
| 								href="https://litellm.vercel.app/docs/proxy/configs#quick-start" | ||||
| 								target="_blank" | ||||
| 							> | ||||
| 								{$i18n.t('Click here for help.')} | ||||
| 								Click here for help. | ||||
| 							</a> | ||||
| 						</div> | ||||
| 
 | ||||
| 						<div> | ||||
| 							<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Delete a model')}</div> | ||||
| 							<div class=" mb-2.5 text-sm font-medium">Delete a model</div> | ||||
| 							<div class="flex w-full"> | ||||
| 								<div class="flex-1 mr-2"> | ||||
| 									<select | ||||
| 										class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||
| 										bind:value={deleteLiteLLMModelId} | ||||
| 										placeholder={$i18n.t('Select a model')} | ||||
| 										placeholder="Select a model" | ||||
| 									> | ||||
| 										{#if !deleteLiteLLMModelId} | ||||
| 											<option value="" disabled selected>{$i18n.t('Select a model')}</option> | ||||
| 											<option value="" disabled selected>Select a model</option> | ||||
| 										{/if} | ||||
| 										{#each liteLLMModelInfo as model} | ||||
| 											<option value={model.model_info.id} class="bg-gray-100 dark:bg-gray-700" | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { models, settings, user } from '$lib/stores'; | ||||
| 
 | ||||
|  | @ -18,8 +17,6 @@ | |||
| 	import Connections from './Settings/Connections.svelte'; | ||||
| 	import Images from './Settings/Images.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	const saveSettings = async (updated) => { | ||||
|  | @ -61,7 +58,7 @@ | |||
| <Modal bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Settings</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -109,7 +106,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('General')}</div> | ||||
| 					<div class=" self-center">General</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				{#if $user?.role === 'admin'} | ||||
|  | @ -134,7 +131,7 @@ | |||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 						<div class=" self-center">{$i18n.t('Connections')}</div> | ||||
| 						<div class=" self-center">Connections</div> | ||||
| 					</button> | ||||
| 
 | ||||
| 					<button | ||||
|  | @ -160,7 +157,7 @@ | |||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 						<div class=" self-center">{$i18n.t('Models')}</div> | ||||
| 						<div class=" self-center">Models</div> | ||||
| 					</button> | ||||
| 				{/if} | ||||
| 
 | ||||
|  | @ -187,7 +184,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Interface')}</div> | ||||
| 					<div class=" self-center">Interface</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
|  | @ -214,7 +211,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Audio')}</div> | ||||
| 					<div class=" self-center">Audio</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				{#if $user.role === 'admin'} | ||||
|  | @ -241,7 +238,7 @@ | |||
| 								/> | ||||
| 							</svg> | ||||
| 						</div> | ||||
| 						<div class=" self-center">{$i18n.t('Images')}</div> | ||||
| 						<div class=" self-center">Images</div> | ||||
| 					</button> | ||||
| 				{/if} | ||||
| 
 | ||||
|  | @ -268,7 +265,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Chats')}</div> | ||||
| 					<div class=" self-center">Chats</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
|  | @ -294,7 +291,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('Account')}</div> | ||||
| 					<div class=" self-center">Account</div> | ||||
| 				</button> | ||||
| 
 | ||||
| 				<button | ||||
|  | @ -320,16 +317,16 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('About')}</div> | ||||
| 					<div class=" self-center">About</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div class="flex-1 md:min-h-[25rem]"> | ||||
| 			<div class="flex-1 md:min-h-[380px]"> | ||||
| 				{#if selectedTab === 'general'} | ||||
| 					<General | ||||
| 						{getModels} | ||||
| 						{saveSettings} | ||||
| 						on:save={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'models'} | ||||
|  | @ -338,28 +335,28 @@ | |||
| 					<Connections | ||||
| 						{getModels} | ||||
| 						on:save={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'interface'} | ||||
| 					<Interface | ||||
| 						{saveSettings} | ||||
| 						on:save={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'audio'} | ||||
| 					<Audio | ||||
| 						{saveSettings} | ||||
| 						on:save={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'images'} | ||||
| 					<Images | ||||
| 						{saveSettings} | ||||
| 						on:save={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'chats'} | ||||
|  | @ -367,7 +364,7 @@ | |||
| 				{:else if selectedTab === 'account'} | ||||
| 					<Account | ||||
| 						saveHandler={() => { | ||||
| 							toast.success($i18n.t('Settings saved successfully!')); | ||||
| 							toast.success('Settings saved successfully!'); | ||||
| 						}} | ||||
| 					/> | ||||
| 				{:else if selectedTab === 'about'} | ||||
|  |  | |||
|  | @ -1,9 +1,6 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let downloadChat: Function; | ||||
| 	export let shareChat: Function; | ||||
| 
 | ||||
|  | @ -20,11 +17,11 @@ | |||
| 				show = false; | ||||
| 			}} | ||||
| 		> | ||||
| 			{$i18n.t('Share to OpenWebUI Community')} | ||||
| 			Share to OpenWebUI Community | ||||
| 		</button> | ||||
| 
 | ||||
| 		<div class="flex justify-center space-x-1 mt-1.5"> | ||||
| 			<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div> | ||||
| 			<div class=" self-center text-gray-400 text-xs font-medium">or</div> | ||||
| 
 | ||||
| 			<button | ||||
| 				class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline" | ||||
|  | @ -34,7 +31,7 @@ | |||
| 					show = false; | ||||
| 				}} | ||||
| 			> | ||||
| 				{$i18n.t('Download as a File')} | ||||
| 				Download as a File | ||||
| 			</button> | ||||
| 		</div> | ||||
| 	</div> | ||||
|  |  | |||
|  | @ -1,16 +1,13 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| </script> | ||||
| 
 | ||||
| <Modal bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Keyboard shortcuts</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -35,7 +32,7 @@ | |||
| 			<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> | ||||
| 				<div class="flex flex-col space-y-3 w-full self-start"> | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Open new chat')}</div> | ||||
| 						<div class=" text-sm">Open new chat</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -59,7 +56,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Focus chat input')}</div> | ||||
| 						<div class=" text-sm">Focus chat input</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -77,7 +74,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Copy last code block')}</div> | ||||
| 						<div class=" text-sm">Copy last code block</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -101,7 +98,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Copy last response')}</div> | ||||
| 						<div class=" text-sm">Copy last response</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -127,7 +124,7 @@ | |||
| 
 | ||||
| 				<div class="flex flex-col space-y-3 w-full self-start"> | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Toggle settings')}</div> | ||||
| 						<div class=" text-sm">Toggle settings</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -144,7 +141,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Toggle sidebar')}</div> | ||||
| 						<div class=" text-sm">Toggle sidebar</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -168,7 +165,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Delete chat')}</div> | ||||
| 						<div class=" text-sm">Delete chat</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  | @ -191,7 +188,7 @@ | |||
| 					</div> | ||||
| 
 | ||||
| 					<div class="w-full flex justify-between items-center"> | ||||
| 						<div class=" text-sm">{$i18n.t('Show shortcuts')}</div> | ||||
| 						<div class=" text-sm">Show shortcuts</div> | ||||
| 
 | ||||
| 						<div class="flex space-x-1 text-xs"> | ||||
| 							<div | ||||
|  |  | |||
|  | @ -1,20 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 
 | ||||
| 	import Tags from '../common/Tags.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let tags; | ||||
| 	export let deleteTag: Function; | ||||
| 	export let addTag: Function; | ||||
| 
 | ||||
| 	export let show = false; | ||||
| </script> | ||||
| 
 | ||||
| <Modal bind:show size="xs"> | ||||
| 	<div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> | ||||
| 		<Tags {tags} {deleteTag} {addTag} /> | ||||
| 	</div> | ||||
| </Modal> | ||||
|  | @ -1,40 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { DropdownMenu } from 'bits-ui'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 
 | ||||
| 	import { flyAndScale } from '$lib/utils/transitions'; | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| </script> | ||||
| 
 | ||||
| <DropdownMenu.Root | ||||
| 	onOpenChange={(state) => { | ||||
| 		dispatch('change', state); | ||||
| 	}} | ||||
| > | ||||
| 	<DropdownMenu.Trigger> | ||||
| 		<slot /> | ||||
| 	</DropdownMenu.Trigger> | ||||
| 
 | ||||
| 	<slot name="content"> | ||||
| 		<DropdownMenu.Content | ||||
| 			class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white" | ||||
| 			sideOffset={8} | ||||
| 			side="bottom" | ||||
| 			align="start" | ||||
| 			transition={flyAndScale} | ||||
| 		> | ||||
| 			<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm  font-medium"> | ||||
| 				<div class="flex items-center">Profile</div> | ||||
| 			</DropdownMenu.Item> | ||||
| 
 | ||||
| 			<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm  font-medium"> | ||||
| 				<div class="flex items-center">Profile</div> | ||||
| 			</DropdownMenu.Item> | ||||
| 
 | ||||
| 			<DropdownMenu.Item class="flex items-center px-3 py-2 text-sm  font-medium"> | ||||
| 				<div class="flex items-center">Profile</div> | ||||
| 			</DropdownMenu.Item> | ||||
| 		</DropdownMenu.Content> | ||||
| 	</slot> | ||||
| </DropdownMenu.Root> | ||||
|  | @ -2,22 +2,6 @@ | |||
| 	export let show = false; | ||||
| 	export let src = ''; | ||||
| 	export let alt = ''; | ||||
| 
 | ||||
| 	const downloadImage = (url, filename) => { | ||||
| 		fetch(url) | ||||
| 			.then((response) => response.blob()) | ||||
| 			.then((blob) => { | ||||
| 				const objectUrl = window.URL.createObjectURL(blob); | ||||
| 				const link = document.createElement('a'); | ||||
| 				link.href = objectUrl; | ||||
| 				link.download = filename; | ||||
| 				document.body.appendChild(link); | ||||
| 				link.click(); | ||||
| 				document.body.removeChild(link); | ||||
| 				window.URL.revokeObjectURL(objectUrl); | ||||
| 			}) | ||||
| 			.catch((error) => console.error('Error downloading image:', error)); | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| {#if show} | ||||
|  | @ -51,7 +35,10 @@ | |||
| 				<button | ||||
| 					class=" p-5" | ||||
| 					on:click={() => { | ||||
| 						downloadImage(src, 'Image.png'); | ||||
| 						const a = document.createElement('a'); | ||||
| 						a.href = src; | ||||
| 						a.download = 'Image.png'; | ||||
| 						a.click(); | ||||
| 					}} | ||||
| 				> | ||||
| 					<svg | ||||
|  |  | |||
|  | @ -2,8 +2,6 @@ | |||
| 	import { onMount } from 'svelte'; | ||||
| 	import { fade } from 'svelte/transition'; | ||||
| 
 | ||||
| 	import { flyAndScale } from '$lib/utils/transitions'; | ||||
| 
 | ||||
| 	export let show = true; | ||||
| 	export let size = 'md'; | ||||
| 
 | ||||
|  | @ -43,10 +41,10 @@ | |||
| 		}} | ||||
| 	> | ||||
| 		<div | ||||
| 			class=" m-auto rounded-2xl max-w-full {sizeToWidth( | ||||
| 			class=" modal-content m-auto rounded-2xl max-w-full {sizeToWidth( | ||||
| 				size | ||||
| 			)} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl" | ||||
| 			in:flyAndScale | ||||
| 			in:fade={{ duration: 10 }} | ||||
| 			on:click={(e) => { | ||||
| 				e.stopPropagation(); | ||||
| 			}} | ||||
|  |  | |||
|  | @ -1,95 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { Select } from 'bits-ui'; | ||||
| 
 | ||||
| 	import { flyAndScale } from '$lib/utils/transitions'; | ||||
| 
 | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	import ChevronDown from '../icons/ChevronDown.svelte'; | ||||
| 	import Check from '../icons/Check.svelte'; | ||||
| 	import Search from '../icons/Search.svelte'; | ||||
| 
 | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	export let value = ''; | ||||
| 	export let placeholder = 'Select a model'; | ||||
| 	export let searchEnabled = true; | ||||
| 	export let searchPlaceholder = 'Search a model'; | ||||
| 
 | ||||
| 	export let items = [ | ||||
| 		{ value: 'mango', label: 'Mango' }, | ||||
| 		{ value: 'watermelon', label: 'Watermelon' }, | ||||
| 		{ value: 'apple', label: 'Apple' }, | ||||
| 		{ value: 'pineapple', label: 'Pineapple' }, | ||||
| 		{ value: 'orange', label: 'Orange' } | ||||
| 	]; | ||||
| 
 | ||||
| 	let searchValue = ''; | ||||
| 
 | ||||
| 	$: filteredItems = searchValue | ||||
| 		? items.filter((item) => item.value.includes(searchValue.toLowerCase())) | ||||
| 		: items; | ||||
| </script> | ||||
| 
 | ||||
| <Select.Root | ||||
| 	{items} | ||||
| 	onOpenChange={() => { | ||||
| 		searchValue = ''; | ||||
| 	}} | ||||
| 	selected={items.find((item) => item.value === value)} | ||||
| 	onSelectedChange={(selectedItem) => { | ||||
| 		value = selectedItem.value; | ||||
| 	}} | ||||
| > | ||||
| 	<Select.Trigger class="relative w-full" aria-label={placeholder}> | ||||
| 		<Select.Value | ||||
| 			class="inline-flex h-input px-0.5 w-full outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400  focus:outline-none" | ||||
| 			{placeholder} | ||||
| 		/> | ||||
| 		<ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> | ||||
| 	</Select.Trigger> | ||||
| 	<Select.Content | ||||
| 		class="w-full rounded-lg  bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/50  outline-none" | ||||
| 		transition={flyAndScale} | ||||
| 		sideOffset={4} | ||||
| 	> | ||||
| 		<slot> | ||||
| 			{#if searchEnabled} | ||||
| 				<div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3"> | ||||
| 					<Search className="size-4" strokeWidth="2.5" /> | ||||
| 
 | ||||
| 					<input | ||||
| 						bind:value={searchValue} | ||||
| 						class="w-full text-sm bg-transparent outline-none" | ||||
| 						placeholder={searchPlaceholder} | ||||
| 					/> | ||||
| 				</div> | ||||
| 
 | ||||
| 				<hr class="border-gray-100 dark:border-gray-800" /> | ||||
| 			{/if} | ||||
| 
 | ||||
| 			<div class="px-3 my-2 max-h-80 overflow-y-auto"> | ||||
| 				{#each filteredItems as item} | ||||
| 					<Select.Item | ||||
| 						class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm  text-gray-700 dark:text-gray-100  outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted" | ||||
| 						value={item.value} | ||||
| 						label={item.label} | ||||
| 					> | ||||
| 						{item.label} | ||||
| 
 | ||||
| 						{#if value === item.value} | ||||
| 							<div class="ml-auto"> | ||||
| 								<Check /> | ||||
| 							</div> | ||||
| 						{/if} | ||||
| 					</Select.Item> | ||||
| 				{:else} | ||||
| 					<div> | ||||
| 						<div class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100"> | ||||
| 							No results found | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 		</slot> | ||||
| 	</Select.Content> | ||||
| </Select.Root> | ||||
|  | @ -8,7 +8,7 @@ | |||
| 	export let addTag: Function; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex flex-row flex-wrap gap-0.5 line-clamp-1"> | ||||
| <div class="flex flex-row space-x-0.5 line-clamp-1"> | ||||
| 	<TagList | ||||
| 		{tags} | ||||
| 		on:delete={(e) => { | ||||
|  | @ -17,7 +17,6 @@ | |||
| 	/> | ||||
| 
 | ||||
| 	<TagInput | ||||
| 		label={tags.length == 0 ? 'Add Tags' : ''} | ||||
| 		on:add={(e) => { | ||||
| 			addTag(e.detail); | ||||
| 		}} | ||||
|  |  | |||
|  | @ -1,31 +1,28 @@ | |||
| <script lang="ts"> | ||||
| 	import { createEventDispatcher, getContext } from 'svelte'; | ||||
| 	import { tags } from '$lib/stores'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	const dispatch = createEventDispatcher(); | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let label = ''; | ||||
| 	let showTagInput = false; | ||||
| 	let tagName = ''; | ||||
| 
 | ||||
| 	const addTagHandler = async () => { | ||||
| 		tagName = tagName.trim(); | ||||
| 		if (tagName !== '') { | ||||
| 			dispatch('add', tagName); | ||||
| 			tagName = ''; | ||||
| 			showTagInput = false; | ||||
| 		} else { | ||||
| 			toast.error('Invalid Tag'); | ||||
| 		} | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex space-x-1 pl-1.5"> | ||||
| 	{#if showTagInput} | ||||
| 		<div class="flex items-center"> | ||||
| 			<button type="button" on:click={addTagHandler}> | ||||
| 			<input | ||||
| 				bind:value={tagName} | ||||
| 				class=" cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[4rem]" | ||||
| 				placeholder="Add a tag" | ||||
| 			/> | ||||
| 
 | ||||
| 			<button | ||||
| 				type="button" | ||||
| 				on:click={() => { | ||||
| 					dispatch('add', tagName); | ||||
| 					tagName = ''; | ||||
| 					showTagInput = false; | ||||
| 				}} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 16 16" | ||||
|  | @ -39,23 +36,9 @@ | |||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 			<input | ||||
| 				bind:value={tagName} | ||||
| 				class=" pl-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]" | ||||
| 				placeholder={$i18n.t('Add a tag')} | ||||
| 				list="tagOptions" | ||||
| 				on:keydown={(event) => { | ||||
| 					if (event.key === 'Enter') { | ||||
| 						addTagHandler(); | ||||
| 					} | ||||
| 				}} | ||||
| 			/> | ||||
| 			<datalist id="tagOptions"> | ||||
| 				{#each $tags as tag} | ||||
| 					<option value={tag.name} /> | ||||
| 				{/each} | ||||
| 			</datalist> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<!-- TODO: Tag Suggestions --> | ||||
| 	{/if} | ||||
| 
 | ||||
| 	<button | ||||
|  | @ -78,8 +61,4 @@ | |||
| 			</svg> | ||||
| 		</div> | ||||
| 	</button> | ||||
| 
 | ||||
| 	{#if label && !showTagInput} | ||||
| 		<span class="text-xs pl-1.5 self-center">{label}</span> | ||||
| 	{/if} | ||||
| </div> | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ | |||
| 
 | ||||
| {#each tags as tag} | ||||
| 	<div | ||||
| 		class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white" | ||||
| 		class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition border dark:border-gray-600 dark:text-white" | ||||
| 	> | ||||
| 		<div class=" text-[0.7rem] font-medium self-center line-clamp-1"> | ||||
| 			{tag.name} | ||||
|  |  | |||
|  | @ -29,6 +29,6 @@ | |||
| 	}); | ||||
| </script> | ||||
| 
 | ||||
| <div bind:this={tooltipElement} aria-label={content} class="flex"> | ||||
| <div bind:this={tooltipElement} aria-label={content}> | ||||
| 	<slot /> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import dayjs from 'dayjs'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
|  | @ -13,8 +13,6 @@ | |||
| 	import { transformFileName } from '$lib/utils'; | ||||
| 	import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 	export let selectedDoc; | ||||
| 	let uploadDocInputElement: HTMLInputElement; | ||||
|  | @ -73,7 +71,7 @@ | |||
| 			inputFiles = null; | ||||
| 			uploadDocInputElement.value = ''; | ||||
| 		} else { | ||||
| 			toast.error($i18n.t(`File not found.`)); | ||||
| 			toast.error(`File not found.`); | ||||
| 		} | ||||
| 
 | ||||
| 		show = false; | ||||
|  | @ -98,7 +96,7 @@ | |||
| <Modal size="sm" bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Add Docs</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -138,7 +136,7 @@ | |||
| 						/> | ||||
| 
 | ||||
| 						<button | ||||
| 							class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark: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" | ||||
| 							on:click={() => { | ||||
| 								uploadDocInputElement.click(); | ||||
|  | @ -147,14 +145,14 @@ | |||
| 							{#if inputFiles} | ||||
| 								{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. | ||||
| 							{:else} | ||||
| 								{$i18n.t('Click here to select documents.')} | ||||
| 								Click here to select documents. | ||||
| 							{/if} | ||||
| 						</button> | ||||
| 					</div> | ||||
| 
 | ||||
| 					<div class=" flex flex-col space-y-1.5"> | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div> | ||||
| 							<div class=" mb-1.5 text-xs text-gray-500">Tags</div> | ||||
| 
 | ||||
| 							<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> | ||||
| 						</div> | ||||
|  | @ -165,7 +163,7 @@ | |||
| 							class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 							type="submit" | ||||
| 						> | ||||
| 							{$i18n.t('Save')} | ||||
| 							Save | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</form> | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| <script lang="ts"> | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import dayjs from 'dayjs'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 
 | ||||
| 	import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
|  | @ -10,8 +10,6 @@ | |||
| 	import Tags from '../common/Tags.svelte'; | ||||
| 	import { addTagById } from '$lib/apis/chats'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 	export let selectedDoc; | ||||
| 
 | ||||
|  | @ -76,7 +74,7 @@ | |||
| <Modal size="sm" bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Edit Doc</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -107,7 +105,7 @@ | |||
| 				> | ||||
| 					<div class=" flex flex-col space-y-1.5"> | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">Name Tag</div> | ||||
| 
 | ||||
| 							<div class="flex flex-1"> | ||||
| 								<div | ||||
|  | @ -136,7 +134,7 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div> | ||||
| 							<div class=" mb-1 text-xs text-gray-500">Title</div> | ||||
| 
 | ||||
| 							<div class="flex-1"> | ||||
| 								<input | ||||
|  | @ -150,7 +148,7 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<div class="flex flex-col w-full"> | ||||
| 							<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div> | ||||
| 							<div class=" mb-1.5 text-xs text-gray-500">Tags</div> | ||||
| 
 | ||||
| 							<Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> | ||||
| 						</div> | ||||
|  | @ -161,7 +159,7 @@ | |||
| 							class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 							type="submit" | ||||
| 						> | ||||
| 							{$i18n.t('Save')} | ||||
| 							Save | ||||
| 						</button> | ||||
| 					</div> | ||||
| 				</form> | ||||
|  |  | |||
|  | @ -5,22 +5,16 @@ | |||
| 		updateRAGConfig, | ||||
| 		getQuerySettings, | ||||
| 		scanDocs, | ||||
| 		updateQuerySettings, | ||||
| 		resetVectorDB | ||||
| 		updateQuerySettings | ||||
| 	} from '$lib/apis/rag'; | ||||
| 
 | ||||
| 	import { documents } from '$lib/stores'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { onMount } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let saveHandler: Function; | ||||
| 
 | ||||
| 	let loading = false; | ||||
| 
 | ||||
| 	let showResetConfirm = false; | ||||
| 
 | ||||
| 	let chunkSize = 0; | ||||
| 	let chunkOverlap = 0; | ||||
| 	let pdfExtractImages = true; | ||||
|  | @ -37,7 +31,7 @@ | |||
| 
 | ||||
| 		if (res) { | ||||
| 			await documents.set(await getDocs(localStorage.token)); | ||||
| 			toast.success($i18n.t('Scan complete!')); | ||||
| 			toast.success('Scan complete!'); | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
|  | @ -75,12 +69,10 @@ | |||
| > | ||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | ||||
| 		<div> | ||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | ||||
| 			<div class=" mb-2 text-sm font-medium">General Settings</div> | ||||
| 
 | ||||
| 			<div class="  flex w-full justify-between"> | ||||
| 				<div class=" self-center text-xs font-medium"> | ||||
| 					{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })} | ||||
| 				</div> | ||||
| 				<div class=" self-center text-xs font-medium">Scan for documents from '/data/docs'</div> | ||||
| 
 | ||||
| 				<button | ||||
| 					class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded flex flex-row space-x-1 items-center {loading | ||||
|  | @ -93,7 +85,7 @@ | |||
| 					type="button" | ||||
| 					disabled={loading} | ||||
| 				> | ||||
| 					<div class="self-center font-medium">{$i18n.t('Scan')}</div> | ||||
| 					<div class="self-center font-medium">Scan</div> | ||||
| 
 | ||||
| 					<!-- <svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
|  | @ -141,18 +133,19 @@ | |||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		<div class=" "> | ||||
| 			<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> | ||||
| 		<div class=" space-y-3"> | ||||
| 			<div class=" space-y-3"> | ||||
| 				<div class=" text-sm font-medium">Chunk Params</div> | ||||
| 
 | ||||
| 			<div class=" flex"> | ||||
| 				<div class="  flex w-full justify-between"> | ||||
| 					<div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div> | ||||
| 				<div class=" flex gap-2"> | ||||
| 					<div class="  flex w-full justify-between gap-2"> | ||||
| 						<div class="self-center text-xs font-medium min-w-fit">Chunk Size</div> | ||||
| 
 | ||||
| 					<div class="self-center p-3"> | ||||
| 						<div class="self-center"> | ||||
| 							<input | ||||
| 								class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 								type="number" | ||||
| 							placeholder={$i18n.t('Enter Chunk Size')} | ||||
| 								placeholder="Enter Chunk Size" | ||||
| 								bind:value={chunkSize} | ||||
| 								autocomplete="off" | ||||
| 								min="0" | ||||
|  | @ -160,14 +153,14 @@ | |||
| 						</div> | ||||
| 					</div> | ||||
| 
 | ||||
| 				<div class="flex w-full"> | ||||
| 					<div class=" self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Overlap')}</div> | ||||
| 					<div class="flex w-full gap-2"> | ||||
| 						<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div> | ||||
| 
 | ||||
| 					<div class="self-center p-3"> | ||||
| 						<div class="self-center"> | ||||
| 							<input | ||||
| 								class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 								type="number" | ||||
| 							placeholder={$i18n.t('Enter Chunk Overlap')} | ||||
| 								placeholder="Enter Chunk Overlap" | ||||
| 								bind:value={chunkOverlap} | ||||
| 								autocomplete="off" | ||||
| 								min="0" | ||||
|  | @ -178,31 +171,31 @@ | |||
| 
 | ||||
| 				<div> | ||||
| 					<div class="flex justify-between items-center text-xs"> | ||||
| 					<div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div> | ||||
| 						<div class=" text-xs font-medium">PDF Extract Images (OCR)</div> | ||||
| 
 | ||||
| 						<button | ||||
| 							class=" text-xs font-medium text-gray-500" | ||||
| 							type="button" | ||||
| 							on:click={() => { | ||||
| 								pdfExtractImages = !pdfExtractImages; | ||||
| 						}}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button | ||||
| 							}}>{pdfExtractImages ? 'On' : 'Off'}</button | ||||
| 						> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 			<div> | ||||
| 			<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> | ||||
| 				<div class=" text-sm font-medium">Query Params</div> | ||||
| 
 | ||||
| 			<div class=" flex"> | ||||
| 				<div class="  flex w-full justify-between"> | ||||
| 					<div class="self-center text-xs font-medium flex-1">{$i18n.t('Top K')}</div> | ||||
| 				<div class=" flex py-2"> | ||||
| 					<div class="  flex w-full justify-between gap-2"> | ||||
| 						<div class="self-center text-xs font-medium flex-1">Top K</div> | ||||
| 
 | ||||
| 					<div class="self-center p-3"> | ||||
| 						<div class="self-center"> | ||||
| 							<input | ||||
| 								class=" w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600" | ||||
| 								type="number" | ||||
| 							placeholder={$i18n.t('Enter Top K')} | ||||
| 								placeholder="Enter Top K" | ||||
| 								bind:value={querySettings.k} | ||||
| 								autocomplete="off" | ||||
| 								min="0" | ||||
|  | @ -227,7 +220,7 @@ | |||
| 				</div> | ||||
| 
 | ||||
| 				<div> | ||||
| 				<div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div> | ||||
| 					<div class=" mb-2.5 text-sm font-medium">RAG Template</div> | ||||
| 					<textarea | ||||
| 						bind:value={querySettings.template} | ||||
| 						class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | ||||
|  | @ -235,100 +228,7 @@ | |||
| 					/> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700" /> | ||||
| 
 | ||||
| 		{#if showResetConfirm} | ||||
| 			<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> | ||||
| 				<div class="flex items-center space-x-3"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 16 16" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 					<span>{$i18n.t('Are you sure?')}</span> | ||||
| 		</div> | ||||
| 
 | ||||
| 				<div class="flex space-x-1.5 items-center"> | ||||
| 					<button | ||||
| 						class="hover:text-white transition" | ||||
| 						on:click={() => { | ||||
| 							const res = resetVectorDB(localStorage.token).catch((error) => { | ||||
| 								toast.error(error); | ||||
| 								return null; | ||||
| 							}); | ||||
| 
 | ||||
| 							if (res) { | ||||
| 								toast.success($i18n.t('Success')); | ||||
| 							} | ||||
| 
 | ||||
| 							showResetConfirm = false; | ||||
| 						}} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								fill-rule="evenodd" | ||||
| 								d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" | ||||
| 								clip-rule="evenodd" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 					<button | ||||
| 						class="hover:text-white transition" | ||||
| 						on:click={() => { | ||||
| 							showResetConfirm = false; | ||||
| 						}} | ||||
| 					> | ||||
| 						<svg | ||||
| 							xmlns="http://www.w3.org/2000/svg" | ||||
| 							viewBox="0 0 20 20" | ||||
| 							fill="currentColor" | ||||
| 							class="w-4 h-4" | ||||
| 						> | ||||
| 							<path | ||||
| 								d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" | ||||
| 							/> | ||||
| 						</svg> | ||||
| 					</button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{:else} | ||||
| 			<button | ||||
| 				class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" | ||||
| 				on:click={() => { | ||||
| 					showResetConfirm = true; | ||||
| 				}} | ||||
| 			> | ||||
| 				<div class=" self-center mr-3"> | ||||
| 					<svg | ||||
| 						xmlns="http://www.w3.org/2000/svg" | ||||
| 						viewBox="0 0 16 16" | ||||
| 						fill="currentColor" | ||||
| 						class="w-4 h-4" | ||||
| 					> | ||||
| 						<path | ||||
| 							fill-rule="evenodd" | ||||
| 							d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" | ||||
| 							clip-rule="evenodd" | ||||
| 						/> | ||||
| 					</svg> | ||||
| 				</div> | ||||
| 				<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
|  | @ -336,7 +236,7 @@ | |||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||
| 			type="submit" | ||||
| 		> | ||||
| 			{$i18n.t('Save')} | ||||
| 			Save | ||||
| 		</button> | ||||
| 	</div> | ||||
| </form> | ||||
|  |  | |||
|  | @ -1,10 +1,7 @@ | |||
| <script> | ||||
| 	import { getContext } from 'svelte'; | ||||
| 	import Modal from '../common/Modal.svelte'; | ||||
| 	import General from './Settings/General.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
| 	export let show = false; | ||||
| 
 | ||||
| 	let selectedTab = 'general'; | ||||
|  | @ -13,7 +10,7 @@ | |||
| <Modal bind:show> | ||||
| 	<div> | ||||
| 		<div class=" flex justify-between dark:text-gray-300 px-5 py-4"> | ||||
| 			<div class=" text-lg font-medium self-center">{$i18n.t('Document Settings')}</div> | ||||
| 			<div class=" text-lg font-medium self-center">Document Settings</div> | ||||
| 			<button | ||||
| 				class="self-center" | ||||
| 				on:click={() => { | ||||
|  | @ -61,7 +58,7 @@ | |||
| 							/> | ||||
| 						</svg> | ||||
| 					</div> | ||||
| 					<div class=" self-center">{$i18n.t('General')}</div> | ||||
| 					<div class=" self-center">General</div> | ||||
| 				</button> | ||||
| 			</div> | ||||
| 			<div class="flex-1 md:min-h-[380px]"> | ||||
|  |  | |||
|  | @ -1,15 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	export let className = 'w-4 h-4'; | ||||
| 	export let strokeWidth = '1.5'; | ||||
| </script> | ||||
| 
 | ||||
| <svg | ||||
| 	xmlns="http://www.w3.org/2000/svg" | ||||
| 	fill="none" | ||||
| 	viewBox="0 0 24 24" | ||||
| 	stroke-width={strokeWidth} | ||||
| 	stroke="currentColor" | ||||
| 	class={className} | ||||
| > | ||||
| 	<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> | ||||
| </svg> | ||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Jannik S
						Jannik S