forked from open-webui/open-webui
		
	Merge branch 'dev' into embedding-model-fix-and-manual-update
This commit is contained in:
		
						commit
						506a061387
					
				
					 60 changed files with 1906 additions and 520 deletions
				
			
		|  | @ -10,3 +10,7 @@ OPENAI_API_KEY='' | |||
| # DO NOT TRACK | ||||
| SCARF_NO_ANALYTICS=true | ||||
| DO_NOT_TRACK=true | ||||
| 
 | ||||
| # Use locally bundled version of the LiteLLM cost map json | ||||
| # to avoid repetitive startup connections | ||||
| LITELLM_LOCAL_MODEL_COST_MAP="True" | ||||
|  |  | |||
							
								
								
									
										11
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/build-release.yml
									
										
									
									
										vendored
									
									
								
							|  | @ -57,3 +57,14 @@ jobs: | |||
|         path: . | ||||
|       env: | ||||
|         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 }}', | ||||
|           }) | ||||
|  |  | |||
							
								
								
									
										35
									
								
								.github/workflows/docker-build.yaml
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.github/workflows/docker-build.yaml
									
										
									
									
										vendored
									
									
								
							|  | @ -1,8 +1,8 @@ | |||
| # | ||||
| name: Create and publish a Docker image | ||||
| name: Create and publish Docker images with specific build args | ||||
| 
 | ||||
| # Configures this workflow to run every time a change is pushed to the branch called `release`. | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|  | @ -23,7 +23,7 @@ jobs: | |||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|       # | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v4 | ||||
|  | @ -41,12 +41,11 @@ jobs: | |||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
| 
 | ||||
|       - name: Extract metadata for Docker images | ||||
|         id: meta | ||||
|       - name: Extract metadata for Docker images (default latest tag) | ||||
|         id: meta-latest | ||||
|         uses: docker/metadata-action@v5 | ||||
|         with: | ||||
|           images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} | ||||
|           # This configuration dynamically generates tags based on the branch, tag, commit, and custom suffix for lite version. | ||||
|           tags: | | ||||
|             type=ref,event=branch | ||||
|             type=ref,event=tag | ||||
|  | @ -56,11 +55,29 @@ jobs: | |||
|           flavor: | | ||||
|             latest=${{ github.ref == 'refs/heads/main' }} | ||||
| 
 | ||||
|       - name: Build and push Docker image | ||||
|       - name: Build and push Docker image (latest) | ||||
|         uses: docker/build-push-action@v5 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           tags: ${{ steps.meta.outputs.tags }} | ||||
|           labels: ${{ steps.meta.outputs.labels }} | ||||
|           tags: ${{ steps.meta-latest.outputs.tags }} | ||||
|           labels: ${{ steps.meta-latest.outputs.labels }} | ||||
| 
 | ||||
|       - name: Build and push Docker image with CUDA | ||||
|         uses: docker/build-push-action@v5 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:cuda | ||||
|           build-args: USE_CUDA=true | ||||
| 
 | ||||
|       - name: Build and push Docker image with Ollama | ||||
|         uses: docker/build-push-action@v5 | ||||
|         with: | ||||
|           context: . | ||||
|           push: true | ||||
|           platforms: linux/amd64,linux/arm64 | ||||
|           tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:ollama | ||||
|           build-args: USE_OLLAMA=true | ||||
|  |  | |||
							
								
								
									
										128
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										128
									
								
								Dockerfile
									
										
									
									
									
								
							|  | @ -1,78 +1,116 @@ | |||
| # 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 node:21-alpine3.19 as build | ||||
| 
 | ||||
| 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 ./ | ||||
| RUN npm ci | ||||
| 
 | ||||
| COPY . . | ||||
| RUN npm run build | ||||
| 
 | ||||
| 
 | ||||
| ######## WebUI backend ######## | ||||
| FROM python:3.11-slim-bookworm as base | ||||
| 
 | ||||
| ENV ENV=prod | ||||
| ENV PORT "" | ||||
| # Use args | ||||
| 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 "" | ||||
| ENV OPENAI_API_KEY "" | ||||
| ## Basis URL Config ## | ||||
| ENV OLLAMA_BASE_URL="/ollama" \ | ||||
|     OPENAI_API_BASE_URL="" | ||||
| 
 | ||||
| ENV WEBUI_SECRET_KEY "" | ||||
| ENV WEBUI_AUTH_TRUSTED_EMAIL_HEADER "" | ||||
| ## API Key and Security Config ## | ||||
| ENV OPENAI_API_KEY="" \ | ||||
|     WEBUI_SECRET_KEY="" \ | ||||
|     SCARF_NO_ANALYTICS=true \ | ||||
|     DO_NOT_TRACK=true | ||||
| 
 | ||||
| ENV SCARF_NO_ANALYTICS true | ||||
| ENV DO_NOT_TRACK true | ||||
| # Use locally bundled version of the LiteLLM cost map json | ||||
| # to avoid repetitive startup connections | ||||
| 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 | ||||
| # 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 persormance and multilangauge support use "intfloat/multilingual-e5-large" (~2.5GB) or "intfloat/multilingual-e5-base" (~1.5GB) | ||||
| # IMPORTANT: If you change the default model (all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. | ||||
| ENV RAG_EMBEDDING_MODEL="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 | ||||
| #### Other models ######################################################### | ||||
| ## whisper TTS model settings ## | ||||
| ENV WHISPER_MODEL="base" \ | ||||
|     WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" | ||||
| 
 | ||||
| ######## 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 | ||||
| 
 | ||||
| # install python dependencies | ||||
| 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 modell 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 | ||||
| 
 | ||||
| RUN if [ "$USE_OLLAMA" = "true" ]; then \ | ||||
|     apt-get update && \ | ||||
|     # 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/* | ||||
|     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 | ||||
| 
 | ||||
| 
 | ||||
| # 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 | ||||
| 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 | ||||
| # 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 built frontend files | ||||
| COPY --from=build /app/build /app/build | ||||
|  | @ -82,4 +120,6 @@ COPY --from=build /app/package.json /app/package.json | |||
| # copy backend files | ||||
| COPY ./backend . | ||||
| 
 | ||||
| EXPOSE 8080 | ||||
| 
 | ||||
| CMD [ "bash", "start.sh"] | ||||
							
								
								
									
										59
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										59
									
								
								README.md
									
										
									
									
									
								
							|  | @ -113,6 +113,65 @@ Don't forget to explore our sibling project, [Open WebUI Community](https://open | |||
| 
 | ||||
| - After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 | ||||
| 
 | ||||
| - **If you want to customize your build with additional args**, use this commands: | ||||
| 
 | ||||
|   > [!NOTE]   | ||||
|   > If you only want to use Open WebUI with Ollama included or CUDA acelleration it's recomented to use our official images with the tags :cuda or :with-ollama | ||||
|   > If you want a combination of both or more customisation options like a different embedding model and/or CUDA version you need to build the image yourself following the instructions below. | ||||
| 
 | ||||
|   **For the build:** | ||||
| 
 | ||||
|   ```bash | ||||
|   docker build -t open-webui | ||||
|   ``` | ||||
| 
 | ||||
|   Optional build ARGS (use them in the docker build command below if needed): | ||||
| 
 | ||||
|       e.g. | ||||
| 
 | ||||
|   ```bash | ||||
|   --build-arg="USE_EMBEDDING_MODEL=intfloat/multilingual-e5-large" | ||||
|   ``` | ||||
| 
 | ||||
|   For "intfloat/multilingual-e5-large" custom embedding model (default is all-MiniLM-L6-v2), only works with [sentence transforer models](https://huggingface.co/models?library=sentence-transformers). Current [Leaderbord](https://huggingface.co/spaces/mteb/leaderboard) of embedding models. | ||||
| 
 | ||||
|   ```bash | ||||
|   --build-arg="USE_OLLAMA=true" | ||||
|   ``` | ||||
| 
 | ||||
|   For including ollama in the image. | ||||
| 
 | ||||
|   ```bash | ||||
|   --build-arg="USE_CUDA=true" | ||||
|   ``` | ||||
| 
 | ||||
|   To use CUDA exeleration for the embedding and whisper models. | ||||
| 
 | ||||
|   > [!NOTE] | ||||
|   > You need to install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your machine to be able to set CUDA as the Docker engine. Only works with Linux - use WSL for Windows! | ||||
| 
 | ||||
|   ```bash | ||||
|   --build-arg="USE_CUDA_VER=cu117" | ||||
|   ``` | ||||
| 
 | ||||
|   For CUDA 11 (default is CUDA 12) | ||||
| 
 | ||||
|   **To run the image:** | ||||
| 
 | ||||
|   - **If you DID NOT use the USE_CUDA=true build ARG**, use this command: | ||||
| 
 | ||||
|   ```bash | ||||
|     docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main | ||||
|   ``` | ||||
| 
 | ||||
|   - **If you DID use the USE_CUDA=true build ARG**, use this command: | ||||
| 
 | ||||
|   ```bash | ||||
|     docker run --gpus all -d -p 3000:8080 -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 | ||||
| 
 | ||||
| If you're experiencing connection issues, it’s often due to the WebUI docker container not being able to reach the Ollama server at 127.0.0.1:11434 (host.docker.internal:11434) inside the container . Use the `--network=host` flag in your docker command to resolve this. Note that the port changes from 3000 to 8080, resulting in the link: `http://localhost:8080`. | ||||
|  |  | |||
|  | @ -28,6 +28,7 @@ from config import ( | |||
|     UPLOAD_DIR, | ||||
|     WHISPER_MODEL, | ||||
|     WHISPER_MODEL_DIR, | ||||
|     DEVICE_TYPE, | ||||
| ) | ||||
| 
 | ||||
| log = logging.getLogger(__name__) | ||||
|  | @ -42,6 +43,10 @@ app.add_middleware( | |||
|     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") | ||||
| def transcribe( | ||||
|  | @ -66,7 +71,7 @@ def transcribe( | |||
| 
 | ||||
|         model = WhisperModel( | ||||
|             WHISPER_MODEL, | ||||
|             device="auto", | ||||
|             device=whisper_device_type, | ||||
|             compute_type="int8", | ||||
|             download_root=WHISPER_MODEL_DIR, | ||||
|         ) | ||||
|  |  | |||
|  | @ -215,7 +215,8 @@ async def get_ollama_versions(url_idx: Optional[int] = None): | |||
| 
 | ||||
|         if len(responses) > 0: | ||||
|             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"]} | ||||
|  |  | |||
|  | @ -58,8 +58,8 @@ from config import ( | |||
|     UPLOAD_DIR, | ||||
|     DOCS_DIR, | ||||
|     RAG_EMBEDDING_MODEL, | ||||
|     RAG_EMBEDDING_MODEL_DEVICE_TYPE, | ||||
|     RAG_EMBEDDING_MODEL_AUTO_UPDATE, | ||||
|     DEVICE_TYPE, | ||||
|     CHROMA_CLIENT, | ||||
|     CHUNK_SIZE, | ||||
|     CHUNK_OVERLAP, | ||||
|  | @ -86,7 +86,7 @@ app.state.TOP_K = 4 | |||
| app.state.sentence_transformer_ef = ( | ||||
|     embedding_functions.SentenceTransformerEmbeddingFunction( | ||||
|         model_name=app.state.RAG_EMBEDDING_MODEL_PATH, | ||||
|         device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, | ||||
|         device=DEVICE_TYPE, | ||||
|     ) | ||||
| ) | ||||
| 
 | ||||
|  | @ -154,7 +154,7 @@ async def update_embedding_model( | |||
|         app.state.sentence_transformer_ef = ( | ||||
|             embedding_functions.SentenceTransformerEmbeddingFunction( | ||||
|                 model_name=app.state.RAG_EMBEDDING_MODEL_PATH, | ||||
|                 device=RAG_EMBEDDING_MODEL_DEVICE_TYPE, | ||||
|                 device=DEVICE_TYPE, | ||||
|             ) | ||||
|         ) | ||||
|     except Exception as e: | ||||
|  | @ -471,25 +471,11 @@ def store_doc( | |||
| 
 | ||||
|     log.info(f"file.content_type: {file.content_type}") | ||||
|     try: | ||||
|         is_valid_filename = True | ||||
|         unsanitized_filename = file.filename | ||||
|         if re.search(r'[\\/:"\*\?<>|\n\t ]', unsanitized_filename) is not None: | ||||
|             is_valid_filename = False | ||||
|         filename = os.path.basename(unsanitized_filename) | ||||
| 
 | ||||
|         unvalidated_file_path = f"{UPLOAD_DIR}/{unsanitized_filename}" | ||||
|         dereferenced_file_path = str(Path(unvalidated_file_path).resolve(strict=False)) | ||||
|         if not dereferenced_file_path.startswith(UPLOAD_DIR): | ||||
|             is_valid_filename = False | ||||
|         file_path = f"{UPLOAD_DIR}/{filename}" | ||||
| 
 | ||||
|         if is_valid_filename: | ||||
|             file_path = dereferenced_file_path | ||||
|         else: | ||||
|             raise HTTPException( | ||||
|                 status_code=status.HTTP_400_BAD_REQUEST, | ||||
|                 detail=ERROR_MESSAGES.DEFAULT(), | ||||
|             ) | ||||
| 
 | ||||
|         filename = file.filename | ||||
|         contents = file.file.read() | ||||
|         with open(file_path, "wb") as f: | ||||
|             f.write(contents) | ||||
|  | @ -500,7 +486,7 @@ def store_doc( | |||
|             collection_name = calculate_sha256(f)[:63] | ||||
|         f.close() | ||||
| 
 | ||||
|         loader, known_type = get_loader(file.filename, file.content_type, file_path) | ||||
|         loader, known_type = get_loader(filename, file.content_type, file_path) | ||||
|         data = loader.load() | ||||
| 
 | ||||
|         try: | ||||
|  |  | |||
|  | @ -86,6 +86,7 @@ class SignupForm(BaseModel): | |||
|     name: str | ||||
|     email: str | ||||
|     password: str | ||||
|     profile_image_url: Optional[str] = "/user.png" | ||||
| 
 | ||||
| 
 | ||||
| class AuthsTable: | ||||
|  | @ -94,7 +95,12 @@ class AuthsTable: | |||
|         self.db.create_tables([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]: | ||||
|         log.info("insert_new_auth") | ||||
| 
 | ||||
|  | @ -105,7 +111,7 @@ class AuthsTable: | |||
|         ) | ||||
|         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: | ||||
|             return user | ||||
|  |  | |||
|  | @ -206,6 +206,18 @@ class ChatTable: | |||
|         except: | ||||
|             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]: | ||||
|         try: | ||||
|             chat = Chat.get(Chat.id == id, Chat.user_id == user_id) | ||||
|  |  | |||
|  | @ -31,7 +31,7 @@ class UserModel(BaseModel): | |||
|     name: str | ||||
|     email: str | ||||
|     role: str = "pending" | ||||
|     profile_image_url: str = "/user.png" | ||||
|     profile_image_url: str | ||||
|     timestamp: int  # timestamp in epoch | ||||
|     api_key: Optional[str] = None | ||||
| 
 | ||||
|  | @ -59,7 +59,12 @@ class UsersTable: | |||
|         self.db.create_tables([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]: | ||||
|         user = UserModel( | ||||
|             **{ | ||||
|  | @ -67,7 +72,7 @@ class UsersTable: | |||
|                 "name": name, | ||||
|                 "email": email, | ||||
|                 "role": role, | ||||
|                 "profile_image_url": "/user.png", | ||||
|                 "profile_image_url": profile_image_url, | ||||
|                 "timestamp": int(time.time()), | ||||
|             } | ||||
|         ) | ||||
|  |  | |||
|  | @ -163,7 +163,11 @@ async def signup(request: Request, form_data: SignupForm): | |||
|         ) | ||||
|         hashed = get_password_hash(form_data.password) | ||||
|         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: | ||||
|  |  | |||
|  | @ -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]) | ||||
| 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) | ||||
| 
 | ||||
|     if chat: | ||||
|  |  | |||
|  | @ -1,16 +1,11 @@ | |||
| from fastapi import APIRouter, UploadFile, File, BackgroundTasks | ||||
| from fastapi import APIRouter, UploadFile, File, Response | ||||
| from fastapi import Depends, HTTPException, status | ||||
| from starlette.responses import StreamingResponse, FileResponse | ||||
| 
 | ||||
| 
 | ||||
| from pydantic import BaseModel | ||||
| 
 | ||||
| 
 | ||||
| from fpdf import FPDF | ||||
| import markdown | ||||
| import requests | ||||
| import os | ||||
| import aiohttp | ||||
| import json | ||||
| 
 | ||||
| 
 | ||||
| from utils.utils import get_admin_user | ||||
|  | @ -18,7 +13,7 @@ from utils.misc import calculate_sha256, get_gravatar_url | |||
| 
 | ||||
| from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR | ||||
| from constants import ERROR_MESSAGES | ||||
| 
 | ||||
| from typing import List | ||||
| 
 | ||||
| router = APIRouter() | ||||
| 
 | ||||
|  | @ -41,6 +36,59 @@ async def get_html_from_markdown( | |||
|     return {"html": markdown.markdown(form_data.md)} | ||||
| 
 | ||||
| 
 | ||||
| class ChatForm(BaseModel): | ||||
|     title: str | ||||
|     messages: List[dict] | ||||
| 
 | ||||
| 
 | ||||
| @router.post("/pdf") | ||||
| async def download_chat_as_pdf( | ||||
|     form_data: ChatForm, | ||||
| ): | ||||
|     pdf = FPDF() | ||||
|     pdf.add_page() | ||||
| 
 | ||||
|     STATIC_DIR = "./static" | ||||
|     FONTS_DIR = f"{STATIC_DIR}/fonts" | ||||
| 
 | ||||
|     pdf.add_font("NotoSans", "", f"{FONTS_DIR}/NotoSans-Regular.ttf") | ||||
|     pdf.add_font("NotoSans", "b", f"{FONTS_DIR}/NotoSans-Bold.ttf") | ||||
|     pdf.add_font("NotoSans", "i", f"{FONTS_DIR}/NotoSans-Italic.ttf") | ||||
|     pdf.add_font("NotoSansKR", "", f"{FONTS_DIR}/NotoSansKR-Regular.ttf") | ||||
|     pdf.add_font("NotoSansJP", "", f"{FONTS_DIR}/NotoSansJP-Regular.ttf") | ||||
| 
 | ||||
|     pdf.set_font("NotoSans", size=12) | ||||
|     pdf.set_fallback_fonts(["NotoSansKR", "NotoSansJP"]) | ||||
| 
 | ||||
|     pdf.set_auto_page_break(auto=True, margin=15) | ||||
| 
 | ||||
|     # Adjust the effective page width for multi_cell | ||||
|     effective_page_width = ( | ||||
|         pdf.w - 2 * pdf.l_margin - 10 | ||||
|     )  # Subtracted an additional 10 for extra padding | ||||
| 
 | ||||
|     # Add chat messages | ||||
|     for message in form_data.messages: | ||||
|         role = message["role"] | ||||
|         content = message["content"] | ||||
|         pdf.set_font("NotoSans", "B", size=14)  # Bold for the role | ||||
|         pdf.multi_cell(effective_page_width, 10, f"{role.upper()}", 0, "L") | ||||
|         pdf.ln(1)  # Extra space between messages | ||||
| 
 | ||||
|         pdf.set_font("NotoSans", size=10)  # Regular for content | ||||
|         pdf.multi_cell(effective_page_width, 6, content, 0, "L") | ||||
|         pdf.ln(1.5)  # Extra space between messages | ||||
| 
 | ||||
|     # Save the pdf with name .pdf | ||||
|     pdf_bytes = pdf.output() | ||||
| 
 | ||||
|     return Response( | ||||
|         content=bytes(pdf_bytes), | ||||
|         media_type="application/pdf", | ||||
|         headers={"Content-Disposition": f"attachment;filename=chat.pdf"}, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| @router.get("/db/download") | ||||
| async def download_db(user=Depends(get_admin_user)): | ||||
| 
 | ||||
|  |  | |||
|  | @ -257,6 +257,7 @@ OLLAMA_API_BASE_URL = os.environ.get( | |||
| 
 | ||||
| OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") | ||||
| 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 != "": | ||||
|     OLLAMA_BASE_URL = ( | ||||
|  | @ -266,9 +267,13 @@ if OLLAMA_BASE_URL == "" and OLLAMA_API_BASE_URL != "": | |||
|     ) | ||||
| 
 | ||||
| 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" | ||||
| 
 | ||||
|     elif K8S_FLAG: | ||||
|         OLLAMA_BASE_URL = "http://ollama-service.open-webui.svc.cluster.local:11434" | ||||
| 
 | ||||
|  | @ -391,13 +396,21 @@ if WEBUI_AUTH and WEBUI_SECRET_KEY == "": | |||
| 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) | ||||
| 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 | ||||
| RAG_EMBEDDING_MODEL_DEVICE_TYPE = os.environ.get( | ||||
|     "RAG_EMBEDDING_MODEL_DEVICE_TYPE", "cpu" | ||||
| ) | ||||
| log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL}"), | ||||
| RAG_EMBEDDING_MODEL_AUTO_UPDATE = False | ||||
| if os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true": | ||||
|     RAG_EMBEDDING_MODEL_AUTO_UPDATE = 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( | ||||
|     path=CHROMA_DATA_PATH, | ||||
|     settings=Settings(allow_reset=True, anonymized_telemetry=False), | ||||
|  |  | |||
|  | @ -42,6 +42,8 @@ xlrd | |||
| opencv-python-headless | ||||
| rapidocr-onnxruntime | ||||
| 
 | ||||
| fpdf2 | ||||
| 
 | ||||
| faster-whisper | ||||
| 
 | ||||
| PyJWT | ||||
|  |  | |||
|  | @ -7,16 +7,26 @@ KEY_FILE=.webui_secret_key | |||
| 
 | ||||
| PORT="${PORT:-8080}" | ||||
| 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 | ||||
|     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. | ||||
|     echo $(head -c 12 /dev/random | base64) > $KEY_FILE | ||||
|     echo $(head -c 12 /dev/random | base64) > "$KEY_FILE" | ||||
|   fi | ||||
| 
 | ||||
|   echo Loading WEBUI_SECRET_KEY from $KEY_FILE | ||||
|   WEBUI_SECRET_KEY=`cat $KEY_FILE` | ||||
|   echo "Loading WEBUI_SECRET_KEY from $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 | ||||
| 
 | ||||
| WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host 0.0.0.0 --port "$PORT" --forwarded-allow-ips '*' | ||||
							
								
								
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Bold.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Bold.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Italic.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Italic.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSans-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSansJP-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSansJP-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSansKR-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/static/fonts/NotoSansKR-Regular.ttf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										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 | ||||
|     tty: true | ||||
|     restart: unless-stopped | ||||
|     image: ollama/ollama:latest | ||||
|     image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest} | ||||
| 
 | ||||
|   open-webui: | ||||
|     build: | ||||
|  | @ -16,7 +16,7 @@ services: | |||
|       args: | ||||
|         OLLAMA_BASE_URL: '/ollama' | ||||
|       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 | ||||
|     volumes: | ||||
|       - open-webui:/app/backend/data | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ ollama | |||
| {{- end -}} | ||||
| 
 | ||||
| {{- define "ollama.url" -}} | ||||
| {{- printf "http://%s.%s.svc.cluster.local:%d/api" (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 }} | ||||
| 
 | ||||
| {{- define "chart.name" -}} | ||||
|  |  | |||
|  | @ -58,7 +58,12 @@ export const userSignIn = async (email: string, password: string) => { | |||
| 	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; | ||||
| 
 | ||||
| 	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({ | ||||
| 			name: name, | ||||
| 			email: email, | ||||
| 			password: password | ||||
| 			password: password, | ||||
| 			profile_image_url: profile_image_url | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
|  |  | |||
|  | @ -22,6 +22,32 @@ export const getGravatarUrl = async (email: string) => { | |||
| 	return res; | ||||
| }; | ||||
| 
 | ||||
| export const downloadChatAsPDF = async (chat: object) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
| 	const blob = await fetch(`${WEBUI_API_BASE_URL}/utils/pdf`, { | ||||
| 		method: 'POST', | ||||
| 		headers: { | ||||
| 			'Content-Type': 'application/json' | ||||
| 		}, | ||||
| 		body: JSON.stringify({ | ||||
| 			title: chat.title, | ||||
| 			messages: chat.messages | ||||
| 		}) | ||||
| 	}) | ||||
| 		.then(async (res) => { | ||||
| 			if (!res.ok) throw await res.json(); | ||||
| 			return res.blob(); | ||||
| 		}) | ||||
| 		.catch((err) => { | ||||
| 			console.log(err); | ||||
| 			error = err; | ||||
| 			return null; | ||||
| 		}); | ||||
| 
 | ||||
| 	return blob; | ||||
| }; | ||||
| 
 | ||||
| export const getHTMLFromMarkdown = async (md: string) => { | ||||
| 	let error = null; | ||||
| 
 | ||||
|  |  | |||
|  | @ -295,6 +295,13 @@ | |||
| 
 | ||||
| 		const dropZone = document.querySelector('body'); | ||||
| 
 | ||||
| 		const handleKeyDown = (event: KeyboardEvent) => { | ||||
| 			if (event.key === 'Escape') { | ||||
| 				console.log('Escape'); | ||||
| 				dragged = false; | ||||
| 			} | ||||
| 		}; | ||||
| 
 | ||||
| 		const onDragOver = (e) => { | ||||
| 			e.preventDefault(); | ||||
| 			dragged = true; | ||||
|  | @ -350,11 +357,15 @@ | |||
| 			dragged = false; | ||||
| 		}; | ||||
| 
 | ||||
| 		window.addEventListener('keydown', handleKeyDown); | ||||
| 
 | ||||
| 		dropZone?.addEventListener('dragover', onDragOver); | ||||
| 		dropZone?.addEventListener('drop', onDrop); | ||||
| 		dropZone?.addEventListener('dragleave', onDragLeave); | ||||
| 
 | ||||
| 		return () => { | ||||
| 			window.removeEventListener('keydown', handleKeyDown); | ||||
| 
 | ||||
| 			dropZone?.removeEventListener('dragover', onDragOver); | ||||
| 			dropZone?.removeEventListener('drop', onDrop); | ||||
| 			dropZone?.removeEventListener('dragleave', onDragLeave); | ||||
|  |  | |||
|  | @ -107,12 +107,8 @@ | |||
| 		await sendPrompt(userPrompt, userMessageId, chatId); | ||||
| 	}; | ||||
| 
 | ||||
| 	const confirmEditResponseMessage = async (messageId, content) => { | ||||
| 		history.messages[messageId].originalContent = history.messages[messageId].content; | ||||
| 		history.messages[messageId].content = content; | ||||
| 
 | ||||
| 	const updateChatMessages = async () => { | ||||
| 		await tick(); | ||||
| 
 | ||||
| 		await updateChatById(localStorage.token, chatId, { | ||||
| 			messages: messages, | ||||
| 			history: history | ||||
|  | @ -121,15 +117,20 @@ | |||
| 		await chats.set(await getChatList(localStorage.token)); | ||||
| 	}; | ||||
| 
 | ||||
| 	const rateMessage = async (messageId, rating) => { | ||||
| 		history.messages[messageId].rating = rating; | ||||
| 		await tick(); | ||||
| 		await updateChatById(localStorage.token, chatId, { | ||||
| 			messages: messages, | ||||
| 			history: history | ||||
| 		}); | ||||
| 	const confirmEditResponseMessage = async (messageId, content) => { | ||||
| 		history.messages[messageId].originalContent = history.messages[messageId].content; | ||||
| 		history.messages[messageId].content = content; | ||||
| 
 | ||||
| 		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) => { | ||||
|  | @ -338,6 +339,7 @@ | |||
| 								siblings={history.messages[message.parentId]?.childrenIds ?? []} | ||||
| 								isLastMessage={messageIdx + 1 === messages.length} | ||||
| 								{readOnly} | ||||
| 								{updateChatMessages} | ||||
| 								{confirmEditResponseMessage} | ||||
| 								{showPreviousMessage} | ||||
| 								{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 { WEBUI_BASE_URL } from '$lib/constants'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
| 	import RateComment from './RateComment.svelte'; | ||||
| 
 | ||||
| 	export let modelfiles = []; | ||||
| 	export let message; | ||||
|  | @ -39,6 +40,7 @@ | |||
| 
 | ||||
| 	export let readOnly = false; | ||||
| 
 | ||||
| 	export let updateChatMessages: Function; | ||||
| 	export let confirmEditResponseMessage: Function; | ||||
| 	export let showPreviousMessage: Function; | ||||
| 	export let showNextMessage: Function; | ||||
|  | @ -60,6 +62,8 @@ | |||
| 	let loadingSpeech = false; | ||||
| 	let generatingImage = false; | ||||
| 
 | ||||
| 	let showRateComment = false; | ||||
| 
 | ||||
| 	$: tokens = marked.lexer(sanitizeResponseContent(message.content)); | ||||
| 
 | ||||
| 	const renderer = new marked.Renderer(); | ||||
|  | @ -536,11 +540,13 @@ | |||
| 												<button | ||||
| 													class="{isLastMessage | ||||
| 														? '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' | ||||
| 														: ''} dark:hover:text-white hover:text-black transition" | ||||
| 													on:click={() => { | ||||
| 														rateMessage(message.id, 1); | ||||
| 														showRateComment = true; | ||||
| 													}} | ||||
| 												> | ||||
| 													<svg | ||||
|  | @ -563,11 +569,13 @@ | |||
| 												<button | ||||
| 													class="{isLastMessage | ||||
| 														? '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' | ||||
| 														: ''} dark:hover:text-white hover:text-black transition" | ||||
| 													on:click={() => { | ||||
| 														rateMessage(message.id, -1); | ||||
| 														showRateComment = true; | ||||
| 													}} | ||||
| 												> | ||||
| 													<svg | ||||
|  | @ -824,6 +832,16 @@ | |||
| 										{/if} | ||||
| 									</div> | ||||
| 								{/if} | ||||
| 
 | ||||
| 								{#if showRateComment} | ||||
| 									<RateComment | ||||
| 										bind:show={showRateComment} | ||||
| 										bind:message | ||||
| 										on:submit={() => { | ||||
| 											updateChatMessages(); | ||||
| 										}} | ||||
| 									/> | ||||
| 								{/if} | ||||
| 							</div> | ||||
| 						{/if} | ||||
| 					</div> | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
| 
 | ||||
| 	import UpdatePassword from './Account/UpdatePassword.svelte'; | ||||
| 	import { getGravatarUrl } from '$lib/apis/utils'; | ||||
| 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; | ||||
| 	import { copyToClipboard } from '$lib/utils'; | ||||
| 	import Plus from '$lib/components/icons/Plus.svelte'; | ||||
| 	import Tooltip from '$lib/components/common/Tooltip.svelte'; | ||||
|  | @ -18,6 +19,8 @@ | |||
| 	let profileImageUrl = ''; | ||||
| 	let name = ''; | ||||
| 
 | ||||
| 	let showAPIKeys = false; | ||||
| 
 | ||||
| 	let showJWTToken = false; | ||||
| 	let JWTTokenCopied = false; | ||||
| 
 | ||||
|  | @ -28,6 +31,12 @@ | |||
| 	let profileImageInputElement: HTMLInputElement; | ||||
| 
 | ||||
| 	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( | ||||
| 			(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 flex-col"> | ||||
| 				<div class="self-center"> | ||||
| 					<div class="self-center mt-2"> | ||||
| 						<button | ||||
| 							class="relative rounded-full dark:bg-gray-700" | ||||
| 							type="button" | ||||
|  | @ -138,9 +148,9 @@ | |||
| 							}} | ||||
| 						> | ||||
| 							<img | ||||
| 							src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} | ||||
| 								src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)} | ||||
| 								alt="profile" | ||||
| 							class=" rounded-full w-16 h-16 object-cover" | ||||
| 								class=" rounded-full size-16 object-cover" | ||||
| 							/> | ||||
| 
 | ||||
| 							<div | ||||
|  | @ -161,23 +171,56 @@ | |||
| 							</div> | ||||
| 						</button> | ||||
| 					</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 | ||||
| 					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 () => { | ||||
| 								const url = await getGravatarUrl($user.email); | ||||
| 
 | ||||
| 								profileImageUrl = url; | ||||
| 							}}>{$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 class="flex-1"> | ||||
| 			<div class="pt-0.5"> | ||||
| 				<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"> | ||||
| 						<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" | ||||
| 							bind:value={name} | ||||
| 							required | ||||
|  | @ -187,11 +230,24 @@ | |||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<hr class=" dark:border-gray-700 my-4" /> | ||||
| 		<div class="py-0.5"> | ||||
| 			<UpdatePassword /> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<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="justify-between w-full"> | ||||
| 					<div class="flex justify-between w-full"> | ||||
|  | @ -201,14 +257,14 @@ | |||
| 					<div class="flex mt-2"> | ||||
| 						<div class="flex w-full"> | ||||
| 							<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'} | ||||
| 								value={localStorage.token} | ||||
| 								disabled | ||||
| 							/> | ||||
| 
 | ||||
| 							<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={() => { | ||||
| 									showJWTToken = !showJWTToken; | ||||
| 								}} | ||||
|  | @ -248,7 +304,7 @@ | |||
| 						</div> | ||||
| 
 | ||||
| 						<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={() => { | ||||
| 								copyToClipboard(localStorage.token); | ||||
| 								JWTTokenCopied = true; | ||||
|  | @ -301,14 +357,14 @@ | |||
| 						{#if APIKey} | ||||
| 							<div class="flex w-full"> | ||||
| 								<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'} | ||||
| 									value={APIKey} | ||||
| 									disabled | ||||
| 								/> | ||||
| 
 | ||||
| 								<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={() => { | ||||
| 										showAPIKey = !showAPIKey; | ||||
| 									}} | ||||
|  | @ -348,7 +404,7 @@ | |||
| 							</div> | ||||
| 
 | ||||
| 							<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={() => { | ||||
| 									copyToClipboard(APIKey); | ||||
| 									APIKeyCopied = true; | ||||
|  | @ -393,7 +449,7 @@ | |||
| 
 | ||||
| 							<Tooltip content="Create new key"> | ||||
| 								<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={() => { | ||||
| 										createAPIKeyHandler(); | ||||
| 									}} | ||||
|  | @ -416,7 +472,7 @@ | |||
| 							</Tooltip> | ||||
| 						{:else} | ||||
| 							<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={() => { | ||||
| 									createAPIKeyHandler(); | ||||
| 								}} | ||||
|  | @ -429,6 +485,7 @@ | |||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 
 | ||||
| 	<div class="flex justify-end pt-3 text-sm font-medium"> | ||||
|  |  | |||
|  | @ -185,7 +185,7 @@ | |||
| 
 | ||||
| 			<div> | ||||
| 				<div class=" py-0.5 flex w-full justify-between"> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div> | ||||
| 					<div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div> | ||||
| 
 | ||||
| 					<button | ||||
| 						class="p-1 px-3 text-xs flex rounded transition" | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ | |||
| 	export let show = true; | ||||
| 	export let size = 'md'; | ||||
| 
 | ||||
| 	let modalElement = null; | ||||
| 	let mounted = false; | ||||
| 
 | ||||
| 	const sizeToWidth = (size) => { | ||||
|  | @ -19,14 +20,23 @@ | |||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	const handleKeyDown = (event: KeyboardEvent) => { | ||||
| 		if (event.key === 'Escape') { | ||||
| 			console.log('Escape'); | ||||
| 			show = false; | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	onMount(() => { | ||||
| 		mounted = true; | ||||
| 	}); | ||||
| 
 | ||||
| 	$: if (mounted) { | ||||
| 		if (show) { | ||||
| 			window.addEventListener('keydown', handleKeyDown); | ||||
| 			document.body.style.overflow = 'hidden'; | ||||
| 		} else { | ||||
| 			window.removeEventListener('keydown', handleKeyDown); | ||||
| 			document.body.style.overflow = 'unset'; | ||||
| 		} | ||||
| 	} | ||||
|  | @ -36,6 +46,7 @@ | |||
| 	<!-- svelte-ignore a11y-click-events-have-key-events --> | ||||
| 	<!-- svelte-ignore a11y-no-static-element-interactions --> | ||||
| 	<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" | ||||
| 		in:fade={{ duration: 10 }} | ||||
| 		on:click={() => { | ||||
|  |  | |||
|  | @ -11,6 +11,8 @@ | |||
| 
 | ||||
| 	import Dropdown from '$lib/components/common/Dropdown.svelte'; | ||||
| 	import Tags from '$lib/components/common/Tags.svelte'; | ||||
| 	import { WEBUI_BASE_URL } from '$lib/constants'; | ||||
| 	import { downloadChatAsPDF } from '$lib/apis/utils'; | ||||
| 
 | ||||
| 	export let shareEnabled: boolean = false; | ||||
| 	export let shareHandler: Function; | ||||
|  | @ -25,7 +27,7 @@ | |||
| 
 | ||||
| 	export let onClose: Function = () => {}; | ||||
| 
 | ||||
| 	const downloadChatAsTxt = async () => { | ||||
| 	const downloadTxt = async () => { | ||||
| 		const _chat = chat.chat; | ||||
| 		console.log('download', chat); | ||||
| 
 | ||||
|  | @ -40,54 +42,29 @@ | |||
| 		saveAs(blob, `chat-${_chat.title}.txt`); | ||||
| 	}; | ||||
| 
 | ||||
| 	const downloadChatAsPdf = async () => { | ||||
| 	const downloadPdf = async () => { | ||||
| 		const _chat = chat.chat; | ||||
| 		console.log('download', chat); | ||||
| 
 | ||||
| 		const doc = new jsPDF(); | ||||
| 		const blob = await downloadChatAsPDF(_chat); | ||||
| 
 | ||||
| 		// Initialize y-coordinate for text placement | ||||
| 		let yPos = 10; | ||||
| 		const pageHeight = doc.internal.pageSize.height; | ||||
| 		// Create a URL for the blob | ||||
| 		const url = window.URL.createObjectURL(blob); | ||||
| 
 | ||||
| 		// Function to check if new text exceeds the current page height | ||||
| 		function checkAndAddNewPage() { | ||||
| 			if (yPos > pageHeight - 10) { | ||||
| 				doc.addPage(); | ||||
| 				yPos = 10; // Reset yPos for the new page | ||||
| 			} | ||||
| 		} | ||||
| 		// Create a link element to trigger the download | ||||
| 		const a = document.createElement('a'); | ||||
| 		a.href = url; | ||||
| 		a.download = `chat-${_chat.title}.pdf`; | ||||
| 
 | ||||
| 		// Function to add text with specific style | ||||
| 		function addStyledText(text, isTitle = false) { | ||||
| 			// Set font style and size based on the parameters | ||||
| 			doc.setFont('helvetica', isTitle ? 'bold' : 'normal'); | ||||
| 			doc.setFontSize(isTitle ? 12 : 10); | ||||
| 		// Append the link to the body and click it programmatically | ||||
| 		document.body.appendChild(a); | ||||
| 		a.click(); | ||||
| 
 | ||||
| 			const textMargin = 7; | ||||
| 		// Remove the link from the body | ||||
| 		document.body.removeChild(a); | ||||
| 
 | ||||
| 			// Split text into lines to ensure it fits within the page width | ||||
| 			const lines = doc.splitTextToSize(text, 180); // Adjust the width as needed | ||||
| 
 | ||||
| 			lines.forEach((line) => { | ||||
| 				checkAndAddNewPage(); // Check if we need a new page before adding more text | ||||
| 				doc.text(line, 10, yPos); | ||||
| 				yPos += textMargin; // Increment yPos for the next line | ||||
| 			}); | ||||
| 
 | ||||
| 			// Add extra space after a block of text | ||||
| 			yPos += 2; | ||||
| 		} | ||||
| 
 | ||||
| 		_chat.messages.forEach((message, i) => { | ||||
| 			// Add user text in bold | ||||
| 			doc.setFont('helvetica', 'normal', 'bold'); | ||||
| 
 | ||||
| 			addStyledText(message.role.toUpperCase(), { isTitle: true }); | ||||
| 			addStyledText(message.content); | ||||
| 		}); | ||||
| 
 | ||||
| 		doc.save(`chat-${_chat.title}.pdf`); | ||||
| 		// Revoke the URL to release memory | ||||
| 		window.URL.revokeObjectURL(url); | ||||
| 	}; | ||||
| </script> | ||||
| 
 | ||||
|  | @ -193,7 +170,7 @@ | |||
| 						<DropdownMenu.Item | ||||
| 							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md" | ||||
| 							on:click={() => { | ||||
| 								downloadChatAsTxt(); | ||||
| 								downloadTxt(); | ||||
| 							}} | ||||
| 						> | ||||
| 							<div class="flex items-center line-clamp-1">Plain text (.txt)</div> | ||||
|  | @ -202,7 +179,7 @@ | |||
| 						<DropdownMenu.Item | ||||
| 							class="flex gap-2 items-center px-3 py-2 text-sm  cursor-pointer dark:hover:bg-gray-850 rounded-md" | ||||
| 							on:click={() => { | ||||
| 								downloadChatAsPdf(); | ||||
| 								downloadPdf(); | ||||
| 							}} | ||||
| 						> | ||||
| 							<div class="flex items-center line-clamp-1">PDF document (.pdf)</div> | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Изтрито {tagName}", | ||||
| 	"Description": "Описание", | ||||
| 	"Desktop Notifications": "Десктоп Известия", | ||||
| 	"Notifications": "Десктоп Известия", | ||||
| 	"Disabled": "Деактивиран", | ||||
| 	"Discover a modelfile": "Откриване на модфайл", | ||||
| 	"Discover a prompt": "Откриване на промпт", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Esborrat {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Esborrat {tagName}", | ||||
| 	"Description": "Descripció", | ||||
| 	"Desktop Notifications": "Notificacions d'Escriptori", | ||||
| 	"Notifications": "Notificacions d'Escriptori", | ||||
| 	"Disabled": "Desactivat", | ||||
| 	"Discover a modelfile": "Descobreix un fitxer de model", | ||||
| 	"Discover a prompt": "Descobreix un prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", | ||||
| 	"Deleted {tagName}": "{tagName} gelöscht", | ||||
| 	"Description": "Beschreibung", | ||||
| 	"Desktop Notifications": "Desktop-Benachrichtigungen", | ||||
| 	"Notifications": "Desktop-Benachrichtigungen", | ||||
| 	"Disabled": "Deaktiviert", | ||||
| 	"Discover a modelfile": "Eine Modelfiles 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 {tagName}": "", | ||||
| 	"Description": "", | ||||
| 	"Desktop Notifications": "", | ||||
| 	"Notifications": "", | ||||
| 	"Disabled": "", | ||||
| 	"Discover a modelfile": "", | ||||
| 	"Discover a prompt": "", | ||||
|  | @ -151,6 +151,7 @@ | |||
| 	"Failed to read clipboard contents": "", | ||||
| 	"File Mode": "", | ||||
| 	"File not found.": "", | ||||
| 	"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", | ||||
| 	"Focus chat input": "", | ||||
| 	"Format your variables using square brackets like this:": "", | ||||
| 	"From (Base Model)": "", | ||||
|  | @ -347,6 +348,7 @@ | |||
| 	"URL Mode": "", | ||||
| 	"Use '#' in the prompt input to load and select your documents.": "", | ||||
| 	"Use Gravatar": "", | ||||
| 	"Use Initials": "", | ||||
| 	"user": "", | ||||
| 	"User Permissions": "", | ||||
| 	"Users": "", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Se borró {tagName}", | ||||
| 	"Description": "Descripción", | ||||
| 	"Desktop Notifications": "Notificaciones", | ||||
| 	"Notifications": "Notificaciones", | ||||
| 	"Disabled": "Desactivado", | ||||
| 	"Discover a modelfile": "Descubre un modelfile", | ||||
| 	"Discover a prompt": "Descubre un Prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", | ||||
| 	"Deleted {tagName}": "{tagName} حذف شد", | ||||
| 	"Description": "توضیحات", | ||||
| 	"Desktop Notifications": "اعلان", | ||||
| 	"Notifications": "اعلان", | ||||
| 	"Disabled": "غیرفعال", | ||||
| 	"Discover a modelfile": "فایل مدل را کشف کنید", | ||||
| 	"Discover a prompt": "یک اعلان را کشف کنید", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | ||||
| 	"Deleted {tagName}": "{tagName} supprimé", | ||||
| 	"Description": "Description", | ||||
| 	"Desktop Notifications": "Notifications de bureau", | ||||
| 	"Notifications": "Notifications de bureau", | ||||
| 	"Disabled": "Désactivé", | ||||
| 	"Discover a modelfile": "Découvrir un fichier de modèle", | ||||
| 	"Discover a prompt": "Découvrir un prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} supprimé", | ||||
| 	"Deleted {tagName}": "{tagName} supprimé", | ||||
| 	"Description": "Description", | ||||
| 	"Desktop Notifications": "Notifications de bureau", | ||||
| 	"Notifications": "Notifications de bureau", | ||||
| 	"Disabled": "Désactivé", | ||||
| 	"Discover a modelfile": "Découvrir un fichier de modèle", | ||||
| 	"Discover a prompt": "Découvrir un prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Eliminato {tagName}", | ||||
| 	"Description": "Descrizione", | ||||
| 	"Desktop Notifications": "Notifiche desktop", | ||||
| 	"Notifications": "Notifiche desktop", | ||||
| 	"Disabled": "Disabilitato", | ||||
| 	"Discover a modelfile": "Scopri un file modello", | ||||
| 	"Discover a prompt": "Scopri un prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", | ||||
| 	"Deleted {tagName}": "{tagName} を削除しました", | ||||
| 	"Description": "説明", | ||||
| 	"Desktop Notifications": "デスクトップ通知", | ||||
| 	"Notifications": "デスクトップ通知", | ||||
| 	"Disabled": "無効", | ||||
| 	"Discover a modelfile": "モデルファイルを見つける", | ||||
| 	"Discover a prompt": "プロンプトを見つける", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", | ||||
| 	"Deleted {tagName}": "{tagName} 삭제됨", | ||||
| 	"Description": "설명", | ||||
| 	"Desktop Notifications": "알림", | ||||
| 	"Notifications": "알림", | ||||
| 	"Disabled": "비활성화", | ||||
| 	"Discover a modelfile": "모델파일 검색", | ||||
| 	"Discover a prompt": "프롬프트 검색", | ||||
|  |  | |||
|  | @ -15,6 +15,10 @@ | |||
| 		"code": "de-DE", | ||||
| 		"title": "Deutsch" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "en-GB", | ||||
| 		"title": "English (GB)" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "es-ES", | ||||
| 		"title": "Spanish" | ||||
|  | @ -51,10 +55,18 @@ | |||
| 		"code": "pt-PT", | ||||
| 		"title": "Portuguese (Portugal)" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "pt-BR", | ||||
| 		"title": "Portuguese (Brazil)" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "ru-RU", | ||||
| 		"title": "Russian (Russia)" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "tr-TR", | ||||
| 		"title": "Turkish" | ||||
| 	}, | ||||
| 	{ | ||||
| 		"code": "uk-UA", | ||||
| 		"title": "Ukrainian" | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", | ||||
| 	"Deleted {tagName}": "{tagName} is verwijderd", | ||||
| 	"Description": "Beschrijving", | ||||
| 	"Desktop Notifications": "Desktop Notificaties", | ||||
| 	"Notifications": "Desktop Notificaties", | ||||
| 	"Disabled": "Uitgeschakeld", | ||||
| 	"Discover a modelfile": "Ontdek een modelfile", | ||||
| 	"Discover a prompt": "Ontdek een prompt", | ||||
|  |  | |||
							
								
								
									
										363
									
								
								src/lib/i18n/locales/pt-BR/translation.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								src/lib/i18n/locales/pt-BR/translation.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,363 @@ | |||
| { | ||||
| 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 's' ou '-1' para não expirar.", | ||||
| 	"(Beta)": "(Beta)", | ||||
| 	"(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)", | ||||
| 	"(latest)": "(mais recente)", | ||||
| 	"{{modelName}} is thinking...": "{{modelName}} está pensando...", | ||||
| 	"{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário", | ||||
| 	"a user": "um usuário", | ||||
| 	"About": "Sobre", | ||||
| 	"Account": "Conta", | ||||
| 	"Action": "Ação", | ||||
| 	"Add a model": "Adicionar um modelo", | ||||
| 	"Add a model tag name": "Adicionar um nome de tag de modelo", | ||||
| 	"Add a short description about what this modelfile does": "Adicione uma breve descrição sobre o que este arquivo de modelo faz", | ||||
| 	"Add a short title for this prompt": "Adicione um título curto para este prompt", | ||||
| 	"Add a tag": "Adicionar uma tag", | ||||
| 	"Add Docs": "Adicionar Documentos", | ||||
| 	"Add Files": "Adicionar Arquivos", | ||||
| 	"Add message": "Adicionar mensagem", | ||||
| 	"add tags": "adicionar tags", | ||||
| 	"Adjusting these settings will apply changes universally to all users.": "Ajustar essas configurações aplicará alterações universalmente a todos os usuários.", | ||||
| 	"admin": "administrador", | ||||
| 	"Admin Panel": "Painel do Administrador", | ||||
| 	"Admin Settings": "Configurações do Administrador", | ||||
| 	"Advanced Parameters": "Parâmetros Avançados", | ||||
| 	"all": "todos", | ||||
| 	"All Users": "Todos os Usuários", | ||||
| 	"Allow": "Permitir", | ||||
| 	"Allow Chat Deletion": "Permitir Exclusão de Bate-papo", | ||||
| 	"alphanumeric characters and hyphens": "caracteres alfanuméricos e hífens", | ||||
| 	"Already have an account?": "Já tem uma conta?", | ||||
| 	"an assistant": "um assistente", | ||||
| 	"and": "e", | ||||
| 	"API Base URL": "URL Base da API", | ||||
| 	"API Key": "Chave da API", | ||||
| 	"API RPM": "API RPM", | ||||
| 	"are allowed - Activate this command by typing": "são permitidos - Ative este comando digitando", | ||||
| 	"Are you sure?": "Tem certeza?", | ||||
| 	"Audio": "Áudio", | ||||
| 	"Auto-playback response": "Reprodução automática da resposta", | ||||
| 	"Auto-send input after 3 sec.": "Enviar entrada automaticamente após 3 segundos.", | ||||
| 	"AUTOMATIC1111 Base URL": "URL Base do AUTOMATIC1111", | ||||
| 	"AUTOMATIC1111 Base URL is required.": "A URL Base do AUTOMATIC1111 é obrigatória.", | ||||
| 	"available!": "disponível!", | ||||
| 	"Back": "Voltar", | ||||
| 	"Builder Mode": "Modo de Construtor", | ||||
| 	"Cancel": "Cancelar", | ||||
| 	"Categories": "Categorias", | ||||
| 	"Change Password": "Alterar Senha", | ||||
| 	"Chat": "Bate-papo", | ||||
| 	"Chat History": "Histórico de Bate-papo", | ||||
| 	"Chat History is off for this browser.": "O histórico de bate-papo está desativado para este navegador.", | ||||
| 	"Chats": "Bate-papos", | ||||
| 	"Check Again": "Verifique novamente", | ||||
| 	"Check for updates": "Verificar atualizações", | ||||
| 	"Checking for updates...": "Verificando atualizações...", | ||||
| 	"Choose a model before saving...": "Escolha um modelo antes de salvar...", | ||||
| 	"Chunk Overlap": "Sobreposição de Fragmento", | ||||
| 	"Chunk Params": "Parâmetros de Fragmento", | ||||
| 	"Chunk Size": "Tamanho do Fragmento", | ||||
| 	"Click here for help.": "Clique aqui para obter ajuda.", | ||||
| 	"Click here to check other modelfiles.": "Clique aqui para verificar outros arquivos de modelo.", | ||||
| 	"Click here to select": "Clique aqui para selecionar", | ||||
| 	"Click here to select documents.": "Clique aqui para selecionar documentos.", | ||||
| 	"click here.": "clique aqui.", | ||||
| 	"Click on the user role button to change a user's role.": "Clique no botão de função do usuário para alterar a função de um usuário.", | ||||
| 	"Close": "Fechar", | ||||
| 	"Collection": "Coleção", | ||||
| 	"Command": "Comando", | ||||
| 	"Confirm Password": "Confirmar Senha", | ||||
| 	"Connections": "Conexões", | ||||
| 	"Content": "Conteúdo", | ||||
| 	"Context Length": "Comprimento do Contexto", | ||||
| 	"Conversation Mode": "Modo de Conversa", | ||||
| 	"Copy last code block": "Copiar último bloco de código", | ||||
| 	"Copy last response": "Copiar última resposta", | ||||
| 	"Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!", | ||||
| 	"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Crie uma frase concisa de 3 a 5 palavras como cabeçalho para a seguinte consulta, aderindo estritamente ao limite de 3 a 5 palavras e evitando o uso da palavra 'título':", | ||||
| 	"Create a modelfile": "Criar um arquivo de modelo", | ||||
| 	"Create Account": "Criar Conta", | ||||
| 	"Created at": "Criado em", | ||||
| 	"Created by": "Criado por", | ||||
| 	"Current Model": "Modelo Atual", | ||||
| 	"Current Password": "Senha Atual", | ||||
| 	"Custom": "Personalizado", | ||||
| 	"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico", | ||||
| 	"Dark": "Escuro", | ||||
| 	"Database": "Banco de dados", | ||||
| 	"DD/MM/YYYY HH:mm": "DD/MM/AAAA HH:mm", | ||||
| 	"Default": "Padrão", | ||||
| 	"Default (Automatic1111)": "Padrão (Automatic1111)", | ||||
| 	"Default (Web API)": "Padrão (API Web)", | ||||
| 	"Default model updated": "Modelo padrão atualizado", | ||||
| 	"Default Prompt Suggestions": "Sugestões de Prompt Padrão", | ||||
| 	"Default User Role": "Função de Usuário Padrão", | ||||
| 	"delete": "excluir", | ||||
| 	"Delete a model": "Excluir um modelo", | ||||
| 	"Delete chat": "Excluir bate-papo", | ||||
| 	"Delete Chats": "Excluir Bate-papos", | ||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | ||||
| 	"Deleted {tagName}": "{tagName} excluído", | ||||
| 	"Description": "Descrição", | ||||
| 	"Notifications": "Notificações da Área de Trabalho", | ||||
| 	"Disabled": "Desativado", | ||||
| 	"Discover a modelfile": "Descobrir um arquivo de modelo", | ||||
| 	"Discover a prompt": "Descobrir um prompt", | ||||
| 	"Discover, download, and explore custom prompts": "Descubra, baixe e explore prompts personalizados", | ||||
| 	"Discover, download, and explore model presets": "Descubra, baixe e explore predefinições de modelo", | ||||
| 	"Display the username instead of You in the Chat": "Exibir o nome de usuário em vez de Você no Bate-papo", | ||||
| 	"Document": "Documento", | ||||
| 	"Document Settings": "Configurações de Documento", | ||||
| 	"Documents": "Documentos", | ||||
| 	"does not make any external connections, and your data stays securely on your locally hosted server.": "não faz conexões externas e seus dados permanecem seguros em seu servidor hospedado localmente.", | ||||
| 	"Don't Allow": "Não Permitir", | ||||
| 	"Don't have an account?": "Não tem uma conta?", | ||||
| 	"Download as a File": "Baixar como Arquivo", | ||||
| 	"Download Database": "Baixar Banco de Dados", | ||||
| 	"Drop any files here to add to the conversation": "Solte os arquivos aqui para adicionar à conversa", | ||||
| 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "por exemplo, '30s', '10m'. Unidades de tempo válidas são 's', 'm', 'h'.", | ||||
| 	"Edit Doc": "Editar Documento", | ||||
| 	"Edit User": "Editar Usuário", | ||||
| 	"Email": "E-mail", | ||||
| 	"Enable Chat History": "Ativar Histórico de Bate-papo", | ||||
| 	"Enable New Sign Ups": "Ativar Novas Inscrições", | ||||
| 	"Enabled": "Ativado", | ||||
| 	"Enter {{role}} message here": "Digite a mensagem de {{role}} aqui", | ||||
| 	"Enter API Key": "Digite a Chave da API", | ||||
| 	"Enter Chunk Overlap": "Digite a Sobreposição de Fragmento", | ||||
| 	"Enter Chunk Size": "Digite o Tamanho do Fragmento", | ||||
| 	"Enter Image Size (e.g. 512x512)": "Digite o Tamanho da Imagem (por exemplo, 512x512)", | ||||
| 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "Digite a URL Base da API LiteLLM (litellm_params.api_base)", | ||||
| 	"Enter LiteLLM API Key (litellm_params.api_key)": "Digite a Chave da API LiteLLM (litellm_params.api_key)", | ||||
| 	"Enter LiteLLM API RPM (litellm_params.rpm)": "Digite o RPM da API LiteLLM (litellm_params.rpm)", | ||||
| 	"Enter LiteLLM Model (litellm_params.model)": "Digite o Modelo LiteLLM (litellm_params.model)", | ||||
| 	"Enter Max Tokens (litellm_params.max_tokens)": "Digite o Máximo de Tokens (litellm_params.max_tokens)", | ||||
| 	"Enter model tag (e.g. {{modelTag}})": "Digite a tag do modelo (por exemplo, {{modelTag}})", | ||||
| 	"Enter Number of Steps (e.g. 50)": "Digite o Número de Etapas (por exemplo, 50)", | ||||
| 	"Enter stop sequence": "Digite a sequência de parada", | ||||
| 	"Enter Top K": "Digite o Top K", | ||||
| 	"Enter URL (e.g. http://127.0.0.1:7860/)": "Digite a URL (por exemplo, http://127.0.0.1:7860/)", | ||||
| 	"Enter Your Email": "Digite seu E-mail", | ||||
| 	"Enter Your Full Name": "Digite seu Nome Completo", | ||||
| 	"Enter Your Password": "Digite sua Senha", | ||||
| 	"Experimental": "Experimental", | ||||
| 	"Export All Chats (All Users)": "Exportar Todos os Bate-papos (Todos os Usuários)", | ||||
| 	"Export Chats": "Exportar Bate-papos", | ||||
| 	"Export Documents Mapping": "Exportar Mapeamento de Documentos", | ||||
| 	"Export Modelfiles": "Exportar Arquivos de Modelo", | ||||
| 	"Export Prompts": "Exportar Prompts", | ||||
| 	"Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência", | ||||
| 	"File Mode": "Modo de Arquivo", | ||||
| 	"File not found.": "Arquivo não encontrado.", | ||||
| 	"Focus chat input": "Focar entrada de bate-papo", | ||||
| 	"Format your variables using square brackets like this:": "Formate suas variáveis usando colchetes como este:", | ||||
| 	"From (Base Model)": "De (Modelo Base)", | ||||
| 	"Full Screen Mode": "Modo de Tela Cheia", | ||||
| 	"General": "Geral", | ||||
| 	"General Settings": "Configurações Gerais", | ||||
| 	"Hello, {{name}}": "Olá, {{name}}", | ||||
| 	"Hide": "Ocultar", | ||||
| 	"Hide Additional Params": "Ocultar Parâmetros Adicionais", | ||||
| 	"How can I help you today?": "Como posso ajudá-lo hoje?", | ||||
| 	"Image Generation (Experimental)": "Geração de Imagens (Experimental)", | ||||
| 	"Image Generation Engine": "Mecanismo de Geração de Imagens", | ||||
| 	"Image Settings": "Configurações de Imagem", | ||||
| 	"Images": "Imagens", | ||||
| 	"Import Chats": "Importar Bate-papos", | ||||
| 	"Import Documents Mapping": "Importar Mapeamento de Documentos", | ||||
| 	"Import Modelfiles": "Importar Arquivos de Modelo", | ||||
| 	"Import Prompts": "Importar Prompts", | ||||
| 	"Include `--api` flag when running stable-diffusion-webui": "Inclua a flag `--api` ao executar stable-diffusion-webui", | ||||
| 	"Interface": "Interface", | ||||
| 	"join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.", | ||||
| 	"JSON": "JSON", | ||||
| 	"JWT Expiration": "Expiração JWT", | ||||
| 	"JWT Token": "Token JWT", | ||||
| 	"Keep Alive": "Manter Vivo", | ||||
| 	"Keyboard shortcuts": "Atalhos de teclado", | ||||
| 	"Language": "Idioma", | ||||
| 	"Light": "Claro", | ||||
| 	"Listening...": "Ouvindo...", | ||||
| 	"LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", | ||||
| 	"Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI", | ||||
| 	"Make sure to enclose them with": "Certifique-se de colocá-los entre", | ||||
| 	"Manage LiteLLM Models": "Gerenciar Modelos LiteLLM", | ||||
| 	"Manage Models": "Gerenciar Modelos", | ||||
| 	"Manage Ollama Models": "Gerenciar Modelos Ollama", | ||||
| 	"Max Tokens": "Máximo de Tokens", | ||||
| 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Máximo de 3 modelos podem ser baixados simultaneamente. Tente novamente mais tarde.", | ||||
| 	"Mirostat": "Mirostat", | ||||
| 	"Mirostat Eta": "Mirostat Eta", | ||||
| 	"Mirostat Tau": "Mirostat Tau", | ||||
| 	"MMMM DD, YYYY": "MMMM DD, AAAA", | ||||
| 	"Model '{{modelName}}' has been successfully downloaded.": "O modelo '{{modelName}}' foi baixado com sucesso.", | ||||
| 	"Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para download.", | ||||
| 	"Model {{modelId}} not found": "Modelo {{modelId}} não encontrado", | ||||
| 	"Model {{modelName}} already exists.": "O modelo {{modelName}} já existe.", | ||||
| 	"Model Name": "Nome do Modelo", | ||||
| 	"Model not selected": "Modelo não selecionado", | ||||
| 	"Model Tag Name": "Nome da Tag do Modelo", | ||||
| 	"Model Whitelisting": "Lista de Permissões de Modelo", | ||||
| 	"Model(s) Whitelisted": "Modelo(s) na Lista de Permissões", | ||||
| 	"Modelfile": "Arquivo de Modelo", | ||||
| 	"Modelfile Advanced Settings": "Configurações Avançadas do Arquivo de Modelo", | ||||
| 	"Modelfile Content": "Conteúdo do Arquivo de Modelo", | ||||
| 	"Modelfiles": "Arquivos de Modelo", | ||||
| 	"Models": "Modelos", | ||||
| 	"My Documents": "Meus Documentos", | ||||
| 	"My Modelfiles": "Meus Arquivos de Modelo", | ||||
| 	"My Prompts": "Meus Prompts", | ||||
| 	"Name": "Nome", | ||||
| 	"Name Tag": "Nome da Tag", | ||||
| 	"Name your modelfile": "Nomeie seu arquivo de modelo", | ||||
| 	"New Chat": "Novo Bate-papo", | ||||
| 	"New Password": "Nova Senha", | ||||
| 	"Not sure what to add?": "Não tem certeza do que adicionar?", | ||||
| 	"Not sure what to write? Switch to": "Não tem certeza do que escrever? Mude para", | ||||
| 	"Off": "Desligado", | ||||
| 	"Okay, Let's Go!": "Ok, Vamos Lá!", | ||||
| 	"Ollama Base URL": "URL Base do Ollama", | ||||
| 	"Ollama Version": "Versão do Ollama", | ||||
| 	"On": "Ligado", | ||||
| 	"Only": "Somente", | ||||
| 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Somente caracteres alfanuméricos e hífens são permitidos na string de comando.", | ||||
| 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Opa! Aguente firme! Seus arquivos ainda estão no forno de processamento. Estamos cozinhando-os com perfeição. Por favor, seja paciente e avisaremos quando estiverem prontos.", | ||||
| 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Opa! Parece que a URL é inválida. Verifique novamente e tente outra vez.", | ||||
| 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Opa! Você está usando um método não suportado (somente frontend). Por favor, sirva o WebUI a partir do backend.", | ||||
| 	"Open": "Abrir", | ||||
| 	"Open AI": "OpenAI", | ||||
| 	"Open AI (Dall-E)": "OpenAI (Dall-E)", | ||||
| 	"Open new chat": "Abrir novo bate-papo", | ||||
| 	"OpenAI API": "API OpenAI", | ||||
| 	"OpenAI API Key": "Chave da API OpenAI", | ||||
| 	"OpenAI API Key is required.": "A Chave da API OpenAI é obrigatória.", | ||||
| 	"or": "ou", | ||||
| 	"Parameters": "Parâmetros", | ||||
| 	"Password": "Senha", | ||||
| 	"PDF Extract Images (OCR)": "Extrair Imagens de PDF (OCR)", | ||||
| 	"pending": "pendente", | ||||
| 	"Permission denied when accessing microphone: {{error}}": "Permissão negada ao acessar o microfone: {{error}}", | ||||
| 	"Playground": "Playground", | ||||
| 	"Profile": "Perfil", | ||||
| 	"Prompt Content": "Conteúdo do Prompt", | ||||
| 	"Prompt suggestions": "Sugestões de Prompt", | ||||
| 	"Prompts": "Prompts", | ||||
| 	"Pull a model from Ollama.com": "Extrair um modelo do Ollama.com", | ||||
| 	"Pull Progress": "Progresso da Extração", | ||||
| 	"Query Params": "Parâmetros de Consulta", | ||||
| 	"RAG Template": "Modelo RAG", | ||||
| 	"Raw Format": "Formato Bruto", | ||||
| 	"Record voice": "Gravar voz", | ||||
| 	"Redirecting you to OpenWebUI Community": "Redirecionando você para a Comunidade OpenWebUI", | ||||
| 	"Release Notes": "Notas de Lançamento", | ||||
| 	"Repeat Last N": "Repetir Últimos N", | ||||
| 	"Repeat Penalty": "Penalidade de Repetição", | ||||
| 	"Request Mode": "Modo de Solicitação", | ||||
| 	"Reset Vector Storage": "Redefinir Armazenamento de Vetor", | ||||
| 	"Response AutoCopy to Clipboard": "Cópia Automática da Resposta para a Área de Transferência", | ||||
| 	"Role": "Função", | ||||
| 	"Rosé Pine": "Rosé Pine", | ||||
| 	"Rosé Pine Dawn": "Rosé Pine Dawn", | ||||
| 	"Save": "Salvar", | ||||
| 	"Save & Create": "Salvar e Criar", | ||||
| 	"Save & Submit": "Salvar e Enviar", | ||||
| 	"Save & Update": "Salvar e Atualizar", | ||||
| 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Salvar logs de bate-papo diretamente no armazenamento do seu navegador não é mais suportado. Reserve um momento para baixar e excluir seus logs de bate-papo clicando no botão abaixo. Não se preocupe, você pode facilmente reimportar seus logs de bate-papo para o backend através de", | ||||
| 	"Scan": "Digitalizar", | ||||
| 	"Scan complete!": "Digitalização concluída!", | ||||
| 	"Scan for documents from {{path}}": "Digitalizar documentos de {{path}}", | ||||
| 	"Search": "Pesquisar", | ||||
| 	"Search Documents": "Pesquisar Documentos", | ||||
| 	"Search Prompts": "Pesquisar Prompts", | ||||
| 	"See readme.md for instructions": "Consulte readme.md para obter instruções", | ||||
| 	"See what's new": "Veja o que há de novo", | ||||
| 	"Seed": "Semente", | ||||
| 	"Select a mode": "Selecione um modo", | ||||
| 	"Select a model": "Selecione um modelo", | ||||
| 	"Select an Ollama instance": "Selecione uma instância Ollama", | ||||
| 	"Send a Message": "Enviar uma Mensagem", | ||||
| 	"Send message": "Enviar mensagem", | ||||
| 	"Server connection verified": "Conexão com o servidor verificada", | ||||
| 	"Set as default": "Definir como padrão", | ||||
| 	"Set Default Model": "Definir Modelo Padrão", | ||||
| 	"Set Image Size": "Definir Tamanho da Imagem", | ||||
| 	"Set Steps": "Definir Etapas", | ||||
| 	"Set Title Auto-Generation Model": "Definir Modelo de Geração Automática de Título", | ||||
| 	"Set Voice": "Definir Voz", | ||||
| 	"Settings": "Configurações", | ||||
| 	"Settings saved successfully!": "Configurações salvas com sucesso!", | ||||
| 	"Share to OpenWebUI Community": "Compartilhar com a Comunidade OpenWebUI", | ||||
| 	"short-summary": "resumo-curto", | ||||
| 	"Show": "Mostrar", | ||||
| 	"Show Additional Params": "Mostrar Parâmetros Adicionais", | ||||
| 	"Show shortcuts": "Mostrar", | ||||
| 	"sidebar": "barra lateral", | ||||
| 	"Sign in": "Entrar", | ||||
| 	"Sign Out": "Sair", | ||||
| 	"Sign up": "Inscrever-se", | ||||
| 	"Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}", | ||||
| 	"Speech-to-Text Engine": "Mecanismo de Fala para Texto", | ||||
| 	"SpeechRecognition API is not supported in this browser.": "A API SpeechRecognition não é suportada neste navegador.", | ||||
| 	"Stop Sequence": "Sequência de Parada", | ||||
| 	"STT Settings": "Configurações STT", | ||||
| 	"Submit": "Enviar", | ||||
| 	"Success": "Sucesso", | ||||
| 	"Successfully updated.": "Atualizado com sucesso.", | ||||
| 	"Sync All": "Sincronizar Tudo", | ||||
| 	"System": "Sistema", | ||||
| 	"System Prompt": "Prompt do Sistema", | ||||
| 	"Tags": "Tags", | ||||
| 	"Temperature": "Temperatura", | ||||
| 	"Template": "Modelo", | ||||
| 	"Text Completion": "Complemento de Texto", | ||||
| 	"Text-to-Speech Engine": "Mecanismo de Texto para Fala", | ||||
| 	"Tfs Z": "Tfs Z", | ||||
| 	"Theme": "Tema", | ||||
| 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Isso garante que suas conversas valiosas sejam salvas com segurança em seu banco de dados de backend. Obrigado!", | ||||
| 	"This setting does not sync across browsers or devices.": "Esta configuração não sincroniza entre navegadores ou dispositivos.", | ||||
| 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Dica: Atualize vários slots de variáveis consecutivamente pressionando a tecla Tab na entrada de bate-papo após cada substituição.", | ||||
| 	"Title": "Título", | ||||
| 	"Title Auto-Generation": "Geração Automática de Título", | ||||
| 	"Title Generation Prompt": "Prompt de Geração de Título", | ||||
| 	"to": "para", | ||||
| 	"To access the available model names for downloading,": "Para acessar os nomes de modelo disponíveis para download,", | ||||
| 	"To access the GGUF models available for downloading,": "Para acessar os modelos GGUF disponíveis para download,", | ||||
| 	"to chat input.": "para a entrada de bate-papo.", | ||||
| 	"Toggle settings": "Alternar configurações", | ||||
| 	"Toggle sidebar": "Alternar barra lateral", | ||||
| 	"Top K": "Top K", | ||||
| 	"Top P": "Top P", | ||||
| 	"Trouble accessing Ollama?": "Problemas para acessar o Ollama?", | ||||
| 	"TTS Settings": "Configurações TTS", | ||||
| 	"Type Hugging Face Resolve (Download) URL": "Digite a URL do Hugging Face Resolve (Download)", | ||||
| 	"Uh-oh! There was an issue connecting to {{provider}}.": "Opa! Houve um problema ao conectar-se a {{provider}}.", | ||||
| 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de arquivo desconhecido '{{file_type}}', mas aceitando e tratando como texto simples", | ||||
| 	"Update password": "Atualizar senha", | ||||
| 	"Upload a GGUF model": "Carregar um modelo GGUF", | ||||
| 	"Upload files": "Carregar arquivos", | ||||
| 	"Upload Progress": "Progresso do Carregamento", | ||||
| 	"URL Mode": "Modo de URL", | ||||
| 	"Use '#' in the prompt input to load and select your documents.": "Use '#' na entrada do prompt para carregar e selecionar seus documentos.", | ||||
| 	"Use Gravatar": "Usar Gravatar", | ||||
| 	"user": "usuário", | ||||
| 	"User Permissions": "Permissões do Usuário", | ||||
| 	"Users": "Usuários", | ||||
| 	"Utilize": "Utilizar", | ||||
| 	"Valid time units:": "Unidades de tempo válidas:", | ||||
| 	"variable": "variável", | ||||
| 	"variable to have them replaced with clipboard content.": "variável para que sejam substituídos pelo conteúdo da área de transferência.", | ||||
| 	"Version": "Versão", | ||||
| 	"Web": "Web", | ||||
| 	"WebUI Add-ons": "Complementos WebUI", | ||||
| 	"WebUI Settings": "Configurações WebUI", | ||||
| 	"WebUI will make requests to": "WebUI fará solicitações para", | ||||
| 	"What’s New in": "O que há de novo em", | ||||
| 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quando o histórico está desativado, novos bate-papos neste navegador não aparecerão em seu histórico em nenhum dos seus dispositivos.", | ||||
| 	"Whisper (Local)": "Whisper (Local)", | ||||
| 	"Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem é você?)", | ||||
| 	"Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].", | ||||
| 	"You": "Você", | ||||
| 	"You're a helpful assistant.": "Você é um assistente útil.", | ||||
| 	"You're now logged in.": "Você está conectado agora." | ||||
| } | ||||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", | ||||
| 	"Deleted {tagName}": "{tagName} excluído", | ||||
| 	"Description": "Descrição", | ||||
| 	"Desktop Notifications": "Notificações da Área de Trabalho", | ||||
| 	"Notifications": "Notificações da Área de Trabalho", | ||||
| 	"Disabled": "Desativado", | ||||
| 	"Discover a modelfile": "Descobrir um arquivo de modelo", | ||||
| 	"Discover a prompt": "Descobrir um prompt", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Удалено {tagName}", | ||||
| 	"Description": "Описание", | ||||
| 	"Desktop Notifications": "Уведомления на рабочем столе", | ||||
| 	"Notifications": "Уведомления на рабочем столе", | ||||
| 	"Disabled": "Отключено", | ||||
| 	"Discover a modelfile": "Найти файл модели", | ||||
| 	"Discover a prompt": "Найти промт", | ||||
|  |  | |||
							
								
								
									
										363
									
								
								src/lib/i18n/locales/tr-TR/translation.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								src/lib/i18n/locales/tr-TR/translation.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,363 @@ | |||
| { | ||||
| 	"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' veya süresiz için '-1'.", | ||||
| 	"(Beta)": "(Beta)", | ||||
| 	"(e.g. `sh webui.sh --api`)": "(örn. `sh webui.sh --api`)", | ||||
| 	"(latest)": "(en son)", | ||||
| 	"{{modelName}} is thinking...": "{{modelName}} düşünüyor...", | ||||
| 	"{{webUIName}} Backend Required": "{{webUIName}} Arkayüz Gerekli", | ||||
| 	"a user": "bir kullanıcı", | ||||
| 	"About": "Hakkında", | ||||
| 	"Account": "Hesap", | ||||
| 	"Action": "Eylem", | ||||
| 	"Add a model": "Bir model ekleyin", | ||||
| 	"Add a model tag name": "Bir model etiket adı ekleyin", | ||||
| 	"Add a short description about what this modelfile does": "Bu model dosyasının ne yaptığı hakkında kısa bir açıklama ekleyin", | ||||
| 	"Add a short title for this prompt": "Bu prompt için kısa bir başlık ekleyin", | ||||
| 	"Add a tag": "Bir etiket ekleyin", | ||||
| 	"Add Docs": "Dökümanlar Ekle", | ||||
| 	"Add Files": "Dosyalar Ekle", | ||||
| 	"Add message": "Mesaj ekle", | ||||
| 	"add tags": "etiketler ekle", | ||||
| 	"Adjusting these settings will apply changes universally to all users.": "Bu ayarları ayarlamak değişiklikleri tüm kullanıcılara evrensel olarak uygular.", | ||||
| 	"admin": "yönetici", | ||||
| 	"Admin Panel": "Yönetici Paneli", | ||||
| 	"Admin Settings": "Yönetici Ayarları", | ||||
| 	"Advanced Parameters": "Gelişmiş Parametreler", | ||||
| 	"all": "tümü", | ||||
| 	"All Users": "Tüm Kullanıcılar", | ||||
| 	"Allow": "İzin ver", | ||||
| 	"Allow Chat Deletion": "Sohbet Silmeye İzin Ver", | ||||
| 	"alphanumeric characters and hyphens": "alfanumerik karakterler ve tireler", | ||||
| 	"Already have an account?": "Zaten bir hesabınız mı var?", | ||||
| 	"an assistant": "bir asistan", | ||||
| 	"and": "ve", | ||||
| 	"API Base URL": "API Temel URL", | ||||
| 	"API Key": "API Anahtarı", | ||||
| 	"API RPM": "API RPM", | ||||
| 	"are allowed - Activate this command by typing": "izin verilir - Bu komutu yazarak etkinleştirin", | ||||
| 	"Are you sure?": "Emin misiniz?", | ||||
| 	"Audio": "Ses", | ||||
| 	"Auto-playback response": "Yanıtı otomatik oynatma", | ||||
| 	"Auto-send input after 3 sec.": "3 saniye sonra otomatik olarak gönder", | ||||
| 	"AUTOMATIC1111 Base URL": "AUTOMATIC1111 Temel URL", | ||||
| 	"AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Temel URL gereklidir.", | ||||
| 	"available!": "mevcut!", | ||||
| 	"Back": "Geri", | ||||
| 	"Builder Mode": "Oluşturucu Modu", | ||||
| 	"Cancel": "İptal", | ||||
| 	"Categories": "Kategoriler", | ||||
| 	"Change Password": "Parola Değiştir", | ||||
| 	"Chat": "Sohbet", | ||||
| 	"Chat History": "Sohbet Geçmişi", | ||||
| 	"Chat History is off for this browser.": "Bu tarayıcı için sohbet geçmişi kapalı.", | ||||
| 	"Chats": "Sohbetler", | ||||
| 	"Check Again": "Tekrar Kontrol Et", | ||||
| 	"Check for updates": "Güncellemeleri kontrol et", | ||||
| 	"Checking for updates...": "Güncellemeler kontrol ediliyor...", | ||||
| 	"Choose a model before saving...": "Kaydetmeden önce bir model seçin...", | ||||
| 	"Chunk Overlap": "Chunk Çakışması", | ||||
| 	"Chunk Params": "Chunk Parametreleri", | ||||
| 	"Chunk Size": "Chunk Boyutu", | ||||
| 	"Click here for help.": "Yardım için buraya tıklayın.", | ||||
| 	"Click here to check other modelfiles.": "Diğer model dosyalarını kontrol etmek için buraya tıklayın.", | ||||
| 	"Click here to select": "Seçmek için buraya tıklayın", | ||||
| 	"Click here to select documents.": "Belgeleri seçmek için buraya tıklayın.", | ||||
| 	"click here.": "buraya tıklayın.", | ||||
| 	"Click on the user role button to change a user's role.": "Bir kullanıcının rolünü değiştirmek için kullanıcı rolü düğmesine tıklayın.", | ||||
| 	"Close": "Kapat", | ||||
| 	"Collection": "Koleksiyon", | ||||
| 	"Command": "Komut", | ||||
| 	"Confirm Password": "Parolayı Onayla", | ||||
| 	"Connections": "Bağlantılar", | ||||
| 	"Content": "İçerik", | ||||
| 	"Context Length": "Bağlam Uzunluğu", | ||||
| 	"Conversation Mode": "Sohbet Modu", | ||||
| 	"Copy last code block": "Son kod bloğunu kopyala", | ||||
| 	"Copy last response": "Son yanıtı kopyala", | ||||
| 	"Copying to clipboard was successful!": "Panoya kopyalama başarılı!", | ||||
| 	"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Aşağıdaki sorgu için başlık olarak 3-5 kelimelik kısa ve öz bir ifade oluşturun, 3-5 kelime sınırına kesinlikle uyun ve 'başlık' kelimesini kullanmaktan kaçının:", | ||||
| 	"Create a modelfile": "Bir model dosyası oluştur", | ||||
| 	"Create Account": "Hesap Oluştur", | ||||
| 	"Created at": "Oluşturulma tarihi", | ||||
| 	"Created by": "Oluşturan", | ||||
| 	"Current Model": "Mevcut Model", | ||||
| 	"Current Password": "Mevcut Parola", | ||||
| 	"Custom": "Özel", | ||||
| 	"Customize Ollama models for a specific purpose": "Ollama modellerini belirli bir amaç için özelleştirin", | ||||
| 	"Dark": "Koyu", | ||||
| 	"Database": "Veritabanı", | ||||
| 	"DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm", | ||||
| 	"Default": "Varsayılan", | ||||
| 	"Default (Automatic1111)": "Varsayılan (Automatic1111)", | ||||
| 	"Default (Web API)": "Varsayılan (Web API)", | ||||
| 	"Default model updated": "Varsayılan model güncellendi", | ||||
| 	"Default Prompt Suggestions": "Varsayılan Prompt Önerileri", | ||||
| 	"Default User Role": "Varsayılan Kullanıcı Rolü", | ||||
| 	"delete": "sil", | ||||
| 	"Delete a model": "Bir modeli sil", | ||||
| 	"Delete chat": "Sohbeti sil", | ||||
| 	"Delete Chats": "Sohbetleri Sil", | ||||
| 	"Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi", | ||||
| 	"Deleted {tagName}": "{tagName} silindi", | ||||
| 	"Description": "Açıklama", | ||||
| 	"Notifications": "Bildirimler", | ||||
| 	"Disabled": "Devre Dışı", | ||||
| 	"Discover a modelfile": "Bir model dosyası keşfedin", | ||||
| 	"Discover a prompt": "Bir prompt keşfedin", | ||||
| 	"Discover, download, and explore custom prompts": "Özel promptları keşfedin, indirin ve inceleyin", | ||||
| 	"Discover, download, and explore model presets": "Model ön ayarlarını keşfedin, indirin ve inceleyin", | ||||
| 	"Display the username instead of You in the Chat": "Sohbet'te Siz yerine kullanıcı adını göster", | ||||
| 	"Document": "Belge", | ||||
| 	"Document Settings": "Belge Ayarları", | ||||
| 	"Documents": "Belgeler", | ||||
| 	"does not make any external connections, and your data stays securely on your locally hosted server.": "herhangi bir harici bağlantı yapmaz ve verileriniz güvenli bir şekilde yerel olarak barındırılan sunucunuzda kalır.", | ||||
| 	"Don't Allow": "İzin Verme", | ||||
| 	"Don't have an account?": "Hesabınız yok mu?", | ||||
| 	"Download as a File": "Dosya olarak indir", | ||||
| 	"Download Database": "Veritabanını İndir", | ||||
| 	"Drop any files here to add to the conversation": "Sohbete eklemek istediğiniz dosyaları buraya bırakın", | ||||
| 	"e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "örn. '30s', '10m'. Geçerli zaman birimleri 's', 'm', 'h'.", | ||||
| 	"Edit Doc": "Belgeyi Düzenle", | ||||
| 	"Edit User": "Kullanıcıyı Düzenle", | ||||
| 	"Email": "E-posta", | ||||
| 	"Enable Chat History": "Sohbet Geçmişini Etkinleştir", | ||||
| 	"Enable New Sign Ups": "Yeni Kayıtları Etkinleştir", | ||||
| 	"Enabled": "Etkin", | ||||
| 	"Enter {{role}} message here": "Buraya {{role}} mesajını girin", | ||||
| 	"Enter API Key": "API Anahtarını Girin", | ||||
| 	"Enter Chunk Overlap": "Chunk Örtüşmesini Girin", | ||||
| 	"Enter Chunk Size": "Chunk Boyutunu Girin", | ||||
| 	"Enter Image Size (e.g. 512x512)": "Görüntü Boyutunu Girin (örn. 512x512)", | ||||
| 	"Enter LiteLLM API Base URL (litellm_params.api_base)": "LiteLLM API Ana URL'sini Girin (litellm_params.api_base)", | ||||
| 	"Enter LiteLLM API Key (litellm_params.api_key)": "LiteLLM API Anahtarını Girin (litellm_params.api_key)", | ||||
| 	"Enter LiteLLM API RPM (litellm_params.rpm)": "LiteLLM API RPM'ini Girin (litellm_params.rpm)", | ||||
| 	"Enter LiteLLM Model (litellm_params.model)": "LiteLLM Modelini Girin (litellm_params.model)", | ||||
| 	"Enter Max Tokens (litellm_params.max_tokens)": "Maksimum Token Sayısını Girin (litellm_params.max_tokens)", | ||||
| 	"Enter model tag (e.g. {{modelTag}})": "Model etiketini girin (örn. {{modelTag}})", | ||||
| 	"Enter Number of Steps (e.g. 50)": "Adım Sayısını Girin (örn. 50)", | ||||
| 	"Enter stop sequence": "Durdurma dizisini girin", | ||||
| 	"Enter Top K": "Top K'yı girin", | ||||
| 	"Enter URL (e.g. http://127.0.0.1:7860/)": "URL'yi Girin (örn. http://127.0.0.1:7860/)", | ||||
| 	"Enter Your Email": "E-postanızı Girin", | ||||
| 	"Enter Your Full Name": "Tam Adınızı Girin", | ||||
| 	"Enter Your Password": "Parolanızı Girin", | ||||
| 	"Experimental": "Deneysel", | ||||
| 	"Export All Chats (All Users)": "Tüm Sohbetleri Dışa Aktar (Tüm Kullanıcılar)", | ||||
| 	"Export Chats": "Sohbetleri Dışa Aktar", | ||||
| 	"Export Documents Mapping": "Belge Eşlemesini Dışa Aktar", | ||||
| 	"Export Modelfiles": "Model Dosyalarını Dışa Aktar", | ||||
| 	"Export Prompts": "Promptları Dışa Aktar", | ||||
| 	"Failed to read clipboard contents": "Pano içeriği okunamadı", | ||||
| 	"File Mode": "Dosya Modu", | ||||
| 	"File not found.": "Dosya bulunamadı.", | ||||
| 	"Focus chat input": "Sohbet girişine odaklan", | ||||
| 	"Format your variables using square brackets like this:": "Değişkenlerinizi şu şekilde kare parantezlerle biçimlendirin:", | ||||
| 	"From (Base Model)": "(Temel Model)'den", | ||||
| 	"Full Screen Mode": "Tam Ekran Modu", | ||||
| 	"General": "Genel", | ||||
| 	"General Settings": "Genel Ayarlar", | ||||
| 	"Hello, {{name}}": "Merhaba, {{name}}", | ||||
| 	"Hide": "Gizle", | ||||
| 	"Hide Additional Params": "Ek Parametreleri Gizle", | ||||
| 	"How can I help you today?": "Bugün size nasıl yardımcı olabilirim?", | ||||
| 	"Image Generation (Experimental)": "Görüntü Oluşturma (Deneysel)", | ||||
| 	"Image Generation Engine": "Görüntü Oluşturma Motoru", | ||||
| 	"Image Settings": "Görüntü Ayarları", | ||||
| 	"Images": "Görüntüler", | ||||
| 	"Import Chats": "Sohbetleri İçe Aktar", | ||||
| 	"Import Documents Mapping": "Belge Eşlemesini İçe Aktar", | ||||
| 	"Import Modelfiles": "Model Dosyalarını İçe Aktar", | ||||
| 	"Import Prompts": "Promptları İçe Aktar", | ||||
| 	"Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui çalıştırılırken `--api` bayrağını dahil edin", | ||||
| 	"Interface": "Arayüz", | ||||
| 	"join our Discord for help.": "yardım için Discord'umuza katılın.", | ||||
| 	"JSON": "JSON", | ||||
| 	"JWT Expiration": "JWT Bitişi", | ||||
| 	"JWT Token": "JWT Token", | ||||
| 	"Keep Alive": "Canlı Tut", | ||||
| 	"Keyboard shortcuts": "Klavye kısayolları", | ||||
| 	"Language": "Dil", | ||||
| 	"Light": "Açık", | ||||
| 	"Listening...": "Dinleniyor...", | ||||
| 	"LLMs can make mistakes. Verify important information.": "LLM'ler hata yapabilir. Önemli bilgileri doğrulayın.", | ||||
| 	"Made by OpenWebUI Community": "OpenWebUI Topluluğu tarafından yapılmıştır", | ||||
| 	"Make sure to enclose them with": "Değişkenlerinizi şu şekilde biçimlendirin:", | ||||
| 	"Manage LiteLLM Models": "LiteLLM Modellerini Yönet", | ||||
| 	"Manage Models": "Modelleri Yönet", | ||||
| 	"Manage Ollama Models": "Ollama Modellerini Yönet", | ||||
| 	"Max Tokens": "Maksimum Token", | ||||
| 	"Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Aynı anda en fazla 3 model indirilebilir. Lütfen daha sonra tekrar deneyin.", | ||||
| 	"Mirostat": "Mirostat", | ||||
| 	"Mirostat Eta": "Mirostat Eta", | ||||
| 	"Mirostat Tau": "Mirostat Tau", | ||||
| 	"MMMM DD, YYYY": "DD MMMM YYYY", | ||||
| 	"Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' başarıyla indirildi.", | ||||
| 	"Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' zaten indirme sırasında.", | ||||
| 	"Model {{modelId}} not found": "{{modelId}} bulunamadı", | ||||
| 	"Model {{modelName}} already exists.": "{{modelName}} zaten mevcut.", | ||||
| 	"Model Name": "Model Adı", | ||||
| 	"Model not selected": "Model seçilmedi", | ||||
| 	"Model Tag Name": "Model Etiket Adı", | ||||
| 	"Model Whitelisting": "Model Beyaz Listeye Alma", | ||||
| 	"Model(s) Whitelisted": "Model(ler) Beyaz Listeye Alındı", | ||||
| 	"Modelfile": "Model Dosyası", | ||||
| 	"Modelfile Advanced Settings": "Model Dosyası Gelişmiş Ayarları", | ||||
| 	"Modelfile Content": "Model Dosyası İçeriği", | ||||
| 	"Modelfiles": "Model Dosyaları", | ||||
| 	"Models": "Modeller", | ||||
| 	"My Documents": "Belgelerim", | ||||
| 	"My Modelfiles": "Model Dosyalarım", | ||||
| 	"My Prompts": "Promptlarım", | ||||
| 	"Name": "Ad", | ||||
| 	"Name Tag": "Ad Etiketi", | ||||
| 	"Name your modelfile": "Model dosyanıza ad verin", | ||||
| 	"New Chat": "Yeni Sohbet", | ||||
| 	"New Password": "Yeni Parola", | ||||
| 	"Not sure what to add?": "Ne ekleyeceğinizden emin değil misiniz?", | ||||
| 	"Not sure what to write? Switch to": "Ne yazacağınızdan emin değil misiniz? Şuraya geçin", | ||||
| 	"Off": "Kapalı", | ||||
| 	"Okay, Let's Go!": "Tamam, Hadi Başlayalım!", | ||||
| 	"Ollama Base URL": "Ollama Temel URL", | ||||
| 	"Ollama Version": "Ollama Sürümü", | ||||
| 	"On": "Açık", | ||||
| 	"Only": "Yalnızca", | ||||
| 	"Only alphanumeric characters and hyphens are allowed in the command string.": "Komut dizisinde yalnızca alfasayısal karakterler ve tireler kabul edilir.", | ||||
| 	"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "Hop! Biraz sabırlı ol! Dosyaların hala hazırlama fırınında. Onları ağzınıza layık olana kadar pişiriyoruz :) Lütfen sabırlı olun; hazır olduklarında size haber vereceğiz.", | ||||
| 	"Oops! Looks like the URL is invalid. Please double-check and try again.": "Hop! URL geçersiz gibi görünüyor. Lütfen tekrar kontrol edin ve yeniden deneyin.", | ||||
| 	"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hop! Desteklenmeyen bir yöntem kullanıyorsunuz (yalnızca önyüz). Lütfen WebUI'yi arkayüzden sunun.", | ||||
| 	"Open": "Aç", | ||||
| 	"Open AI": "Open AI", | ||||
| 	"Open AI (Dall-E)": "Open AI (Dall-E)", | ||||
| 	"Open new chat": "Yeni sohbet aç", | ||||
| 	"OpenAI API": "OpenAI API", | ||||
| 	"OpenAI API Key": "OpenAI API Anahtarı", | ||||
| 	"OpenAI API Key is required.": "OpenAI API Anahtarı gereklidir.", | ||||
| 	"or": "veya", | ||||
| 	"Parameters": "Parametreler", | ||||
| 	"Password": "Parola", | ||||
| 	"PDF Extract Images (OCR)": "PDF Görüntülerini Çıkart (OCR)", | ||||
| 	"pending": "beklemede", | ||||
| 	"Permission denied when accessing microphone: {{error}}": "Mikrofona erişim izni reddedildi: {{error}}", | ||||
| 	"Playground": "Oyun Alanı", | ||||
| 	"Profile": "Profil", | ||||
| 	"Prompt Content": "Prompt İçeriği", | ||||
| 	"Prompt suggestions": "Prompt önerileri", | ||||
| 	"Prompts": "Promptlar", | ||||
| 	"Pull a model from Ollama.com": "Ollama.com'dan bir model çekin", | ||||
| 	"Pull Progress": "Çekme İlerlemesi", | ||||
| 	"Query Params": "Sorgu Parametreleri", | ||||
| 	"RAG Template": "RAG Şablonu", | ||||
| 	"Raw Format": "Ham Format", | ||||
| 	"Record voice": "Ses kaydı yap", | ||||
| 	"Redirecting you to OpenWebUI Community": "OpenWebUI Topluluğuna yönlendiriliyorsunuz", | ||||
| 	"Release Notes": "Sürüm Notları", | ||||
| 	"Repeat Last N": "Son N'yi Tekrar Et", | ||||
| 	"Repeat Penalty": "Tekrar Cezası", | ||||
| 	"Request Mode": "İstek Modu", | ||||
| 	"Reset Vector Storage": "Vektör Depolamayı Sıfırla", | ||||
| 	"Response AutoCopy to Clipboard": "Yanıtı Panoya Otomatik Kopyala", | ||||
| 	"Role": "Rol", | ||||
| 	"Rosé Pine": "Rosé Pine", | ||||
| 	"Rosé Pine Dawn": "Rosé Pine Dawn", | ||||
| 	"Save": "Kaydet", | ||||
| 	"Save & Create": "Kaydet ve Oluştur", | ||||
| 	"Save & Submit": "Kaydet ve Gönder", | ||||
| 	"Save & Update": "Kaydet ve Güncelle", | ||||
| 	"Saving chat logs directly to your browser's storage is no longer supported. Please take a moment to download and delete your chat logs by clicking the button below. Don't worry, you can easily re-import your chat logs to the backend through": "Sohbet kayıtlarının doğrudan tarayıcınızın depolama alanına kaydedilmesi artık desteklenmemektedir. Lütfen aşağıdaki butona tıklayarak sohbet kayıtlarınızı indirmek ve silmek için bir dakikanızı ayırın. Endişelenmeyin, sohbet günlüklerinizi arkayüze kolayca yeniden aktarabilirsiniz:", | ||||
| 	"Scan": "Tarama", | ||||
| 	"Scan complete!": "Tarama tamamlandı!", | ||||
| 	"Scan for documents from {{path}}": "{{path}} dizininden belgeleri tarayın", | ||||
| 	"Search": "Ara", | ||||
| 	"Search Documents": "Belgeleri Ara", | ||||
| 	"Search Prompts": "Prompt Ara", | ||||
| 	"See readme.md for instructions": "Yönergeler için readme.md dosyasına bakın", | ||||
| 	"See what's new": "Yeniliklere göz atın", | ||||
| 	"Seed": "Seed", | ||||
| 	"Select a mode": "Bir mod seç", | ||||
| 	"Select a model": "Bir model seç", | ||||
| 	"Select an Ollama instance": "Bir Ollama örneği seçin", | ||||
| 	"Send a Message": "Bir Mesaj Gönder", | ||||
| 	"Send message": "Mesaj gönder", | ||||
| 	"Server connection verified": "Sunucu bağlantısı doğrulandı", | ||||
| 	"Set as default": "Varsayılan olarak ayarla", | ||||
| 	"Set Default Model": "Varsayılan Modeli Ayarla", | ||||
| 	"Set Image Size": "Görüntü Boyutunu Ayarla", | ||||
| 	"Set Steps": "Adımları Ayarla", | ||||
| 	"Set Title Auto-Generation Model": "Otomatik Başlık Oluşturma Modelini Ayarla", | ||||
| 	"Set Voice": "Ses Ayarla", | ||||
| 	"Settings": "Ayarlar", | ||||
| 	"Settings saved successfully!": "Ayarlar başarıyla kaydedildi!", | ||||
| 	"Share to OpenWebUI Community": "OpenWebUI Topluluğu ile Paylaş", | ||||
| 	"short-summary": "kısa-özet", | ||||
| 	"Show": "Göster", | ||||
| 	"Show Additional Params": "Ek Parametreleri Göster", | ||||
| 	"Show shortcuts": "Kısayolları göster", | ||||
| 	"sidebar": "kenar çubuğu", | ||||
| 	"Sign in": "Oturum aç", | ||||
| 	"Sign Out": "Çıkış Yap", | ||||
| 	"Sign up": "Kaydol", | ||||
| 	"Speech recognition error: {{error}}": "Konuşma tanıma hatası: {{error}}", | ||||
| 	"Speech-to-Text Engine": "Konuşmadan Metne Motoru", | ||||
| 	"SpeechRecognition API is not supported in this browser.": "SpeechRecognition API bu tarayıcıda desteklenmiyor.", | ||||
| 	"Stop Sequence": "Diziyi Durdur", | ||||
| 	"STT Settings": "STT Ayarları", | ||||
| 	"Submit": "Gönder", | ||||
| 	"Success": "Başarılı", | ||||
| 	"Successfully updated.": "Başarıyla güncellendi.", | ||||
| 	"Sync All": "Tümünü Senkronize Et", | ||||
| 	"System": "Sistem", | ||||
| 	"System Prompt": "Sistem Promptu", | ||||
| 	"Tags": "Etiketler", | ||||
| 	"Temperature": "Temperature", | ||||
| 	"Template": "Şablon", | ||||
| 	"Text Completion": "Metin Tamamlama", | ||||
| 	"Text-to-Speech Engine": "Metinden Sese Motoru", | ||||
| 	"Tfs Z": "Tfs Z", | ||||
| 	"Theme": "Tema", | ||||
| 	"This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Bu, önemli konuşmalarınızın güvenli bir şekilde arkayüz veritabanınıza kaydedildiğini garantiler. Teşekkür ederiz!", | ||||
| 	"This setting does not sync across browsers or devices.": "Bu ayar tarayıcılar veya cihazlar arasında senkronize edilmez.", | ||||
| 	"Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "İpucu: Her değiştirmeden sonra sohbet girişinde tab tuşuna basarak birden fazla değişken yuvasını art arda güncelleyin.", | ||||
| 	"Title": "Başlık", | ||||
| 	"Title Auto-Generation": "Otomatik Başlık Oluşturma", | ||||
| 	"Title Generation Prompt": "Başlık Oluşturma Promptu", | ||||
| 	"to": "için", | ||||
| 	"To access the available model names for downloading,": "İndirilebilir mevcut model adlarına erişmek için,", | ||||
| 	"To access the GGUF models available for downloading,": "İndirilebilir mevcut GGUF modellerine erişmek için,", | ||||
| 	"to chat input.": "sohbet girişine.", | ||||
| 	"Toggle settings": "Ayarları Aç/Kapat", | ||||
| 	"Toggle sidebar": "Kenar Çubuğunu Aç/Kapat", | ||||
| 	"Top K": "Top K", | ||||
| 	"Top P": "Top P", | ||||
| 	"Trouble accessing Ollama?": "Ollama'ya erişmede sorun mu yaşıyorsunuz?", | ||||
| 	"TTS Settings": "TTS Ayarları", | ||||
| 	"Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (Download) URL'sini Yazın", | ||||
| 	"Uh-oh! There was an issue connecting to {{provider}}.": "Ah! {{provider}}'a bağlanırken bir sorun oluştu.", | ||||
| 	"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Bilinmeyen Dosya Türü '{{file_type}}', ancak düz metin olarak kabul ediliyor ve işleniyor", | ||||
| 	"Update password": "Parolayı Güncelle", | ||||
| 	"Upload a GGUF model": "Bir GGUF modeli yükle", | ||||
| 	"Upload files": "Dosyaları Yükle", | ||||
| 	"Upload Progress": "Yükleme İlerlemesi", | ||||
| 	"URL Mode": "URL Modu", | ||||
| 	"Use '#' in the prompt input to load and select your documents.": "Belgelerinizi yüklemek ve seçmek için promptda '#' kullanın.", | ||||
| 	"Use Gravatar": "Gravatar Kullan", | ||||
| 	"user": "kullanıcı", | ||||
| 	"User Permissions": "Kullanıcı İzinleri", | ||||
| 	"Users": "Kullanıcılar", | ||||
| 	"Utilize": "Kullan", | ||||
| 	"Valid time units:": "Geçerli zaman birimleri:", | ||||
| 	"variable": "değişken", | ||||
| 	"variable to have them replaced with clipboard content.": "panodaki içerikle değiştirilmesi için değişken.", | ||||
| 	"Version": "Sürüm", | ||||
| 	"Web": "Web", | ||||
| 	"WebUI Add-ons": "WebUI Eklentileri", | ||||
| 	"WebUI Settings": "WebUI Ayarları", | ||||
| 	"WebUI will make requests to": "WebUI, isteklerde bulunacak:", | ||||
| 	"What’s New in": "Yenilikler:", | ||||
| 	"When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Geçmiş kapatıldığında, bu tarayıcıdaki yeni sohbetler hiçbir cihazınızdaki geçmişinizde görünmez.", | ||||
| 	"Whisper (Local)": "Whisper (Yerel)", | ||||
| 	"Write a prompt suggestion (e.g. Who are you?)": "Bir prompt önerisi yazın (örn. Sen kimsin?)", | ||||
| 	"Write a summary in 50 words that summarizes [topic or keyword].": "[Konuyu veya anahtar kelimeyi] özetleyen 50 kelimelik bir özet yazın.", | ||||
| 	"You": "Siz", | ||||
| 	"You're a helpful assistant.": "Sen yardımcı bir asistansın.", | ||||
| 	"You're now logged in.": "Şimdi oturum açtınız." | ||||
| } | ||||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Видалено {tagName}", | ||||
| 	"Description": "Опис", | ||||
| 	"Desktop Notifications": "Сповіщення", | ||||
| 	"Notifications": "Сповіщення", | ||||
| 	"Disabled": "Вимкнено", | ||||
| 	"Discover a modelfile": "Знайти файл моделі", | ||||
| 	"Discover a prompt": "Знайти промт", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "Đã xóa {tagName}", | ||||
| 	"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", | ||||
| 	"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", | ||||
|  |  | |||
|  | @ -100,7 +100,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "已删除{{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "已删除{tagName}", | ||||
| 	"Description": "描述", | ||||
| 	"Desktop Notifications": "桌面通知", | ||||
| 	"Notifications": "桌面通知", | ||||
| 	"Disabled": "禁用", | ||||
| 	"Discover a modelfile": "探索模型文件", | ||||
| 	"Discover a prompt": "探索提示词", | ||||
|  |  | |||
|  | @ -101,7 +101,7 @@ | |||
| 	"Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", | ||||
| 	"Deleted {tagName}": "已刪除 {tagName}", | ||||
| 	"Description": "描述", | ||||
| 	"Desktop Notifications": "桌面通知", | ||||
| 	"Notifications": "桌面通知", | ||||
| 	"Disabled": "已停用", | ||||
| 	"Discover a modelfile": "發現新 Modelfile", | ||||
| 	"Discover a prompt": "發現新提示詞", | ||||
|  |  | |||
|  | @ -111,6 +111,82 @@ export const getGravatarURL = (email) => { | |||
| 	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) => { | ||||
| 	if (!navigator.clipboard) { | ||||
| 		const textArea = document.createElement('textarea'); | ||||
|  |  | |||
|  | @ -4,6 +4,8 @@ | |||
| 	import { goto } from '$app/navigation'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 
 | ||||
| 	import dayjs from 'dayjs'; | ||||
| 
 | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 
 | ||||
| 	import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; | ||||
|  | @ -16,6 +18,7 @@ | |||
| 	let loaded = false; | ||||
| 	let users = []; | ||||
| 
 | ||||
| 	let search = ''; | ||||
| 	let selectedUser = null; | ||||
| 
 | ||||
| 	let showSettingsModal = false; | ||||
|  | @ -80,20 +83,15 @@ | |||
| 
 | ||||
| <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} | ||||
| 		<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=" flex flex-col justify-center"> | ||||
| 						<div class=" px-5 pt-3"> | ||||
| 							<div class=" flex justify-between items-center"> | ||||
| 							<div class="flex items-center text-2xl font-semibold"> | ||||
| 								{$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="flex items-center text-2xl font-semibold">Dashboard</div> | ||||
| 								<div> | ||||
| 									<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" | ||||
|  | @ -119,32 +117,64 @@ | |||
| 									</button> | ||||
| 								</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> | ||||
| 
 | ||||
| 						<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"> | ||||
| 								<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto"> | ||||
| 									<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> | ||||
| 											<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('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> | ||||
| 									</thead> | ||||
| 									<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"> | ||||
| 												<td class="px-3 py-2 min-w-[7rem] w-28"> | ||||
| 													<button | ||||
| 														class=" flex items-center gap-2 text-xs px-3 py-0.5 rounded-lg {user.role === | ||||
| 															'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 === | ||||
| 															'pending' && 'text-gray-600 dark:text-gray-200 bg-gray-200/30'}" | ||||
| 														on:click={() => { | ||||
|  | @ -180,7 +210,11 @@ | |||
| 												<td class=" px-3 py-2"> {user.email} </td> | ||||
| 
 | ||||
| 												<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 | ||||
| 															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 () => { | ||||
|  | @ -232,6 +266,11 @@ | |||
| 									</tbody> | ||||
| 								</table> | ||||
| 							</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> | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
| 	import { WEBUI_NAME, config, user } from '$lib/stores'; | ||||
| 	import { onMount, getContext } from 'svelte'; | ||||
| 	import { toast } from 'svelte-sonner'; | ||||
| 	import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
|  | @ -36,10 +37,12 @@ | |||
| 	}; | ||||
| 
 | ||||
| 	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); | ||||
| 				return null; | ||||
| 		}); | ||||
| 			} | ||||
| 		); | ||||
| 
 | ||||
| 		await setSessionUser(sessionUser); | ||||
| 	}; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 lainedfles
						lainedfles