forked from open-webui/open-webui
		
	
						commit
						78284e49d7
					
				
					 61 changed files with 1837 additions and 723 deletions
				
			
		
							
								
								
									
										11
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -57,3 +57,14 @@ jobs: | ||||||
|         path: . |         path: . | ||||||
|       env: |       env: | ||||||
|         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |         GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |     - name: Trigger Docker build workflow | ||||||
|  |       uses: actions/github-script@v7 | ||||||
|  |       with: | ||||||
|  |         script: | | ||||||
|  |           github.rest.actions.createWorkflowDispatch({ | ||||||
|  |             owner: context.repo.owner, | ||||||
|  |             repo: context.repo.repo, | ||||||
|  |             workflow_id: 'docker-build.yaml', | ||||||
|  |             ref: 'v${{ steps.get_version.outputs.version }}', | ||||||
|  |           }) | ||||||
|  |  | ||||||
							
								
								
									
										352
									
								
								.github/workflows/docker-build.yaml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										352
									
								
								.github/workflows/docker-build.yaml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,8 +1,7 @@ | ||||||
| # | name: Create and publish Docker images with specific build args | ||||||
| name: Create and publish a Docker image |  | ||||||
| 
 | 
 | ||||||
| # Configures this workflow to run every time a change is pushed to the branch called `release`. |  | ||||||
| on: | on: | ||||||
|  |   workflow_dispatch: | ||||||
|   push: |   push: | ||||||
|     branches: |     branches: | ||||||
|       - main |       - main | ||||||
|  | @ -10,30 +9,39 @@ on: | ||||||
|     tags: |     tags: | ||||||
|       - v* |       - v* | ||||||
| 
 | 
 | ||||||
| # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. |  | ||||||
| env: | env: | ||||||
|   REGISTRY: ghcr.io |   REGISTRY: ghcr.io | ||||||
|   IMAGE_NAME: ${{ github.repository }} |   IMAGE_NAME: ${{ github.repository }} | ||||||
|  |   FULL_IMAGE_NAME: ghcr.io/${{ github.repository }} | ||||||
| 
 | 
 | ||||||
| # There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. |  | ||||||
| jobs: | jobs: | ||||||
|   build-and-push-image: |   build-main-image: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. |  | ||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|       # |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         platform: | ||||||
|  |           - linux/amd64 | ||||||
|  |           - linux/arm64 | ||||||
|  | 
 | ||||||
|     steps: |     steps: | ||||||
|  |       - name: Prepare | ||||||
|  |         run: | | ||||||
|  |           platform=${{ matrix.platform }} | ||||||
|  |           echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV | ||||||
|  | 
 | ||||||
|       - name: Checkout repository |       - name: Checkout repository | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|       # Required for multi architecture build | 
 | ||||||
|       - name: Set up QEMU |       - name: Set up QEMU | ||||||
|         uses: docker/setup-qemu-action@v3 |         uses: docker/setup-qemu-action@v3 | ||||||
|       # Required for multi architecture build | 
 | ||||||
|       - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|         uses: docker/setup-buildx-action@v3 |         uses: docker/setup-buildx-action@v3 | ||||||
|       # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. | 
 | ||||||
|       - name: Log in to the Container registry |       - name: Log in to the Container registry | ||||||
|         uses: docker/login-action@v3 |         uses: docker/login-action@v3 | ||||||
|         with: |         with: | ||||||
|  | @ -41,12 +49,11 @@ jobs: | ||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
| 
 | 
 | ||||||
|       - name: Extract metadata for Docker images |       - name: Extract metadata for Docker images (default latest tag) | ||||||
|         id: meta |         id: meta | ||||||
|         uses: docker/metadata-action@v5 |         uses: docker/metadata-action@v5 | ||||||
|         with: |         with: | ||||||
|           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|           # This configuration dynamically generates tags based on the branch, tag, commit, and custom suffix for lite version. |  | ||||||
|           tags: | |           tags: | | ||||||
|             type=ref,event=branch |             type=ref,event=branch | ||||||
|             type=ref,event=tag |             type=ref,event=tag | ||||||
|  | @ -56,11 +63,322 @@ jobs: | ||||||
|           flavor: | |           flavor: | | ||||||
|             latest=${{ github.ref == 'refs/heads/main' }} |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
| 
 | 
 | ||||||
|       - name: Build and push Docker image |       - name: Build Docker image (latest) | ||||||
|         uses: docker/build-push-action@v5 |         uses: docker/build-push-action@v5 | ||||||
|  |         id: build | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           push: true |           push: true | ||||||
|           platforms: linux/amd64,linux/arm64 |           platforms: ${{ matrix.platform }} | ||||||
|           tags: ${{ steps.meta.outputs.tags }} |  | ||||||
|           labels: ${{ steps.meta.outputs.labels }} |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | ||||||
|  |           cache-from: type=gha | ||||||
|  |           cache-to: type=gha,mode=max | ||||||
|  | 
 | ||||||
|  |       - name: Export digest | ||||||
|  |         run: | | ||||||
|  |           mkdir -p /tmp/digests | ||||||
|  |           digest="${{ steps.build.outputs.digest }}" | ||||||
|  |           touch "/tmp/digests/${digest#sha256:}" | ||||||
|  | 
 | ||||||
|  |       - name: Upload digest | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: digests-main-${{ env.PLATFORM_PAIR }} | ||||||
|  |           path: /tmp/digests/* | ||||||
|  |           if-no-files-found: error | ||||||
|  |           retention-days: 1 | ||||||
|  | 
 | ||||||
|  |   build-cuda-image: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     permissions: | ||||||
|  |       contents: read | ||||||
|  |       packages: write | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         platform: | ||||||
|  |           - linux/amd64 | ||||||
|  |           - linux/arm64 | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |       - name: Prepare | ||||||
|  |         run: | | ||||||
|  |           platform=${{ matrix.platform }} | ||||||
|  |           echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV | ||||||
|  | 
 | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  | 
 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Log in to the Container registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ${{ env.REGISTRY }} | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |       - name: Extract metadata for Docker images (default latest tag) | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=tag | ||||||
|  |             type=sha,prefix=git- | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |             type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda | ||||||
|  |           flavor: | | ||||||
|  |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
|  |             suffix=-cuda,onlatest=true | ||||||
|  | 
 | ||||||
|  |       - name: Build Docker image (cuda) | ||||||
|  |         uses: docker/build-push-action@v5 | ||||||
|  |         id: build | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           push: true | ||||||
|  |           platforms: ${{ matrix.platform }} | ||||||
|  |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | ||||||
|  |           cache-from: type=gha | ||||||
|  |           cache-to: type=gha,mode=max | ||||||
|  |           build-args: USE_CUDA=true | ||||||
|  | 
 | ||||||
|  |       - name: Export digest | ||||||
|  |         run: | | ||||||
|  |           mkdir -p /tmp/digests | ||||||
|  |           digest="${{ steps.build.outputs.digest }}" | ||||||
|  |           touch "/tmp/digests/${digest#sha256:}" | ||||||
|  | 
 | ||||||
|  |       - name: Upload digest | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: digests-cuda-${{ env.PLATFORM_PAIR }} | ||||||
|  |           path: /tmp/digests/* | ||||||
|  |           if-no-files-found: error | ||||||
|  |           retention-days: 1 | ||||||
|  | 
 | ||||||
|  |   build-ollama-image: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     permissions: | ||||||
|  |       contents: read | ||||||
|  |       packages: write | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         platform: | ||||||
|  |           - linux/amd64 | ||||||
|  |           - linux/arm64 | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |       - name: Prepare | ||||||
|  |         run: | | ||||||
|  |           platform=${{ matrix.platform }} | ||||||
|  |           echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV | ||||||
|  | 
 | ||||||
|  |       - name: Checkout repository | ||||||
|  |         uses: actions/checkout@v4 | ||||||
|  | 
 | ||||||
|  |       - name: Set up QEMU | ||||||
|  |         uses: docker/setup-qemu-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Log in to the Container registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ${{ env.REGISTRY }} | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |       - name: Extract metadata for Docker images (ollama tag) | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=tag | ||||||
|  |             type=sha,prefix=git- | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |             type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama | ||||||
|  |           flavor: | | ||||||
|  |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
|  |             suffix=-ollama,onlatest=true | ||||||
|  | 
 | ||||||
|  |       - name: Build Docker image (ollama) | ||||||
|  |         uses: docker/build-push-action@v5 | ||||||
|  |         id: build | ||||||
|  |         with: | ||||||
|  |           context: . | ||||||
|  |           push: true | ||||||
|  |           platforms: ${{ matrix.platform }} | ||||||
|  |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|  |           outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true | ||||||
|  |           cache-from: type=gha | ||||||
|  |           cache-to: type=gha,mode=max | ||||||
|  |           build-args: USE_OLLAMA=true | ||||||
|  | 
 | ||||||
|  |       - name: Export digest | ||||||
|  |         run: | | ||||||
|  |           mkdir -p /tmp/digests | ||||||
|  |           digest="${{ steps.build.outputs.digest }}" | ||||||
|  |           touch "/tmp/digests/${digest#sha256:}" | ||||||
|  | 
 | ||||||
|  |       - name: Upload digest | ||||||
|  |         uses: actions/upload-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           name: digests-ollama-${{ env.PLATFORM_PAIR }} | ||||||
|  |           path: /tmp/digests/* | ||||||
|  |           if-no-files-found: error | ||||||
|  |           retention-days: 1 | ||||||
|  | 
 | ||||||
|  |   merge-main-images: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: [ build-main-image ] | ||||||
|  |     steps: | ||||||
|  |       - name: Download digests | ||||||
|  |         uses: actions/download-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           pattern: digests-main-* | ||||||
|  |           path: /tmp/digests | ||||||
|  |           merge-multiple: true | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Log in to the Container registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ${{ env.REGISTRY }} | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |       - name: Extract metadata for Docker images (default latest tag) | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=tag | ||||||
|  |             type=sha,prefix=git- | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |           flavor: | | ||||||
|  |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
|  | 
 | ||||||
|  |       - name: Create manifest list and push | ||||||
|  |         working-directory: /tmp/digests | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||||
|  |             $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) | ||||||
|  | 
 | ||||||
|  |       - name: Inspect image | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |   merge-cuda-images: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: [ build-cuda-image ] | ||||||
|  |     steps: | ||||||
|  |       - name: Download digests | ||||||
|  |         uses: actions/download-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           pattern: digests-cuda-* | ||||||
|  |           path: /tmp/digests | ||||||
|  |           merge-multiple: true | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Log in to the Container registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ${{ env.REGISTRY }} | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |       - name: Extract metadata for Docker images (default latest tag) | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=tag | ||||||
|  |             type=sha,prefix=git- | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |             type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda | ||||||
|  |           flavor: | | ||||||
|  |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
|  |             suffix=-cuda,onlatest=true | ||||||
|  | 
 | ||||||
|  |       - name: Create manifest list and push | ||||||
|  |         working-directory: /tmp/digests | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||||
|  |             $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) | ||||||
|  | 
 | ||||||
|  |       - name: Inspect image | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} | ||||||
|  | 
 | ||||||
|  |   merge-ollama-images: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: [ build-ollama-image ] | ||||||
|  |     steps: | ||||||
|  |       - name: Download digests | ||||||
|  |         uses: actions/download-artifact@v4 | ||||||
|  |         with: | ||||||
|  |           pattern: digests-ollama-* | ||||||
|  |           path: /tmp/digests | ||||||
|  |           merge-multiple: true | ||||||
|  | 
 | ||||||
|  |       - name: Set up Docker Buildx | ||||||
|  |         uses: docker/setup-buildx-action@v3 | ||||||
|  | 
 | ||||||
|  |       - name: Log in to the Container registry | ||||||
|  |         uses: docker/login-action@v3 | ||||||
|  |         with: | ||||||
|  |           registry: ${{ env.REGISTRY }} | ||||||
|  |           username: ${{ github.actor }} | ||||||
|  |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  | 
 | ||||||
|  |       - name: Extract metadata for Docker images (default ollama tag) | ||||||
|  |         id: meta | ||||||
|  |         uses: docker/metadata-action@v5 | ||||||
|  |         with: | ||||||
|  |           images: ${{ env.FULL_IMAGE_NAME }} | ||||||
|  |           tags: | | ||||||
|  |             type=ref,event=branch | ||||||
|  |             type=ref,event=tag | ||||||
|  |             type=sha,prefix=git- | ||||||
|  |             type=semver,pattern={{version}} | ||||||
|  |             type=semver,pattern={{major}}.{{minor}} | ||||||
|  |             type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama | ||||||
|  |           flavor: | | ||||||
|  |             latest=${{ github.ref == 'refs/heads/main' }} | ||||||
|  |             suffix=-ollama,onlatest=true | ||||||
|  | 
 | ||||||
|  |       - name: Create manifest list and push | ||||||
|  |         working-directory: /tmp/digests | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ | ||||||
|  |             $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) | ||||||
|  | 
 | ||||||
|  |       - name: Inspect image | ||||||
|  |         run: | | ||||||
|  |           docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										20
									
								
								CHANGELOG.md
									
										
									
									
									
								
							|  | @ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. | ||||||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), | ||||||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||||||
| 
 | 
 | ||||||
|  | ## [0.1.118] - 2024-04-10 | ||||||
|  | 
 | ||||||
|  | ### Added | ||||||
|  | 
 | ||||||
|  | - **🦙 Ollama and CUDA Images**: Added support for `:ollama` and `:cuda` tagged images. | ||||||
|  | - **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback. | ||||||
|  | - **👤 User Initials Profile Photo**: User initials are now the default profile photo. | ||||||
|  | - **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings. | ||||||
|  | - **🌍 Additional Language Support**: Added Turkish language support. | ||||||
|  | 
 | ||||||
|  | ### Fixed | ||||||
|  | 
 | ||||||
|  | - **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions. | ||||||
|  | - **🛠 Modal Close**: Modals can now be closed using the Esc key. | ||||||
|  | 
 | ||||||
|  | ### Changed | ||||||
|  | 
 | ||||||
|  | - **🎨 Admin Panel Styling**: Refreshed styling for the admin panel. | ||||||
|  | - **🐳 Docker Image Build**: Updated docker image build process for improved efficiency. | ||||||
|  | 
 | ||||||
| ## [0.1.117] - 2024-04-03 | ## [0.1.117] - 2024-04-03 | ||||||
| 
 | 
 | ||||||
| ### Added | ### Added | ||||||
|  |  | ||||||
							
								
								
									
										128
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										128
									
								
								Dockerfile
									
										
									
									
									
								
							|  | @ -1,82 +1,116 @@ | ||||||
| # syntax=docker/dockerfile:1 | # syntax=docker/dockerfile:1 | ||||||
|  | # Initialize device type args | ||||||
|  | # use build args in the docker build commmand with --build-arg="BUILDARG=true" | ||||||
|  | ARG USE_CUDA=false | ||||||
|  | ARG USE_OLLAMA=false | ||||||
|  | # Tested with cu117 for CUDA 11 and cu121 for CUDA 12 (default) | ||||||
|  | ARG USE_CUDA_VER=cu121 | ||||||
|  | # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers | ||||||
|  | # 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. | ||||||
|  | ARG USE_EMBEDDING_MODEL=all-MiniLM-L6-v2 | ||||||
| 
 | 
 | ||||||
| FROM node:alpine as build | ######## WebUI frontend ######## | ||||||
|  | FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build | ||||||
| 
 | 
 | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| 
 | 
 | ||||||
| # wget embedding model weight from alpine (does not exist from slim-buster) |  | ||||||
| RUN wget "https://chroma-onnx-models.s3.amazonaws.com/all-MiniLM-L6-v2/onnx.tar.gz" -O - | \ |  | ||||||
|     tar -xzf - -C /app |  | ||||||
| 
 |  | ||||||
| COPY package.json package-lock.json ./ | COPY package.json package-lock.json ./ | ||||||
| RUN npm ci | RUN npm ci | ||||||
| 
 | 
 | ||||||
| COPY . . | COPY . . | ||||||
| RUN npm run build | RUN npm run build | ||||||
| 
 | 
 | ||||||
| 
 | ######## WebUI backend ######## | ||||||
| FROM python:3.11-slim-bookworm as base | FROM python:3.11-slim-bookworm as base | ||||||
| 
 | 
 | ||||||
| ENV ENV=prod | # Use args | ||||||
| ENV PORT "" | ARG USE_CUDA | ||||||
|  | ARG USE_OLLAMA | ||||||
|  | ARG USE_CUDA_VER | ||||||
|  | ARG USE_EMBEDDING_MODEL | ||||||
| 
 | 
 | ||||||
| ENV OLLAMA_BASE_URL "/ollama" | ## Basis ## | ||||||
|  | ENV ENV=prod \ | ||||||
|  |     PORT=8080 \ | ||||||
|  |     # pass build args to the build | ||||||
|  |     USE_OLLAMA_DOCKER=${USE_OLLAMA} \ | ||||||
|  |     USE_CUDA_DOCKER=${USE_CUDA} \ | ||||||
|  |     USE_CUDA_DOCKER_VER=${USE_CUDA_VER} \ | ||||||
|  |     USE_EMBEDDING_MODEL_DOCKER=${USE_EMBEDDING_MODEL} | ||||||
| 
 | 
 | ||||||
| ENV OPENAI_API_BASE_URL "" | ## Basis URL Config ## | ||||||
| ENV OPENAI_API_KEY "" | ENV OLLAMA_BASE_URL="/ollama" \ | ||||||
|  |     OPENAI_API_BASE_URL="" | ||||||
| 
 | 
 | ||||||
| ENV WEBUI_SECRET_KEY "" | ## API Key and Security Config ## | ||||||
| ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER "" | ENV OPENAI_API_KEY="" \ | ||||||
| 
 |     WEBUI_SECRET_KEY="" \ | ||||||
| ENV SCARF_NO_ANALYTICS true |     SCARF_NO_ANALYTICS=true \ | ||||||
| ENV DO_NOT_TRACK true |     DO_NOT_TRACK=true | ||||||
| 
 | 
 | ||||||
| # Use locally bundled version of the LiteLLM cost map json | # Use locally bundled version of the LiteLLM cost map json | ||||||
| # to avoid repetitive startup connections | # to avoid repetitive startup connections | ||||||
| ENV LITELLM_LOCAL_MODEL_COST_MAP="True" | ENV LITELLM_LOCAL_MODEL_COST_MAP="True" | ||||||
| 
 | 
 | ||||||
| ######## Preloaded models ######## |  | ||||||
| # whisper TTS Settings |  | ||||||
| ENV WHISPER_MODEL="base" |  | ||||||
| ENV WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" |  | ||||||
| 
 | 
 | ||||||
| # RAG Embedding Model Settings | #### Other models ######################################################### | ||||||
| # any sentence transformer model; models to use can be found at https://huggingface.co/models?library=sentence-transformers | ## whisper TTS model settings ## | ||||||
| # Leaderboard: https://huggingface.co/spaces/mteb/leaderboard  | ENV WHISPER_MODEL="base" \ | ||||||
| # for better persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) |     WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" | ||||||
| # 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="all-MiniLM-L6-v2" |  | ||||||
| # device type for whisper tts and embbeding models - "cpu" (default), "cuda" (nvidia gpu and CUDA required) or "mps" (apple silicon) - choosing this right can lead to better performance |  | ||||||
| ENV RAG_EMBEDDING_MODEL_DEVICE_TYPE="cpu" |  | ||||||
| ENV RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" |  | ||||||
| ENV SENTENCE_TRANSFORMERS_HOME $RAG_EMBEDDING_MODEL_DIR |  | ||||||
| 
 | 
 | ||||||
| ######## Preloaded models ######## | ## RAG Embedding model settings ## | ||||||
|  | ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \ | ||||||
|  |     RAG_EMBEDDING_MODEL_DIR="/app/backend/data/cache/embedding/models" \ | ||||||
|  |     SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" | ||||||
|  | #### Other models ########################################################## | ||||||
| 
 | 
 | ||||||
| WORKDIR /app/backend | WORKDIR /app/backend | ||||||
| 
 | 
 | ||||||
|  | RUN if [ "$USE_OLLAMA" = "true" ]; then \ | ||||||
|  |         apt-get update && \ | ||||||
|  |         # Install pandoc and netcat | ||||||
|  |         apt-get install -y --no-install-recommends pandoc netcat-openbsd && \ | ||||||
|  |         # for RAG OCR | ||||||
|  |         apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \ | ||||||
|  |         # install helper tools | ||||||
|  |         apt-get install -y --no-install-recommends curl && \ | ||||||
|  |         # install ollama | ||||||
|  |         curl -fsSL https://ollama.com/install.sh | sh && \ | ||||||
|  |         # cleanup | ||||||
|  |         rm -rf /var/lib/apt/lists/*; \ | ||||||
|  |     else \ | ||||||
|  |         apt-get update && \ | ||||||
|  |         # Install pandoc and netcat | ||||||
|  |         apt-get install -y --no-install-recommends pandoc netcat-openbsd && \ | ||||||
|  |         # for RAG OCR | ||||||
|  |         apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \ | ||||||
|  |         # cleanup | ||||||
|  |         rm -rf /var/lib/apt/lists/*; \ | ||||||
|  |     fi | ||||||
|  | 
 | ||||||
| # install python dependencies | # install python dependencies | ||||||
| COPY ./backend/requirements.txt ./requirements.txt | COPY ./backend/requirements.txt ./requirements.txt | ||||||
| 
 | 
 | ||||||
| RUN apt-get update && apt-get install ffmpeg libsm6 libxext6  -y | RUN if [ "$USE_CUDA" = "true" ]; then \ | ||||||
|  |         # If you use CUDA the whisper and embedding model will be downloaded on first use | ||||||
|  |         pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --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')"; \ | ||||||
|  |     else \ | ||||||
|  |         pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --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')"; \ | ||||||
|  |     fi | ||||||
| 
 | 
 | ||||||
| RUN pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir |  | ||||||
| RUN pip3 install -r requirements.txt --no-cache-dir |  | ||||||
| 
 | 
 | ||||||
| # Install pandoc and netcat |  | ||||||
| # RUN python -c "import pypandoc; pypandoc.download_pandoc()" |  | ||||||
| RUN apt-get update \ |  | ||||||
|     && apt-get install -y pandoc netcat-openbsd \ |  | ||||||
|     && rm -rf /var/lib/apt/lists/* |  | ||||||
| 
 |  | ||||||
| # preload embedding model |  | ||||||
| RUN python -c "import os; from chromadb.utils import embedding_functions; sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction(model_name=os.environ['RAG_EMBEDDING_MODEL'], device=os.environ['RAG_EMBEDDING_MODEL_DEVICE_TYPE'])" |  | ||||||
| # preload tts model |  | ||||||
| RUN python -c "import os; from faster_whisper import WhisperModel; WhisperModel(os.environ['WHISPER_MODEL'], device='auto', compute_type='int8', download_root=os.environ['WHISPER_MODEL_DIR'])" |  | ||||||
| 
 | 
 | ||||||
| # copy embedding weight from build | # copy embedding weight from build | ||||||
| RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 | # RUN mkdir -p /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2 | ||||||
| COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx | # COPY --from=build /app/onnx /root/.cache/chroma/onnx_models/all-MiniLM-L6-v2/onnx | ||||||
| 
 | 
 | ||||||
| # copy built frontend files | # copy built frontend files | ||||||
| COPY --from=build /app/build /app/build | COPY --from=build /app/build /app/build | ||||||
|  | @ -86,4 +120,6 @@ COPY --from=build /app/package.json /app/package.json | ||||||
| # copy backend files | # copy backend files | ||||||
| COPY ./backend . | COPY ./backend . | ||||||
| 
 | 
 | ||||||
|  | EXPOSE 8080 | ||||||
|  | 
 | ||||||
| CMD [ "bash", "start.sh"] | CMD [ "bash", "start.sh"] | ||||||
							
								
								
									
										25
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										25
									
								
								README.md
									
										
									
									
									
								
							|  | @ -94,24 +94,27 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open | ||||||
| 
 | 
 | ||||||
| ### Quick Start with Docker 🐳 | ### Quick Start with Docker 🐳 | ||||||
| 
 | 
 | ||||||
| > [!IMPORTANT] | > [!WARNING] | ||||||
| > When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. | > When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. | ||||||
| 
 | 
 | ||||||
| - **If Ollama is on your computer**, use this command: | > [!TIP]   | ||||||
|  | > If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system. | ||||||
| 
 | 
 | ||||||
|   ```bash | **If Ollama is on your computer**, use this command: | ||||||
|   docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main |  | ||||||
|   ``` |  | ||||||
| 
 | 
 | ||||||
| - **If Ollama is on a Different Server**, use this command: | ```bash | ||||||
|  | docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||||
|  | ``` | ||||||
| 
 | 
 | ||||||
| - To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL: | **If Ollama is on a Different Server**, use this command: | ||||||
| 
 | 
 | ||||||
|   ```bash | To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL: | ||||||
|   docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main |  | ||||||
|   ``` |  | ||||||
| 
 | 
 | ||||||
| - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 | ```bash | ||||||
|  | docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 | ||||||
| 
 | 
 | ||||||
| #### Open WebUI: Server Connection Error | #### Open WebUI: Server Connection Error | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ from config import ( | ||||||
|     UPLOAD_DIR, |     UPLOAD_DIR, | ||||||
|     WHISPER_MODEL, |     WHISPER_MODEL, | ||||||
|     WHISPER_MODEL_DIR, |     WHISPER_MODEL_DIR, | ||||||
|  |     DEVICE_TYPE, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
|  | @ -42,6 +43,10 @@ app.add_middleware( | ||||||
|     allow_headers=["*"], |     allow_headers=["*"], | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | # setting device type for whisper model | ||||||
|  | whisper_device_type = DEVICE_TYPE if DEVICE_TYPE and DEVICE_TYPE == "cuda" else "cpu" | ||||||
|  | log.info(f"whisper_device_type: {whisper_device_type}") | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @app.post("/transcribe") | @app.post("/transcribe") | ||||||
| def transcribe( | def transcribe( | ||||||
|  | @ -66,7 +71,7 @@ def transcribe( | ||||||
| 
 | 
 | ||||||
|         model = WhisperModel( |         model = WhisperModel( | ||||||
|             WHISPER_MODEL, |             WHISPER_MODEL, | ||||||
|             device="auto", |             device=whisper_device_type, | ||||||
|             compute_type="int8", |             compute_type="int8", | ||||||
|             download_root=WHISPER_MODEL_DIR, |             download_root=WHISPER_MODEL_DIR, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None): | ||||||
| 
 | 
 | ||||||
|         if len(responses) > 0: |         if len(responses) > 0: | ||||||
|             lowest_version = min( |             lowest_version = min( | ||||||
|                 responses, key=lambda x: tuple(map(int, x["version"].split("."))) |                 responses, | ||||||
|  |                 key=lambda x: tuple(map(int, x["version"].split("-")[0].split("."))), | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             return {"version": lowest_version["version"]} |             return {"version": lowest_version["version"]} | ||||||
|  |  | ||||||
|  | @ -13,8 +13,8 @@ import os, shutil, logging, re | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import List | from typing import List | ||||||
| 
 | 
 | ||||||
| from sentence_transformers import SentenceTransformer |  | ||||||
| from chromadb.utils import embedding_functions | from chromadb.utils import embedding_functions | ||||||
|  | from chromadb.utils.batch_utils import create_batches | ||||||
| 
 | 
 | ||||||
| from langchain_community.document_loaders import ( | from langchain_community.document_loaders import ( | ||||||
|     WebBaseLoader, |     WebBaseLoader, | ||||||
|  | @ -45,7 +45,7 @@ from apps.web.models.documents import ( | ||||||
|     DocumentResponse, |     DocumentResponse, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| from apps.rag.utils import query_doc, query_collection | from apps.rag.utils import query_doc, query_collection, get_embedding_model_path | ||||||
| 
 | 
 | ||||||
| from utils.misc import ( | from utils.misc import ( | ||||||
|     calculate_sha256, |     calculate_sha256, | ||||||
|  | @ -59,7 +59,8 @@ from config import ( | ||||||
|     UPLOAD_DIR, |     UPLOAD_DIR, | ||||||
|     DOCS_DIR, |     DOCS_DIR, | ||||||
|     RAG_EMBEDDING_MODEL, |     RAG_EMBEDDING_MODEL, | ||||||
|     RAG_EMBEDDING_MODEL_DEVICE_TYPE, |     RAG_EMBEDDING_MODEL_AUTO_UPDATE, | ||||||
|  |     DEVICE_TYPE, | ||||||
|     CHROMA_CLIENT, |     CHROMA_CLIENT, | ||||||
|     CHUNK_SIZE, |     CHUNK_SIZE, | ||||||
|     CHUNK_OVERLAP, |     CHUNK_OVERLAP, | ||||||
|  | @ -71,28 +72,25 @@ from constants import ERROR_MESSAGES | ||||||
| log = logging.getLogger(__name__) | log = logging.getLogger(__name__) | ||||||
| log.setLevel(SRC_LOG_LEVELS["RAG"]) | log.setLevel(SRC_LOG_LEVELS["RAG"]) | ||||||
| 
 | 
 | ||||||
| # |  | ||||||
| # if RAG_EMBEDDING_MODEL: |  | ||||||
| #    sentence_transformer_ef = SentenceTransformer( |  | ||||||
| #        model_name_or_path=RAG_EMBEDDING_MODEL, |  | ||||||
| #        cache_folder=RAG_EMBEDDING_MODEL_DIR, |  | ||||||
| #        device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, |  | ||||||
| #    ) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| app = FastAPI() | app = FastAPI() | ||||||
| 
 | 
 | ||||||
| app.state.PDF_EXTRACT_IMAGES = False | app.state.PDF_EXTRACT_IMAGES = False | ||||||
| app.state.CHUNK_SIZE = CHUNK_SIZE | app.state.CHUNK_SIZE = CHUNK_SIZE | ||||||
| app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | app.state.CHUNK_OVERLAP = CHUNK_OVERLAP | ||||||
| app.state.RAG_TEMPLATE = RAG_TEMPLATE | app.state.RAG_TEMPLATE = RAG_TEMPLATE | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL | app.state.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| app.state.TOP_K = 4 | app.state.TOP_K = 4 | ||||||
| 
 | 
 | ||||||
| app.state.sentence_transformer_ef = ( | app.state.sentence_transformer_ef = ( | ||||||
|     embedding_functions.SentenceTransformerEmbeddingFunction( |     embedding_functions.SentenceTransformerEmbeddingFunction( | ||||||
|         model_name=app.state.RAG_EMBEDDING_MODEL, |         model_name=get_embedding_model_path( | ||||||
|         device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, |             app.state.RAG_EMBEDDING_MODEL, RAG_EMBEDDING_MODEL_AUTO_UPDATE | ||||||
|  |         ), | ||||||
|  |         device=DEVICE_TYPE, | ||||||
|     ) |     ) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -143,19 +141,34 @@ class EmbeddingModelUpdateForm(BaseModel): | ||||||
| async def update_embedding_model( | async def update_embedding_model( | ||||||
|     form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) |     form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) | ||||||
| ): | ): | ||||||
|     app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model | 
 | ||||||
|     app.state.sentence_transformer_ef = ( |     log.info( | ||||||
|  |         f"Updating embedding model: {app.state.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}" | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         sentence_transformer_ef = ( | ||||||
|             embedding_functions.SentenceTransformerEmbeddingFunction( |             embedding_functions.SentenceTransformerEmbeddingFunction( | ||||||
|             model_name=app.state.RAG_EMBEDDING_MODEL, |                 model_name=get_embedding_model_path(form_data.embedding_model, True), | ||||||
|             device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, |                 device=DEVICE_TYPE, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |         app.state.RAG_EMBEDDING_MODEL = form_data.embedding_model | ||||||
|  |         app.state.sentence_transformer_ef = sentence_transformer_ef | ||||||
|  | 
 | ||||||
|         return { |         return { | ||||||
|             "status": True, |             "status": True, | ||||||
|             "embedding_model": app.state.RAG_EMBEDDING_MODEL, |             "embedding_model": app.state.RAG_EMBEDDING_MODEL, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     except Exception as e: | ||||||
|  |         log.exception(f"Problem updating embedding model: {e}") | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, | ||||||
|  |             detail=ERROR_MESSAGES.DEFAULT(e), | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @app.get("/config") | @app.get("/config") | ||||||
| async def get_rag_config(user=Depends(get_admin_user)): | async def get_rag_config(user=Depends(get_admin_user)): | ||||||
|  | @ -341,9 +354,14 @@ def store_docs_in_vector_db(docs, collection_name, overwrite: bool = False) -> b | ||||||
|             embedding_function=app.state.sentence_transformer_ef, |             embedding_function=app.state.sentence_transformer_ef, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         collection.add( |         for batch in create_batches( | ||||||
|             documents=texts, metadatas=metadatas, ids=[str(uuid.uuid1()) for _ in texts] |             api=CHROMA_CLIENT, | ||||||
|         ) |             ids=[str(uuid.uuid1()) for _ in texts], | ||||||
|  |             metadatas=metadatas, | ||||||
|  |             documents=texts, | ||||||
|  |         ): | ||||||
|  |             collection.add(*batch) | ||||||
|  | 
 | ||||||
|         return True |         return True | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         log.exception(e) |         log.exception(e) | ||||||
|  |  | ||||||
|  | @ -1,6 +1,8 @@ | ||||||
|  | import os | ||||||
| import re | import re | ||||||
| import logging | import logging | ||||||
| from typing import List | from typing import List | ||||||
|  | from huggingface_hub import snapshot_download | ||||||
| 
 | 
 | ||||||
| from config import SRC_LOG_LEVELS, CHROMA_CLIENT | from config import SRC_LOG_LEVELS, CHROMA_CLIENT | ||||||
| 
 | 
 | ||||||
|  | @ -188,3 +190,43 @@ def rag_messages(docs, messages, template, k, embedding_function): | ||||||
|     messages[last_user_message_idx] = new_user_message |     messages[last_user_message_idx] = new_user_message | ||||||
| 
 | 
 | ||||||
|     return messages |     return messages | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_embedding_model_path( | ||||||
|  |     embedding_model: str, update_embedding_model: bool = False | ||||||
|  | ): | ||||||
|  |     # Construct huggingface_hub kwargs with local_files_only to return the snapshot path | ||||||
|  |     cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME") | ||||||
|  | 
 | ||||||
|  |     local_files_only = not update_embedding_model | ||||||
|  | 
 | ||||||
|  |     snapshot_kwargs = { | ||||||
|  |         "cache_dir": cache_dir, | ||||||
|  |         "local_files_only": local_files_only, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     log.debug(f"embedding_model: {embedding_model}") | ||||||
|  |     log.debug(f"snapshot_kwargs: {snapshot_kwargs}") | ||||||
|  | 
 | ||||||
|  |     # Inspiration from upstream sentence_transformers | ||||||
|  |     if ( | ||||||
|  |         os.path.exists(embedding_model) | ||||||
|  |         or ("\\" in embedding_model or embedding_model.count("/") > 1) | ||||||
|  |         and local_files_only | ||||||
|  |     ): | ||||||
|  |         # If fully qualified path exists, return input, else set repo_id | ||||||
|  |         return embedding_model | ||||||
|  |     elif "/" not in embedding_model: | ||||||
|  |         # Set valid repo_id for model short-name | ||||||
|  |         embedding_model = "sentence-transformers" + "/" + embedding_model | ||||||
|  | 
 | ||||||
|  |     snapshot_kwargs["repo_id"] = embedding_model | ||||||
|  | 
 | ||||||
|  |     # Attempt to query the huggingface_hub library to determine the local path and/or to update | ||||||
|  |     try: | ||||||
|  |         embedding_model_repo_path = snapshot_download(**snapshot_kwargs) | ||||||
|  |         log.debug(f"embedding_model_repo_path: {embedding_model_repo_path}") | ||||||
|  |         return embedding_model_repo_path | ||||||
|  |     except Exception as e: | ||||||
|  |         log.exception(f"Cannot determine embedding model snapshot path: {e}") | ||||||
|  |         return embedding_model | ||||||
|  |  | ||||||
|  | @ -86,6 +86,7 @@ class SignupForm(BaseModel): | ||||||
|     name: str |     name: str | ||||||
|     email: str |     email: str | ||||||
|     password: str |     password: str | ||||||
|  |     profile_image_url: Optional[str] = "/user.png" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class AuthsTable: | class AuthsTable: | ||||||
|  | @ -94,7 +95,12 @@ class AuthsTable: | ||||||
|         self.db.create_tables([Auth]) |         self.db.create_tables([Auth]) | ||||||
| 
 | 
 | ||||||
|     def insert_new_auth( |     def insert_new_auth( | ||||||
|         self, email: str, password: str, name: str, role: str = "pending" |         self, | ||||||
|  |         email: str, | ||||||
|  |         password: str, | ||||||
|  |         name: str, | ||||||
|  |         profile_image_url: str = "/user.png", | ||||||
|  |         role: str = "pending", | ||||||
|     ) -> Optional[UserModel]: |     ) -> Optional[UserModel]: | ||||||
|         log.info("insert_new_auth") |         log.info("insert_new_auth") | ||||||
| 
 | 
 | ||||||
|  | @ -105,7 +111,7 @@ class AuthsTable: | ||||||
|         ) |         ) | ||||||
|         result = Auth.create(**auth.model_dump()) |         result = Auth.create(**auth.model_dump()) | ||||||
| 
 | 
 | ||||||
|         user = Users.insert_new_user(id, name, email, role) |         user = Users.insert_new_user(id, name, email, profile_image_url, role) | ||||||
| 
 | 
 | ||||||
|         if result and user: |         if result and user: | ||||||
|             return user |             return user | ||||||
|  |  | ||||||
|  | @ -206,6 +206,18 @@ class ChatTable: | ||||||
|         except: |         except: | ||||||
|             return None |             return None | ||||||
| 
 | 
 | ||||||
|  |     def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]: | ||||||
|  |         try: | ||||||
|  |             chat = Chat.get(Chat.share_id == id) | ||||||
|  | 
 | ||||||
|  |             if chat: | ||||||
|  |                 chat = Chat.get(Chat.id == id) | ||||||
|  |                 return ChatModel(**model_to_dict(chat)) | ||||||
|  |             else: | ||||||
|  |                 return None | ||||||
|  |         except: | ||||||
|  |             return None | ||||||
|  | 
 | ||||||
|     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: |     def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: | ||||||
|         try: |         try: | ||||||
|             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) |             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) | ||||||
|  |  | ||||||
|  | @ -31,7 +31,7 @@ class UserModel(BaseModel): | ||||||
|     name: str |     name: str | ||||||
|     email: str |     email: str | ||||||
|     role: str = "pending" |     role: str = "pending" | ||||||
|     profile_image_url: str = "/user.png" |     profile_image_url: str | ||||||
|     timestamp: int  # timestamp in epoch |     timestamp: int  # timestamp in epoch | ||||||
|     api_key: Optional[str] = None |     api_key: Optional[str] = None | ||||||
| 
 | 
 | ||||||
|  | @ -59,7 +59,12 @@ class UsersTable: | ||||||
|         self.db.create_tables([User]) |         self.db.create_tables([User]) | ||||||
| 
 | 
 | ||||||
|     def insert_new_user( |     def insert_new_user( | ||||||
|         self, id: str, name: str, email: str, role: str = "pending" |         self, | ||||||
|  |         id: str, | ||||||
|  |         name: str, | ||||||
|  |         email: str, | ||||||
|  |         profile_image_url: str = "/user.png", | ||||||
|  |         role: str = "pending", | ||||||
|     ) -> Optional[UserModel]: |     ) -> Optional[UserModel]: | ||||||
|         user = UserModel( |         user = UserModel( | ||||||
|             **{ |             **{ | ||||||
|  | @ -67,7 +72,7 @@ class UsersTable: | ||||||
|                 "name": name, |                 "name": name, | ||||||
|                 "email": email, |                 "email": email, | ||||||
|                 "role": role, |                 "role": role, | ||||||
|                 "profile_image_url": "/user.png", |                 "profile_image_url": profile_image_url, | ||||||
|                 "timestamp": int(time.time()), |                 "timestamp": int(time.time()), | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm): | ||||||
|         ) |         ) | ||||||
|         hashed = get_password_hash(form_data.password) |         hashed = get_password_hash(form_data.password) | ||||||
|         user = Auths.insert_new_auth( |         user = Auths.insert_new_auth( | ||||||
|             form_data.email.lower(), hashed, form_data.name, role |             form_data.email.lower(), | ||||||
|  |             hashed, | ||||||
|  |             form_data.name, | ||||||
|  |             form_data.profile_image_url, | ||||||
|  |             role, | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         if user: |         if user: | ||||||
|  |  | ||||||
|  | @ -251,6 +251,14 @@ async def delete_shared_chat_by_id(id: str, user=Depends(get_current_user)): | ||||||
| 
 | 
 | ||||||
| @router.get("/share/{share_id}", response_model=Optional[ChatResponse]) | @router.get("/share/{share_id}", response_model=Optional[ChatResponse]) | ||||||
| async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): | async def get_shared_chat_by_id(share_id: str, user=Depends(get_current_user)): | ||||||
|  |     if user.role == "pending": | ||||||
|  |         raise HTTPException( | ||||||
|  |             status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     if user.role == "user": | ||||||
|  |         chat = Chats.get_chat_by_share_id(share_id) | ||||||
|  |     elif user.role == "admin": | ||||||
|         chat = Chats.get_chat_by_id(share_id) |         chat = Chats.get_chat_by_id(share_id) | ||||||
| 
 | 
 | ||||||
|     if chat: |     if chat: | ||||||
|  |  | ||||||
|  | @ -28,8 +28,6 @@ except ImportError: | ||||||
| WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") | WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") | ||||||
| WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" | WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" | ||||||
| 
 | 
 | ||||||
| shutil.copyfile("../build/favicon.png", "./static/favicon.png") |  | ||||||
| 
 |  | ||||||
| #################################### | #################################### | ||||||
| # ENV (dev,test,prod) | # ENV (dev,test,prod) | ||||||
| #################################### | #################################### | ||||||
|  | @ -103,6 +101,26 @@ for version in soup.find_all("h2"): | ||||||
| 
 | 
 | ||||||
| CHANGELOG = changelog_json | CHANGELOG = changelog_json | ||||||
| 
 | 
 | ||||||
|  | #################################### | ||||||
|  | # DATA/FRONTEND BUILD DIR | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
|  | DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve()) | ||||||
|  | FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build"))) | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     with open(f"{DATA_DIR}/config.json", "r") as f: | ||||||
|  |         CONFIG_DATA = json.load(f) | ||||||
|  | except: | ||||||
|  |     CONFIG_DATA = {} | ||||||
|  | 
 | ||||||
|  | #################################### | ||||||
|  | # Static DIR | ||||||
|  | #################################### | ||||||
|  | 
 | ||||||
|  | STATIC_DIR = str(Path(os.getenv("STATIC_DIR", "./static")).resolve()) | ||||||
|  | 
 | ||||||
|  | shutil.copyfile(f"{FRONTEND_BUILD_DIR}/favicon.png", f"{STATIC_DIR}/favicon.png") | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # LOGGING | # LOGGING | ||||||
|  | @ -165,7 +183,7 @@ if CUSTOM_NAME: | ||||||
| 
 | 
 | ||||||
|                 r = requests.get(url, stream=True) |                 r = requests.get(url, stream=True) | ||||||
|                 if r.status_code == 200: |                 if r.status_code == 200: | ||||||
|                     with open("./static/favicon.png", "wb") as f: |                     with open(f"{STATIC_DIR}/favicon.png", "wb") as f: | ||||||
|                         r.raw.decode_content = True |                         r.raw.decode_content = True | ||||||
|                         shutil.copyfileobj(r.raw, f) |                         shutil.copyfileobj(r.raw, f) | ||||||
| 
 | 
 | ||||||
|  | @ -177,18 +195,6 @@ else: | ||||||
|     if WEBUI_NAME != "Open WebUI": |     if WEBUI_NAME != "Open WebUI": | ||||||
|         WEBUI_NAME += " (Open WebUI)" |         WEBUI_NAME += " (Open WebUI)" | ||||||
| 
 | 
 | ||||||
| #################################### |  | ||||||
| # DATA/FRONTEND BUILD DIR |  | ||||||
| #################################### |  | ||||||
| 
 |  | ||||||
| DATA_DIR = str(Path(os.getenv("DATA_DIR", "./data")).resolve()) |  | ||||||
| FRONTEND_BUILD_DIR = str(Path(os.getenv("FRONTEND_BUILD_DIR", "../build"))) |  | ||||||
| 
 |  | ||||||
| try: |  | ||||||
|     with open(f"{DATA_DIR}/config.json", "r") as f: |  | ||||||
|         CONFIG_DATA = json.load(f) |  | ||||||
| except: |  | ||||||
|     CONFIG_DATA = {} |  | ||||||
| 
 | 
 | ||||||
| #################################### | #################################### | ||||||
| # File Upload DIR | # File Upload DIR | ||||||
|  | @ -257,6 +263,7 @@ OLLAMA_API_BASE_URL = os.environ.get( | ||||||
| 
 | 
 | ||||||
| OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | ||||||
| K8S_FLAG = os.environ.get("K8S_FLAG", "") | K8S_FLAG = os.environ.get("K8S_FLAG", "") | ||||||
|  | USE_OLLAMA_DOCKER = os.environ.get("USE_OLLAMA_DOCKER", "false") | ||||||
| 
 | 
 | ||||||
| if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||||
|     OLLAMA_BASE_URL = ( |     OLLAMA_BASE_URL = ( | ||||||
|  | @ -266,9 +273,13 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| if ENV == "prod": | if ENV == "prod": | ||||||
|     if OLLAMA_BASE_URL == "/ollama": |     if OLLAMA_BASE_URL == "/ollama" and not K8S_FLAG: | ||||||
|  |         if USE_OLLAMA_DOCKER.lower() == "true": | ||||||
|  |             # if you use all-in-one docker container (Open WebUI + Ollama) | ||||||
|  |             # with the docker build arg USE_OLLAMA=true (--build-arg="USE_OLLAMA=true") this only works with http://localhost:11434 | ||||||
|  |             OLLAMA_BASE_URL = "http://localhost:11434" | ||||||
|  |         else: | ||||||
|             OLLAMA_BASE_URL = "http://host.docker.internal:11434" |             OLLAMA_BASE_URL = "http://host.docker.internal:11434" | ||||||
| 
 |  | ||||||
|     elif K8S_FLAG: |     elif K8S_FLAG: | ||||||
|         OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" |         OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" | ||||||
| 
 | 
 | ||||||
|  | @ -391,10 +402,22 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "": | ||||||
| CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" | CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" | ||||||
| # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2) | # this uses the model defined in the Dockerfile ENV variable. If you dont use docker or docker based deployments such as k8s, the default embedding model will be used (all-MiniLM-L6-v2) | ||||||
| RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") | RAG_EMBEDDING_MODEL = os.environ.get("RAG_EMBEDDING_MODEL", "all-MiniLM-L6-v2") | ||||||
| # device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance | log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"), | ||||||
| RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get( | 
 | ||||||
|     "RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu" | RAG_EMBEDDING_MODEL_AUTO_UPDATE = ( | ||||||
|  |     os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true" | ||||||
| ) | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # device type ebbeding models - "cpu" (default), "cuda" (nvidia gpu required) or "mps" (apple silicon) - choosing this right can lead to better performance | ||||||
|  | USE_CUDA = os.environ.get("USE_CUDA_DOCKER", "false") | ||||||
|  | 
 | ||||||
|  | if USE_CUDA.lower() == "true": | ||||||
|  |     DEVICE_TYPE = "cuda" | ||||||
|  | else: | ||||||
|  |     DEVICE_TYPE = "cpu" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| CHROMA_CLIENT = chromadb.PersistentClient( | CHROMA_CLIENT = chromadb.PersistentClient( | ||||||
|     path=CHROMA_DATA_PATH, |     path=CHROMA_DATA_PATH, | ||||||
|     settings=Settings(allow_reset=True, anonymized_telemetry=False), |     settings=Settings(allow_reset=True, anonymized_telemetry=False), | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import time | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
| import logging | import logging | ||||||
|  | import aiohttp | ||||||
| import requests | import requests | ||||||
| 
 | 
 | ||||||
| from fastapi import FastAPI, Request, Depends, status | from fastapi import FastAPI, Request, Depends, status | ||||||
|  | @ -18,6 +19,7 @@ from starlette.middleware.base import BaseHTTPMiddleware | ||||||
| 
 | 
 | ||||||
| from apps.ollama.main import app as ollama_app | from apps.ollama.main import app as ollama_app | ||||||
| from apps.openai.main import app as openai_app | from apps.openai.main import app as openai_app | ||||||
|  | 
 | ||||||
| from apps.litellm.main import app as litellm_app, startup as litellm_app_startup | from apps.litellm.main import app as litellm_app, startup as litellm_app_startup | ||||||
| from apps.audio.main import app as audio_app | from apps.audio.main import app as audio_app | ||||||
| from apps.images.main import app as images_app | from apps.images.main import app as images_app | ||||||
|  | @ -38,6 +40,8 @@ from config import ( | ||||||
|     VERSION, |     VERSION, | ||||||
|     CHANGELOG, |     CHANGELOG, | ||||||
|     FRONTEND_BUILD_DIR, |     FRONTEND_BUILD_DIR, | ||||||
|  |     CACHE_DIR, | ||||||
|  |     STATIC_DIR, | ||||||
|     MODEL_FILTER_ENABLED, |     MODEL_FILTER_ENABLED, | ||||||
|     MODEL_FILTER_LIST, |     MODEL_FILTER_LIST, | ||||||
|     GLOBAL_LOG_LEVEL, |     GLOBAL_LOG_LEVEL, | ||||||
|  | @ -269,14 +273,16 @@ async def get_app_changelog(): | ||||||
| @app.get("/api/version/updates") | @app.get("/api/version/updates") | ||||||
| async def get_app_latest_release_version(): | async def get_app_latest_release_version(): | ||||||
|     try: |     try: | ||||||
|         response = requests.get( |         async with aiohttp.ClientSession() as session: | ||||||
|             f"https://api.github.com/repos/open-webui/open-webui/releases/latest" |             async with session.get( | ||||||
|         ) |                 "https://api.github.com/repos/open-webui/open-webui/releases/latest" | ||||||
|  |             ) as response: | ||||||
|                 response.raise_for_status() |                 response.raise_for_status() | ||||||
|         latest_version = response.json()["tag_name"] |                 data = await response.json() | ||||||
|  |                 latest_version = data["tag_name"] | ||||||
| 
 | 
 | ||||||
|                 return {"current": VERSION, "latest": latest_version[1:]} |                 return {"current": VERSION, "latest": latest_version[1:]} | ||||||
|     except Exception as e: |     except aiohttp.ClientError as e: | ||||||
|         raise HTTPException( |         raise HTTPException( | ||||||
|             status_code=status.HTTP_503_SERVICE_UNAVAILABLE, |             status_code=status.HTTP_503_SERVICE_UNAVAILABLE, | ||||||
|             detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, |             detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, | ||||||
|  | @ -297,9 +303,8 @@ async def get_manifest_json(): | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| app.mount("/static", StaticFiles(directory="static"), name="static") | app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") | ||||||
| app.mount("/cache", StaticFiles(directory="data/cache"), name="cache") | app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache") | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| app.mount( | app.mount( | ||||||
|     "/", |     "/", | ||||||
|  |  | ||||||
|  | @ -7,16 +7,26 @@ KEY_FILE=.webui_secret_key | ||||||
| 
 | 
 | ||||||
| PORT="${PORT:-8080}" | PORT="${PORT:-8080}" | ||||||
| if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then | if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then | ||||||
|   echo No WEBUI_SECRET_KEY provided |   echo "No WEBUI_SECRET_KEY provided" | ||||||
| 
 | 
 | ||||||
|   if ! [ -e "$KEY_FILE" ]; then |   if ! [ -e "$KEY_FILE" ]; then | ||||||
|     echo Generating WEBUI_SECRET_KEY |     echo "Generating WEBUI_SECRET_KEY" | ||||||
|     # Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one. |     # Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one. | ||||||
|     echo $(head -c 12 /dev/random | base64) > $KEY_FILE |     echo $(head -c 12 /dev/random | base64) > "$KEY_FILE" | ||||||
|   fi |   fi | ||||||
| 
 | 
 | ||||||
|   echo Loading WEBUI_SECRET_KEY from $KEY_FILE |   echo "Loading WEBUI_SECRET_KEY from $KEY_FILE" | ||||||
|   WEBUI_SECRET_KEY=`cat $KEY_FILE` |   WEBUI_SECRET_KEY=$(cat "$KEY_FILE") | ||||||
|  | fi | ||||||
|  | 
 | ||||||
|  | if [ "$USE_OLLAMA_DOCKER" = "true" ]; then | ||||||
|  |     echo "USE_OLLAMA is set to true, starting ollama serve." | ||||||
|  |     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 | fi | ||||||
| 
 | 
 | ||||||
| WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' | WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' | ||||||
							
								
								
									
										8
									
								
								docker-compose.amdgpu.yaml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								docker-compose.amdgpu.yaml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | services: | ||||||
|  |   ollama: | ||||||
|  |     devices: | ||||||
|  |       - /dev/kfd:/dev/kfd | ||||||
|  |       - /dev/dri:/dev/dri | ||||||
|  |     image: ollama/ollama:${OLLAMA_DOCKER_TAG-rocm} | ||||||
|  |     environment: | ||||||
|  |       - 'HSA_OVERRIDE_GFX_VERSION=${HSA_OVERRIDE_GFX_VERSION-11.0.0}' | ||||||
|  | @ -8,7 +8,7 @@ services: | ||||||
|     pull_policy: always |     pull_policy: always | ||||||
|     tty: true |     tty: true | ||||||
|     restart: unless-stopped |     restart: unless-stopped | ||||||
|     image: ollama/ollama:latest |     image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest} | ||||||
| 
 | 
 | ||||||
|   open-webui: |   open-webui: | ||||||
|     build: |     build: | ||||||
|  | @ -16,7 +16,7 @@ services: | ||||||
|       args: |       args: | ||||||
|         OLLAMA_BASE_URL: '/ollama' |         OLLAMA_BASE_URL: '/ollama' | ||||||
|       dockerfile: Dockerfile |       dockerfile: Dockerfile | ||||||
|     image: ghcr.io/open-webui/open-webui:main |     image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main} | ||||||
|     container_name: open-webui |     container_name: open-webui | ||||||
|     volumes: |     volumes: | ||||||
|       - open-webui:/app/backend/data |       - open-webui:/app/backend/data | ||||||
|  |  | ||||||
|  | @ -7,8 +7,12 @@ ollama | ||||||
| {{- end -}} | {{- end -}} | ||||||
| 
 | 
 | ||||||
| {{- define "ollama.url" -}} | {{- define "ollama.url" -}} | ||||||
|  | {{- if .Values.ollama.externalHost }} | ||||||
|  | {{- printf .Values.ollama.externalHost }} | ||||||
|  | {{- else }} | ||||||
| {{- printf "http://%s.%s.svc.cluster.local:%d/" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }} | {{- printf "http://%s.%s.svc.cluster.local:%d/" (include "ollama.name" .) (.Release.Namespace) (.Values.ollama.service.port | int) }} | ||||||
| {{- end }} | {{- end }} | ||||||
|  | {{- end }} | ||||||
| 
 | 
 | ||||||
| {{- define "chart.name" -}} | {{- define "chart.name" -}} | ||||||
| {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | {{- if not .Values.ollama.externalHost }} | ||||||
| apiVersion: v1 | apiVersion: v1 | ||||||
| kind: Service | kind: Service | ||||||
| metadata: | metadata: | ||||||
|  | @ -19,3 +20,4 @@ spec: | ||||||
|     port: {{ .port }} |     port: {{ .port }} | ||||||
|     targetPort: http |     targetPort: http | ||||||
| {{- end }} | {{- end }} | ||||||
|  | {{- end }} | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | {{- if not .Values.ollama.externalHost }} | ||||||
| apiVersion: apps/v1 | apiVersion: apps/v1 | ||||||
| kind: StatefulSet | kind: StatefulSet | ||||||
| metadata: | metadata: | ||||||
|  | @ -94,3 +95,4 @@ spec: | ||||||
|         {{- toYaml . | nindent 8 }} |         {{- toYaml . | nindent 8 }} | ||||||
|       {{- end }} |       {{- end }} | ||||||
|       {{- end }} |       {{- end }} | ||||||
|  | {{- end }} | ||||||
|  |  | ||||||
|  | @ -17,7 +17,9 @@ spec: | ||||||
|   resources: |   resources: | ||||||
|     requests: |     requests: | ||||||
|       storage: {{ .Values.webui.persistence.size }} |       storage: {{ .Values.webui.persistence.size }} | ||||||
|  |   {{- if .Values.webui.persistence.storageClass }} | ||||||
|   storageClassName: {{ .Values.webui.persistence.storageClass }} |   storageClassName: {{ .Values.webui.persistence.storageClass }} | ||||||
|  |   {{- end }} | ||||||
|   {{- with .Values.webui.persistence.selector }} |   {{- with .Values.webui.persistence.selector }} | ||||||
|   selector: |   selector: | ||||||
|     {{- toYaml . | nindent 4 }} |     {{- toYaml . | nindent 4 }} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| nameOverride: "" | nameOverride: "" | ||||||
| 
 | 
 | ||||||
| ollama: | ollama: | ||||||
|  |   externalHost: "" | ||||||
|   annotations: {} |   annotations: {} | ||||||
|   podAnnotations: {} |   podAnnotations: {} | ||||||
|   replicaCount: 1 |   replicaCount: 1 | ||||||
|  |  | ||||||
							
								
								
									
										10
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -1,12 +1,12 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.117", | 	"version": "0.1.118", | ||||||
| 	"lockfileVersion": 3, | 	"lockfileVersion": 3, | ||||||
| 	"requires": true, | 	"requires": true, | ||||||
| 	"packages": { | 	"packages": { | ||||||
| 		"": { | 		"": { | ||||||
| 			"name": "open-webui", | 			"name": "open-webui", | ||||||
| 			"version": "0.1.117", | 			"version": "0.1.118", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"@sveltejs/adapter-node": "^1.3.1", | 				"@sveltejs/adapter-node": "^1.3.1", | ||||||
| 				"async": "^3.2.5", | 				"async": "^3.2.5", | ||||||
|  | @ -5688,9 +5688,9 @@ | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		"node_modules/undici": { | 		"node_modules/undici": { | ||||||
| 			"version": "5.28.3", | 			"version": "5.28.4", | ||||||
| 			"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", | 			"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", | ||||||
| 			"integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", | 			"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", | ||||||
| 			"dependencies": { | 			"dependencies": { | ||||||
| 				"@fastify/busboy": "^2.0.0" | 				"@fastify/busboy": "^2.0.0" | ||||||
| 			}, | 			}, | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
| 	"name": "open-webui", | 	"name": "open-webui", | ||||||
| 	"version": "0.1.117", | 	"version": "0.1.118", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite dev --host", | 		"dev": "vite dev --host", | ||||||
|  |  | ||||||
|  | @ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => { | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const userSignUp = async (name: string, email: string, password: string) => { | export const userSignUp = async ( | ||||||
|  | 	name: string, | ||||||
|  | 	email: string, | ||||||
|  | 	password: string, | ||||||
|  | 	profile_image_url: string | ||||||
|  | ) => { | ||||||
| 	let error = null; | 	let error = null; | ||||||
| 
 | 
 | ||||||
| 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { | 	const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup`, { | ||||||
|  | @ -69,7 +74,8 @@ export const userSignUp = async (name: string, email: string, password: string) | ||||||
| 		body: JSON.stringify({ | 		body: JSON.stringify({ | ||||||
| 			name: name, | 			name: name, | ||||||
| 			email: email, | 			email: email, | ||||||
| 			password: password | 			password: password, | ||||||
|  | 			profile_image_url: profile_image_url | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
| 		.then(async (res) => { | 		.then(async (res) => { | ||||||
|  |  | ||||||
|  | @ -345,3 +345,64 @@ export const resetVectorDB = async (token: string) => { | ||||||
| 
 | 
 | ||||||
| 	return res; | 	return res; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export const getEmbeddingModel = async (token: string) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${RAG_API_BASE_URL}/embedding/model`, { | ||||||
|  | 		method: 'GET', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type EmbeddingModelUpdateForm = { | ||||||
|  | 	embedding_model: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const updateEmbeddingModel = async (token: string, payload: EmbeddingModelUpdateForm) => { | ||||||
|  | 	let error = null; | ||||||
|  | 
 | ||||||
|  | 	const res = await fetch(`${RAG_API_BASE_URL}/embedding/model/update`, { | ||||||
|  | 		method: 'POST', | ||||||
|  | 		headers: { | ||||||
|  | 			'Content-Type': 'application/json', | ||||||
|  | 			Authorization: `Bearer ${token}` | ||||||
|  | 		}, | ||||||
|  | 		body: JSON.stringify({ | ||||||
|  | 			...payload | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 		.then(async (res) => { | ||||||
|  | 			if (!res.ok) throw await res.json(); | ||||||
|  | 			return res.json(); | ||||||
|  | 		}) | ||||||
|  | 		.catch((err) => { | ||||||
|  | 			console.log(err); | ||||||
|  | 			error = err.detail; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 	if (error) { | ||||||
|  | 		throw error; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return res; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | @ -295,6 +295,13 @@ | ||||||
| 
 | 
 | ||||||
| 		const dropZone = document.querySelector('body'); | 		const dropZone = document.querySelector('body'); | ||||||
| 
 | 
 | ||||||
|  | 		const handleKeyDown = (event: KeyboardEvent) => { | ||||||
|  | 			if (event.key === 'Escape') { | ||||||
|  | 				console.log('Escape'); | ||||||
|  | 				dragged = false; | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
| 		const onDragOver = (e) => { | 		const onDragOver = (e) => { | ||||||
| 			e.preventDefault(); | 			e.preventDefault(); | ||||||
| 			dragged = true; | 			dragged = true; | ||||||
|  | @ -350,11 +357,15 @@ | ||||||
| 			dragged = false; | 			dragged = false; | ||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
|  | 		window.addEventListener('keydown', handleKeyDown); | ||||||
|  | 
 | ||||||
| 		dropZone?.addEventListener('dragover', onDragOver); | 		dropZone?.addEventListener('dragover', onDragOver); | ||||||
| 		dropZone?.addEventListener('drop', onDrop); | 		dropZone?.addEventListener('drop', onDrop); | ||||||
| 		dropZone?.addEventListener('dragleave', onDragLeave); | 		dropZone?.addEventListener('dragleave', onDragLeave); | ||||||
| 
 | 
 | ||||||
| 		return () => { | 		return () => { | ||||||
|  | 			window.removeEventListener('keydown', handleKeyDown); | ||||||
|  | 
 | ||||||
| 			dropZone?.removeEventListener('dragover', onDragOver); | 			dropZone?.removeEventListener('dragover', onDragOver); | ||||||
| 			dropZone?.removeEventListener('drop', onDrop); | 			dropZone?.removeEventListener('drop', onDrop); | ||||||
| 			dropZone?.removeEventListener('dragleave', onDragLeave); | 			dropZone?.removeEventListener('dragleave', onDragLeave); | ||||||
|  |  | ||||||
|  | @ -107,12 +107,8 @@ | ||||||
| 		await sendPrompt(userPrompt, userMessageId, chatId); | 		await sendPrompt(userPrompt, userMessageId, chatId); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const confirmEditResponseMessage = async (messageId, content) => { | 	const updateChatMessages = async () => { | ||||||
| 		history.messages[messageId].originalContent = history.messages[messageId].content; |  | ||||||
| 		history.messages[messageId].content = content; |  | ||||||
| 
 |  | ||||||
| 		await tick(); | 		await tick(); | ||||||
| 
 |  | ||||||
| 		await updateChatById(localStorage.token, chatId, { | 		await updateChatById(localStorage.token, chatId, { | ||||||
| 			messages: messages, | 			messages: messages, | ||||||
| 			history: history | 			history: history | ||||||
|  | @ -121,15 +117,20 @@ | ||||||
| 		await chats.set(await getChatList(localStorage.token)); | 		await chats.set(await getChatList(localStorage.token)); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const rateMessage = async (messageId, rating) => { | 	const confirmEditResponseMessage = async (messageId, content) => { | ||||||
| 		history.messages[messageId].rating = rating; | 		history.messages[messageId].originalContent = history.messages[messageId].content; | ||||||
| 		await tick(); | 		history.messages[messageId].content = content; | ||||||
| 		await updateChatById(localStorage.token, chatId, { |  | ||||||
| 			messages: messages, |  | ||||||
| 			history: history |  | ||||||
| 		}); |  | ||||||
| 
 | 
 | ||||||
| 		await chats.set(await getChatList(localStorage.token)); | 		await updateChatMessages(); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const rateMessage = async (messageId, rating) => { | ||||||
|  | 		history.messages[messageId].annotation = { | ||||||
|  | 			...history.messages[messageId].annotation, | ||||||
|  | 			rating: rating | ||||||
|  | 		}; | ||||||
|  | 
 | ||||||
|  | 		await updateChatMessages(); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const showPreviousMessage = async (message) => { | 	const showPreviousMessage = async (message) => { | ||||||
|  | @ -338,6 +339,7 @@ | ||||||
| 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||||
| 								isLastMessage={messageIdx + 1 === messages.length} | 								isLastMessage={messageIdx + 1 === messages.length} | ||||||
| 								{readOnly} | 								{readOnly} | ||||||
|  | 								{updateChatMessages} | ||||||
| 								{confirmEditResponseMessage} | 								{confirmEditResponseMessage} | ||||||
| 								{showPreviousMessage} | 								{showPreviousMessage} | ||||||
| 								{showNextMessage} | 								{showNextMessage} | ||||||
|  |  | ||||||
							
								
								
									
										117
									
								
								src/lib/components/chat/Messages/RateComment.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/lib/components/chat/Messages/RateComment.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,117 @@ | ||||||
|  | <script lang="ts"> | ||||||
|  | 	import { toast } from 'svelte-sonner'; | ||||||
|  | 
 | ||||||
|  | 	import { createEventDispatcher, onMount } from 'svelte'; | ||||||
|  | 
 | ||||||
|  | 	const dispatch = createEventDispatcher(); | ||||||
|  | 
 | ||||||
|  | 	export let show = false; | ||||||
|  | 	export let message; | ||||||
|  | 
 | ||||||
|  | 	const LIKE_REASONS = [ | ||||||
|  | 		`Accurate information`, | ||||||
|  | 		`Followed instructions perfectly`, | ||||||
|  | 		`Showcased creativity`, | ||||||
|  | 		`Positive attitude`, | ||||||
|  | 		`Attention to detail`, | ||||||
|  | 		`Thorough explanation`, | ||||||
|  | 		`Other` | ||||||
|  | 	]; | ||||||
|  | 
 | ||||||
|  | 	const DISLIKE_REASONS = [ | ||||||
|  | 		`Don't like the style`, | ||||||
|  | 		`Not factually correct`, | ||||||
|  | 		`Didn't fully follow instructions`, | ||||||
|  | 		`Refused when it shouldn't have`, | ||||||
|  | 		`Being Lazy`, | ||||||
|  | 		`Other` | ||||||
|  | 	]; | ||||||
|  | 
 | ||||||
|  | 	let reasons = []; | ||||||
|  | 	let selectedReason = null; | ||||||
|  | 	let comment = ''; | ||||||
|  | 
 | ||||||
|  | 	$: if (message.annotation.rating === 1) { | ||||||
|  | 		reasons = LIKE_REASONS; | ||||||
|  | 	} else if (message.annotation.rating === -1) { | ||||||
|  | 		reasons = DISLIKE_REASONS; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	onMount(() => { | ||||||
|  | 		selectedReason = message.annotation.reason; | ||||||
|  | 		comment = message.annotation.comment; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	const submitHandler = () => { | ||||||
|  | 		console.log('submitHandler'); | ||||||
|  | 
 | ||||||
|  | 		message.annotation.reason = selectedReason; | ||||||
|  | 		message.annotation.comment = comment; | ||||||
|  | 
 | ||||||
|  | 		dispatch('submit'); | ||||||
|  | 
 | ||||||
|  | 		toast.success('Thanks for your feedback!'); | ||||||
|  | 		show = false; | ||||||
|  | 	}; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <div class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"> | ||||||
|  | 	<div class="flex justify-between items-center"> | ||||||
|  | 		<div class=" text-sm">Tell us more:</div> | ||||||
|  | 
 | ||||||
|  | 		<button | ||||||
|  | 			on:click={() => { | ||||||
|  | 				show = false; | ||||||
|  | 			}} | ||||||
|  | 		> | ||||||
|  | 			<svg | ||||||
|  | 				xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 				fill="none" | ||||||
|  | 				viewBox="0 0 24 24" | ||||||
|  | 				stroke-width="1.5" | ||||||
|  | 				stroke="currentColor" | ||||||
|  | 				class="size-4" | ||||||
|  | 			> | ||||||
|  | 				<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> | ||||||
|  | 			</svg> | ||||||
|  | 		</button> | ||||||
|  | 	</div> | ||||||
|  | 
 | ||||||
|  | 	{#if reasons.length > 0} | ||||||
|  | 		<div class="flex flex-wrap gap-2 text-sm mt-2.5"> | ||||||
|  | 			{#each reasons as reason} | ||||||
|  | 				<button | ||||||
|  | 					class="px-3.5 py-1 border dark:border-gray-850 dark:hover:bg-gray-850 {selectedReason === | ||||||
|  | 					reason | ||||||
|  | 						? 'dark:bg-gray-800' | ||||||
|  | 						: ''} transition rounded-lg" | ||||||
|  | 					on:click={() => { | ||||||
|  | 						selectedReason = reason; | ||||||
|  | 					}} | ||||||
|  | 				> | ||||||
|  | 					{reason} | ||||||
|  | 				</button> | ||||||
|  | 			{/each} | ||||||
|  | 		</div> | ||||||
|  | 	{/if} | ||||||
|  | 
 | ||||||
|  | 	<div class="mt-2"> | ||||||
|  | 		<textarea | ||||||
|  | 			bind:value={comment} | ||||||
|  | 			class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl" | ||||||
|  | 			placeholder="Feel free to add specific details" | ||||||
|  | 			rows="2" | ||||||
|  | 		/> | ||||||
|  | 	</div> | ||||||
|  | 
 | ||||||
|  | 	<div class="mt-2 flex justify-end"> | ||||||
|  | 		<button | ||||||
|  | 			class=" bg-emerald-700 text-white text-sm font-medium rounded-lg px-3.5 py-1.5" | ||||||
|  | 			on:click={() => { | ||||||
|  | 				submitHandler(); | ||||||
|  | 			}} | ||||||
|  | 		> | ||||||
|  | 			Submit | ||||||
|  | 		</button> | ||||||
|  | 	</div> | ||||||
|  | </div> | ||||||
|  | @ -30,6 +30,7 @@ | ||||||
| 	import Image from '$lib/components/common/Image.svelte'; | 	import Image from '$lib/components/common/Image.svelte'; | ||||||
| 	import { WEBUI_BASE_URL } from '$lib/constants'; | 	import { WEBUI_BASE_URL } from '$lib/constants'; | ||||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
|  | 	import RateComment from './RateComment.svelte'; | ||||||
| 
 | 
 | ||||||
| 	export let modelfiles = []; | 	export let modelfiles = []; | ||||||
| 	export let message; | 	export let message; | ||||||
|  | @ -39,6 +40,7 @@ | ||||||
| 
 | 
 | ||||||
| 	export let readOnly = false; | 	export let readOnly = false; | ||||||
| 
 | 
 | ||||||
|  | 	export let updateChatMessages: Function; | ||||||
| 	export let confirmEditResponseMessage: Function; | 	export let confirmEditResponseMessage: Function; | ||||||
| 	export let showPreviousMessage: Function; | 	export let showPreviousMessage: Function; | ||||||
| 	export let showNextMessage: Function; | 	export let showNextMessage: Function; | ||||||
|  | @ -60,6 +62,8 @@ | ||||||
| 	let loadingSpeech = false; | 	let loadingSpeech = false; | ||||||
| 	let generatingImage = false; | 	let generatingImage = false; | ||||||
| 
 | 
 | ||||||
|  | 	let showRateComment = false; | ||||||
|  | 
 | ||||||
| 	$: tokens = marked.lexer(sanitizeResponseContent(message.content)); | 	$: tokens = marked.lexer(sanitizeResponseContent(message.content)); | ||||||
| 
 | 
 | ||||||
| 	const renderer = new marked.Renderer(); | 	const renderer = new marked.Renderer(); | ||||||
|  | @ -536,11 +540,13 @@ | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
| 														? 'visible' | 														? 'visible' | ||||||
| 														: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1 | 														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation | ||||||
|  | 														?.rating === 1 | ||||||
| 														? 'bg-gray-100 dark:bg-gray-800' | 														? 'bg-gray-100 dark:bg-gray-800' | ||||||
| 														: ''} dark:hover:text-white hover:text-black transition" | 														: ''} dark:hover:text-white hover:text-black transition" | ||||||
| 													on:click={() => { | 													on:click={() => { | ||||||
| 														rateMessage(message.id, 1); | 														rateMessage(message.id, 1); | ||||||
|  | 														showRateComment = true; | ||||||
| 													}} | 													}} | ||||||
| 												> | 												> | ||||||
| 													<svg | 													<svg | ||||||
|  | @ -563,11 +569,13 @@ | ||||||
| 												<button | 												<button | ||||||
| 													class="{isLastMessage | 													class="{isLastMessage | ||||||
| 														? 'visible' | 														? 'visible' | ||||||
| 														: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1 | 														: 'invisible group-hover:visible'} p-1 rounded {message?.annotation | ||||||
|  | 														?.rating === -1 | ||||||
| 														? 'bg-gray-100 dark:bg-gray-800' | 														? 'bg-gray-100 dark:bg-gray-800' | ||||||
| 														: ''} dark:hover:text-white hover:text-black transition" | 														: ''} dark:hover:text-white hover:text-black transition" | ||||||
| 													on:click={() => { | 													on:click={() => { | ||||||
| 														rateMessage(message.id, -1); | 														rateMessage(message.id, -1); | ||||||
|  | 														showRateComment = true; | ||||||
| 													}} | 													}} | ||||||
| 												> | 												> | ||||||
| 													<svg | 													<svg | ||||||
|  | @ -824,6 +832,16 @@ | ||||||
| 										{/if} | 										{/if} | ||||||
| 									</div> | 									</div> | ||||||
| 								{/if} | 								{/if} | ||||||
|  | 
 | ||||||
|  | 								{#if showRateComment} | ||||||
|  | 									<RateComment | ||||||
|  | 										bind:show={showRateComment} | ||||||
|  | 										bind:message | ||||||
|  | 										on:submit={() => { | ||||||
|  | 											updateChatMessages(); | ||||||
|  | 										}} | ||||||
|  | 									/> | ||||||
|  | 								{/if} | ||||||
| 							</div> | 							</div> | ||||||
| 						{/if} | 						{/if} | ||||||
| 					</div> | 					</div> | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
| 
 | 
 | ||||||
| 	import UpdatePassword from './Account/UpdatePassword.svelte'; | 	import UpdatePassword from './Account/UpdatePassword.svelte'; | ||||||
| 	import { getGravatarUrl } from '$lib/apis/utils'; | 	import { getGravatarUrl } from '$lib/apis/utils'; | ||||||
|  | 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; | ||||||
| 	import { copyToClipboard } from '$lib/utils'; | 	import { copyToClipboard } from '$lib/utils'; | ||||||
| 	import Plus from '$lib/components/icons/Plus.svelte'; | 	import Plus from '$lib/components/icons/Plus.svelte'; | ||||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
|  | @ -18,6 +19,8 @@ | ||||||
| 	let profileImageUrl = ''; | 	let profileImageUrl = ''; | ||||||
| 	let name = ''; | 	let name = ''; | ||||||
| 
 | 
 | ||||||
|  | 	let showAPIKeys = false; | ||||||
|  | 
 | ||||||
| 	let showJWTToken = false; | 	let showJWTToken = false; | ||||||
| 	let JWTTokenCopied = false; | 	let JWTTokenCopied = false; | ||||||
| 
 | 
 | ||||||
|  | @ -28,6 +31,12 @@ | ||||||
| 	let profileImageInputElement: HTMLInputElement; | 	let profileImageInputElement: HTMLInputElement; | ||||||
| 
 | 
 | ||||||
| 	const submitHandler = async () => { | 	const submitHandler = async () => { | ||||||
|  | 		if (name !== $user.name) { | ||||||
|  | 			if (profileImageUrl === generateInitialsImage($user.name) || profileImageUrl === '') { | ||||||
|  | 				profileImageUrl = generateInitialsImage(name); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( | 		const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( | ||||||
| 			(error) => { | 			(error) => { | ||||||
| 				toast.error(error); | 				toast.error(error); | ||||||
|  | @ -125,11 +134,12 @@ | ||||||
| 			}} | 			}} | ||||||
| 		/> | 		/> | ||||||
| 
 | 
 | ||||||
| 		<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div> | 		<div class="space-y-1"> | ||||||
|  | 			<!-- <div class=" text-sm font-medium">{$i18n.t('Account')}</div> --> | ||||||
| 
 | 
 | ||||||
| 			<div class="flex space-x-5"> | 			<div class="flex space-x-5"> | ||||||
| 				<div class="flex flex-col"> | 				<div class="flex flex-col"> | ||||||
| 				<div class="self-center"> | 					<div class="self-center mt-2"> | ||||||
| 						<button | 						<button | ||||||
| 							class="relative rounded-full dark:bg-gray-700" | 							class="relative rounded-full dark:bg-gray-700" | ||||||
| 							type="button" | 							type="button" | ||||||
|  | @ -138,9 +148,9 @@ | ||||||
| 							}} | 							}} | ||||||
| 						> | 						> | ||||||
| 							<img | 							<img | ||||||
| 							src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} | 								src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)} | ||||||
| 								alt="profile" | 								alt="profile" | ||||||
| 							class=" rounded-full w-16 h-16 object-cover" | 								class=" rounded-full size-16 object-cover" | ||||||
| 							/> | 							/> | ||||||
| 
 | 
 | ||||||
| 							<div | 							<div | ||||||
|  | @ -161,23 +171,56 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 						</button> | 						</button> | ||||||
| 					</div> | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="flex-1 flex flex-col self-center gap-0.5"> | ||||||
|  | 					<div class=" mb-0.5 text-sm font-medium">{$i18n.t('Profile Image')}</div> | ||||||
|  | 
 | ||||||
|  | 					<div> | ||||||
| 						<button | 						<button | ||||||
| 					class=" text-xs text-gray-600" | 							class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850" | ||||||
|  | 							on:click={async () => { | ||||||
|  | 								if (canvasPixelTest()) { | ||||||
|  | 									profileImageUrl = generateInitialsImage(name); | ||||||
|  | 								} else { | ||||||
|  | 									toast.info( | ||||||
|  | 										$i18n.t( | ||||||
|  | 											'Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.' | ||||||
|  | 										), | ||||||
|  | 										{ | ||||||
|  | 											duration: 1000 * 10 | ||||||
|  | 										} | ||||||
|  | 									); | ||||||
|  | 								} | ||||||
|  | 							}}>{$i18n.t('Use Initials')}</button | ||||||
|  | 						> | ||||||
|  | 
 | ||||||
|  | 						<button | ||||||
|  | 							class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-full px-4 py-0.5 bg-gray-100 dark:bg-gray-850" | ||||||
| 							on:click={async () => { | 							on:click={async () => { | ||||||
| 								const url = await getGravatarUrl($user.email); | 								const url = await getGravatarUrl($user.email); | ||||||
| 
 | 
 | ||||||
| 								profileImageUrl = url; | 								profileImageUrl = url; | ||||||
| 							}}>{$i18n.t('Use Gravatar')}</button | 							}}>{$i18n.t('Use Gravatar')}</button | ||||||
| 						> | 						> | ||||||
|  | 
 | ||||||
|  | 						<button | ||||||
|  | 							class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg px-2 py-1" | ||||||
|  | 							on:click={async () => { | ||||||
|  | 								profileImageUrl = '/user.png'; | ||||||
|  | 							}}>{$i18n.t('Remove')}</button | ||||||
|  | 						> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 
 | ||||||
| 			<div class="flex-1"> | 			<div class="pt-0.5"> | ||||||
| 				<div class="flex flex-col w-full"> | 				<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 font-medium">{$i18n.t('Name')}</div> | ||||||
| 
 | 
 | ||||||
| 					<div class="flex-1"> | 					<div class="flex-1"> | ||||||
| 						<input | 						<input | ||||||
| 							class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" | 							class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 							type="text" | 							type="text" | ||||||
| 							bind:value={name} | 							bind:value={name} | ||||||
| 							required | 							required | ||||||
|  | @ -187,11 +230,24 @@ | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<hr class=" dark:border-gray-700 my-4" /> | 		<div class="py-0.5"> | ||||||
| 			<UpdatePassword /> | 			<UpdatePassword /> | ||||||
|  | 		</div> | ||||||
| 
 | 
 | ||||||
| 		<hr class=" dark:border-gray-700 my-4" /> | 		<hr class=" dark:border-gray-700 my-4" /> | ||||||
| 
 | 
 | ||||||
|  | 		<div class="flex justify-between items-center text-sm"> | ||||||
|  | 			<div class="  font-medium">{$i18n.t('API keys')}</div> | ||||||
|  | 			<button | ||||||
|  | 				class=" text-xs font-medium text-gray-500" | ||||||
|  | 				type="button" | ||||||
|  | 				on:click={() => { | ||||||
|  | 					showAPIKeys = !showAPIKeys; | ||||||
|  | 				}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button | ||||||
|  | 			> | ||||||
|  | 		</div> | ||||||
|  | 
 | ||||||
|  | 		{#if showAPIKeys} | ||||||
| 			<div class="flex flex-col gap-4"> | 			<div class="flex flex-col gap-4"> | ||||||
| 				<div class="justify-between w-full"> | 				<div class="justify-between w-full"> | ||||||
| 					<div class="flex justify-between w-full"> | 					<div class="flex justify-between w-full"> | ||||||
|  | @ -201,14 +257,14 @@ | ||||||
| 					<div class="flex mt-2"> | 					<div class="flex mt-2"> | ||||||
| 						<div class="flex w-full"> | 						<div class="flex w-full"> | ||||||
| 							<input | 							<input | ||||||
| 							class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" | 								class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 								type={showJWTToken ? 'text' : 'password'} | 								type={showJWTToken ? 'text' : 'password'} | ||||||
| 								value={localStorage.token} | 								value={localStorage.token} | ||||||
| 								disabled | 								disabled | ||||||
| 							/> | 							/> | ||||||
| 
 | 
 | ||||||
| 							<button | 							<button | ||||||
| 							class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" | 								class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									showJWTToken = !showJWTToken; | 									showJWTToken = !showJWTToken; | ||||||
| 								}} | 								}} | ||||||
|  | @ -248,7 +304,7 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<button | 						<button | ||||||
| 						class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" | 							class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" | ||||||
| 							on:click={() => { | 							on:click={() => { | ||||||
| 								copyToClipboard(localStorage.token); | 								copyToClipboard(localStorage.token); | ||||||
| 								JWTTokenCopied = true; | 								JWTTokenCopied = true; | ||||||
|  | @ -301,14 +357,14 @@ | ||||||
| 						{#if APIKey} | 						{#if APIKey} | ||||||
| 							<div class="flex w-full"> | 							<div class="flex w-full"> | ||||||
| 								<input | 								<input | ||||||
| 								class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-800 outline-none" | 									class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									type={showAPIKey ? 'text' : 'password'} | 									type={showAPIKey ? 'text' : 'password'} | ||||||
| 									value={APIKey} | 									value={APIKey} | ||||||
| 									disabled | 									disabled | ||||||
| 								/> | 								/> | ||||||
| 
 | 
 | ||||||
| 								<button | 								<button | ||||||
| 								class="px-2 transition rounded-r-lg bg-white dark:bg-gray-800" | 									class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850" | ||||||
| 									on:click={() => { | 									on:click={() => { | ||||||
| 										showAPIKey = !showAPIKey; | 										showAPIKey = !showAPIKey; | ||||||
| 									}} | 									}} | ||||||
|  | @ -348,7 +404,7 @@ | ||||||
| 							</div> | 							</div> | ||||||
| 
 | 
 | ||||||
| 							<button | 							<button | ||||||
| 							class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-800 transition rounded-lg" | 								class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									copyToClipboard(APIKey); | 									copyToClipboard(APIKey); | ||||||
| 									APIKeyCopied = true; | 									APIKeyCopied = true; | ||||||
|  | @ -393,7 +449,7 @@ | ||||||
| 
 | 
 | ||||||
| 							<Tooltip content="Create new key"> | 							<Tooltip content="Create new key"> | ||||||
| 								<button | 								<button | ||||||
| 								class=" px-1.5 py-1 dark:hover:bg-gray-800transition rounded-lg" | 									class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" | ||||||
| 									on:click={() => { | 									on:click={() => { | ||||||
| 										createAPIKeyHandler(); | 										createAPIKeyHandler(); | ||||||
| 									}} | 									}} | ||||||
|  | @ -416,7 +472,7 @@ | ||||||
| 							</Tooltip> | 							</Tooltip> | ||||||
| 						{:else} | 						{:else} | ||||||
| 							<button | 							<button | ||||||
| 							class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition" | 								class="flex gap-1.5 items-center font-medium px-3.5 py-1.5 rounded-lg bg-gray-100/70 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-850 transition" | ||||||
| 								on:click={() => { | 								on:click={() => { | ||||||
| 									createAPIKeyHandler(); | 									createAPIKeyHandler(); | ||||||
| 								}} | 								}} | ||||||
|  | @ -429,6 +485,7 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  | 		{/if} | ||||||
| 	</div> | 	</div> | ||||||
| 
 | 
 | ||||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
|  |  | ||||||
|  | @ -185,7 +185,7 @@ | ||||||
| 
 | 
 | ||||||
| 			<div> | 			<div> | ||||||
| 				<div class=" py-0.5 flex w-full justify-between"> | 				<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">{$i18n.t('Notifications')}</div> | ||||||
| 
 | 
 | ||||||
| 					<button | 					<button | ||||||
| 						class="p-1 px-3 text-xs flex rounded transition" | 						class="p-1 px-3 text-xs flex rounded transition" | ||||||
|  |  | ||||||
|  | @ -7,6 +7,7 @@ | ||||||
| 	export let show = true; | 	export let show = true; | ||||||
| 	export let size = 'md'; | 	export let size = 'md'; | ||||||
| 
 | 
 | ||||||
|  | 	let modalElement = null; | ||||||
| 	let mounted = false; | 	let mounted = false; | ||||||
| 
 | 
 | ||||||
| 	const sizeToWidth = (size) => { | 	const sizeToWidth = (size) => { | ||||||
|  | @ -19,14 +20,23 @@ | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const handleKeyDown = (event: KeyboardEvent) => { | ||||||
|  | 		if (event.key === 'Escape') { | ||||||
|  | 			console.log('Escape'); | ||||||
|  | 			show = false; | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	onMount(() => { | 	onMount(() => { | ||||||
| 		mounted = true; | 		mounted = true; | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	$: if (mounted) { | 	$: if (mounted) { | ||||||
| 		if (show) { | 		if (show) { | ||||||
|  | 			window.addEventListener('keydown', handleKeyDown); | ||||||
| 			document.body.style.overflow = 'hidden'; | 			document.body.style.overflow = 'hidden'; | ||||||
| 		} else { | 		} else { | ||||||
|  | 			window.removeEventListener('keydown', handleKeyDown); | ||||||
| 			document.body.style.overflow = 'unset'; | 			document.body.style.overflow = 'unset'; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -36,6 +46,7 @@ | ||||||
| 	<!-- svelte-ignore a11y-click-events-have-key-events --> | 	<!-- svelte-ignore a11y-click-events-have-key-events --> | ||||||
| 	<!-- svelte-ignore a11y-no-static-element-interactions --> | 	<!-- svelte-ignore a11y-no-static-element-interactions --> | ||||||
| 	<div | 	<div | ||||||
|  | 		bind:this={modalElement} | ||||||
| 		class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" | 		class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" | ||||||
| 		in:fade={{ duration: 10 }} | 		in:fade={{ duration: 10 }} | ||||||
| 		on:click={() => { | 		on:click={() => { | ||||||
|  |  | ||||||
|  | @ -6,18 +6,23 @@ | ||||||
| 		getQuerySettings, | 		getQuerySettings, | ||||||
| 		scanDocs, | 		scanDocs, | ||||||
| 		updateQuerySettings, | 		updateQuerySettings, | ||||||
| 		resetVectorDB | 		resetVectorDB, | ||||||
|  | 		getEmbeddingModel, | ||||||
|  | 		updateEmbeddingModel | ||||||
| 	} from '$lib/apis/rag'; | 	} from '$lib/apis/rag'; | ||||||
| 
 | 
 | ||||||
| 	import { documents } from '$lib/stores'; | 	import { documents } from '$lib/stores'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
| 
 | 
 | ||||||
|  | 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||||
|  | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
| 	export let saveHandler: Function; | 	export let saveHandler: Function; | ||||||
| 
 | 
 | ||||||
| 	let loading = false; | 	let scanDirLoading = false; | ||||||
|  | 	let updateEmbeddingModelLoading = false; | ||||||
| 
 | 
 | ||||||
| 	let showResetConfirm = false; | 	let showResetConfirm = false; | ||||||
| 
 | 
 | ||||||
|  | @ -30,10 +35,12 @@ | ||||||
| 		k: 4 | 		k: 4 | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	let embeddingModel = ''; | ||||||
|  | 
 | ||||||
| 	const scanHandler = async () => { | 	const scanHandler = async () => { | ||||||
| 		loading = true; | 		scanDirLoading = true; | ||||||
| 		const res = await scanDocs(localStorage.token); | 		const res = await scanDocs(localStorage.token); | ||||||
| 		loading = false; | 		scanDirLoading = false; | ||||||
| 
 | 
 | ||||||
| 		if (res) { | 		if (res) { | ||||||
| 			await documents.set(await getDocs(localStorage.token)); | 			await documents.set(await getDocs(localStorage.token)); | ||||||
|  | @ -41,6 +48,38 @@ | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
|  | 	const embeddingModelUpdateHandler = async () => { | ||||||
|  | 		if (embeddingModel.split('/').length - 1 > 1) { | ||||||
|  | 			toast.error( | ||||||
|  | 				$i18n.t( | ||||||
|  | 					'Model filesystem path detected. Model shortname is required for update, cannot continue.' | ||||||
|  | 				) | ||||||
|  | 			); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		console.log('Update embedding model attempt:', embeddingModel); | ||||||
|  | 
 | ||||||
|  | 		updateEmbeddingModelLoading = true; | ||||||
|  | 		const res = await updateEmbeddingModel(localStorage.token, { | ||||||
|  | 			embedding_model: embeddingModel | ||||||
|  | 		}).catch(async (error) => { | ||||||
|  | 			toast.error(error); | ||||||
|  | 			embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model; | ||||||
|  | 			return null; | ||||||
|  | 		}); | ||||||
|  | 		updateEmbeddingModelLoading = false; | ||||||
|  | 
 | ||||||
|  | 		if (res) { | ||||||
|  | 			console.log('embeddingModelUpdateHandler:', res); | ||||||
|  | 			if (res.status === true) { | ||||||
|  | 				toast.success($i18n.t('Model {{embedding_model}} update complete!', res), { | ||||||
|  | 					duration: 1000 * 10 | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
| 	const submitHandler = async () => { | 	const submitHandler = async () => { | ||||||
| 		const res = await updateRAGConfig(localStorage.token, { | 		const res = await updateRAGConfig(localStorage.token, { | ||||||
| 			pdf_extract_images: pdfExtractImages, | 			pdf_extract_images: pdfExtractImages, | ||||||
|  | @ -62,6 +101,8 @@ | ||||||
| 			chunkOverlap = res.chunk.chunk_overlap; | 			chunkOverlap = res.chunk.chunk_overlap; | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model; | ||||||
|  | 
 | ||||||
| 		querySettings = await getQuerySettings(localStorage.token); | 		querySettings = await getQuerySettings(localStorage.token); | ||||||
| 	}); | 	}); | ||||||
| </script> | </script> | ||||||
|  | @ -73,7 +114,7 @@ | ||||||
| 		saveHandler(); | 		saveHandler(); | ||||||
| 	}} | 	}} | ||||||
| > | > | ||||||
| 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> | 	<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]"> | ||||||
| 		<div> | 		<div> | ||||||
| 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | 			<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> | ||||||
| 
 | 
 | ||||||
|  | @ -83,7 +124,7 @@ | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 				<button | 				<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 | 					class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading | ||||||
| 						? ' cursor-not-allowed' | 						? ' cursor-not-allowed' | ||||||
| 						: ''}" | 						: ''}" | ||||||
| 					on:click={() => { | 					on:click={() => { | ||||||
|  | @ -91,24 +132,11 @@ | ||||||
| 						console.log('check'); | 						console.log('check'); | ||||||
| 					}} | 					}} | ||||||
| 					type="button" | 					type="button" | ||||||
| 					disabled={loading} | 					disabled={scanDirLoading} | ||||||
| 				> | 				> | ||||||
| 					<div class="self-center font-medium">{$i18n.t('Scan')}</div> | 					<div class="self-center font-medium">{$i18n.t('Scan')}</div> | ||||||
| 
 | 
 | ||||||
| 					<!-- <svg | 					{#if scanDirLoading} | ||||||
| 						xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 						viewBox="0 0 16 16" |  | ||||||
| 						fill="currentColor" |  | ||||||
| 						class="w-3 h-3" |  | ||||||
| 					> |  | ||||||
| 						<path |  | ||||||
| 							fill-rule="evenodd" |  | ||||||
| 							d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z" |  | ||||||
| 							clip-rule="evenodd" |  | ||||||
| 						/> |  | ||||||
| 					</svg> --> |  | ||||||
| 
 |  | ||||||
| 					{#if loading} |  | ||||||
| 						<div class="ml-3 self-center"> | 						<div class="ml-3 self-center"> | ||||||
| 							<svg | 							<svg | ||||||
| 								class=" w-3 h-3" | 								class=" w-3 h-3" | ||||||
|  | @ -141,6 +169,78 @@ | ||||||
| 
 | 
 | ||||||
| 		<hr class=" dark:border-gray-700" /> | 		<hr class=" dark:border-gray-700" /> | ||||||
| 
 | 
 | ||||||
|  | 		<div class="space-y-2"> | ||||||
|  | 			<div> | ||||||
|  | 				<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</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('Update embedding model (e.g. {{model}})', { | ||||||
|  | 								model: embeddingModel.slice(-40) | ||||||
|  | 							})} | ||||||
|  | 							bind:value={embeddingModel} | ||||||
|  | 						/> | ||||||
|  | 					</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" | ||||||
|  | 						on:click={() => { | ||||||
|  | 							embeddingModelUpdateHandler(); | ||||||
|  | 						}} | ||||||
|  | 						disabled={updateEmbeddingModelLoading} | ||||||
|  | 					> | ||||||
|  | 						{#if updateEmbeddingModelLoading} | ||||||
|  | 							<div class="self-center"> | ||||||
|  | 								<svg | ||||||
|  | 									class=" w-4 h-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> | ||||||
|  | 						{:else} | ||||||
|  | 							<svg | ||||||
|  | 								xmlns="http://www.w3.org/2000/svg" | ||||||
|  | 								viewBox="0 0 16 16" | ||||||
|  | 								fill="currentColor" | ||||||
|  | 								class="w-4 h-4" | ||||||
|  | 							> | ||||||
|  | 								<path | ||||||
|  | 									d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" | ||||||
|  | 								/> | ||||||
|  | 								<path | ||||||
|  | 									d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" | ||||||
|  | 								/> | ||||||
|  | 							</svg> | ||||||
|  | 						{/if} | ||||||
|  | 					</button> | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> | ||||||
|  | 					{$i18n.t( | ||||||
|  | 						'Warning: If you update or change your embedding model, you will need to re-import all documents.' | ||||||
|  | 					)} | ||||||
|  | 				</div> | ||||||
|  | 
 | ||||||
|  | 				<hr class=" dark:border-gray-700 my-3" /> | ||||||
|  | 
 | ||||||
| 				<div class=" "> | 				<div class=" "> | ||||||
| 					<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> | 					<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> | ||||||
| 
 | 
 | ||||||
|  | @ -150,7 +250,7 @@ | ||||||
| 
 | 
 | ||||||
| 							<div class="self-center p-3"> | 							<div class="self-center p-3"> | ||||||
| 								<input | 								<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" | 									class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									type="number" | 									type="number" | ||||||
| 									placeholder={$i18n.t('Enter Chunk Size')} | 									placeholder={$i18n.t('Enter Chunk Size')} | ||||||
| 									bind:value={chunkSize} | 									bind:value={chunkSize} | ||||||
|  | @ -161,11 +261,13 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<div class="flex w-full"> | 						<div class="flex w-full"> | ||||||
| 					<div class=" self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Overlap')}</div> | 							<div class=" self-center text-xs font-medium min-w-fit"> | ||||||
|  | 								{$i18n.t('Chunk Overlap')} | ||||||
|  | 							</div> | ||||||
| 
 | 
 | ||||||
| 							<div class="self-center p-3"> | 							<div class="self-center p-3"> | ||||||
| 								<input | 								<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" | 									class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									type="number" | 									type="number" | ||||||
| 									placeholder={$i18n.t('Enter Chunk Overlap')} | 									placeholder={$i18n.t('Enter Chunk Overlap')} | ||||||
| 									bind:value={chunkOverlap} | 									bind:value={chunkOverlap} | ||||||
|  | @ -176,7 +278,7 @@ | ||||||
| 						</div> | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 
 | 
 | ||||||
| 			<div> | 					<div class="pr-2"> | ||||||
| 						<div class="flex justify-between items-center text-xs"> | 						<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">{$i18n.t('PDF Extract Images (OCR)')}</div> | ||||||
| 
 | 
 | ||||||
|  | @ -191,6 +293,8 @@ | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
|  | 				<hr class=" dark:border-gray-700 my-3" /> | ||||||
|  | 
 | ||||||
| 				<div> | 				<div> | ||||||
| 					<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> | 					<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> | ||||||
| 
 | 
 | ||||||
|  | @ -200,7 +304,7 @@ | ||||||
| 
 | 
 | ||||||
| 							<div class="self-center p-3"> | 							<div class="self-center p-3"> | ||||||
| 								<input | 								<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" | 									class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
| 									type="number" | 									type="number" | ||||||
| 									placeholder={$i18n.t('Enter Top K')} | 									placeholder={$i18n.t('Enter Top K')} | ||||||
| 									bind:value={querySettings.k} | 									bind:value={querySettings.k} | ||||||
|  | @ -209,34 +313,19 @@ | ||||||
| 								/> | 								/> | ||||||
| 							</div> | 							</div> | ||||||
| 						</div> | 						</div> | ||||||
| 
 |  | ||||||
| 				<!-- <div class="flex w-full"> |  | ||||||
| 						<div class=" self-center text-xs font-medium min-w-fit">Chunk Overlap</div> |  | ||||||
| 	 |  | ||||||
| 						<div class="self-center p-3"> |  | ||||||
| 							<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="Enter Chunk Overlap" |  | ||||||
| 								bind:value={chunkOverlap} |  | ||||||
| 								autocomplete="off" |  | ||||||
| 								min="0" |  | ||||||
| 							/> |  | ||||||
| 						</div> |  | ||||||
| 					</div> --> |  | ||||||
| 					</div> | 					</div> | ||||||
| 
 | 
 | ||||||
| 					<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">{$i18n.t('RAG Template')}</div> | ||||||
| 						<textarea | 						<textarea | ||||||
| 							bind:value={querySettings.template} | 							bind:value={querySettings.template} | ||||||
| 					class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" | 							class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" | ||||||
| 							rows="4" | 							rows="4" | ||||||
| 						/> | 						/> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 
 | 
 | ||||||
| 		<hr class=" dark:border-gray-700" /> | 				<hr class=" dark:border-gray-700 my-3" /> | ||||||
| 
 | 
 | ||||||
| 				{#if showResetConfirm} | 				{#if showResetConfirm} | ||||||
| 					<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> | 					<div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> | ||||||
|  | @ -330,7 +419,8 @@ | ||||||
| 					</button> | 					</button> | ||||||
| 				{/if} | 				{/if} | ||||||
| 			</div> | 			</div> | ||||||
| 
 | 		</div> | ||||||
|  | 	</div> | ||||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||||
| 		<button | 		<button | ||||||
| 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | 			class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded" | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Изтрито {tagName}", | 	"Deleted {tagName}": "Изтрито {tagName}", | ||||||
| 	"Description": "Описание", | 	"Description": "Описание", | ||||||
| 	"Desktop Notifications": "Десктоп Известия", | 	"Notifications": "Десктоп Известия", | ||||||
| 	"Disabled": "Деактивиран", | 	"Disabled": "Деактивиран", | ||||||
| 	"Discover a modelfile": "Откриване на модфайл", | 	"Discover a modelfile": "Откриване на модфайл", | ||||||
| 	"Discover a prompt": "Откриване на промпт", | 	"Discover a prompt": "Откриване на промпт", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Esborrat {tagName}", | 	"Deleted {tagName}": "Esborrat {tagName}", | ||||||
| 	"Description": "Descripció", | 	"Description": "Descripció", | ||||||
| 	"Desktop Notifications": "Notificacions d'Escriptori", | 	"Notifications": "Notificacions d'Escriptori", | ||||||
| 	"Disabled": "Desactivat", | 	"Disabled": "Desactivat", | ||||||
| 	"Discover a modelfile": "Descobreix un fitxer de model", | 	"Discover a modelfile": "Descobreix un fitxer de model", | ||||||
| 	"Discover a prompt": "Descobreix un prompt", | 	"Discover a prompt": "Descobreix un prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", | ||||||
| 	"Deleted {tagName}": "{tagName} gelöscht", | 	"Deleted {tagName}": "{tagName} gelöscht", | ||||||
| 	"Description": "Beschreibung", | 	"Description": "Beschreibung", | ||||||
| 	"Desktop Notifications": "Desktop-Benachrichtigungen", | 	"Notifications": "Desktop-Benachrichtigungen", | ||||||
| 	"Disabled": "Deaktiviert", | 	"Disabled": "Deaktiviert", | ||||||
| 	"Discover a modelfile": "Eine Modelfiles entdecken", | 	"Discover a modelfile": "Eine Modelfiles entdecken", | ||||||
| 	"Discover a prompt": "Einen Prompt entdecken", | 	"Discover a prompt": "Einen Prompt entdecken", | ||||||
|  |  | ||||||
							
								
								
									
										64
									
								
								src/lib/i18n/locales/en-GB/translation.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/lib/i18n/locales/en-GB/translation.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,64 @@ | ||||||
|  | { | ||||||
|  | 	"analyze": "analyse", | ||||||
|  | 	"analyzed": "analysed", | ||||||
|  | 	"analyzes": "analyses", | ||||||
|  | 	"apologize": "apologise", | ||||||
|  | 	"apologized": "apologised", | ||||||
|  | 	"apologizes": "apologises", | ||||||
|  | 	"apologizing": "apologising", | ||||||
|  | 	"canceled": "cancelled", | ||||||
|  | 	"canceling": "cancelling", | ||||||
|  | 	"capitalize": "capitalise", | ||||||
|  | 	"capitalized": "capitalised", | ||||||
|  | 	"capitalizes": "capitalises", | ||||||
|  | 	"center": "centre", | ||||||
|  | 	"centered": "centred", | ||||||
|  | 	"color": "colour", | ||||||
|  | 	"colorize": "colourise", | ||||||
|  | 	"customize": "customise", | ||||||
|  | 	"customizes": "customises", | ||||||
|  | 	"defense": "defence", | ||||||
|  | 	"dialog": "dialogue", | ||||||
|  | 	"emphasize": "emphasise", | ||||||
|  | 	"emphasized": "emphasised", | ||||||
|  | 	"emphasizes": "emphasises", | ||||||
|  | 	"favor": "favour", | ||||||
|  | 	"favorable": "favourable", | ||||||
|  | 	"favorite": "favourite", | ||||||
|  | 	"favoritism": "favouritism", | ||||||
|  | 	"labor": "labour", | ||||||
|  | 	"labored": "laboured", | ||||||
|  | 	"laboring": "labouring", | ||||||
|  | 	"maximize": "maximise", | ||||||
|  | 	"maximizes": "maximises", | ||||||
|  | 	"minimize": "minimise", | ||||||
|  | 	"minimizes": "minimises", | ||||||
|  | 	"neighbor": "neighbour", | ||||||
|  | 	"neighborhood": "neighbourhood", | ||||||
|  | 	"offense": "offence", | ||||||
|  | 	"organize": "organise", | ||||||
|  | 	"organizes": "organises", | ||||||
|  | 	"personalize": "personalise", | ||||||
|  | 	"personalizes": "personalises", | ||||||
|  | 	"program": "programme", | ||||||
|  | 	"programmed": "programmed", | ||||||
|  | 	"programs": "programmes", | ||||||
|  | 	"quantization": "quantisation", | ||||||
|  | 	"quantize": "quantise", | ||||||
|  | 	"randomize": "randomise", | ||||||
|  | 	"randomizes": "randomises", | ||||||
|  | 	"realize": "realise", | ||||||
|  | 	"realizes": "realises", | ||||||
|  | 	"recognize": "recognise", | ||||||
|  | 	"recognizes": "recognises", | ||||||
|  | 	"summarize": "summarise", | ||||||
|  | 	"summarizes": "summarises", | ||||||
|  | 	"theater": "theatre", | ||||||
|  | 	"theaters": "theatres", | ||||||
|  | 	"toward": "towards", | ||||||
|  | 	"traveled": "travelled", | ||||||
|  | 	"traveler": "traveller", | ||||||
|  | 	"traveling": "travelling", | ||||||
|  | 	"utilize": "utilise", | ||||||
|  | 	"utilizes": "utilises" | ||||||
|  | } | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "", | 	"Deleted {{deleteModelTag}}": "", | ||||||
| 	"Deleted {tagName}": "", | 	"Deleted {tagName}": "", | ||||||
| 	"Description": "", | 	"Description": "", | ||||||
| 	"Desktop Notifications": "", | 	"Notifications": "", | ||||||
| 	"Disabled": "", | 	"Disabled": "", | ||||||
| 	"Discover a modelfile": "", | 	"Discover a modelfile": "", | ||||||
| 	"Discover a prompt": "", | 	"Discover a prompt": "", | ||||||
|  | @ -120,6 +120,7 @@ | ||||||
| 	"Edit Doc": "", | 	"Edit Doc": "", | ||||||
| 	"Edit User": "", | 	"Edit User": "", | ||||||
| 	"Email": "", | 	"Email": "", | ||||||
|  | 	"Embedding model: {{embedding_model}}": "", | ||||||
| 	"Enable Chat History": "", | 	"Enable Chat History": "", | ||||||
| 	"Enable New Sign Ups": "", | 	"Enable New Sign Ups": "", | ||||||
| 	"Enabled": "", | 	"Enabled": "", | ||||||
|  | @ -150,6 +151,7 @@ | ||||||
| 	"Failed to read clipboard contents": "", | 	"Failed to read clipboard contents": "", | ||||||
| 	"File Mode": "", | 	"File Mode": "", | ||||||
| 	"File not found.": "", | 	"File not found.": "", | ||||||
|  | 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", | ||||||
| 	"Focus chat input": "", | 	"Focus chat input": "", | ||||||
| 	"Format your variables using square brackets like this:": "", | 	"Format your variables using square brackets like this:": "", | ||||||
| 	"From (Base Model)": "", | 	"From (Base Model)": "", | ||||||
|  | @ -193,8 +195,11 @@ | ||||||
| 	"MMMM DD, YYYY": "", | 	"MMMM DD, YYYY": "", | ||||||
| 	"Model '{{modelName}}' has been successfully downloaded.": "", | 	"Model '{{modelName}}' has been successfully downloaded.": "", | ||||||
| 	"Model '{{modelTag}}' is already in queue for downloading.": "", | 	"Model '{{modelTag}}' is already in queue for downloading.": "", | ||||||
|  | 	"Model {{embedding_model}} update complete!": "", | ||||||
|  | 	"Model {{embedding_model}} update failed or not required!": "", | ||||||
| 	"Model {{modelId}} not found": "", | 	"Model {{modelId}} not found": "", | ||||||
| 	"Model {{modelName}} already exists.": "", | 	"Model {{modelName}} already exists.": "", | ||||||
|  | 	"Model filesystem path detected. Model shortname is required for update, cannot continue.": "", | ||||||
| 	"Model Name": "", | 	"Model Name": "", | ||||||
| 	"Model not selected": "", | 	"Model not selected": "", | ||||||
| 	"Model Tag Name": "", | 	"Model Tag Name": "", | ||||||
|  | @ -332,7 +337,10 @@ | ||||||
| 	"TTS Settings": "", | 	"TTS Settings": "", | ||||||
| 	"Type Hugging Face Resolve (Download) URL": "", | 	"Type Hugging Face Resolve (Download) URL": "", | ||||||
| 	"Uh-oh! There was an issue connecting to {{provider}}.": "", | 	"Uh-oh! There was an issue connecting to {{provider}}.": "", | ||||||
|  | 	"Understand that updating or changing your embedding model requires reset of the vector database and re-import of all documents. You have been warned!": "", | ||||||
| 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "", | 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "", | ||||||
|  | 	"Update": "", | ||||||
|  | 	"Update embedding model {{embedding_model}}": "", | ||||||
| 	"Update password": "", | 	"Update password": "", | ||||||
| 	"Upload a GGUF model": "", | 	"Upload a GGUF model": "", | ||||||
| 	"Upload files": "", | 	"Upload files": "", | ||||||
|  | @ -340,6 +348,7 @@ | ||||||
| 	"URL Mode": "", | 	"URL Mode": "", | ||||||
| 	"Use '#' in the prompt input to load and select your documents.": "", | 	"Use '#' in the prompt input to load and select your documents.": "", | ||||||
| 	"Use Gravatar": "", | 	"Use Gravatar": "", | ||||||
|  | 	"Use Initials": "", | ||||||
| 	"user": "", | 	"user": "", | ||||||
| 	"User Permissions": "", | 	"User Permissions": "", | ||||||
| 	"Users": "", | 	"Users": "", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Se borró {tagName}", | 	"Deleted {tagName}": "Se borró {tagName}", | ||||||
| 	"Description": "Descripción", | 	"Description": "Descripción", | ||||||
| 	"Desktop Notifications": "Notificaciones", | 	"Notifications": "Notificaciones", | ||||||
| 	"Disabled": "Desactivado", | 	"Disabled": "Desactivado", | ||||||
| 	"Discover a modelfile": "Descubre un modelfile", | 	"Discover a modelfile": "Descubre un modelfile", | ||||||
| 	"Discover a prompt": "Descubre un Prompt", | 	"Discover a prompt": "Descubre un Prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", | ||||||
| 	"Deleted {tagName}": "{tagName} حذف شد", | 	"Deleted {tagName}": "{tagName} حذف شد", | ||||||
| 	"Description": "توضیحات", | 	"Description": "توضیحات", | ||||||
| 	"Desktop Notifications": "اعلان", | 	"Notifications": "اعلان", | ||||||
| 	"Disabled": "غیرفعال", | 	"Disabled": "غیرفعال", | ||||||
| 	"Discover a modelfile": "فایل مدل را کشف کنید", | 	"Discover a modelfile": "فایل مدل را کشف کنید", | ||||||
| 	"Discover a prompt": "یک اعلان را کشف کنید", | 	"Discover a prompt": "یک اعلان را کشف کنید", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | ||||||
| 	"Deleted {tagName}": "{tagName} supprimé", | 	"Deleted {tagName}": "{tagName} supprimé", | ||||||
| 	"Description": "Description", | 	"Description": "Description", | ||||||
| 	"Desktop Notifications": "Notifications de bureau", | 	"Notifications": "Notifications de bureau", | ||||||
| 	"Disabled": "Désactivé", | 	"Disabled": "Désactivé", | ||||||
| 	"Discover a modelfile": "Découvrir un fichier de modèle", | 	"Discover a modelfile": "Découvrir un fichier de modèle", | ||||||
| 	"Discover a prompt": "Découvrir un prompt", | 	"Discover a prompt": "Découvrir un prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | ||||||
| 	"Deleted {tagName}": "{tagName} supprimé", | 	"Deleted {tagName}": "{tagName} supprimé", | ||||||
| 	"Description": "Description", | 	"Description": "Description", | ||||||
| 	"Desktop Notifications": "Notifications de bureau", | 	"Notifications": "Notifications de bureau", | ||||||
| 	"Disabled": "Désactivé", | 	"Disabled": "Désactivé", | ||||||
| 	"Discover a modelfile": "Découvrir un fichier de modèle", | 	"Discover a modelfile": "Découvrir un fichier de modèle", | ||||||
| 	"Discover a prompt": "Découvrir un prompt", | 	"Discover a prompt": "Découvrir un prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Eliminato {tagName}", | 	"Deleted {tagName}": "Eliminato {tagName}", | ||||||
| 	"Description": "Descrizione", | 	"Description": "Descrizione", | ||||||
| 	"Desktop Notifications": "Notifiche desktop", | 	"Notifications": "Notifiche desktop", | ||||||
| 	"Disabled": "Disabilitato", | 	"Disabled": "Disabilitato", | ||||||
| 	"Discover a modelfile": "Scopri un file modello", | 	"Discover a modelfile": "Scopri un file modello", | ||||||
| 	"Discover a prompt": "Scopri un prompt", | 	"Discover a prompt": "Scopri un prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", | ||||||
| 	"Deleted {tagName}": "{tagName} を削除しました", | 	"Deleted {tagName}": "{tagName} を削除しました", | ||||||
| 	"Description": "説明", | 	"Description": "説明", | ||||||
| 	"Desktop Notifications": "デスクトップ通知", | 	"Notifications": "デスクトップ通知", | ||||||
| 	"Disabled": "無効", | 	"Disabled": "無効", | ||||||
| 	"Discover a modelfile": "モデルファイルを見つける", | 	"Discover a modelfile": "モデルファイルを見つける", | ||||||
| 	"Discover a prompt": "プロンプトを見つける", | 	"Discover a prompt": "プロンプトを見つける", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", | ||||||
| 	"Deleted {tagName}": "{tagName} 삭제됨", | 	"Deleted {tagName}": "{tagName} 삭제됨", | ||||||
| 	"Description": "설명", | 	"Description": "설명", | ||||||
| 	"Desktop Notifications": "알림", | 	"Notifications": "알림", | ||||||
| 	"Disabled": "비활성화", | 	"Disabled": "비활성화", | ||||||
| 	"Discover a modelfile": "모델파일 검색", | 	"Discover a modelfile": "모델파일 검색", | ||||||
| 	"Discover a prompt": "프롬프트 검색", | 	"Discover a prompt": "프롬프트 검색", | ||||||
|  |  | ||||||
|  | @ -15,6 +15,10 @@ | ||||||
| 		"code": "de-DE", | 		"code": "de-DE", | ||||||
| 		"title": "Deutsch" | 		"title": "Deutsch" | ||||||
| 	}, | 	}, | ||||||
|  | 	{ | ||||||
|  | 		"code": "en-GB", | ||||||
|  | 		"title": "English (GB)" | ||||||
|  | 	}, | ||||||
| 	{ | 	{ | ||||||
| 		"code": "es-ES", | 		"code": "es-ES", | ||||||
| 		"title": "Spanish" | 		"title": "Spanish" | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", | ||||||
| 	"Deleted {tagName}": "{tagName} is verwijderd", | 	"Deleted {tagName}": "{tagName} is verwijderd", | ||||||
| 	"Description": "Beschrijving", | 	"Description": "Beschrijving", | ||||||
| 	"Desktop Notifications": "Desktop Notificaties", | 	"Notifications": "Desktop Notificaties", | ||||||
| 	"Disabled": "Uitgeschakeld", | 	"Disabled": "Uitgeschakeld", | ||||||
| 	"Discover a modelfile": "Ontdek een modelfile", | 	"Discover a modelfile": "Ontdek een modelfile", | ||||||
| 	"Discover a prompt": "Ontdek een prompt", | 	"Discover a prompt": "Ontdek een prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | ||||||
| 	"Deleted {tagName}": "{tagName} excluído", | 	"Deleted {tagName}": "{tagName} excluído", | ||||||
| 	"Description": "Descrição", | 	"Description": "Descrição", | ||||||
| 	"Desktop Notifications": "Notificações da Área de Trabalho", | 	"Notifications": "Notificações da Área de Trabalho", | ||||||
| 	"Disabled": "Desativado", | 	"Disabled": "Desativado", | ||||||
| 	"Discover a modelfile": "Descobrir um arquivo de modelo", | 	"Discover a modelfile": "Descobrir um arquivo de modelo", | ||||||
| 	"Discover a prompt": "Descobrir um prompt", | 	"Discover a prompt": "Descobrir um prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | ||||||
| 	"Deleted {tagName}": "{tagName} excluído", | 	"Deleted {tagName}": "{tagName} excluído", | ||||||
| 	"Description": "Descrição", | 	"Description": "Descrição", | ||||||
| 	"Desktop Notifications": "Notificações da Área de Trabalho", | 	"Notifications": "Notificações da Área de Trabalho", | ||||||
| 	"Disabled": "Desativado", | 	"Disabled": "Desativado", | ||||||
| 	"Discover a modelfile": "Descobrir um arquivo de modelo", | 	"Discover a modelfile": "Descobrir um arquivo de modelo", | ||||||
| 	"Discover a prompt": "Descobrir um prompt", | 	"Discover a prompt": "Descobrir um prompt", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Удалено {tagName}", | 	"Deleted {tagName}": "Удалено {tagName}", | ||||||
| 	"Description": "Описание", | 	"Description": "Описание", | ||||||
| 	"Desktop Notifications": "Уведомления на рабочем столе", | 	"Notifications": "Уведомления на рабочем столе", | ||||||
| 	"Disabled": "Отключено", | 	"Disabled": "Отключено", | ||||||
| 	"Discover a modelfile": "Найти файл модели", | 	"Discover a modelfile": "Найти файл модели", | ||||||
| 	"Discover a prompt": "Найти промт", | 	"Discover a prompt": "Найти промт", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi", | 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi", | ||||||
| 	"Deleted {tagName}": "{tagName} silindi", | 	"Deleted {tagName}": "{tagName} silindi", | ||||||
| 	"Description": "Açıklama", | 	"Description": "Açıklama", | ||||||
| 	"Desktop Notifications": "Masaüstü Bildirimleri", | 	"Notifications": "Bildirimler", | ||||||
| 	"Disabled": "Devre Dışı", | 	"Disabled": "Devre Dışı", | ||||||
| 	"Discover a modelfile": "Bir model dosyası keşfedin", | 	"Discover a modelfile": "Bir model dosyası keşfedin", | ||||||
| 	"Discover a prompt": "Bir prompt keşfedin", | 	"Discover a prompt": "Bir prompt keşfedin", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Видалено {tagName}", | 	"Deleted {tagName}": "Видалено {tagName}", | ||||||
| 	"Description": "Опис", | 	"Description": "Опис", | ||||||
| 	"Desktop Notifications": "Сповіщення", | 	"Notifications": "Сповіщення", | ||||||
| 	"Disabled": "Вимкнено", | 	"Disabled": "Вимкнено", | ||||||
| 	"Discover a modelfile": "Знайти файл моделі", | 	"Discover a modelfile": "Знайти файл моделі", | ||||||
| 	"Discover a prompt": "Знайти промт", | 	"Discover a prompt": "Знайти промт", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "Đã xóa {tagName}", | 	"Deleted {tagName}": "Đã xóa {tagName}", | ||||||
| 	"Description": "Mô tả", | 	"Description": "Mô tả", | ||||||
| 	"Desktop Notifications": "Thông báo trên máy tính (Notification)", | 	"Notifications": "Thông báo trên máy tính (Notification)", | ||||||
| 	"Disabled": "Đã vô hiệu hóa", | 	"Disabled": "Đã vô hiệu hóa", | ||||||
| 	"Discover a modelfile": "Khám phá thêm các mô hình mới", | 	"Discover a modelfile": "Khám phá thêm các mô hình mới", | ||||||
| 	"Discover a prompt": "Khám phá thêm prompt mới", | 	"Discover a prompt": "Khám phá thêm prompt mới", | ||||||
|  |  | ||||||
|  | @ -100,7 +100,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "已删除{tagName}", | 	"Deleted {tagName}": "已删除{tagName}", | ||||||
| 	"Description": "描述", | 	"Description": "描述", | ||||||
| 	"Desktop Notifications": "桌面通知", | 	"Notifications": "桌面通知", | ||||||
| 	"Disabled": "禁用", | 	"Disabled": "禁用", | ||||||
| 	"Discover a modelfile": "探索模型文件", | 	"Discover a modelfile": "探索模型文件", | ||||||
| 	"Discover a prompt": "探索提示词", | 	"Discover a prompt": "探索提示词", | ||||||
|  |  | ||||||
|  | @ -101,7 +101,7 @@ | ||||||
| 	"Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", | 	"Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", | ||||||
| 	"Deleted {tagName}": "已刪除 {tagName}", | 	"Deleted {tagName}": "已刪除 {tagName}", | ||||||
| 	"Description": "描述", | 	"Description": "描述", | ||||||
| 	"Desktop Notifications": "桌面通知", | 	"Notifications": "桌面通知", | ||||||
| 	"Disabled": "已停用", | 	"Disabled": "已停用", | ||||||
| 	"Discover a modelfile": "發現新 Modelfile", | 	"Discover a modelfile": "發現新 Modelfile", | ||||||
| 	"Discover a prompt": "發現新提示詞", | 	"Discover a prompt": "發現新提示詞", | ||||||
|  |  | ||||||
|  | @ -111,6 +111,82 @@ export const getGravatarURL = (email) => { | ||||||
| 	return `https://www.gravatar.com/avatar/${hash}`; | 	return `https://www.gravatar.com/avatar/${hash}`; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const canvasPixelTest = () => { | ||||||
|  | 	// Test a 1x1 pixel to potentially identify browser/plugin fingerprint blocking or spoofing
 | ||||||
|  | 	// Inspiration: https://github.com/kkapsner/CanvasBlocker/blob/master/test/detectionTest.js
 | ||||||
|  | 	const canvas = document.createElement('canvas'); | ||||||
|  | 	const ctx = canvas.getContext('2d'); | ||||||
|  | 	canvas.height = 1; | ||||||
|  | 	canvas.width = 1; | ||||||
|  | 	const imageData = new ImageData(canvas.width, canvas.height); | ||||||
|  | 	const pixelValues = imageData.data; | ||||||
|  | 
 | ||||||
|  | 	// Generate RGB test data
 | ||||||
|  | 	for (let i = 0; i < imageData.data.length; i += 1) { | ||||||
|  | 		if (i % 4 !== 3) { | ||||||
|  | 			pixelValues[i] = Math.floor(256 * Math.random()); | ||||||
|  | 		} else { | ||||||
|  | 			pixelValues[i] = 255; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx.putImageData(imageData, 0, 0); | ||||||
|  | 	const p = ctx.getImageData(0, 0, canvas.width, canvas.height).data; | ||||||
|  | 
 | ||||||
|  | 	// Read RGB data and fail if unmatched
 | ||||||
|  | 	for (let i = 0; i < p.length; i += 1) { | ||||||
|  | 		if (p[i] !== pixelValues[i]) { | ||||||
|  | 			console.log( | ||||||
|  | 				'canvasPixelTest: Wrong canvas pixel RGB value detected:', | ||||||
|  | 				p[i], | ||||||
|  | 				'at:', | ||||||
|  | 				i, | ||||||
|  | 				'expected:', | ||||||
|  | 				pixelValues[i] | ||||||
|  | 			); | ||||||
|  | 			console.log('canvasPixelTest: Canvas blocking or spoofing is likely'); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const generateInitialsImage = (name) => { | ||||||
|  | 	const canvas = document.createElement('canvas'); | ||||||
|  | 	const ctx = canvas.getContext('2d'); | ||||||
|  | 	canvas.width = 100; | ||||||
|  | 	canvas.height = 100; | ||||||
|  | 
 | ||||||
|  | 	if (!canvasPixelTest()) { | ||||||
|  | 		console.log( | ||||||
|  | 			'generateInitialsImage: failed pixel test, fingerprint evasion is likely. Using default image.' | ||||||
|  | 		); | ||||||
|  | 		return '/user.png'; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	ctx.fillStyle = '#F39C12'; | ||||||
|  | 	ctx.fillRect(0, 0, canvas.width, canvas.height); | ||||||
|  | 
 | ||||||
|  | 	ctx.fillStyle = '#FFFFFF'; | ||||||
|  | 	ctx.font = '40px Helvetica'; | ||||||
|  | 	ctx.textAlign = 'center'; | ||||||
|  | 	ctx.textBaseline = 'middle'; | ||||||
|  | 
 | ||||||
|  | 	const sanitizedName = name.trim(); | ||||||
|  | 	const initials = | ||||||
|  | 		sanitizedName.length > 0 | ||||||
|  | 			? sanitizedName[0] + | ||||||
|  | 			  (sanitizedName.split(' ').length > 1 | ||||||
|  | 					? sanitizedName[sanitizedName.lastIndexOf(' ') + 1] | ||||||
|  | 					: '') | ||||||
|  | 			: ''; | ||||||
|  | 
 | ||||||
|  | 	ctx.fillText(initials.toUpperCase(), canvas.width / 2, canvas.height / 2); | ||||||
|  | 
 | ||||||
|  | 	return canvas.toDataURL(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const copyToClipboard = (text) => { | export const copyToClipboard = (text) => { | ||||||
| 	if (!navigator.clipboard) { | 	if (!navigator.clipboard) { | ||||||
| 		const textArea = document.createElement('textarea'); | 		const textArea = document.createElement('textarea'); | ||||||
|  |  | ||||||
|  | @ -4,6 +4,8 @@ | ||||||
| 	import { goto } from '$app/navigation'; | 	import { goto } from '$app/navigation'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 
 | 
 | ||||||
|  | 	import dayjs from 'dayjs'; | ||||||
|  | 
 | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
| 
 | 
 | ||||||
| 	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; | 	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; | ||||||
|  | @ -16,6 +18,7 @@ | ||||||
| 	let loaded = false; | 	let loaded = false; | ||||||
| 	let users = []; | 	let users = []; | ||||||
| 
 | 
 | ||||||
|  | 	let search = ''; | ||||||
| 	let selectedUser = null; | 	let selectedUser = null; | ||||||
| 
 | 
 | ||||||
| 	let showSettingsModal = false; | 	let showSettingsModal = false; | ||||||
|  | @ -80,20 +83,15 @@ | ||||||
| 
 | 
 | ||||||
| <SettingsModal bind:show={showSettingsModal} /> | <SettingsModal bind:show={showSettingsModal} /> | ||||||
| 
 | 
 | ||||||
| <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white font-mona"> | <div class="min-h-screen max-h-[100dvh] w-full flex justify-center dark:text-white"> | ||||||
| 	{#if loaded} | 	{#if loaded} | ||||||
| 		<div class=" flex flex-col justify-between w-full overflow-y-auto"> | 		<div class=" flex flex-col justify-between w-full overflow-y-auto"> | ||||||
| 			<div class="max-w-2xl mx-auto w-full px-3 md:px-0 my-10"> | 			<div class=" mx-auto w-full"> | ||||||
| 				<div class="w-full"> | 				<div class="w-full"> | ||||||
| 					<div class=" flex flex-col justify-center"> | 					<div class=" flex flex-col justify-center"> | ||||||
|  | 						<div class=" px-5 pt-3"> | ||||||
| 							<div class=" flex justify-between items-center"> | 							<div class=" flex justify-between items-center"> | ||||||
| 							<div class="flex items-center text-2xl font-semibold"> | 								<div class="flex items-center text-2xl font-semibold">Dashboard</div> | ||||||
| 								{$i18n.t('All Users')} |  | ||||||
| 								<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> |  | ||||||
| 								<span class="text-lg font-medium text-gray-500 dark:text-gray-300" |  | ||||||
| 									>{users.length}</span |  | ||||||
| 								> |  | ||||||
| 							</div> |  | ||||||
| 								<div> | 								<div> | ||||||
| 									<button | 									<button | ||||||
| 										class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition" | 										class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition" | ||||||
|  | @ -119,32 +117,64 @@ | ||||||
| 									</button> | 									</button> | ||||||
| 								</div> | 								</div> | ||||||
| 							</div> | 							</div> | ||||||
| 						<div class=" text-gray-500 text-xs mt-1"> |  | ||||||
| 							ⓘ {$i18n.t("Click on the user role button to change a user's role.")} |  | ||||||
| 						</div> | 						</div> | ||||||
| 
 | 
 | ||||||
| 						<hr class=" my-3 dark:border-gray-600" /> | 						<div class="px-5 flex text-sm gap-2.5"> | ||||||
|  | 							<div class="py-3 border-b font-medium text-gray-100 cursor-pointer">Overview</div> | ||||||
|  | 							<!-- <div class="py-3 text-gray-300 cursor-pointer">Users</div> --> | ||||||
|  | 						</div> | ||||||
|  | 
 | ||||||
|  | 						<hr class=" mb-3 dark:border-gray-800" /> | ||||||
|  | 
 | ||||||
|  | 						<div class="px-5"> | ||||||
|  | 							<div class="mt-0.5 mb-3 flex justify-between"> | ||||||
|  | 								<div class="flex text-lg font-medium px-0.5"> | ||||||
|  | 									{$i18n.t('All Users')} | ||||||
|  | 									<div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> | ||||||
|  | 									<span class="text-lg font-medium text-gray-500 dark:text-gray-300" | ||||||
|  | 										>{users.length}</span | ||||||
|  | 									> | ||||||
|  | 								</div> | ||||||
|  | 
 | ||||||
|  | 								<div class=""> | ||||||
|  | 									<input | ||||||
|  | 										class=" w-60 rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" | ||||||
|  | 										placeholder={$i18n.t('Search')} | ||||||
|  | 										bind:value={search} | ||||||
|  | 									/> | ||||||
|  | 								</div> | ||||||
|  | 							</div> | ||||||
| 
 | 
 | ||||||
| 							<div class="scrollbar-hidden relative overflow-x-auto whitespace-nowrap"> | 							<div class="scrollbar-hidden relative overflow-x-auto whitespace-nowrap"> | ||||||
| 								<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto"> | 								<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto"> | ||||||
| 									<thead | 									<thead | ||||||
| 									class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400" | 										class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400" | ||||||
| 									> | 									> | ||||||
| 										<tr> | 										<tr> | ||||||
| 											<th scope="col" class="px-3 py-2"> {$i18n.t('Role')} </th> | 											<th scope="col" class="px-3 py-2"> {$i18n.t('Role')} </th> | ||||||
| 											<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> | 											<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> | ||||||
| 											<th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th> | 											<th scope="col" class="px-3 py-2"> {$i18n.t('Email')} </th> | ||||||
| 										<th scope="col" class="px-3 py-2"> {$i18n.t('Action')} </th> | 											<th scope="col" class="px-3 py-2"> {$i18n.t('Created at')} </th> | ||||||
|  | 											<th scope="col" class="px-3 py-2 text-right" /> | ||||||
| 										</tr> | 										</tr> | ||||||
| 									</thead> | 									</thead> | ||||||
| 									<tbody> | 									<tbody> | ||||||
| 									{#each users as user} | 										{#each users.filter((user) => { | ||||||
|  | 											if (search === '') { | ||||||
|  | 												return true; | ||||||
|  | 											} else { | ||||||
|  | 												let name = user.name.toLowerCase(); | ||||||
|  | 												const query = search.toLowerCase(); | ||||||
|  | 												return name.includes(query); | ||||||
|  | 											} | ||||||
|  | 										}) as user} | ||||||
| 											<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> | 											<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> | ||||||
| 												<td class="px-3 py-2 min-w-[7rem] w-28"> | 												<td class="px-3 py-2 min-w-[7rem] w-28"> | ||||||
| 													<button | 													<button | ||||||
| 														class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === | 														class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === | ||||||
| 															'admin' && | 															'admin' && | ||||||
| 														'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {user.role === 'user' && | 															'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {user.role === | ||||||
|  | 															'user' && | ||||||
| 															'text-green-600 dark:text-green-200 bg-green-200/30'} {user.role === | 															'text-green-600 dark:text-green-200 bg-green-200/30'} {user.role === | ||||||
| 															'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" | 															'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" | ||||||
| 														on:click={() => { | 														on:click={() => { | ||||||
|  | @ -179,8 +209,12 @@ | ||||||
| 												</td> | 												</td> | ||||||
| 												<td class=" px-3 py-2"> {user.email} </td> | 												<td class=" px-3 py-2"> {user.email} </td> | ||||||
| 
 | 
 | ||||||
| 											<td class="px-3 py-2"> | 												<td class=" px-3 py-2"> | ||||||
| 												<div class="flex justify-start w-full"> | 													{dayjs(user.timestamp * 1000).format($i18n.t('MMMM DD, YYYY'))} | ||||||
|  | 												</td> | ||||||
|  | 
 | ||||||
|  | 												<td class="px-3 py-2 text-right"> | ||||||
|  | 													<div class="flex justify-end w-full"> | ||||||
| 														<button | 														<button | ||||||
| 															class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" | 															class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" | ||||||
| 															on:click={async () => { | 															on:click={async () => { | ||||||
|  | @ -232,6 +266,11 @@ | ||||||
| 									</tbody> | 									</tbody> | ||||||
| 								</table> | 								</table> | ||||||
| 							</div> | 							</div> | ||||||
|  | 
 | ||||||
|  | 							<div class=" text-gray-500 text-xs mt-2 text-right"> | ||||||
|  | 								ⓘ {$i18n.t("Click on the user role button to change a user's role.")} | ||||||
|  | 							</div> | ||||||
|  | 						</div> | ||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</div> | ||||||
| 			</div> | 			</div> | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ | ||||||
| 	import { WEBUI_NAME, config, user } from '$lib/stores'; | 	import { WEBUI_NAME, config, user } from '$lib/stores'; | ||||||
| 	import { onMount, getContext } from 'svelte'; | 	import { onMount, getContext } from 'svelte'; | ||||||
| 	import { toast } from 'svelte-sonner'; | 	import { toast } from 'svelte-sonner'; | ||||||
|  | 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; | ||||||
| 
 | 
 | ||||||
| 	const i18n = getContext('i18n'); | 	const i18n = getContext('i18n'); | ||||||
| 
 | 
 | ||||||
|  | @ -36,10 +37,12 @@ | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	const signUpHandler = async () => { | 	const signUpHandler = async () => { | ||||||
| 		const sessionUser = await userSignUp(name, email, password).catch((error) => { | 		const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch( | ||||||
|  | 			(error) => { | ||||||
| 				toast.error(error); | 				toast.error(error); | ||||||
| 				return null; | 				return null; | ||||||
| 		}); | 			} | ||||||
|  | 		); | ||||||
| 
 | 
 | ||||||
| 		await setSessionUser(sessionUser); | 		await setSessionUser(sessionUser); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy Jaeryang Baek
						Timothy Jaeryang Baek