diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..d7e7167586afa2a49b59fb3e56602e771d6c2cf2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.github +.DS_Store +docs +kubernetes +node_modules +/.svelte-kit +/package +.env +.env.* +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +__pycache__ +.idea +venv +_old +uploads +.ipynb_checkpoints +**/*.db +_test \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..c38bf88bfb96e3a4f87e9f920096a93379a1e677 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# Ollama URL for the backend to connect +# The path '/ollama' will be redirected to the specified backend URL +OLLAMA_BASE_URL='http://localhost:11434' + +OPENAI_API_BASE_URL='' +OPENAI_API_KEY='' + +# AUTOMATIC1111_BASE_URL="http://localhost:7860" + +# DO NOT TRACK +SCARF_NO_ANALYTICS=true +DO_NOT_TRACK=true +ANONYMIZED_TELEMETRY=false \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..38972655faff07d2cc0383044bbf9f43b22c2248 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000000000000000000000000000000000000..cea095ea1aa19e444dc44264c138c95a82dfa04e --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,31 @@ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'plugin:cypress/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ] +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..f4382913f4ec5654c4d8c8ba17b25b64afc603d0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +*.ttf filter=lfs diff=lfs merge=lfs -text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..ef274fa9184f884a8f4af07f0c246d0592eafe42 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: tjbck diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000000000000000000000000000000000000..10958583fcc4d0e2d53dafcfa685e216b707e8ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,63 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +# Bug Report + +## Description + +**Bug Summary:** +[Provide a brief but clear summary of the bug] + +**Steps to Reproduce:** +[Outline the steps to reproduce the bug. Be as detailed as possible.] + +**Expected Behavior:** +[Describe what you expected to happen.] + +**Actual Behavior:** +[Describe what actually happened.] + +## Environment + +- **Open WebUI Version:** [e.g., 0.1.120] +- **Ollama (if applicable):** [e.g., 0.1.30, 0.1.32-rc1] + +- **Operating System:** [e.g., Windows 10, macOS Big Sur, Ubuntu 20.04] +- **Browser (if applicable):** [e.g., Chrome 100.0, Firefox 98.0] + +## Reproduction Details + +**Confirmation:** + +- [ ] I have read and followed all the instructions provided in the README.md. +- [ ] I am on the latest version of both Open WebUI and Ollama. +- [ ] I have included the browser console logs. +- [ ] I have included the Docker container logs. + +## Logs and Screenshots + +**Browser Console Logs:** +[Include relevant browser console logs, if applicable] + +**Docker Container Logs:** +[Include relevant Docker container logs, if applicable] + +**Screenshots (if applicable):** +[Attach any relevant screenshots to help illustrate the issue] + +## Installation Method + +[Describe the method you used to install the project, e.g., manual installation, Docker, package manager, etc.] + +## Additional Information + +[Include any additional details that may help in understanding and reproducing the issue. This could include specific configurations, error messages, or anything else relevant to the bug.] + +## Note + +If the bug report is incomplete or does not follow the provided instructions, it may not be addressed. Please ensure that you have followed the steps outlined in the README.md and troubleshooting.md documents, and provide all necessary information for us to reproduce and address the issue. Thank you! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000000000000000000000000000000000000..2f28cead03959213f1f1abd3f8a3a76f280bc1a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000000000000000000000000000000..166376305aa5ae7b8d3532b121bd44bd5230b757 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: '/backend' + schedule: + interval: weekly + target-branch: 'dev' + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + # Check for updates to GitHub Actions every week + interval: 'weekly' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000000000000000000000000000000000..2a45c2c16e41cccaea1dd2e67f7568fef1206959 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,72 @@ +# Pull Request Checklist + +### Note to first-time contributors: Please open a discussion post in [Discussions](https://github.com/open-webui/open-webui/discussions) and describe your changes before submitting a pull request. + +**Before submitting, make sure you've checked the following:** + +- [ ] **Target branch:** Please verify that the pull request targets the `dev` branch. +- [ ] **Description:** Provide a concise description of the changes made in this pull request. +- [ ] **Changelog:** Ensure a changelog entry following the format of [Keep a Changelog](https://keepachangelog.com/) is added at the bottom of the PR description. +- [ ] **Documentation:** Have you updated relevant documentation [Open WebUI Docs](https://github.com/open-webui/docs), or other documentation sources? +- [ ] **Dependencies:** Are there any new dependencies? Have you updated the dependency versions in the documentation? +- [ ] **Testing:** Have you written and run sufficient tests for validating the changes? +- [ ] **Code review:** Have you performed a self-review of your code, addressing any coding standard issues and ensuring adherence to the project's coding standards? +- [ ] **Prefix:** To cleary categorize this pull request, prefix the pull request title, using one of the following: + - **BREAKING CHANGE**: Significant changes that may affect compatibility + - **build**: Changes that affect the build system or external dependencies + - **ci**: Changes to our continuous integration processes or workflows + - **chore**: Refactor, cleanup, or other non-functional code changes + - **docs**: Documentation update or addition + - **feat**: Introduces a new feature or enhancement to the codebase + - **fix**: Bug fix or error correction + - **i18n**: Internationalization or localization changes + - **perf**: Performance improvement + - **refactor**: Code restructuring for better maintainability, readability, or scalability + - **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.) + - **test**: Adding missing tests or correcting existing tests + - **WIP**: Work in progress, a temporary label for incomplete or ongoing work + +# Changelog Entry + +### Description + +- [Concisely describe the changes made in this pull request, including any relevant motivation and impact (e.g., fixing a bug, adding a feature, or improving performance)] + +### Added + +- [List any new features, functionalities, or additions] + +### Changed + +- [List any changes, updates, refactorings, or optimizations] + +### Deprecated + +- [List any deprecated functionality or features that have been removed] + +### Removed + +- [List any removed features, files, or functionalities] + +### Fixed + +- [List any fixes, corrections, or bug fixes] + +### Security + +- [List any new or updated security-related changes, including vulnerability fixes] + +### Breaking Changes + +- **BREAKING CHANGE**: [List any breaking changes affecting compatibility or functionality] + +--- + +### Additional Information + +- [Insert any additional context, notes, or explanations for the changes] + - [Reference any related issues, commits, or other relevant information] + +### Screenshots or Videos + +- [Attach any relevant screenshots or videos demonstrating the changes] diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 0000000000000000000000000000000000000000..cae363f42a93e1b76b521a76a8bcdb19e97e76e3 --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + push: + branches: + - main # or whatever branch you want to use + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for changes in package.json + run: | + git diff --cached --diff-filter=d package.json || { + echo "No changes to package.json" + exit 1 + } + + - name: Get version number from package.json + id: get_version + run: | + VERSION=$(jq -r '.version' package.json) + echo "::set-output name=version::$VERSION" + + - name: Extract latest CHANGELOG entry + id: changelog + run: | + CHANGELOG_CONTENT=$(awk 'BEGIN {print_section=0;} /^## \[/ {if (print_section == 0) {print_section=1;} else {exit;}} print_section {print;}' CHANGELOG.md) + CHANGELOG_ESCAPED=$(echo "$CHANGELOG_CONTENT" | sed ':a;N;$!ba;s/\n/%0A/g') + echo "Extracted latest release notes from CHANGELOG.md:" + echo -e "$CHANGELOG_CONTENT" + echo "::set-output name=content::$CHANGELOG_ESCAPED" + + - name: Create GitHub release + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const changelog = `${{ steps.changelog.outputs.content }}`; + const release = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: `v${{ steps.get_version.outputs.version }}`, + name: `v${{ steps.get_version.outputs.version }}`, + body: changelog, + }) + console.log(`Created release ${release.data.html_url}`) + + - name: Upload package to GitHub release + uses: actions/upload-artifact@v4 + with: + name: package + 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 }}', + }) diff --git a/.github/workflows/deploy-to-hf-spaces.yml b/.github/workflows/deploy-to-hf-spaces.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ebb2bfdc8c015c5637ec6806606408e7d389df4 --- /dev/null +++ b/.github/workflows/deploy-to-hf-spaces.yml @@ -0,0 +1,65 @@ +name: Deploy to HuggingFace Spaces + +on: + push: + branches: + - hf-space + workflow_dispatch: + +jobs: + check-secret: + runs-on: ubuntu-latest + outputs: + token-set: ${{ steps.check-key.outputs.defined }} + steps: + - id: check-key + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + if: "${{ env.HF_TOKEN != '' }}" + run: echo "defined=true" >> $GITHUB_OUTPUT + + deploy: + runs-on: ubuntu-latest + needs: [check-secret] + if: needs.check-secret.outputs.token-set == 'true' + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + HF_USERNAME: ${{ secrets.HF_USERNAME }} + HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Remove git history + run: rm -rf .git + + - name: Prepend YAML front matter to README.md + run: | + cat <<EOF >README.md + --- + title: Open WebUI + emoji: 🐳 + colorFrom: purple + colorTo: gray + sdk: docker + app_port: 8080 + hf_oauth: true + hf_oauth_scopes: + - email + --- + $(cat README.md) + EOF + + - name: Configure git + run: | + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions[bot]" + + - name: Set up Git and push to Space + run: | + git init --initial-branch=main + git lfs track "*.ttf" + rm demo.gif + git add . + git commit -m "GitHub deploy: ${{ github.sha }}" + git push --force https://${HF_USERNAME}:${HF_TOKEN}@huggingface.co/spaces/${HF_USERNAME}/${HF_SPACE_NAME} main diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0e62be3d9036b6f6f497bd2c9b7ad12ad89e6752 --- /dev/null +++ b/.github/workflows/docker-build.yaml @@ -0,0 +1,478 @@ +name: Create and publish Docker images with specific build args + +on: + workflow_dispatch: + push: + branches: + - main + - dev + tags: + - v* + +env: + REGISTRY: ghcr.io + +jobs: + build-main-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (default latest tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-${{ matrix.platform }}- + latest=false + + - name: Build Docker image (latest) + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + build-args: | + BUILD_HASH=${{ github.sha }} + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-main-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + build-cuda-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (cuda tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + suffix=-cuda,onlatest=true + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-cuda-${{ matrix.platform }}- + latest=false + + - name: Build Docker image (cuda) + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + build-args: | + BUILD_HASH=${{ github.sha }} + USE_CUDA=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-cuda-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + build-ollama-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + strategy: + fail-fast: false + matrix: + platform: + - linux/amd64 + - linux/arm64 + + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Prepare + run: | + platform=${{ matrix.platform }} + echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (ollama tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + suffix=-ollama,onlatest=true + + - name: Extract metadata for Docker cache + id: cache-meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + ${{ github.ref_type == 'tag' && 'type=raw,value=main' || '' }} + flavor: | + prefix=cache-ollama-${{ matrix.platform }}- + latest=false + + - name: Build Docker image (ollama) + uses: docker/build-push-action@v5 + id: build + with: + context: . + push: true + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta.outputs.labels }} + outputs: type=image,name=${{ env.FULL_IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true + cache-from: type=registry,ref=${{ steps.cache-meta.outputs.tags }} + cache-to: type=registry,ref=${{ steps.cache-meta.outputs.tags }},mode=max + build-args: | + BUILD_HASH=${{ github.sha }} + USE_OLLAMA=true + + - name: Export digest + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest + uses: actions/upload-artifact@v4 + with: + name: digests-ollama-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-main-images: + runs-on: ubuntu-latest + needs: [ build-main-image ] + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-main-* + path: /tmp/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (default latest tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + + merge-cuda-images: + runs-on: ubuntu-latest + needs: [ build-cuda-image ] + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-cuda-* + path: /tmp/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (default latest tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=cuda + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + suffix=-cuda,onlatest=true + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} + + merge-ollama-images: + runs-on: ubuntu-latest + needs: [ build-ollama-image ] + steps: + # GitHub Packages requires the entire repository name to be in lowercase + # although the repository owner has a lowercase username, this prevents some people from running actions after forking + - name: Set repository and image name to lowercase + run: | + echo "IMAGE_NAME=${IMAGE_NAME,,}" >>${GITHUB_ENV} + echo "FULL_IMAGE_NAME=ghcr.io/${IMAGE_NAME,,}" >>${GITHUB_ENV} + env: + IMAGE_NAME: '${{ github.repository }}' + + - name: Download digests + uses: actions/download-artifact@v4 + with: + pattern: digests-ollama-* + path: /tmp/digests + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Docker images (default ollama tag) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.FULL_IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=git- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,enable=${{ github.ref == 'refs/heads/main' }},prefix=,suffix=,value=ollama + flavor: | + latest=${{ github.ref == 'refs/heads/main' }} + suffix=-ollama,onlatest=true + + - name: Create manifest list and push + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf '${{ env.FULL_IMAGE_NAME }}@sha256:%s ' *) + + - name: Inspect image + run: | + docker buildx imagetools inspect ${{ env.FULL_IMAGE_NAME }}:${{ steps.meta.outputs.version }} diff --git a/.github/workflows/format-backend.yaml b/.github/workflows/format-backend.yaml new file mode 100644 index 0000000000000000000000000000000000000000..2e980de41f2d2d4118d8d3556113a08a6dfdb00a --- /dev/null +++ b/.github/workflows/format-backend.yaml @@ -0,0 +1,39 @@ +name: Python CI + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + build: + name: 'Format Backend' + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: [3.11] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install black + + - name: Format backend + run: npm run format:backend + + - name: Check for changes after format + run: git diff --exit-code diff --git a/.github/workflows/format-build-frontend.yaml b/.github/workflows/format-build-frontend.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9ee57f475a176a7c5b50e7227793a9d41ef3efdc --- /dev/null +++ b/.github/workflows/format-build-frontend.yaml @@ -0,0 +1,57 @@ +name: Frontend Build + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + build: + name: 'Format & Build Frontend' + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' # Or specify any other version you want to use + + - name: Install Dependencies + run: npm install + + - name: Format Frontend + run: npm run format + + - name: Run i18next + run: npm run i18n:parse + + - name: Check for Changes After Format + run: git diff --exit-code + + - name: Build Frontend + run: npm run build + + test-frontend: + name: 'Frontend Unit Tests' + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Dependencies + run: npm ci + + - name: Run vitest + run: npm run test:frontend diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000000000000000000000000000000000000..639ea789fb12cc6d52d57c81f747e8ffed212de3 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,243 @@ +name: Integration Test + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + +jobs: + cypress-run: + name: Run Cypress Integration Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Build and run Compose Stack + run: | + docker compose \ + --file docker-compose.yaml \ + --file docker-compose.api.yaml \ + --file docker-compose.a1111-test.yaml \ + up --detach --build + + - name: Wait for Ollama to be up + timeout-minutes: 5 + run: | + until curl --output /dev/null --silent --fail http://localhost:11434; do + printf '.' + sleep 1 + done + echo "Service is up!" + + - name: Delete Docker build cache + run: | + docker builder prune --all --force + + - name: Preload Ollama model + run: | + docker exec ollama ollama pull qwen:0.5b-chat-v1.5-q2_K + + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + browser: chrome + wait-on: 'http://localhost:3000' + config: baseUrl=http://localhost:3000 + + - uses: actions/upload-artifact@v4 + if: always() + name: Upload Cypress videos + with: + name: cypress-videos + path: cypress/videos + if-no-files-found: ignore + + - name: Extract Compose logs + if: always() + run: | + docker compose logs > compose-logs.txt + + - uses: actions/upload-artifact@v4 + if: always() + name: Upload Compose logs + with: + name: compose-logs + path: compose-logs.txt + if-no-files-found: ignore + + # pytest: + # name: Run Backend Tests + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + + # - name: Set up Python + # uses: actions/setup-python@v4 + # with: + # python-version: ${{ matrix.python-version }} + + # - name: Install dependencies + # run: | + # python -m pip install --upgrade pip + # pip install -r backend/requirements.txt + + # - name: pytest run + # run: | + # ls -al + # cd backend + # PYTHONPATH=. pytest . -o log_cli=true -o log_cli_level=INFO + + migration_test: + name: Run Migration Tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + # mysql: + # image: mysql + # env: + # MYSQL_ROOT_PASSWORD: mysql + # MYSQL_DATABASE: mysql + # options: >- + # --health-cmd "mysqladmin ping -h localhost" + # --health-interval 10s + # --health-timeout 5s + # --health-retries 5 + # ports: + # - 3306:3306 + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Set up uv + uses: yezz123/setup-uv@v4 + with: + uv-venv: venv + + - name: Activate virtualenv + run: | + . venv/bin/activate + echo PATH=$PATH >> $GITHUB_ENV + + - name: Install dependencies + run: | + uv pip install -r backend/requirements.txt + + - name: Test backend with SQLite + id: sqlite + env: + WEBUI_SECRET_KEY: secret-key + GLOBAL_LOG_LEVEL: debug + run: | + cd backend + uvicorn main:app --port "8080" --forwarded-allow-ips '*' & + UVICORN_PID=$! + # Wait up to 40 seconds for the server to start + for i in {1..40}; do + curl -s http://localhost:8080/api/config > /dev/null && break + sleep 1 + if [ $i -eq 40 ]; then + echo "Server failed to start" + kill -9 $UVICORN_PID + exit 1 + fi + done + # Check that the server is still running after 5 seconds + sleep 5 + if ! kill -0 $UVICORN_PID; then + echo "Server has stopped" + exit 1 + fi + + - name: Test backend with Postgres + if: success() || steps.sqlite.conclusion == 'failure' + env: + WEBUI_SECRET_KEY: secret-key + GLOBAL_LOG_LEVEL: debug + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres + run: | + cd backend + uvicorn main:app --port "8081" --forwarded-allow-ips '*' & + UVICORN_PID=$! + # Wait up to 20 seconds for the server to start + for i in {1..20}; do + curl -s http://localhost:8081/api/config > /dev/null && break + sleep 1 + if [ $i -eq 20 ]; then + echo "Server failed to start" + kill -9 $UVICORN_PID + exit 1 + fi + done + # Check that the server is still running after 5 seconds + sleep 5 + if ! kill -0 $UVICORN_PID; then + echo "Server has stopped" + exit 1 + fi + + # Check that service will reconnect to postgres when connection will be closed + status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) + if [[ "$status_code" -ne 200 ]] ; then + echo "Server has failed before postgres reconnect check" + exit 1 + fi + + echo "Terminating all connections to postgres..." + python -c "import os, psycopg2 as pg2; \ + conn = pg2.connect(dsn=os.environ['DATABASE_URL'].replace('+pool', '')); \ + cur = conn.cursor(); \ + cur.execute('SELECT pg_terminate_backend(psa.pid) FROM pg_stat_activity psa WHERE datname = current_database() AND pid <> pg_backend_pid();')" + + status_code=$(curl --write-out %{http_code} -s --output /dev/null http://localhost:8081/health/db) + if [[ "$status_code" -ne 200 ]] ; then + echo "Server has not reconnected to postgres after connection was closed: returned status $status_code" + exit 1 + fi + +# - name: Test backend with MySQL +# if: success() || steps.sqlite.conclusion == 'failure' || steps.postgres.conclusion == 'failure' +# env: +# WEBUI_SECRET_KEY: secret-key +# GLOBAL_LOG_LEVEL: debug +# DATABASE_URL: mysql://root:mysql@localhost:3306/mysql +# run: | +# cd backend +# uvicorn main:app --port "8083" --forwarded-allow-ips '*' & +# UVICORN_PID=$! +# # Wait up to 20 seconds for the server to start +# for i in {1..20}; do +# curl -s http://localhost:8083/api/config > /dev/null && break +# sleep 1 +# if [ $i -eq 20 ]; then +# echo "Server failed to start" +# kill -9 $UVICORN_PID +# exit 1 +# fi +# done +# # Check that the server is still running after 5 seconds +# sleep 5 +# if ! kill -0 $UVICORN_PID; then +# echo "Server has stopped" +# exit 1 +# fi diff --git a/.github/workflows/lint-backend.disabled b/.github/workflows/lint-backend.disabled new file mode 100644 index 0000000000000000000000000000000000000000..d220031cc352bb5d2456b89b81ce33be26a09fcf --- /dev/null +++ b/.github/workflows/lint-backend.disabled @@ -0,0 +1,27 @@ +name: Python CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Lint Backend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + strategy: + matrix: + node-version: + - latest + steps: + - uses: actions/checkout@v4 + - name: Use Python + uses: actions/setup-python@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + - name: Lint backend + run: bun run lint:backend diff --git a/.github/workflows/lint-frontend.disabled b/.github/workflows/lint-frontend.disabled new file mode 100644 index 0000000000000000000000000000000000000000..2c1cd3c5a574989def4319d07405f0bb621d74df --- /dev/null +++ b/.github/workflows/lint-frontend.disabled @@ -0,0 +1,21 @@ +name: Bun CI +on: + push: + branches: ['main'] + pull_request: +jobs: + build: + name: 'Lint Frontend' + env: + PUBLIC_API_BASE_URL: '' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Bun + uses: oven-sh/setup-bun@v1 + - run: bun --version + - name: Install frontend dependencies + run: bun install --frozen-lockfile + - run: bun run lint:frontend + - run: bun run lint:types + if: success() || failure() \ No newline at end of file diff --git a/.github/workflows/release-pypi.yml b/.github/workflows/release-pypi.yml new file mode 100644 index 0000000000000000000000000000000000000000..70a19c64a616d4e55bdc2042c3f19ba29cab7c87 --- /dev/null +++ b/.github/workflows/release-pypi.yml @@ -0,0 +1,31 @@ +name: Release to PyPI + +on: + push: + branches: + - main # or whatever branch you want to use + +jobs: + release: + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/open-webui + permissions: + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Build + run: | + python -m pip install --upgrade pip + pip install build + python -m build . + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/sync-hf-spaces-with-dev.yml b/.github/workflows/sync-hf-spaces-with-dev.yml new file mode 100644 index 0000000000000000000000000000000000000000..62354c90dfc0accf813690210e45e33f5c717871 --- /dev/null +++ b/.github/workflows/sync-hf-spaces-with-dev.yml @@ -0,0 +1,30 @@ +name: Sync hf-spaces with dev + +on: + push: + branches: + - dev + - hf-spaces + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Sync with dev + run: | + git checkout dev + git fetch origin + git checkout hf-space + git pull + git merge origin/dev + git push origin hf-space diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..32271f8087e213e83089162bd0b1ec99c60d45ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,309 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Pyodide distribution +static/pyodide/* +!static/pyodide/pyodide-lock.json + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# cypress artifacts +cypress/videos +cypress/screenshots +.vscode/settings.json diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..b6f27f135954640c8cc5bfd7b8c9922ca6eb2aad --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000000000000000000000000000000..82c49125724030d1010bb123df7dae758236ff11 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,316 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock + +kubernetes/ + +# Copy of .gitignore +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# cypress artifacts +cypress/videos +cypress/screenshots + + + +/static/* \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000000000000000000000000000000000..a77fddea90975988d17a7e8b2f61720939a947f5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..a3bceff25832c8336ff54e989922f3edf93b3801 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,744 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.10] - 2024-07-17 + +### Fixed + +- **🔄 Improved File Upload**: Addressed the issue where file uploads lacked animation. +- **💬 Chat Continuity**: Fixed a problem where existing chats were not functioning properly in some instances. +- **🗂️ Chat File Reset**: Resolved the issue of chat files not resetting for new conversations, now ensuring a clean slate for each chat session. +- **📁 Document Workspace Uploads**: Corrected the handling of document uploads in the workspace using the Files API. + +## [0.3.9] - 2024-07-17 + +### Added + +- **📁 Files Chat Controls**: We've reverted to the old file handling behavior where uploaded files are always included. You can now manage files directly within the chat controls section, giving you the ability to remove files as needed. +- **🔧 "Action" Function Support**: Introducing a new "Action" function to write custom buttons to the message toolbar. This feature enables more interactive messaging, with documentation coming soon. +- **📜 Citations Handling**: For newly uploaded files in documents workspace, citations will now display the actual filename. Additionally, you can click on these filenames to open the file in a new tab for easier access. +- **🛠️ Event Emitter and Call Updates**: Enhanced 'event_emitter' to allow message replacement and 'event_call' to support text input for Tools and Functions. Detailed documentation will be provided shortly. +- **🎨 Styling Refactor**: Various styling updates for a cleaner and more cohesive user interface. +- **🌐 Enhanced Translations**: Improved translations for Catalan, Ukrainian, and Brazilian Portuguese. + +### Fixed + +- **🔧 Chat Controls Priority**: Resolved an issue where Chat Controls values were being overridden by model information parameters. The priority is now Chat Controls, followed by Global Settings, then Model Settings. +- **🪲 Debug Logs**: Fixed an issue where debug logs were not being logged properly. +- **🔑 Automatic1111 Auth Key**: The auth key for Automatic1111 is no longer required. +- **📝 Title Generation**: Ensured that the title generation runs only once, even when multiple models are in a chat. +- **✅ Boolean Values in Params**: Added support for boolean values in parameters. +- **🖼️ Files Overlay Styling**: Fixed the styling issue with the files overlay. + +### Changed + +- **⬆️ Dependency Updates** + - Upgraded 'pydantic' from version 2.7.1 to 2.8.2. + - Upgraded 'sqlalchemy' from version 2.0.30 to 2.0.31. + - Upgraded 'unstructured' from version 0.14.9 to 0.14.10. + - Upgraded 'chromadb' from version 0.5.3 to 0.5.4. + +## [0.3.8] - 2024-07-09 + +### Added + +- **💬 Chat Controls**: Easily adjust parameters for each chat session, offering more precise control over your interactions. +- **📌 Pinned Chats**: Support for pinned chats, allowing you to keep important conversations easily accessible. +- **📄 Apache Tika Integration**: Added support for using Apache Tika as a document loader, enhancing document processing capabilities. +- **🛠️ Custom Environment for OpenID Claims**: Allows setting custom claims for OpenID, providing more flexibility in user authentication. +- **🔧 Enhanced Tools & Functions API**: Introduced 'event_emitter' and 'event_call', now you can also add citations for better documentation and tracking. Detailed documentation will be provided on our documentation website. +- **↔️ Sideways Scrolling in Settings**: Settings tabs container now supports horizontal scrolling for easier navigation. +- **🌑 Darker OLED Theme**: Includes a new, darker OLED theme and improved styling for the light theme, enhancing visual appeal. +- **🌐 Language Updates**: Updated translations for Indonesian, German, French, and Catalan languages, expanding accessibility. + +### Fixed + +- **⏰ OpenAI Streaming Timeout**: Resolved issues with OpenAI streaming response using the 'AIOHTTP_CLIENT_TIMEOUT' setting, ensuring reliable performance. +- **💡 User Valves**: Fixed malfunctioning user valves, ensuring proper functionality. +- **🔄 Collapsible Components**: Addressed issues with collapsible components not working, restoring expected behavior. + +### Changed + +- **🗃️ Database Backend**: Switched from Peewee to SQLAlchemy for improved concurrency support, enhancing database performance. +- **⬆️ ChromaDB Update**: Upgraded to version 0.5.3. Ensure your remote ChromaDB instance matches this version. +- **🔤 Primary Font Styling**: Updated primary font to Archivo for better visual consistency. +- **🔄 Font Change for Windows**: Replaced Arimo with Inter font for Windows users, improving readability. +- **🚀 Lazy Loading**: Implemented lazy loading for 'faster_whisper' and 'sentence_transformers' to reduce startup memory usage. +- **📋 Task Generation Payload**: Task generations now include only the "task" field in the body instead of "title". + +## [0.3.7] - 2024-06-29 + +### Added + +- **🌐 Enhanced Internationalization (i18n)**: Newly introduced Indonesian translation, and updated translations for Turkish, Chinese, and Catalan languages to improve user accessibility. + +### Fixed + +- **🕵️♂️ Browser Language Detection**: Corrected the issue where the application was not properly detecting and adapting to the browser's language settings. +- **🔐 OIDC Admin Role Assignment**: Fixed a bug where the admin role was not being assigned to the first user who signed up via OpenID Connect (OIDC). +- **💬 Chat/Completions Endpoint**: Resolved an issue where the chat/completions endpoint was non-functional when the stream option was set to False. +- **🚫 'WEBUI_AUTH' Configuration**: Addressed the problem where setting 'WEBUI_AUTH' to False was not being applied correctly. + +### Changed + +- **📦 Dependency Update**: Upgraded 'authlib' from version 1.3.0 to 1.3.1 to ensure better security and performance enhancements. + +## [0.3.6] - 2024-06-27 + +### Added + +- **✨ "Functions" Feature**: You can now utilize "Functions" like filters (middleware) and pipe (model) functions directly within the WebUI. While largely compatible with Pipelines, these native functions can be executed easily within Open WebUI. Example use cases for filter functions include usage monitoring, real-time translation, moderation, and automemory. For pipe functions, the scope ranges from Cohere and Anthropic integration directly within Open WebUI, enabling "Valves" for per-user OpenAI API key usage, and much more. If you encounter issues, SAFE_MODE has been introduced. +- **📁 Files API**: Compatible with OpenAI, this feature allows for custom Retrieval-Augmented Generation (RAG) in conjunction with the Filter Function. More examples will be shared on our community platform and official documentation website. +- **🛠️ Tool Enhancements**: Tools now support citations and "Valves". Documentation will be available shortly. +- **🔗 Iframe Support via Files API**: Enables rendering HTML directly into your chat interface using functions and tools. Use cases include playing games like DOOM and Snake, displaying a weather applet, and implementing Anthropic "artifacts"-like features. Stay tuned for updates on our community platform and documentation. +- **🔒 Experimental OAuth Support**: New experimental OAuth support. Check our documentation for more details. +- **🖼️ Custom Background Support**: Set a custom background from Settings > Interface to personalize your experience. +- **🔑 AUTOMATIC1111_API_AUTH Support**: Enhanced security for the AUTOMATIC1111 API. +- **🎨 Code Highlight Optimization**: Improved code highlighting features. +- **🎙️ Voice Interruption Feature**: Reintroduced and now toggleable from Settings > Interface. +- **💤 Wakelock API**: Now in use to prevent screen dimming during important tasks. +- **🔐 API Key Privacy**: All API keys are now hidden by default for better security. +- **🔍 New Web Search Provider**: Added jina_search as a new option. +- **🌐 Enhanced Internationalization (i18n)**: Improved Korean translation and updated Chinese and Ukrainian translations. + +### Fixed + +- **🔧 Conversation Mode Issue**: Fixed the issue where Conversation Mode remained active after being removed from settings. +- **📏 Scroll Button Obstruction**: Resolved the issue where the scrollToBottom button container obstructed clicks on buttons beneath it. + +### Changed + +- **⏲️ AIOHTTP_CLIENT_TIMEOUT**: Now set to 'None' by default for improved configuration flexibility. +- **📞 Voice Call Enhancements**: Improved by skipping code blocks and expressions during calls. +- **🚫 Error Message Handling**: Disabled the continuation of operations with error messages. +- **🗂️ Playground Relocation**: Moved the Playground from the workspace to the user menu for better user experience. + +## [0.3.5] - 2024-06-16 + +### Added + +- **📞 Enhanced Voice Call**: Text-to-speech (TTS) callback now operates in real-time for each sentence, reducing latency by not waiting for full completion. +- **👆 Tap to Interrupt**: During a call, you can now stop the assistant from speaking by simply tapping, instead of using voice. This resolves the issue of the speaker's voice being mistakenly registered as input. +- **😊 Emoji Call**: Toggle this feature on from the Settings > Interface, allowing LLMs to express emotions using emojis during voice calls for a more dynamic interaction. +- **🖱️ Quick Archive/Delete**: Use the Shift key + mouseover on the chat list to swiftly archive or delete items. +- **📝 Markdown Support in Model Descriptions**: You can now format model descriptions with markdown, enabling bold text, links, etc. +- **🧠 Editable Memories**: Adds the capability to modify memories. +- **📋 Admin Panel Sorting**: Introduces the ability to sort users/chats within the admin panel. +- **🌑 Dark Mode for Quick Selectors**: Dark mode now available for chat quick selectors (prompts, models, documents). +- **🔧 Advanced Parameters**: Adds 'num_keep' and 'num_batch' to advanced parameters for customization. +- **📅 Dynamic System Prompts**: New variables '{{CURRENT_DATETIME}}', '{{CURRENT_TIME}}', '{{USER_LOCATION}}' added for system prompts. Ensure '{{USER_LOCATION}}' is toggled on from Settings > Interface. +- **🌐 Tavily Web Search**: Includes Tavily as a web search provider option. +- **🖊️ Federated Auth Usernames**: Ability to set user names for federated authentication. +- **🔗 Auto Clean URLs**: When adding connection URLs, trailing slashes are now automatically removed. +- **🌐 Enhanced Translations**: Improved Chinese and Swedish translations. + +### Fixed + +- **⏳ AIOHTTP_CLIENT_TIMEOUT**: Introduced a new environment variable 'AIOHTTP_CLIENT_TIMEOUT' for requests to Ollama lasting longer than 5 minutes. Default is 300 seconds; set to blank ('') for no timeout. +- **❌ Message Delete Freeze**: Resolved an issue where message deletion would sometimes cause the web UI to freeze. + +## [0.3.4] - 2024-06-12 + +### Fixed + +- **🔒 Mixed Content with HTTPS Issue**: Resolved a problem where mixed content (HTTP and HTTPS) was causing security warnings and blocking resources on HTTPS sites. +- **🔍 Web Search Issue**: Addressed the problem where web search functionality was not working correctly. The 'ENABLE_RAG_LOCAL_WEB_FETCH' option has been reintroduced to restore proper web searching capabilities. +- **💾 RAG Template Not Being Saved**: Fixed an issue where the RAG template was not being saved correctly, ensuring your custom templates are now preserved as expected. + +## [0.3.3] - 2024-06-12 + +### Added + +- **🛠️ Native Python Function Calling**: Introducing native Python function calling within Open WebUI. We’ve also included a built-in code editor to seamlessly develop and integrate function code within the 'Tools' workspace. With this, you can significantly enhance your LLM’s capabilities by creating custom RAG pipelines, web search tools, and even agent-like features such as sending Discord messages. +- **🌐 DuckDuckGo Integration**: Added DuckDuckGo as a web search provider, giving you more search options. +- **🌏 Enhanced Translations**: Improved translations for Vietnamese and Chinese languages, making the interface more accessible. + +### Fixed + +- **🔗 Web Search URL Error Handling**: Fixed the issue where a single URL error would disrupt the data loading process in Web Search mode. Now, such errors will be handled gracefully to ensure uninterrupted data loading. +- **🖥️ Frontend Responsiveness**: Resolved the problem where the frontend would stop responding if the backend encounters an error while downloading a model. Improved error handling to maintain frontend stability. +- **🔧 Dependency Issues in pip**: Fixed issues related to pip installations, ensuring all dependencies are correctly managed to prevent installation errors. + +## [0.3.2] - 2024-06-10 + +### Added + +- **🔍 Web Search Query Status**: The web search query will now persist in the results section to aid in easier debugging and tracking of search queries. +- **🌐 New Web Search Provider**: We have added Serply as a new option for web search providers, giving you more choices for your search needs. +- **🌏 Improved Translations**: We've enhanced translations for Chinese and Portuguese. + +### Fixed + +- **🎤 Audio File Upload Issue**: The bug that prevented audio files from being uploaded in chat input has been fixed, ensuring smooth communication. +- **💬 Message Input Handling**: Improved the handling of message inputs by instantly clearing images and text after sending, along with immediate visual indications when a response message is loading, enhancing user feedback. +- **⚙️ Parameter Registration and Validation**: Fixed the issue where parameters were not registering in certain cases and addressed the problem where users were unable to save due to invalid input errors. + +## [0.3.1] - 2024-06-09 + +### Fixed + +- **💬 Chat Functionality**: Resolved the issue where chat functionality was not working for specific models. + +## [0.3.0] - 2024-06-09 + +### Added + +- **📚 Knowledge Support for Models**: Attach documents directly to models from the models workspace, enhancing the information available to each model. +- **🎙️ Hands-Free Voice Call Feature**: Initiate voice calls without needing to use your hands, making interactions more seamless. +- **📹 Video Call Feature**: Enable video calls with supported vision models like Llava and GPT-4o, adding a visual dimension to your communications. +- **🎛️ Enhanced UI for Voice Recording**: Improved user interface for the voice recording feature, making it more intuitive and user-friendly. +- **🌐 External STT Support**: Now support for external Speech-To-Text services, providing more flexibility in choosing your STT provider. +- **⚙️ Unified Settings**: Consolidated settings including document settings under a new admin settings section for easier management. +- **🌑 Dark Mode Splash Screen**: A new splash screen for dark mode, ensuring a consistent and visually appealing experience for dark mode users. +- **📥 Upload Pipeline**: Directly upload pipelines from the admin settings > pipelines section, streamlining the pipeline management process. +- **🌍 Improved Language Support**: Enhanced support for Chinese and Ukrainian languages, better catering to a global user base. + +### Fixed + +- **🛠️ Playground Issue**: Fixed the playground not functioning properly, ensuring a smoother user experience. +- **🔥 Temperature Parameter Issue**: Corrected the issue where the temperature value '0' was not being passed correctly. +- **📝 Prompt Input Clearing**: Resolved prompt input textarea not being cleared right away, ensuring a clean slate for new inputs. +- **✨ Various UI Styling Issues**: Fixed numerous user interface styling problems for a more cohesive look. +- **👥 Active Users Display**: Fixed active users showing active sessions instead of actual users, now reflecting accurate user activity. +- **🌐 Community Platform Compatibility**: The Community Platform is back online and fully compatible with Open WebUI. + +### Changed + +- **📝 RAG Implementation**: Updated the RAG (Retrieval-Augmented Generation) implementation to use a system prompt for context, instead of overriding the user's prompt. +- **🔄 Settings Relocation**: Moved Models, Connections, Audio, and Images settings to the admin settings for better organization. +- **✍️ Improved Title Generation**: Enhanced the default prompt for title generation, yielding better results. +- **🔧 Backend Task Management**: Tasks like title generation and search query generation are now managed on the backend side and controlled only by the admin. +- **🔍 Editable Search Query Prompt**: You can now edit the search query generation prompt, offering more control over how queries are generated. +- **📏 Prompt Length Threshold**: Set the prompt length threshold for search query generation from the admin settings, giving more customization options. +- **📣 Settings Consolidation**: Merged the Banners admin setting with the Interface admin setting for a more streamlined settings area. + +## [0.2.5] - 2024-06-05 + +### Added + +- **👥 Active Users Indicator**: Now you can see how many people are currently active and what they are running. This helps you gauge when performance might slow down due to a high number of users. +- **🗂️ Create Ollama Modelfile**: The option to create a modelfile for Ollama has been reintroduced in the Settings > Models section, making it easier to manage your models. +- **⚙️ Default Model Setting**: Added an option to set the default model from Settings > Interface. This feature is now easily accessible, especially convenient for mobile users as it was previously hidden. +- **🌐 Enhanced Translations**: We've improved the Chinese translations and added support for Turkmen and Norwegian languages to make the interface more accessible globally. + +### Fixed + +- **📱 Mobile View Improvements**: The UI now uses dvh (dynamic viewport height) instead of vh (viewport height), providing a better and more responsive experience for mobile users. + +## [0.2.4] - 2024-06-03 + +### Added + +- **👤 Improved Account Pending Page**: The account pending page now displays admin details by default to avoid confusion. You can disable this feature in the admin settings if needed. +- **🌐 HTTP Proxy Support**: We have enabled the use of the 'http_proxy' environment variable in OpenAI and Ollama API calls, making it easier to configure network settings. +- **❓ Quick Access to Documentation**: You can now easily access Open WebUI documents via a question mark button located at the bottom right corner of the screen (available on larger screens like PCs). +- **🌍 Enhanced Translation**: Improvements have been made to translations. + +### Fixed + +- **🔍 SearxNG Web Search**: Fixed the issue where the SearxNG web search functionality was not working properly. + +## [0.2.3] - 2024-06-03 + +### Added + +- **📁 Export Chat as JSON**: You can now export individual chats as JSON files from the navbar menu by navigating to 'Download > Export Chat'. This makes sharing specific conversations easier. +- **✏️ Edit Titles with Double Click**: Double-click on titles to rename them quickly and efficiently. +- **🧩 Batch Multiple Embeddings**: Introduced 'RAG_EMBEDDING_OPENAI_BATCH_SIZE' to process multiple embeddings in a batch, enhancing performance for large datasets. +- **🌍 Improved Translations**: Enhanced the translation quality across various languages for a better user experience. + +### Fixed + +- **🛠️ Modelfile Migration Script**: Fixed an issue where the modelfile migration script would fail if an invalid modelfile was encountered. +- **💬 Zhuyin Input Method on Mac**: Resolved an issue where using the Zhuyin input method in the Web UI on a Mac caused text to send immediately upon pressing the enter key, leading to incorrect input. +- **🔊 Local TTS Voice Selection**: Fixed the issue where the selected local Text-to-Speech (TTS) voice was not being displayed in settings. + +## [0.2.2] - 2024-06-02 + +### Added + +- **🌊 Mermaid Rendering Support**: We've included support for Mermaid rendering. This allows you to create beautiful diagrams and flowcharts directly within Open WebUI. +- **🔄 New Environment Variable 'RESET_CONFIG_ON_START'**: Introducing a new environment variable: 'RESET_CONFIG_ON_START'. Set this variable to reset your configuration settings upon starting the application, making it easier to revert to default settings. + +### Fixed + +- **🔧 Pipelines Filter Issue**: We've addressed an issue with the pipelines where filters were not functioning as expected. + +## [0.2.1] - 2024-06-02 + +### Added + +- **🖱️ Single Model Export Button**: Easily export models with just one click using the new single model export button. +- **🖥️ Advanced Parameters Support**: Added support for 'num_thread', 'use_mmap', and 'use_mlock' parameters for Ollama. +- **🌐 Improved Vietnamese Translation**: Enhanced Vietnamese language support for a better user experience for our Vietnamese-speaking community. + +### Fixed + +- **🔧 OpenAI URL API Save Issue**: Corrected a problem preventing the saving of OpenAI URL API settings. +- **🚫 Display Issue with Disabled Ollama API**: Fixed the display bug causing models to appear in settings when the Ollama API was disabled. + +### Changed + +- **💡 Versioning Update**: As a reminder from our previous update, version 0.2.y will focus primarily on bug fixes, while major updates will be designated as 0.x from now on for better version tracking. + +## [0.2.0] - 2024-06-01 + +### Added + +- **🔧 Pipelines Support**: Open WebUI now includes a plugin framework for enhanced customization and functionality (https://github.com/open-webui/pipelines). Easily add custom logic and integrate Python libraries, from AI agents to home automation APIs. +- **🔗 Function Calling via Pipelines**: Integrate function calling seamlessly through Pipelines. +- **⚖️ User Rate Limiting via Pipelines**: Implement user-specific rate limits to manage API usage efficiently. +- **📊 Usage Monitoring with Langfuse**: Track and analyze usage statistics with Langfuse integration through Pipelines. +- **🕒 Conversation Turn Limits**: Set limits on conversation turns to manage interactions better through Pipelines. +- **🛡️ Toxic Message Filtering**: Automatically filter out toxic messages to maintain a safe environment using Pipelines. +- **🔍 Web Search Support**: Introducing built-in web search capabilities via RAG API, allowing users to search using SearXNG, Google Programmatic Search Engine, Brave Search, serpstack, and serper. Activate it effortlessly by adding necessary variables from Document settings > Web Params. +- **🗂️ Models Workspace**: Create and manage model presets for both Ollama/OpenAI API. Note: The old Modelfiles workspace is deprecated. +- **🛠️ Model Builder Feature**: Build and edit all models with persistent builder mode. +- **🏷️ Model Tagging Support**: Organize models with tagging features in the models workspace. +- **📋 Model Ordering Support**: Effortlessly organize models by dragging and dropping them into the desired positions within the models workspace. +- **📈 OpenAI Generation Stats**: Access detailed generation statistics for OpenAI models. +- **📅 System Prompt Variables**: New variables added: '{{CURRENT_DATE}}' and '{{USER_NAME}}' for dynamic prompts. +- **📢 Global Banner Support**: Manage global banners from admin settings > banners. +- **🗃️ Enhanced Archived Chats Modal**: Search and export archived chats easily. +- **📂 Archive All Button**: Quickly archive all chats from settings > chats. +- **🌐 Improved Translations**: Added and improved translations for French, Croatian, Cebuano, and Vietnamese. + +### Fixed + +- **🔍 Archived Chats Visibility**: Resolved issue with archived chats not showing in the admin panel. +- **💬 Message Styling**: Fixed styling issues affecting message appearance. +- **🔗 Shared Chat Responses**: Corrected the issue where shared chat response messages were not readonly. +- **🖥️ UI Enhancement**: Fixed the scrollbar overlapping issue with the message box in the user interface. + +### Changed + +- **💾 User Settings Storage**: User settings are now saved on the backend, ensuring consistency across all devices. +- **📡 Unified API Requests**: The API request for getting models is now unified to '/api/models' for easier usage. +- **🔄 Versioning Update**: Our versioning will now follow the format 0.x for major updates and 0.x.y for patches. +- **📦 Export All Chats (All Users)**: Moved this functionality to the Admin Panel settings for better organization and accessibility. + +### Removed + +- **🚫 Bundled LiteLLM Support Deprecated**: Migrate your LiteLLM config.yaml to a self-hosted LiteLLM instance. LiteLLM can still be added via OpenAI Connections. Download the LiteLLM config.yaml from admin settings > database > export LiteLLM config.yaml. + +## [0.1.125] - 2024-05-19 + +### Added + +- **🔄 Updated UI**: Chat interface revamped with chat bubbles. Easily switch back to the old style via settings > interface > chat bubble UI. +- **📂 Enhanced Sidebar UI**: Model files, documents, prompts, and playground merged into Workspace for streamlined access. +- **🚀 Improved Many Model Interaction**: All responses now displayed simultaneously for a smoother experience. +- **🐍 Python Code Execution**: Execute Python code locally in the browser with libraries like 'requests', 'beautifulsoup4', 'numpy', 'pandas', 'seaborn', 'matplotlib', 'scikit-learn', 'scipy', 'regex'. +- **🧠 Experimental Memory Feature**: Manually input personal information you want LLMs to remember via settings > personalization > memory. +- **💾 Persistent Settings**: Settings now saved as config.json for convenience. +- **🩺 Health Check Endpoint**: Added for Docker deployment. +- **↕️ RTL Support**: Toggle chat direction via settings > interface > chat direction. +- **🖥️ PowerPoint Support**: RAG pipeline now supports PowerPoint documents. +- **🌐 Language Updates**: Ukrainian, Turkish, Arabic, Chinese, Serbian, Vietnamese updated; Punjabi added. + +### Changed + +- **👤 Shared Chat Update**: Shared chat now includes creator user information. + +## [0.1.124] - 2024-05-08 + +### Added + +- **🖼️ Improved Chat Sidebar**: Now conveniently displays time ranges and organizes chats by today, yesterday, and more. +- **📜 Citations in RAG Feature**: Easily track the context fed to the LLM with added citations in the RAG feature. +- **🔒 Auth Disable Option**: Introducing the ability to disable authentication. Set 'WEBUI_AUTH' to False to disable authentication. Note: Only applicable for fresh installations without existing users. +- **📹 Enhanced YouTube RAG Pipeline**: Now supports non-English videos for an enriched experience. +- **🔊 Specify OpenAI TTS Models**: Customize your TTS experience by specifying OpenAI TTS models. +- **🔧 Additional Environment Variables**: Discover more environment variables in our comprehensive documentation at Open WebUI Documentation (https://docs.openwebui.com). +- **🌐 Language Support**: Arabic, Finnish, and Hindi added; Improved support for German, Vietnamese, and Chinese. + +### Fixed + +- **🛠️ Model Selector Styling**: Addressed styling issues for improved user experience. +- **⚠️ Warning Messages**: Resolved backend warning messages. + +### Changed + +- **📝 Title Generation**: Limited output to 50 tokens. +- **📦 Helm Charts**: Removed Helm charts, now available in a separate repository (https://github.com/open-webui/helm-charts). + +## [0.1.123] - 2024-05-02 + +### Added + +- **🎨 New Landing Page Design**: Refreshed design for a more modern look and optimized use of screen space. +- **📹 Youtube RAG Pipeline**: Introduces dedicated RAG pipeline for Youtube videos, enabling interaction with video transcriptions directly. +- **🔧 Enhanced Admin Panel**: Streamlined user management with options to add users directly or in bulk via CSV import. +- **👥 '@' Model Integration**: Easily switch to specific models during conversations; old collaborative chat feature phased out. +- **🌐 Language Enhancements**: Swedish translation added, plus improvements to German, Spanish, and the addition of Doge translation. + +### Fixed + +- **🗑️ Delete Chat Shortcut**: Addressed issue where shortcut wasn't functioning. +- **🖼️ Modal Closing Bug**: Resolved unexpected closure of modal when dragging from within. +- **✏️ Edit Button Styling**: Fixed styling inconsistency with edit buttons. +- **🌐 Image Generation Compatibility Issue**: Rectified image generation compatibility issue with third-party APIs. +- **📱 iOS PWA Icon Fix**: Corrected iOS PWA home screen icon shape. +- **🔍 Scroll Gesture Bug**: Adjusted gesture sensitivity to prevent accidental activation when scrolling through code on mobile; now requires scrolling from the leftmost side to open the sidebar. + +### Changed + +- **🔄 Unlimited Context Length**: Advanced settings now allow unlimited max context length (previously limited to 16000). +- **👑 Super Admin Assignment**: The first signup is automatically assigned a super admin role, unchangeable by other admins. +- **🛡️ Admin User Restrictions**: User action buttons from the admin panel are now disabled for users with admin roles. +- **🔝 Default Model Selector**: Set as default model option now exclusively available on the landing page. + +## [0.1.122] - 2024-04-27 + +### Added + +- **🌟 Enhanced RAG Pipeline**: Now with hybrid searching via 'BM25', reranking powered by 'CrossEncoder', and configurable relevance score thresholds. +- **🛢️ External Database Support**: Seamlessly connect to custom SQLite or Postgres databases using the 'DATABASE_URL' environment variable. +- **🌐 Remote ChromaDB Support**: Introducing the capability to connect to remote ChromaDB servers. +- **👨💼 Improved Admin Panel**: Admins can now conveniently check users' chat lists and last active status directly from the admin panel. +- **🎨 Splash Screen**: Introducing a loading splash screen for a smoother user experience. +- **🌍 Language Support Expansion**: Added support for Bangla (bn-BD), along with enhancements to Chinese, Spanish, and Ukrainian translations. +- **💻 Improved LaTeX Rendering Performance**: Enjoy faster rendering times for LaTeX equations. +- **🔧 More Environment Variables**: Explore additional environment variables in our documentation (https://docs.openwebui.com), including the 'ENABLE_LITELLM' option to manage memory usage. + +### Fixed + +- **🔧 Ollama Compatibility**: Resolved errors occurring when Ollama server version isn't an integer, such as SHA builds or RCs. +- **🐛 Various OpenAI API Issues**: Addressed several issues related to the OpenAI API. +- **🛑 Stop Sequence Issue**: Fixed the problem where the stop sequence with a backslash '\' was not functioning. +- **🔤 Font Fallback**: Corrected font fallback issue. + +### Changed + +- **⌨️ Prompt Input Behavior on Mobile**: Enter key prompt submission disabled on mobile devices for improved user experience. + +## [0.1.121] - 2024-04-24 + +### Fixed + +- **🔧 Translation Issues**: Addressed various translation discrepancies. +- **🔒 LiteLLM Security Fix**: Updated LiteLLM version to resolve a security vulnerability. +- **🖥️ HTML Tag Display**: Rectified the issue where the '< br >' tag wasn't displaying correctly. +- **🔗 WebSocket Connection**: Resolved the failure of WebSocket connection under HTTPS security for ComfyUI server. +- **📜 FileReader Optimization**: Implemented FileReader initialization per image in multi-file drag & drop to ensure reusability. +- **🏷️ Tag Display**: Corrected tag display inconsistencies. +- **📦 Archived Chat Styling**: Fixed styling issues in archived chat. +- **🔖 Safari Copy Button Bug**: Addressed the bug where the copy button failed to copy links in Safari. + +## [0.1.120] - 2024-04-20 + +### Added + +- **📦 Archive Chat Feature**: Easily archive chats with a new sidebar button, and access archived chats via the profile button > archived chats. +- **🔊 Configurable Text-to-Speech Endpoint**: Customize your Text-to-Speech experience with configurable OpenAI endpoints. +- **🛠️ Improved Error Handling**: Enhanced error message handling for connection failures. +- **⌨️ Enhanced Shortcut**: When editing messages, use ctrl/cmd+enter to save and submit, and esc to close. +- **🌐 Language Support**: Added support for Georgian and enhanced translations for Portuguese and Vietnamese. + +### Fixed + +- **🔧 Model Selector**: Resolved issue where default model selection was not saving. +- **🔗 Share Link Copy Button**: Fixed bug where the copy button wasn't copying links in Safari. +- **🎨 Light Theme Styling**: Addressed styling issue with the light theme. + +## [0.1.119] - 2024-04-16 + +### Added + +- **🌟 Enhanced RAG Embedding Support**: Ollama, and OpenAI models can now be used for RAG embedding model. +- **🔄 Seamless Integration**: Copy 'ollama run <model name>' directly from Ollama page to easily select and pull models. +- **🏷️ Tagging Feature**: Add tags to chats directly via the sidebar chat menu. +- **📱 Mobile Accessibility**: Swipe left and right on mobile to effortlessly open and close the sidebar. +- **🔍 Improved Navigation**: Admin panel now supports pagination for user list. +- **🌍 Additional Language Support**: Added Polish language support. + +### Fixed + +- **🌍 Language Enhancements**: Vietnamese and Spanish translations have been improved. +- **🔧 Helm Fixes**: Resolved issues with Helm trailing slash and manifest.json. + +### Changed + +- **🐳 Docker Optimization**: Updated docker image build process to utilize 'uv' for significantly faster builds compared to 'pip3'. + +## [0.1.118] - 2024-04-10 + +### Added + +- **🦙 Ollama and CUDA Images**: Added support for ':ollama' and ':cuda' tagged images. +- **👍 Enhanced Response Rating**: Now you can annotate your ratings for better feedback. +- **👤 User Initials Profile Photo**: User initials are now the default profile photo. +- **🔍 Update RAG Embedding Model**: Customize RAG embedding model directly in document settings. +- **🌍 Additional Language Support**: Added Turkish language support. + +### Fixed + +- **🔒 Share Chat Permission**: Resolved issue with chat sharing permissions. +- **🛠 Modal Close**: Modals can now be closed using the Esc key. + +### Changed + +- **🎨 Admin Panel Styling**: Refreshed styling for the admin panel. +- **🐳 Docker Image Build**: Updated docker image build process for improved efficiency. + +## [0.1.117] - 2024-04-03 + +### Added + +- 🗨️ **Local Chat Sharing**: Share chat links seamlessly between users. +- 🔑 **API Key Generation Support**: Generate secret keys to leverage Open WebUI with OpenAI libraries. +- 📄 **Chat Download as PDF**: Easily download chats in PDF format. +- 📝 **Improved Logging**: Enhancements to logging functionality. +- 📧 **Trusted Email Authentication**: Authenticate using a trusted email header. + +### Fixed + +- 🌷 **Enhanced Dutch Translation**: Improved translation for Dutch users. +- ⚪ **White Theme Styling**: Resolved styling issue with the white theme. +- 📜 **LaTeX Chat Screen Overflow**: Fixed screen overflow issue with LaTeX rendering. +- 🔒 **Security Patches**: Applied necessary security patches. + +## [0.1.116] - 2024-03-31 + +### Added + +- **🔄 Enhanced UI**: Model selector now conveniently located in the navbar, enabling seamless switching between multiple models during conversations. +- **🔍 Improved Model Selector**: Directly pull a model from the selector/Models now display detailed information for better understanding. +- **💬 Webhook Support**: Now compatible with Google Chat and Microsoft Teams. +- **🌐 Localization**: Korean translation (I18n) now available. +- **🌑 Dark Theme**: OLED dark theme introduced for reduced strain during prolonged usage. +- **🏷️ Tag Autocomplete**: Dropdown feature added for effortless chat tagging. + +### Fixed + +- **🔽 Auto-Scrolling**: Addressed OpenAI auto-scrolling issue. +- **🏷️ Tag Validation**: Implemented tag validation to prevent empty string tags. +- **🚫 Model Whitelisting**: Resolved LiteLLM model whitelisting issue. +- **✅ Spelling**: Corrected various spelling issues for improved readability. + +## [0.1.115] - 2024-03-24 + +### Added + +- **🔍 Custom Model Selector**: Easily find and select custom models with the new search filter feature. +- **🛑 Cancel Model Download**: Added the ability to cancel model downloads. +- **🎨 Image Generation ComfyUI**: Image generation now supports ComfyUI. +- **🌟 Updated Light Theme**: Updated the light theme for a fresh look. +- **🌍 Additional Language Support**: Now supporting Bulgarian, Italian, Portuguese, Japanese, and Dutch. + +### Fixed + +- **🔧 Fixed Broken Experimental GGUF Upload**: Resolved issues with experimental GGUF upload functionality. + +### Changed + +- **🔄 Vector Storage Reset Button**: Moved the reset vector storage button to document settings. + +## [0.1.114] - 2024-03-20 + +### Added + +- **🔗 Webhook Integration**: Now you can subscribe to new user sign-up events via webhook. Simply navigate to the admin panel > admin settings > webhook URL. +- **🛡️ Enhanced Model Filtering**: Alongside Ollama, OpenAI proxy model whitelisting, we've added model filtering functionality for LiteLLM proxy. +- **🌍 Expanded Language Support**: Spanish, Catalan, and Vietnamese languages are now available, with improvements made to others. + +### Fixed + +- **🔧 Input Field Spelling**: Resolved issue with spelling mistakes in input fields. +- **🖊️ Light Mode Styling**: Fixed styling issue with light mode in document adding. + +### Changed + +- **🔄 Language Sorting**: Languages are now sorted alphabetically by their code for improved organization. + +## [0.1.113] - 2024-03-18 + +### Added + +- 🌍 **Localization**: You can now change the UI language in Settings > General. We support Ukrainian, German, Farsi (Persian), Traditional and Simplified Chinese and French translations. You can help us to translate the UI into your language! More info in our [CONTRIBUTION.md](https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization). +- 🎨 **System-wide Theme**: Introducing a new system-wide theme for enhanced visual experience. + +### Fixed + +- 🌑 **Dark Background on Select Fields**: Improved readability by adding a dark background to select fields, addressing issues on certain browsers/devices. +- **Multiple OPENAI_API_BASE_URLS Issue**: Resolved issue where multiple base URLs caused conflicts when one wasn't functioning. +- **RAG Encoding Issue**: Fixed encoding problem in RAG. +- **npm Audit Fix**: Addressed npm audit findings. +- **Reduced Scroll Threshold**: Improved auto-scroll experience by reducing the scroll threshold from 50px to 5px. + +### Changed + +- 🔄 **Sidebar UI Update**: Updated sidebar UI to feature a chat menu dropdown, replacing two icons for improved navigation. + +## [0.1.112] - 2024-03-15 + +### Fixed + +- 🗨️ Resolved chat malfunction after image generation. +- 🎨 Fixed various RAG issues. +- 🧪 Rectified experimental broken GGUF upload logic. + +## [0.1.111] - 2024-03-10 + +### Added + +- 🛡️ **Model Whitelisting**: Admins now have the ability to whitelist models for users with the 'user' role. +- 🔄 **Update All Models**: Added a convenient button to update all models at once. +- 📄 **Toggle PDF OCR**: Users can now toggle PDF OCR option for improved parsing performance. +- 🎨 **DALL-E Integration**: Introduced DALL-E integration for image generation alongside automatic1111. +- 🛠️ **RAG API Refactoring**: Refactored RAG logic and exposed its API, with additional documentation to follow. + +### Fixed + +- 🔒 **Max Token Settings**: Added max token settings for anthropic/claude-3-sonnet-20240229 (Issue #1094). +- 🔧 **Misalignment Issue**: Corrected misalignment of Edit and Delete Icons when Chat Title is Empty (Issue #1104). +- 🔄 **Context Loss Fix**: Resolved RAG losing context on model response regeneration with Groq models via API key (Issue #1105). +- 📁 **File Handling Bug**: Addressed File Not Found Notification when Dropping a Conversation Element (Issue #1098). +- 🖱️ **Dragged File Styling**: Fixed dragged file layover styling issue. + +## [0.1.110] - 2024-03-06 + +### Added + +- **🌐 Multiple OpenAI Servers Support**: Enjoy seamless integration with multiple OpenAI-compatible APIs, now supported natively. + +### Fixed + +- **🔍 OCR Issue**: Resolved PDF parsing issue caused by OCR malfunction. +- **🚫 RAG Issue**: Fixed the RAG functionality, ensuring it operates smoothly. +- **📄 "Add Docs" Model Button**: Addressed the non-functional behavior of the "Add Docs" model button. + +## [0.1.109] - 2024-03-06 + +### Added + +- **🔄 Multiple Ollama Servers Support**: Enjoy enhanced scalability and performance with support for multiple Ollama servers in a single WebUI. Load balancing features are now available, providing improved efficiency (#788, #278). +- **🔧 Support for Claude 3 and Gemini**: Responding to user requests, we've expanded our toolset to include Claude 3 and Gemini, offering a wider range of functionalities within our platform (#1064). +- **🔍 OCR Functionality for PDF Loader**: We've augmented our PDF loader with Optical Character Recognition (OCR) capabilities. Now, extract text from scanned documents and images within PDFs, broadening the scope of content processing (#1050). + +### Fixed + +- **🛠️ RAG Collection**: Implemented a dynamic mechanism to recreate RAG collections, ensuring users have up-to-date and accurate data (#1031). +- **📝 User Agent Headers**: Fixed issue of RAG web requests being sent with empty user_agent headers, reducing rejections from certain websites. Realistic headers are now utilized for these requests (#1024). +- **⏹️ Playground Cancel Functionality**: Introducing a new "Cancel" option for stopping Ollama generation in the Playground, enhancing user control and usability (#1006). +- **🔤 Typographical Error in 'ASSISTANT' Field**: Corrected a typographical error in the 'ASSISTANT' field within the GGUF model upload template for accuracy and consistency (#1061). + +### Changed + +- **🔄 Refactored Message Deletion Logic**: Streamlined message deletion process for improved efficiency and user experience, simplifying interactions within the platform (#1004). +- **⚠️ Deprecation of `OLLAMA_API_BASE_URL`**: Deprecated `OLLAMA_API_BASE_URL` environment variable; recommend using `OLLAMA_BASE_URL` instead. Refer to our documentation for further details. + +## [0.1.108] - 2024-03-02 + +### Added + +- **🎮 Playground Feature (Beta)**: Explore the full potential of the raw API through an intuitive UI with our new playground feature, accessible to admins. Simply click on the bottom name area of the sidebar to access it. The playground feature offers two modes text completion (notebook) and chat completion. As it's in beta, please report any issues you encounter. +- **🛠️ Direct Database Download for Admins**: Admins can now download the database directly from the WebUI via the admin settings. +- **🎨 Additional RAG Settings**: Customize your RAG process with the ability to edit the TOP K value. Navigate to Documents > Settings > General to make changes. +- **🖥️ UI Improvements**: Tooltips now available in the input area and sidebar handle. More tooltips will be added across other parts of the UI. + +### Fixed + +- Resolved input autofocus issue on mobile when the sidebar is open, making it easier to use. +- Corrected numbered list display issue in Safari (#963). +- Restricted user ability to delete chats without proper permissions (#993). + +### Changed + +- **Simplified Ollama Settings**: Ollama settings now don't require the `/api` suffix. You can now utilize the Ollama base URL directly, e.g., `http://localhost:11434`. Also, an `OLLAMA_BASE_URL` environment variable has been added. +- **Database Renaming**: Starting from this release, `ollama.db` will be automatically renamed to `webui.db`. + +## [0.1.107] - 2024-03-01 + +### Added + +- **🚀 Makefile and LLM Update Script**: Included Makefile and a script for LLM updates in the repository. + +### Fixed + +- Corrected issue where links in the settings modal didn't appear clickable (#960). +- Fixed problem with web UI port not taking effect due to incorrect environment variable name in run-compose.sh (#996). +- Enhanced user experience by displaying chat in browser title and enabling automatic scrolling to the bottom (#992). + +### Changed + +- Upgraded toast library from `svelte-french-toast` to `svelte-sonner` for a more polished UI. +- Enhanced accessibility with the addition of dark mode on the authentication page. + +## [0.1.106] - 2024-02-27 + +### Added + +- **🎯 Auto-focus Feature**: The input area now automatically focuses when initiating or opening a chat conversation. + +### Fixed + +- Corrected typo from "HuggingFace" to "Hugging Face" (Issue #924). +- Resolved bug causing errors in chat completion API calls to OpenAI due to missing "num_ctx" parameter (Issue #927). +- Fixed issues preventing text editing, selection, and cursor retention in the input field (Issue #940). +- Fixed a bug where defining an OpenAI-compatible API server using 'OPENAI_API_BASE_URL' containing 'openai' string resulted in hiding models not containing 'gpt' string from the model menu. (Issue #930) + +## [0.1.105] - 2024-02-25 + +### Added + +- **📄 Document Selection**: Now you can select and delete multiple documents at once for easier management. + +### Changed + +- **🏷️ Document Pre-tagging**: Simply click the "+" button at the top, enter tag names in the popup window, or select from a list of existing tags. Then, upload files with the added tags for streamlined organization. + +## [0.1.104] - 2024-02-25 + +### Added + +- **🔄 Check for Updates**: Keep your system current by checking for updates conveniently located in Settings > About. +- **🗑️ Automatic Tag Deletion**: Unused tags on the sidebar will now be deleted automatically with just a click. + +### Changed + +- **🎨 Modernized Styling**: Enjoy a refreshed look with updated styling for a more contemporary experience. + +## [0.1.103] - 2024-02-25 + +### Added + +- **🔗 Built-in LiteLLM Proxy**: Now includes LiteLLM proxy within Open WebUI for enhanced functionality. + + - Easily integrate existing LiteLLM configurations using `-v /path/to/config.yaml:/app/backend/data/litellm/config.yaml` flag. + - When utilizing Docker container to run Open WebUI, ensure connections to localhost use `host.docker.internal`. + +- **🖼️ Image Generation Enhancements**: Introducing Advanced Settings with Image Preview Feature. + - Customize image generation by setting the number of steps; defaults to A1111 value. + +### Fixed + +- Resolved issue with RAG scan halting document loading upon encountering unsupported MIME types or exceptions (Issue #866). + +### Changed + +- Ollama is no longer required to run Open WebUI. +- Access our comprehensive documentation at [Open WebUI Documentation](https://docs.openwebui.com/). + +## [0.1.102] - 2024-02-22 + +### Added + +- **🖼️ Image Generation**: Generate Images using the AUTOMATIC1111/stable-diffusion-webui API. You can set this up in Settings > Images. +- **📝 Change title generation prompt**: Change the prompt used to generate titles for your chats. You can set this up in the Settings > Interface. +- **🤖 Change embedding model**: Change the embedding model used to generate embeddings for your chats in the Dockerfile. Use any sentence transformer model from huggingface.co. +- **📢 CHANGELOG.md/Popup**: This popup will show you the latest changes. + +## [0.1.101] - 2024-02-22 + +### Fixed + +- LaTex output formatting issue (#828) + +### Changed + +- Instead of having the previous 1.0.0-alpha.101, we switched to semantic versioning as a way to respect global conventions. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..37ac5263cbfdbb5a938da361397352443e57dc33 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contribute to a positive environment for our community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, without their explicit permission +- **Spamming of any kind** +- Aggressive sales tactics targeting our community members are strictly prohibited. You can mention your product if it's relevant to the discussion, but under no circumstances should you push it forcefully +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, spamming, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at hello@openwebui.com. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Temporary Ban + +**Community Impact**: Any violation of community standards, including but not limited to inappropriate language, unprofessional behavior, harassment, or spamming. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 2. Permanent Ban + +**Community Impact**: Repeated or severe violations of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/Caddyfile.localhost b/Caddyfile.localhost new file mode 100644 index 0000000000000000000000000000000000000000..80728eedf6ac60dc6454ef6933b490eeed4648be --- /dev/null +++ b/Caddyfile.localhost @@ -0,0 +1,64 @@ +# Run with +# caddy run --envfile ./example.env --config ./Caddyfile.localhost +# +# This is configured for +# - Automatic HTTPS (even for localhost) +# - Reverse Proxying to Ollama API Base URL (http://localhost:11434/api) +# - CORS +# - HTTP Basic Auth API Tokens (uncomment basicauth section) + + +# CORS Preflight (OPTIONS) + Request (GET, POST, PATCH, PUT, DELETE) +(cors-api) { + @match-cors-api-preflight method OPTIONS + handle @match-cors-api-preflight { + header { + Access-Control-Allow-Origin "{http.request.header.origin}" + Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" + Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With" + Access-Control-Allow-Credentials "true" + Access-Control-Max-Age "3600" + defer + } + respond "" 204 + } + + @match-cors-api-request { + not { + header Origin "{http.request.scheme}://{http.request.host}" + } + header Origin "{http.request.header.origin}" + } + handle @match-cors-api-request { + header { + Access-Control-Allow-Origin "{http.request.header.origin}" + Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" + Access-Control-Allow-Headers "Origin, Accept, Authorization, Content-Type, X-Requested-With" + Access-Control-Allow-Credentials "true" + Access-Control-Max-Age "3600" + defer + } + } +} + +# replace localhost with example.com or whatever +localhost { + ## HTTP Basic Auth + ## (uncomment to enable) + # basicauth { + # # see .example.env for how to generate tokens + # {env.OLLAMA_API_ID} {env.OLLAMA_API_TOKEN_DIGEST} + # } + + handle /api/* { + # Comment to disable CORS + import cors-api + + reverse_proxy localhost:11434 + } + + # Same-Origin Static Web Server + file_server { + root ./build/ + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..a217595ea864027350be67650e3af33bf06d2003 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,161 @@ +# 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 embedding model (sentence-transformers/all-MiniLM-L6-v2) and vice versa, you aren't able to use RAG Chat with your previous documents loaded in the WebUI! You need to re-embed them. +ARG USE_EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2 +ARG USE_RERANKING_MODEL="" +ARG BUILD_HASH=dev-build +# Override at your own risk - non-root configurations are untested +ARG UID=0 +ARG GID=0 + +######## WebUI frontend ######## +FROM --platform=$BUILDPLATFORM node:21-alpine3.19 as build +ARG BUILD_HASH + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +ENV APP_BUILD_HASH=${BUILD_HASH} +RUN npm run build + +######## WebUI backend ######## +FROM python:3.11-slim-bookworm as base + +# Use args +ARG USE_CUDA +ARG USE_OLLAMA +ARG USE_CUDA_VER +ARG USE_EMBEDDING_MODEL +ARG USE_RERANKING_MODEL +ARG UID +ARG GID + +## 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} \ + USE_RERANKING_MODEL_DOCKER=${USE_RERANKING_MODEL} + +## Basis URL Config ## +ENV OLLAMA_BASE_URL="/ollama" \ + OPENAI_API_BASE_URL="" + +## API Key and Security Config ## +ENV OPENAI_API_KEY="" \ + WEBUI_SECRET_KEY="" \ + SCARF_NO_ANALYTICS=true \ + DO_NOT_TRACK=true \ + ANONYMIZED_TELEMETRY=false + +#### Other models ######################################################### +## whisper TTS model settings ## +ENV WHISPER_MODEL="base" \ + WHISPER_MODEL_DIR="/app/backend/data/cache/whisper/models" + +## RAG Embedding model settings ## +ENV RAG_EMBEDDING_MODEL="$USE_EMBEDDING_MODEL_DOCKER" \ + RAG_RERANKING_MODEL="$USE_RERANKING_MODEL_DOCKER" \ + SENTENCE_TRANSFORMERS_HOME="/app/backend/data/cache/embedding/models" + +## Hugging Face download cache ## +ENV HF_HOME="/app/backend/data/cache/embedding/models" +#### Other models ########################################################## + +WORKDIR /app/backend + +ENV HOME /root +# Create user and group if not root +RUN if [ $UID -ne 0 ]; then \ + if [ $GID -ne 0 ]; then \ + addgroup --gid $GID app; \ + fi; \ + adduser --uid $UID --gid $GID --home $HOME --disabled-password --no-create-home app; \ + fi + +RUN mkdir -p $HOME/.cache/chroma +RUN echo -n 00000000-0000-0000-0000-000000000000 > $HOME/.cache/chroma/telemetry_user_id + +# Make sure the user has access to the app and root directory +RUN chown -R $UID:$GID /app $HOME + +RUN if [ "$USE_OLLAMA" = "true" ]; then \ + apt-get update && \ + # Install pandoc and netcat + apt-get install -y --no-install-recommends pandoc netcat-openbsd curl && \ + apt-get install -y --no-install-recommends gcc python3-dev && \ + # for RAG OCR + apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \ + # install helper tools + apt-get install -y --no-install-recommends curl jq && \ + # install ollama + curl -fsSL https://ollama.com/install.sh | sh && \ + # cleanup + rm -rf /var/lib/apt/lists/*; \ + else \ + apt-get update && \ + # Install pandoc, netcat and gcc + apt-get install -y --no-install-recommends pandoc gcc netcat-openbsd curl jq && \ + apt-get install -y --no-install-recommends gcc python3-dev && \ + # for RAG OCR + apt-get install -y --no-install-recommends ffmpeg libsm6 libxext6 && \ + # cleanup + rm -rf /var/lib/apt/lists/*; \ + fi + +# install python dependencies +COPY --chown=$UID:$GID ./backend/requirements.txt ./requirements.txt + +RUN pip3 install uv && \ + if [ "$USE_CUDA" = "true" ]; then \ + # If you use CUDA the whisper and embedding model will be downloaded on first use + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/$USE_CUDA_DOCKER_VER --no-cache-dir && \ + uv pip install --system -r requirements.txt --no-cache-dir && \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ + 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'])"; \ + else \ + pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu --no-cache-dir && \ + uv pip install --system -r requirements.txt --no-cache-dir && \ + python -c "import os; from sentence_transformers import SentenceTransformer; SentenceTransformer(os.environ['RAG_EMBEDDING_MODEL'], device='cpu')" && \ + 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'])"; \ + fi; \ + chown -R $UID:$GID /app/backend/data/ + + + +# 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 + +# copy built frontend files +COPY --chown=$UID:$GID --from=build /app/build /app/build +COPY --chown=$UID:$GID --from=build /app/CHANGELOG.md /app/CHANGELOG.md +COPY --chown=$UID:$GID --from=build /app/package.json /app/package.json + +# copy backend files +COPY --chown=$UID:$GID ./backend . + +EXPOSE 8080 + +HEALTHCHECK CMD curl --silent --fail http://localhost:8080/health | jq -e '.status == true' || exit 1 + +USER $UID:$GID + +ARG BUILD_HASH +ENV WEBUI_BUILD_VERSION=${BUILD_HASH} + +CMD [ "bash", "start.sh"] diff --git a/INSTALLATION.md b/INSTALLATION.md new file mode 100644 index 0000000000000000000000000000000000000000..4298b173e9fd5c372a81c86fc4121de6a57fb81b --- /dev/null +++ b/INSTALLATION.md @@ -0,0 +1,35 @@ +### Installing Both Ollama and Open WebUI Using Kustomize + +For cpu-only pod + +```bash +kubectl apply -f ./kubernetes/manifest/base +``` + +For gpu-enabled pod + +```bash +kubectl apply -k ./kubernetes/manifest +``` + +### Installing Both Ollama and Open WebUI Using Helm + +Package Helm file first + +```bash +helm package ./kubernetes/helm/ +``` + +For cpu-only pod + +```bash +helm install ollama-webui ./ollama-webui-*.tgz +``` + +For gpu-enabled pod + +```bash +helm install ollama-webui ./ollama-webui-*.tgz --set ollama.resources.limits.nvidia.com/gpu="1" +``` + +Check the `kubernetes/helm/values.yaml` file to know which parameters are available for customization diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..515e64df6c00c937379ad187de6204770db0fe18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Timothy Jaeryang Baek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..4b60b049658173c1e8b64dd79919fdd04d5d4f24 --- /dev/null +++ b/Makefile @@ -0,0 +1,33 @@ + +ifneq ($(shell which docker-compose 2>/dev/null),) + DOCKER_COMPOSE := docker-compose +else + DOCKER_COMPOSE := docker compose +endif + +install: + $(DOCKER_COMPOSE) up -d + +remove: + @chmod +x confirm_remove.sh + @./confirm_remove.sh + +start: + $(DOCKER_COMPOSE) start +startAndBuild: + $(DOCKER_COMPOSE) up -d --build + +stop: + $(DOCKER_COMPOSE) stop + +update: + # Calls the LLM update script + chmod +x update_ollama_models.sh + @./update_ollama_models.sh + @git pull + $(DOCKER_COMPOSE) down + # Make sure the ollama-webui container is stopped before rebuilding + @docker stop open-webui || true + $(DOCKER_COMPOSE) up --build -d + $(DOCKER_COMPOSE) start + diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..94bc79cd6886a04e45c46650b6d10bb4be7f8672 --- /dev/null +++ b/README.md @@ -0,0 +1,223 @@ +--- +title: Open WebUI +emoji: 🐳 +colorFrom: purple +colorTo: gray +sdk: docker +app_port: 8080 +hf_oauth: true +hf_oauth_scopes: + - email +--- +--- +title: Open WebUI +emoji: 🐳 +colorFrom: purple +colorTo: gray +sdk: docker +app_port: 8080 +--- + +# Open WebUI (Formerly Ollama WebUI) 👋 + + + + + + + + + +[](https://discord.gg/5rJgQTnV4s) +[](https://github.com/sponsors/tjbck) + +Open WebUI is an [extensible](https://github.com/open-webui/pipelines), feature-rich, and user-friendly self-hosted WebUI designed to operate entirely offline. It supports various LLM runners, including Ollama and OpenAI-compatible APIs. For more information, be sure to check out our [Open WebUI Documentation](https://docs.openwebui.com/). + + + +## Key Features of Open WebUI ⭐ + +- 🚀 **Effortless Setup**: Install seamlessly using Docker or Kubernetes (kubectl, kustomize or helm) for a hassle-free experience with support for both `:ollama` and `:cuda` tagged images. + +- 🤝 **Ollama/OpenAI API Integration**: Effortlessly integrate OpenAI-compatible APIs for versatile conversations alongside Ollama models. Customize the OpenAI API URL to link with **LMStudio, GroqCloud, Mistral, OpenRouter, and more**. + +- 🧩 **Pipelines, Open WebUI Plugin Support**: Seamlessly integrate custom logic and Python libraries into Open WebUI using [Pipelines Plugin Framework](https://github.com/open-webui/pipelines). Launch your Pipelines instance, set the OpenAI URL to the Pipelines URL, and explore endless possibilities. [Examples](https://github.com/open-webui/pipelines/tree/main/examples) include **Function Calling**, User **Rate Limiting** to control access, **Usage Monitoring** with tools like Langfuse, **Live Translation with LibreTranslate** for multilingual support, **Toxic Message Filtering** and much more. + +- 📱 **Responsive Design**: Enjoy a seamless experience across Desktop PC, Laptop, and Mobile devices. + +- 📱 **Progressive Web App (PWA) for Mobile**: Enjoy a native app-like experience on your mobile device with our PWA, providing offline access on localhost and a seamless user interface. + +- ✒️🔢 **Full Markdown and LaTeX Support**: Elevate your LLM experience with comprehensive Markdown and LaTeX capabilities for enriched interaction. + +- 🎤📹 **Hands-Free Voice/Video Call**: Experience seamless communication with integrated hands-free voice and video call features, allowing for a more dynamic and interactive chat environment. + +- 🛠️ **Model Builder**: Easily create Ollama models via the Web UI. Create and add custom characters/agents, customize chat elements, and import models effortlessly through [Open WebUI Community](https://openwebui.com/) integration. + +- 🐍 **Native Python Function Calling Tool**: Enhance your LLMs with built-in code editor support in the tools workspace. Bring Your Own Function (BYOF) by simply adding your pure Python functions, enabling seamless integration with LLMs. + +- 📚 **Local RAG Integration**: Dive into the future of chat interactions with groundbreaking Retrieval Augmented Generation (RAG) support. This feature seamlessly integrates document interactions into your chat experience. You can load documents directly into the chat or add files to your document library, effortlessly accessing them using the `#` command before a query. + +- 🔍 **Web Search for RAG**: Perform web searches using providers like `SearXNG`, `Google PSE`, `Brave Search`, `serpstack`, `serper`, `Serply`, `DuckDuckGo` and `TavilySearch` and inject the results directly into your chat experience. + +- 🌐 **Web Browsing Capability**: Seamlessly integrate websites into your chat experience using the `#` command followed by a URL. This feature allows you to incorporate web content directly into your conversations, enhancing the richness and depth of your interactions. + +- 🎨 **Image Generation Integration**: Seamlessly incorporate image generation capabilities using options such as AUTOMATIC1111 API or ComfyUI (local), and OpenAI's DALL-E (external), enriching your chat experience with dynamic visual content. + +- ⚙️ **Many Models Conversations**: Effortlessly engage with various models simultaneously, harnessing their unique strengths for optimal responses. Enhance your experience by leveraging a diverse set of models in parallel. + +- 🔐 **Role-Based Access Control (RBAC)**: Ensure secure access with restricted permissions; only authorized individuals can access your Ollama, and exclusive model creation/pulling rights are reserved for administrators. + +- 🌐🌍 **Multilingual Support**: Experience Open WebUI in your preferred language with our internationalization (i18n) support. Join us in expanding our supported languages! We're actively seeking contributors! + +- 🌟 **Continuous Updates**: We are committed to improving Open WebUI with regular updates, fixes, and new features. + +Want to learn more about Open WebUI's features? Check out our [Open WebUI documentation](https://docs.openwebui.com/features) for a comprehensive overview! + +## 🔗 Also Check Out Open WebUI Community! + +Don't forget to explore our sibling project, [Open WebUI Community](https://openwebui.com/), where you can discover, download, and explore customized Modelfiles. Open WebUI Community offers a wide range of exciting possibilities for enhancing your chat interactions with Open WebUI! 🚀 + +## How to Install 🚀 + +> [!NOTE] +> Please note that for certain Docker environments, additional configurations might be needed. If you encounter any connection issues, our detailed guide on [Open WebUI Documentation](https://docs.openwebui.com/) is ready to assist you. + +### Quick Start with Docker 🐳 + +> [!WARNING] +> When using Docker to install Open WebUI, make sure to include the `-v open-webui:/app/backend/data` in your Docker command. This step is crucial as it ensures your database is properly mounted and prevents any loss of data. + +> [!TIP] +> If you wish to utilize Open WebUI with Ollama included or CUDA acceleration, we recommend utilizing our official images tagged with either `:cuda` or `:ollama`. To enable CUDA, you must install the [Nvidia CUDA container toolkit](https://docs.nvidia.com/dgx/nvidia-container-runtime-upgrade/) on your Linux/WSL system. + +### Installation with Default Configuration + +- **If Ollama is on your computer**, use this command: + + ```bash + docker run -d -p 3000:8080 --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main + ``` + +- **If Ollama is on a Different Server**, use this command: + + To connect to Ollama on another server, change the `OLLAMA_BASE_URL` to the server's URL: + + ```bash + docker run -d -p 3000:8080 -e OLLAMA_BASE_URL=https://example.com -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main + ``` + + - **To run Open WebUI with Nvidia GPU support**, use this command: + + ```bash + docker run -d -p 3000:8080 --gpus all --add-host=host.docker.internal:host-gateway -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:cuda + ``` + +### Installation for OpenAI API Usage Only + +- **If you're only using OpenAI API**, use this command: + + ```bash + docker run -d -p 3000:8080 -e OPENAI_API_KEY=your_secret_key -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:main + ``` + +### Installing Open WebUI with Bundled Ollama Support + +This installation method uses a single container image that bundles Open WebUI with Ollama, allowing for a streamlined setup via a single command. Choose the appropriate command based on your hardware setup: + +- **With GPU Support**: + Utilize GPU resources by running the following command: + + ```bash + docker run -d -p 3000:8080 --gpus=all -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama + ``` + +- **For CPU Only**: + If you're not using a GPU, use this command instead: + + ```bash + docker run -d -p 3000:8080 -v ollama:/root/.ollama -v open-webui:/app/backend/data --name open-webui --restart always ghcr.io/open-webui/open-webui:ollama + ``` + +Both commands facilitate a built-in, hassle-free installation of both Open WebUI and Ollama, ensuring that you can get everything up and running swiftly. + +After installation, you can access Open WebUI at [http://localhost:3000](http://localhost:3000). Enjoy! 😄 + +### Other Installation Methods + +We offer various installation alternatives, including non-Docker native installation methods, Docker Compose, Kustomize, and Helm. Visit our [Open WebUI Documentation](https://docs.openwebui.com/getting-started/) or join our [Discord community](https://discord.gg/5rJgQTnV4s) for comprehensive guidance. + +### Troubleshooting + +Encountering connection issues? Our [Open WebUI Documentation](https://docs.openwebui.com/troubleshooting/) has got you covered. For further assistance and to join our vibrant community, visit the [Open WebUI Discord](https://discord.gg/5rJgQTnV4s). + +#### 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`. + +**Example Docker Command**: + +```bash +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main +``` + +### Keeping Your Docker Installation Up-to-Date + +In case you want to update your local Docker installation to the latest version, you can do it with [Watchtower](https://containrrr.dev/watchtower/): + +```bash +docker run --rm --volume /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower --run-once open-webui +``` + +In the last part of the command, replace `open-webui` with your container name if it is different. + +Check our Migration Guide available in our [Open WebUI Documentation](https://docs.openwebui.com/migration/). + +### Using the Dev Branch 🌙 + +> [!WARNING] +> The `:dev` branch contains the latest unstable features and changes. Use it at your own risk as it may have bugs or incomplete features. + +If you want to try out the latest bleeding-edge features and are okay with occasional instability, you can use the `:dev` tag like this: + +```bash +docker run -d -p 3000:8080 -v open-webui:/app/backend/data --name open-webui --add-host=host.docker.internal:host-gateway --restart always ghcr.io/open-webui/open-webui:dev +``` + +## What's Next? 🌟 + +Discover upcoming features on our roadmap in the [Open WebUI Documentation](https://docs.openwebui.com/roadmap/). + +## Supporters ✨ + +A big shoutout to our amazing supporters who's helping to make this project possible! 🙏 + +### Platinum Sponsors 🤍 + +- We're looking for Sponsors! + +### Acknowledgments + +Special thanks to [Prof. Lawrence Kim](https://www.lhkim.com/) and [Prof. Nick Vincent](https://www.nickmvincent.com/) for their invaluable support and guidance in shaping this project into a research endeavor. Grateful for your mentorship throughout the journey! 🙌 + +## License 📜 + +This project is licensed under the [MIT License](LICENSE) - see the [LICENSE](LICENSE) file for details. 📄 + +## Support 💬 + +If you have any questions, suggestions, or need assistance, please open an issue or join our +[Open WebUI Discord community](https://discord.gg/5rJgQTnV4s) to connect with us! 🤝 + +## Star History + +<a href="https://star-history.com/#open-webui/open-webui&Date"> + <picture> + <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date&theme=dark" /> + <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" /> + <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=open-webui/open-webui&type=Date" /> + </picture> +</a> + +--- + +Created by [Timothy J. Baek](https://github.com/tjbck) - Let's make Open WebUI even more amazing together! 💪 diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000000000000000000000000000000000000..9bf242381ce42f62a025445a6f33432b33b6716d --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,36 @@ +# Open WebUI Troubleshooting Guide + +## Understanding the Open WebUI Architecture + +The Open WebUI system is designed to streamline interactions between the client (your browser) and the Ollama API. At the heart of this design is a backend reverse proxy, enhancing security and resolving CORS issues. + +- **How it Works**: The Open WebUI is designed to interact with the Ollama API through a specific route. When a request is made from the WebUI to Ollama, it is not directly sent to the Ollama API. Initially, the request is sent to the Open WebUI backend via `/ollama` route. From there, the backend is responsible for forwarding the request to the Ollama API. This forwarding is accomplished by using the route specified in the `OLLAMA_BASE_URL` environment variable. Therefore, a request made to `/ollama` in the WebUI is effectively the same as making a request to `OLLAMA_BASE_URL` in the backend. For instance, a request to `/ollama/api/tags` in the WebUI is equivalent to `OLLAMA_BASE_URL/api/tags` in the backend. + +- **Security Benefits**: This design prevents direct exposure of the Ollama API to the frontend, safeguarding against potential CORS (Cross-Origin Resource Sharing) issues and unauthorized access. Requiring authentication to access the Ollama API further enhances this security layer. + +## 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`. + +**Example Docker Command**: + +```bash +docker run -d --network=host -v open-webui:/app/backend/data -e OLLAMA_BASE_URL=http://127.0.0.1:11434 --name open-webui --restart always ghcr.io/open-webui/open-webui:main +``` + +### Error on Slow Reponses for Ollama + +Open WebUI has a default timeout of 5 minutes for Ollama to finish generating the response. If needed, this can be adjusted via the environment variable AIOHTTP_CLIENT_TIMEOUT, which sets the timeout in seconds. + +### General Connection Errors + +**Ensure Ollama Version is Up-to-Date**: Always start by checking that you have the latest version of Ollama. Visit [Ollama's official site](https://ollama.com/) for the latest updates. + +**Troubleshooting Steps**: + +1. **Verify Ollama URL Format**: + - When running the Web UI container, ensure the `OLLAMA_BASE_URL` is correctly set. (e.g., `http://192.168.1.1:11434` for different host setups). + - In the Open WebUI, navigate to "Settings" > "General". + - Confirm that the Ollama Server URL is correctly set to `[OLLAMA URL]` (e.g., `http://localhost:11434`). + +By following these enhanced troubleshooting steps, connection issues should be effectively resolved. For further assistance or queries, feel free to reach out to us on our community Discord. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..97ab32835d90e779180b09b866c7eecbdb1433ac --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,14 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test +!/data +/data/* +!/data/litellm +/data/litellm/* +!data/litellm/config.yaml + +!data/config.json \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..ea83b34f43e5b879df2dff7ff295e8daf11d448e --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,16 @@ +__pycache__ +.env +_old +uploads +.ipynb_checkpoints +*.db +_test +Pipfile +!/data +/data/* +!/data/litellm +/data/litellm/* +!data/litellm/config.yaml + +!data/config.json +.webui_secret_key \ No newline at end of file diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000000000000000000000000000000000000..4eff85f0c621c16f5afc80a4d92dc75da1483ac8 --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,114 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +# sqlalchemy.url = REPLACE_WITH_DATABASE_URL + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/apps/audio/main.py b/backend/apps/audio/main.py new file mode 100644 index 0000000000000000000000000000000000000000..c565bf481baf8da9e6fc01bb95b0d05bf7605136 --- /dev/null +++ b/backend/apps/audio/main.py @@ -0,0 +1,437 @@ +import os +import logging +from fastapi import ( + FastAPI, + Request, + Depends, + HTTPException, + status, + UploadFile, + File, + Form, +) + +from fastapi.responses import StreamingResponse, JSONResponse, FileResponse + +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +import uuid +import requests +import hashlib +from pathlib import Path +import json + +from constants import ERROR_MESSAGES +from utils.utils import ( + decode_token, + get_current_user, + get_verified_user, + get_admin_user, +) +from utils.misc import calculate_sha256 + +from config import ( + SRC_LOG_LEVELS, + CACHE_DIR, + UPLOAD_DIR, + WHISPER_MODEL, + WHISPER_MODEL_DIR, + WHISPER_MODEL_AUTO_UPDATE, + DEVICE_TYPE, + AUDIO_STT_OPENAI_API_BASE_URL, + AUDIO_STT_OPENAI_API_KEY, + AUDIO_TTS_OPENAI_API_BASE_URL, + AUDIO_TTS_OPENAI_API_KEY, + AUDIO_TTS_API_KEY, + AUDIO_STT_ENGINE, + AUDIO_STT_MODEL, + AUDIO_TTS_ENGINE, + AUDIO_TTS_MODEL, + AUDIO_TTS_VOICE, + AppConfig, +) + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["AUDIO"]) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.config = AppConfig() + +app.state.config.STT_OPENAI_API_BASE_URL = AUDIO_STT_OPENAI_API_BASE_URL +app.state.config.STT_OPENAI_API_KEY = AUDIO_STT_OPENAI_API_KEY +app.state.config.STT_ENGINE = AUDIO_STT_ENGINE +app.state.config.STT_MODEL = AUDIO_STT_MODEL + +app.state.config.TTS_OPENAI_API_BASE_URL = AUDIO_TTS_OPENAI_API_BASE_URL +app.state.config.TTS_OPENAI_API_KEY = AUDIO_TTS_OPENAI_API_KEY +app.state.config.TTS_ENGINE = AUDIO_TTS_ENGINE +app.state.config.TTS_MODEL = AUDIO_TTS_MODEL +app.state.config.TTS_VOICE = AUDIO_TTS_VOICE +app.state.config.TTS_API_KEY = AUDIO_TTS_API_KEY + +# 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}") + +SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") +SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +class TTSConfigForm(BaseModel): + OPENAI_API_BASE_URL: str + OPENAI_API_KEY: str + API_KEY: str + ENGINE: str + MODEL: str + VOICE: str + + +class STTConfigForm(BaseModel): + OPENAI_API_BASE_URL: str + OPENAI_API_KEY: str + ENGINE: str + MODEL: str + + +class AudioConfigUpdateForm(BaseModel): + tts: TTSConfigForm + stt: STTConfigForm + + +from pydub import AudioSegment +from pydub.utils import mediainfo + + +def is_mp4_audio(file_path): + """Check if the given file is an MP4 audio file.""" + if not os.path.isfile(file_path): + print(f"File not found: {file_path}") + return False + + info = mediainfo(file_path) + if ( + info.get("codec_name") == "aac" + and info.get("codec_type") == "audio" + and info.get("codec_tag_string") == "mp4a" + ): + return True + return False + + +def convert_mp4_to_wav(file_path, output_path): + """Convert MP4 audio file to WAV format.""" + audio = AudioSegment.from_file(file_path, format="mp4") + audio.export(output_path, format="wav") + print(f"Converted {file_path} to {output_path}") + + +@app.get("/config") +async def get_audio_config(user=Depends(get_admin_user)): + return { + "tts": { + "OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY, + "API_KEY": app.state.config.TTS_API_KEY, + "ENGINE": app.state.config.TTS_ENGINE, + "MODEL": app.state.config.TTS_MODEL, + "VOICE": app.state.config.TTS_VOICE, + }, + "stt": { + "OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY, + "ENGINE": app.state.config.STT_ENGINE, + "MODEL": app.state.config.STT_MODEL, + }, + } + + +@app.post("/config/update") +async def update_audio_config( + form_data: AudioConfigUpdateForm, user=Depends(get_admin_user) +): + app.state.config.TTS_OPENAI_API_BASE_URL = form_data.tts.OPENAI_API_BASE_URL + app.state.config.TTS_OPENAI_API_KEY = form_data.tts.OPENAI_API_KEY + app.state.config.TTS_API_KEY = form_data.tts.API_KEY + app.state.config.TTS_ENGINE = form_data.tts.ENGINE + app.state.config.TTS_MODEL = form_data.tts.MODEL + app.state.config.TTS_VOICE = form_data.tts.VOICE + + app.state.config.STT_OPENAI_API_BASE_URL = form_data.stt.OPENAI_API_BASE_URL + app.state.config.STT_OPENAI_API_KEY = form_data.stt.OPENAI_API_KEY + app.state.config.STT_ENGINE = form_data.stt.ENGINE + app.state.config.STT_MODEL = form_data.stt.MODEL + + return { + "tts": { + "OPENAI_API_BASE_URL": app.state.config.TTS_OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.TTS_OPENAI_API_KEY, + "API_KEY": app.state.config.TTS_API_KEY, + "ENGINE": app.state.config.TTS_ENGINE, + "MODEL": app.state.config.TTS_MODEL, + "VOICE": app.state.config.TTS_VOICE, + }, + "stt": { + "OPENAI_API_BASE_URL": app.state.config.STT_OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.STT_OPENAI_API_KEY, + "ENGINE": app.state.config.STT_ENGINE, + "MODEL": app.state.config.STT_MODEL, + }, + } + + +@app.post("/speech") +async def speech(request: Request, user=Depends(get_verified_user)): + body = await request.body() + name = hashlib.sha256(body).hexdigest() + + file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") + file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + if app.state.config.TTS_ENGINE == "openai": + headers = {} + headers["Authorization"] = f"Bearer {app.state.config.TTS_OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + try: + body = body.decode("utf-8") + body = json.loads(body) + body["model"] = app.state.config.TTS_MODEL + body = json.dumps(body).encode("utf-8") + except Exception as e: + pass + + r = None + try: + r = requests.post( + url=f"{app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech", + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']['message']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r != None else 500, + detail=error_detail, + ) + + elif app.state.config.TTS_ENGINE == "elevenlabs": + + payload = None + try: + payload = json.loads(body.decode("utf-8")) + except Exception as e: + log.exception(e) + pass + + url = f"https://api.elevenlabs.io/v1/text-to-speech/{payload['voice']}" + + headers = { + "Accept": "audio/mpeg", + "Content-Type": "application/json", + "xi-api-key": app.state.config.TTS_API_KEY, + } + + data = { + "text": payload["input"], + "model_id": app.state.config.TTS_MODEL, + "voice_settings": {"stability": 0.5, "similarity_boost": 0.5}, + } + + try: + r = requests.post(url, json=data, headers=headers) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']['message']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r != None else 500, + detail=error_detail, + ) + + +@app.post("/transcriptions") +def transcribe( + file: UploadFile = File(...), + user=Depends(get_current_user), +): + log.info(f"file.content_type: {file.content_type}") + + if file.content_type not in ["audio/mpeg", "audio/wav"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_NOT_SUPPORTED, + ) + + try: + ext = file.filename.split(".")[-1] + + id = uuid.uuid4() + filename = f"{id}.{ext}" + + file_dir = f"{CACHE_DIR}/audio/transcriptions" + os.makedirs(file_dir, exist_ok=True) + file_path = f"{file_dir}/{filename}" + + print(filename) + + contents = file.file.read() + with open(file_path, "wb") as f: + f.write(contents) + f.close() + + if app.state.config.STT_ENGINE == "": + from faster_whisper import WhisperModel + + whisper_kwargs = { + "model_size_or_path": WHISPER_MODEL, + "device": whisper_device_type, + "compute_type": "int8", + "download_root": WHISPER_MODEL_DIR, + "local_files_only": not WHISPER_MODEL_AUTO_UPDATE, + } + + log.debug(f"whisper_kwargs: {whisper_kwargs}") + + try: + model = WhisperModel(**whisper_kwargs) + except: + log.warning( + "WhisperModel initialization failed, attempting download with local_files_only=False" + ) + whisper_kwargs["local_files_only"] = False + model = WhisperModel(**whisper_kwargs) + + segments, info = model.transcribe(file_path, beam_size=5) + log.info( + "Detected language '%s' with probability %f" + % (info.language, info.language_probability) + ) + + transcript = "".join([segment.text for segment in list(segments)]) + + data = {"text": transcript.strip()} + + # save the transcript to a json file + transcript_file = f"{file_dir}/{id}.json" + with open(transcript_file, "w") as f: + json.dump(data, f) + + print(data) + + return data + + elif app.state.config.STT_ENGINE == "openai": + if is_mp4_audio(file_path): + print("is_mp4_audio") + os.rename(file_path, file_path.replace(".wav", ".mp4")) + # Convert MP4 audio file to WAV format + convert_mp4_to_wav(file_path.replace(".wav", ".mp4"), file_path) + + headers = {"Authorization": f"Bearer {app.state.config.STT_OPENAI_API_KEY}"} + + files = {"file": (filename, open(file_path, "rb"))} + data = {"model": app.state.config.STT_MODEL} + + print(files, data) + + r = None + try: + r = requests.post( + url=f"{app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions", + headers=headers, + files=files, + data=data, + ) + + r.raise_for_status() + + data = r.json() + + # save the transcript to a json file + transcript_file = f"{file_dir}/{id}.json" + with open(transcript_file, "w") as f: + json.dump(data, f) + + print(data) + return data + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']['message']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r != None else 500, + detail=error_detail, + ) + + except Exception as e: + log.exception(e) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) diff --git a/backend/apps/images/main.py b/backend/apps/images/main.py new file mode 100644 index 0000000000000000000000000000000000000000..9ae0ad67b7e05c00cd9551f0cbda1cc9814573aa --- /dev/null +++ b/backend/apps/images/main.py @@ -0,0 +1,568 @@ +import re +import requests +import base64 +from fastapi import ( + FastAPI, + Request, + Depends, + HTTPException, + status, + UploadFile, + File, + Form, +) +from fastapi.middleware.cors import CORSMiddleware + +from constants import ERROR_MESSAGES +from utils.utils import ( + get_verified_user, + get_admin_user, +) + +from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image +from utils.misc import calculate_sha256 +from typing import Optional +from pydantic import BaseModel +from pathlib import Path +import mimetypes +import uuid +import base64 +import json +import logging + +from config import ( + SRC_LOG_LEVELS, + CACHE_DIR, + IMAGE_GENERATION_ENGINE, + ENABLE_IMAGE_GENERATION, + AUTOMATIC1111_BASE_URL, + AUTOMATIC1111_API_AUTH, + COMFYUI_BASE_URL, + COMFYUI_CFG_SCALE, + COMFYUI_SAMPLER, + COMFYUI_SCHEDULER, + COMFYUI_SD3, + IMAGES_OPENAI_API_BASE_URL, + IMAGES_OPENAI_API_KEY, + IMAGE_GENERATION_MODEL, + IMAGE_SIZE, + IMAGE_STEPS, + AppConfig, +) + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["IMAGES"]) + +IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/") +IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.config = AppConfig() + +app.state.config.ENGINE = IMAGE_GENERATION_ENGINE +app.state.config.ENABLED = ENABLE_IMAGE_GENERATION + +app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL +app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY + +app.state.config.MODEL = IMAGE_GENERATION_MODEL + +app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL +app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH +app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL + +app.state.config.IMAGE_SIZE = IMAGE_SIZE +app.state.config.IMAGE_STEPS = IMAGE_STEPS +app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE +app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER +app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER +app.state.config.COMFYUI_SD3 = COMFYUI_SD3 + + +def get_automatic1111_api_auth(): + if app.state.config.AUTOMATIC1111_API_AUTH == None: + return "" + else: + auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8") + auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string) + auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8") + return f"Basic {auth1111_base64_encoded_string}" + + +@app.get("/config") +async def get_config(request: Request, user=Depends(get_admin_user)): + return { + "engine": app.state.config.ENGINE, + "enabled": app.state.config.ENABLED, + } + + +class ConfigUpdateForm(BaseModel): + engine: str + enabled: bool + + +@app.post("/config/update") +async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): + app.state.config.ENGINE = form_data.engine + app.state.config.ENABLED = form_data.enabled + return { + "engine": app.state.config.ENGINE, + "enabled": app.state.config.ENABLED, + } + + +class EngineUrlUpdateForm(BaseModel): + AUTOMATIC1111_BASE_URL: Optional[str] = None + AUTOMATIC1111_API_AUTH: Optional[str] = None + COMFYUI_BASE_URL: Optional[str] = None + + +@app.get("/url") +async def get_engine_url(user=Depends(get_admin_user)): + return { + "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL, + "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH, + "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL, + } + + +@app.post("/url/update") +async def update_engine_url( + form_data: EngineUrlUpdateForm, user=Depends(get_admin_user) +): + if form_data.AUTOMATIC1111_BASE_URL == None: + app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL + else: + url = form_data.AUTOMATIC1111_BASE_URL.strip("/") + try: + r = requests.head(url) + app.state.config.AUTOMATIC1111_BASE_URL = url + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + if form_data.COMFYUI_BASE_URL == None: + app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL + else: + url = form_data.COMFYUI_BASE_URL.strip("/") + + try: + r = requests.head(url) + app.state.config.COMFYUI_BASE_URL = url + except Exception as e: + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + if form_data.AUTOMATIC1111_API_AUTH == None: + app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH + else: + app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH + + return { + "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL, + "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH, + "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL, + "status": True, + } + + +class OpenAIConfigUpdateForm(BaseModel): + url: str + key: str + + +@app.get("/openai/config") +async def get_openai_config(user=Depends(get_admin_user)): + return { + "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY, + } + + +@app.post("/openai/config/update") +async def update_openai_config( + form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user) +): + if form_data.key == "": + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) + + app.state.config.OPENAI_API_BASE_URL = form_data.url + app.state.config.OPENAI_API_KEY = form_data.key + + return { + "status": True, + "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL, + "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY, + } + + +class ImageSizeUpdateForm(BaseModel): + size: str + + +@app.get("/size") +async def get_image_size(user=Depends(get_admin_user)): + return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE} + + +@app.post("/size/update") +async def update_image_size( + form_data: ImageSizeUpdateForm, user=Depends(get_admin_user) +): + pattern = r"^\d+x\d+$" # Regular expression pattern + if re.match(pattern, form_data.size): + app.state.config.IMAGE_SIZE = form_data.size + return { + "IMAGE_SIZE": app.state.config.IMAGE_SIZE, + "status": True, + } + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."), + ) + + +class ImageStepsUpdateForm(BaseModel): + steps: int + + +@app.get("/steps") +async def get_image_size(user=Depends(get_admin_user)): + return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS} + + +@app.post("/steps/update") +async def update_image_size( + form_data: ImageStepsUpdateForm, user=Depends(get_admin_user) +): + if form_data.steps >= 0: + app.state.config.IMAGE_STEPS = form_data.steps + return { + "IMAGE_STEPS": app.state.config.IMAGE_STEPS, + "status": True, + } + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."), + ) + + +@app.get("/models") +def get_models(user=Depends(get_verified_user)): + try: + if app.state.config.ENGINE == "openai": + return [ + {"id": "dall-e-2", "name": "DALL·E 2"}, + {"id": "dall-e-3", "name": "DALL·E 3"}, + ] + elif app.state.config.ENGINE == "comfyui": + + r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info") + info = r.json() + + return list( + map( + lambda model: {"id": model, "name": model}, + info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0], + ) + ) + + else: + r = requests.get( + url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models", + headers={"authorization": get_automatic1111_api_auth()}, + ) + models = r.json() + return list( + map( + lambda model: {"id": model["title"], "name": model["model_name"]}, + models, + ) + ) + except Exception as e: + app.state.config.ENABLED = False + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +@app.get("/models/default") +async def get_default_model(user=Depends(get_admin_user)): + try: + if app.state.config.ENGINE == "openai": + return { + "model": ( + app.state.config.MODEL if app.state.config.MODEL else "dall-e-2" + ) + } + elif app.state.config.ENGINE == "comfyui": + return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")} + else: + r = requests.get( + url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", + headers={"authorization": get_automatic1111_api_auth()}, + ) + options = r.json() + return {"model": options["sd_model_checkpoint"]} + except Exception as e: + app.state.config.ENABLED = False + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e)) + + +class UpdateModelForm(BaseModel): + model: str + + +def set_model_handler(model: str): + if app.state.config.ENGINE in ["openai", "comfyui"]: + app.state.config.MODEL = model + return app.state.config.MODEL + else: + api_auth = get_automatic1111_api_auth() + r = requests.get( + url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", + headers={"authorization": api_auth}, + ) + options = r.json() + + if model != options["sd_model_checkpoint"]: + options["sd_model_checkpoint"] = model + r = requests.post( + url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", + json=options, + headers={"authorization": api_auth}, + ) + + return options + + +@app.post("/models/default/update") +def update_default_model( + form_data: UpdateModelForm, + user=Depends(get_verified_user), +): + return set_model_handler(form_data.model) + + +class GenerateImageForm(BaseModel): + model: Optional[str] = None + prompt: str + n: int = 1 + size: Optional[str] = None + negative_prompt: Optional[str] = None + + +def save_b64_image(b64_str): + try: + image_id = str(uuid.uuid4()) + + if "," in b64_str: + header, encoded = b64_str.split(",", 1) + mime_type = header.split(";")[0] + + img_data = base64.b64decode(encoded) + image_format = mimetypes.guess_extension(mime_type) + + image_filename = f"{image_id}{image_format}" + file_path = IMAGE_CACHE_DIR / f"{image_filename}" + with open(file_path, "wb") as f: + f.write(img_data) + return image_filename + else: + image_filename = f"{image_id}.png" + file_path = IMAGE_CACHE_DIR.joinpath(image_filename) + + img_data = base64.b64decode(b64_str) + + # Write the image data to a file + with open(file_path, "wb") as f: + f.write(img_data) + return image_filename + + except Exception as e: + log.exception(f"Error saving image: {e}") + return None + + +def save_url_image(url): + image_id = str(uuid.uuid4()) + try: + r = requests.get(url) + r.raise_for_status() + if r.headers["content-type"].split("/")[0] == "image": + + mime_type = r.headers["content-type"] + image_format = mimetypes.guess_extension(mime_type) + + if not image_format: + raise ValueError("Could not determine image type from MIME type") + + image_filename = f"{image_id}{image_format}" + + file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}") + with open(file_path, "wb") as image_file: + for chunk in r.iter_content(chunk_size=8192): + image_file.write(chunk) + return image_filename + else: + log.error(f"Url does not point to an image.") + return None + + except Exception as e: + log.exception(f"Error saving image: {e}") + return None + + +@app.post("/generations") +async def image_generations( + form_data: GenerateImageForm, + user=Depends(get_verified_user), +): + width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x"))) + + r = None + try: + if app.state.config.ENGINE == "openai": + + headers = {} + headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + data = { + "model": ( + app.state.config.MODEL + if app.state.config.MODEL != "" + else "dall-e-2" + ), + "prompt": form_data.prompt, + "n": form_data.n, + "size": ( + form_data.size if form_data.size else app.state.config.IMAGE_SIZE + ), + "response_format": "b64_json", + } + + r = requests.post( + url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations", + json=data, + headers=headers, + ) + + r.raise_for_status() + res = r.json() + + images = [] + + for image in res["data"]: + image_filename = save_b64_image(image["b64_json"]) + images.append({"url": f"/cache/image/generations/{image_filename}"}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") + + with open(file_body_path, "w") as f: + json.dump(data, f) + + return images + + elif app.state.config.ENGINE == "comfyui": + + data = { + "prompt": form_data.prompt, + "width": width, + "height": height, + "n": form_data.n, + } + + if app.state.config.IMAGE_STEPS is not None: + data["steps"] = app.state.config.IMAGE_STEPS + + if form_data.negative_prompt is not None: + data["negative_prompt"] = form_data.negative_prompt + + if app.state.config.COMFYUI_CFG_SCALE: + data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE + + if app.state.config.COMFYUI_SAMPLER is not None: + data["sampler"] = app.state.config.COMFYUI_SAMPLER + + if app.state.config.COMFYUI_SCHEDULER is not None: + data["scheduler"] = app.state.config.COMFYUI_SCHEDULER + + if app.state.config.COMFYUI_SD3 is not None: + data["sd3"] = app.state.config.COMFYUI_SD3 + + data = ImageGenerationPayload(**data) + + res = comfyui_generate_image( + app.state.config.MODEL, + data, + user.id, + app.state.config.COMFYUI_BASE_URL, + ) + log.debug(f"res: {res}") + + images = [] + + for image in res["data"]: + image_filename = save_url_image(image["url"]) + images.append({"url": f"/cache/image/generations/{image_filename}"}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") + + with open(file_body_path, "w") as f: + json.dump(data.model_dump(exclude_none=True), f) + + log.debug(f"images: {images}") + return images + else: + if form_data.model: + set_model_handler(form_data.model) + + data = { + "prompt": form_data.prompt, + "batch_size": form_data.n, + "width": width, + "height": height, + } + + if app.state.config.IMAGE_STEPS is not None: + data["steps"] = app.state.config.IMAGE_STEPS + + if form_data.negative_prompt is not None: + data["negative_prompt"] = form_data.negative_prompt + + r = requests.post( + url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img", + json=data, + headers={"authorization": get_automatic1111_api_auth()}, + ) + + res = r.json() + + log.debug(f"res: {res}") + + images = [] + + for image in res["images"]: + image_filename = save_b64_image(image) + images.append({"url": f"/cache/image/generations/{image_filename}"}) + file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json") + + with open(file_body_path, "w") as f: + json.dump({**data, "info": res["info"]}, f) + + return images + + except Exception as e: + error = e + + if r != None: + data = r.json() + if "error" in data: + error = data["error"]["message"] + raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error)) diff --git a/backend/apps/images/utils/comfyui.py b/backend/apps/images/utils/comfyui.py new file mode 100644 index 0000000000000000000000000000000000000000..599b1f337996764668aa9fcf57e3abae0055b5ab --- /dev/null +++ b/backend/apps/images/utils/comfyui.py @@ -0,0 +1,250 @@ +import websocket # NOTE: websocket-client (https://github.com/websocket-client/websocket-client) +import uuid +import json +import urllib.request +import urllib.parse +import random +import logging + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["COMFYUI"]) + +from pydantic import BaseModel + +from typing import Optional + +COMFYUI_DEFAULT_PROMPT = """ +{ + "3": { + "inputs": { + "seed": 0, + "steps": 20, + "cfg": 8, + "sampler_name": "euler", + "scheduler": "normal", + "denoise": 1, + "model": [ + "4", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "class_type": "KSampler", + "_meta": { + "title": "KSampler" + } + }, + "4": { + "inputs": { + "ckpt_name": "model.safetensors" + }, + "class_type": "CheckpointLoaderSimple", + "_meta": { + "title": "Load Checkpoint" + } + }, + "5": { + "inputs": { + "width": 512, + "height": 512, + "batch_size": 1 + }, + "class_type": "EmptyLatentImage", + "_meta": { + "title": "Empty Latent Image" + } + }, + "6": { + "inputs": { + "text": "Prompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "7": { + "inputs": { + "text": "Negative Prompt", + "clip": [ + "4", + 1 + ] + }, + "class_type": "CLIPTextEncode", + "_meta": { + "title": "CLIP Text Encode (Prompt)" + } + }, + "8": { + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "9": { + "inputs": { + "filename_prefix": "ComfyUI", + "images": [ + "8", + 0 + ] + }, + "class_type": "SaveImage", + "_meta": { + "title": "Save Image" + } + } +} +""" + + +def queue_prompt(prompt, client_id, base_url): + log.info("queue_prompt") + p = {"prompt": prompt, "client_id": client_id} + data = json.dumps(p).encode("utf-8") + req = urllib.request.Request(f"{base_url}/prompt", data=data) + return json.loads(urllib.request.urlopen(req).read()) + + +def get_image(filename, subfolder, folder_type, base_url): + log.info("get_image") + data = {"filename": filename, "subfolder": subfolder, "type": folder_type} + url_values = urllib.parse.urlencode(data) + with urllib.request.urlopen(f"{base_url}/view?{url_values}") as response: + return response.read() + + +def get_image_url(filename, subfolder, folder_type, base_url): + log.info("get_image") + data = {"filename": filename, "subfolder": subfolder, "type": folder_type} + url_values = urllib.parse.urlencode(data) + return f"{base_url}/view?{url_values}" + + +def get_history(prompt_id, base_url): + log.info("get_history") + with urllib.request.urlopen(f"{base_url}/history/{prompt_id}") as response: + return json.loads(response.read()) + + +def get_images(ws, prompt, client_id, base_url): + prompt_id = queue_prompt(prompt, client_id, base_url)["prompt_id"] + output_images = [] + while True: + out = ws.recv() + if isinstance(out, str): + message = json.loads(out) + if message["type"] == "executing": + data = message["data"] + if data["node"] is None and data["prompt_id"] == prompt_id: + break # Execution is done + else: + continue # previews are binary data + + history = get_history(prompt_id, base_url)[prompt_id] + for o in history["outputs"]: + for node_id in history["outputs"]: + node_output = history["outputs"][node_id] + if "images" in node_output: + for image in node_output["images"]: + url = get_image_url( + image["filename"], image["subfolder"], image["type"], base_url + ) + output_images.append({"url": url}) + return {"data": output_images} + + +class ImageGenerationPayload(BaseModel): + prompt: str + negative_prompt: Optional[str] = "" + steps: Optional[int] = None + seed: Optional[int] = None + width: int + height: int + n: int = 1 + cfg_scale: Optional[float] = None + sampler: Optional[str] = None + scheduler: Optional[str] = None + sd3: Optional[bool] = None + + +def comfyui_generate_image( + model: str, payload: ImageGenerationPayload, client_id, base_url +): + ws_url = base_url.replace("http://", "ws://").replace("https://", "wss://") + + comfyui_prompt = json.loads(COMFYUI_DEFAULT_PROMPT) + + if payload.cfg_scale: + comfyui_prompt["3"]["inputs"]["cfg"] = payload.cfg_scale + + if payload.sampler: + comfyui_prompt["3"]["inputs"]["sampler"] = payload.sampler + + if payload.scheduler: + comfyui_prompt["3"]["inputs"]["scheduler"] = payload.scheduler + + if payload.sd3: + comfyui_prompt["5"]["class_type"] = "EmptySD3LatentImage" + + comfyui_prompt["4"]["inputs"]["ckpt_name"] = model + comfyui_prompt["5"]["inputs"]["batch_size"] = payload.n + comfyui_prompt["5"]["inputs"]["width"] = payload.width + comfyui_prompt["5"]["inputs"]["height"] = payload.height + + # set the text prompt for our positive CLIPTextEncode + comfyui_prompt["6"]["inputs"]["text"] = payload.prompt + comfyui_prompt["7"]["inputs"]["text"] = payload.negative_prompt + + if payload.steps: + comfyui_prompt["3"]["inputs"]["steps"] = payload.steps + + comfyui_prompt["3"]["inputs"]["seed"] = ( + payload.seed if payload.seed else random.randint(0, 18446744073709551614) + ) + + try: + ws = websocket.WebSocket() + ws.connect(f"{ws_url}/ws?clientId={client_id}") + log.info("WebSocket connection established.") + except Exception as e: + log.exception(f"Failed to connect to WebSocket server: {e}") + return None + + try: + images = get_images(ws, comfyui_prompt, client_id, base_url) + except Exception as e: + log.exception(f"Error while receiving images: {e}") + images = None + + ws.close() + + return images diff --git a/backend/apps/ollama/main.py b/backend/apps/ollama/main.py new file mode 100644 index 0000000000000000000000000000000000000000..0e4d54e465158b100d1963c27fdd2c148f646342 --- /dev/null +++ b/backend/apps/ollama/main.py @@ -0,0 +1,1280 @@ +from fastapi import ( + FastAPI, + Request, + Response, + HTTPException, + Depends, + status, + UploadFile, + File, + BackgroundTasks, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse +from fastapi.concurrency import run_in_threadpool + +from pydantic import BaseModel, ConfigDict + +import os +import re +import copy +import random +import requests +import json +import uuid +import aiohttp +import asyncio +import logging +import time +from urllib.parse import urlparse +from typing import Optional, List, Union + +from starlette.background import BackgroundTask + +from apps.webui.models.models import Models +from apps.webui.models.users import Users +from constants import ERROR_MESSAGES +from utils.utils import ( + decode_token, + get_current_user, + get_verified_user, + get_admin_user, +) +from utils.task import prompt_template + + +from config import ( + SRC_LOG_LEVELS, + OLLAMA_BASE_URLS, + ENABLE_OLLAMA_API, + AIOHTTP_CLIENT_TIMEOUT, + ENABLE_MODEL_FILTER, + MODEL_FILTER_LIST, + UPLOAD_DIR, + AppConfig, +) +from utils.misc import calculate_sha256, add_or_update_system_message + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OLLAMA"]) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.state.config = AppConfig() + +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API +app.state.config.OLLAMA_BASE_URLS = OLLAMA_BASE_URLS +app.state.MODELS = {} + + +# TODO: Implement a more intelligent load balancing mechanism for distributing requests among multiple backend instances. +# Current implementation uses a simple round-robin approach (random.choice). Consider incorporating algorithms like weighted round-robin, +# least connections, or least response time for better resource utilization and performance optimization. + + +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response + + +@app.head("/") +@app.get("/") +async def get_status(): + return {"status": True} + + +@app.get("/config") +async def get_config(user=Depends(get_admin_user)): + return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API} + + +class OllamaConfigForm(BaseModel): + enable_ollama_api: Optional[bool] = None + + +@app.post("/config/update") +async def update_config(form_data: OllamaConfigForm, user=Depends(get_admin_user)): + app.state.config.ENABLE_OLLAMA_API = form_data.enable_ollama_api + return {"ENABLE_OLLAMA_API": app.state.config.ENABLE_OLLAMA_API} + + +@app.get("/urls") +async def get_ollama_api_urls(user=Depends(get_admin_user)): + return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS} + + +class UrlUpdateForm(BaseModel): + urls: List[str] + + +@app.post("/urls/update") +async def update_ollama_api_url(form_data: UrlUpdateForm, user=Depends(get_admin_user)): + app.state.config.OLLAMA_BASE_URLS = form_data.urls + + log.info(f"app.state.config.OLLAMA_BASE_URLS: {app.state.config.OLLAMA_BASE_URLS}") + return {"OLLAMA_BASE_URLS": app.state.config.OLLAMA_BASE_URLS} + + +async def fetch_url(url): + timeout = aiohttp.ClientTimeout(total=5) + try: + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(url) as response: + return await response.json() + except Exception as e: + # Handle connection error here + log.error(f"Connection error: {e}") + return None + + +async def cleanup_response( + response: Optional[aiohttp.ClientResponse], + session: Optional[aiohttp.ClientSession], +): + if response: + response.close() + if session: + await session.close() + + +async def post_streaming_url(url: str, payload: str, stream: bool = True): + r = None + try: + session = aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) + r = await session.post(url, data=payload) + r.raise_for_status() + + if stream: + return StreamingResponse( + r.content, + status_code=r.status, + headers=dict(r.headers), + background=BackgroundTask( + cleanup_response, response=r, session=session + ), + ) + else: + res = await r.json() + await cleanup_response(r, session) + return res + + except Exception as e: + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = await r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status if r else 500, + detail=error_detail, + ) + + +def merge_models_lists(model_lists): + merged_models = {} + + for idx, model_list in enumerate(model_lists): + if model_list is not None: + for model in model_list: + digest = model["digest"] + if digest not in merged_models: + model["urls"] = [idx] + merged_models[digest] = model + else: + merged_models[digest]["urls"].append(idx) + + return list(merged_models.values()) + + +async def get_all_models(): + log.info("get_all_models()") + + if app.state.config.ENABLE_OLLAMA_API: + tasks = [ + fetch_url(f"{url}/api/tags") for url in app.state.config.OLLAMA_BASE_URLS + ] + responses = await asyncio.gather(*tasks) + + models = { + "models": merge_models_lists( + map( + lambda response: response["models"] if response else None, responses + ) + ) + } + + else: + models = {"models": []} + + app.state.MODELS = {model["model"]: model for model in models["models"]} + + return models + + +@app.get("/api/tags") +@app.get("/api/tags/{url_idx}") +async def get_ollama_tags( + url_idx: Optional[int] = None, user=Depends(get_verified_user) +): + if url_idx == None: + models = await get_all_models() + + if app.state.config.ENABLE_MODEL_FILTER: + if user.role == "user": + models["models"] = list( + filter( + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, + models["models"], + ) + ) + return models + return models + else: + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + + r = None + try: + r = requests.request(method="GET", url=f"{url}/api/tags") + r.raise_for_status() + + return r.json() + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.get("/api/version") +@app.get("/api/version/{url_idx}") +async def get_ollama_versions(url_idx: Optional[int] = None): + if app.state.config.ENABLE_OLLAMA_API: + if url_idx == None: + + # returns lowest version + tasks = [ + fetch_url(f"{url}/api/version") + for url in app.state.config.OLLAMA_BASE_URLS + ] + responses = await asyncio.gather(*tasks) + responses = list(filter(lambda x: x is not None, responses)) + + if len(responses) > 0: + lowest_version = min( + responses, + key=lambda x: tuple( + map(int, re.sub(r"^v|-.*", "", x["version"]).split(".")) + ), + ) + + return {"version": lowest_version["version"]} + else: + raise HTTPException( + status_code=500, + detail=ERROR_MESSAGES.OLLAMA_NOT_FOUND, + ) + else: + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + + r = None + try: + r = requests.request(method="GET", url=f"{url}/api/version") + r.raise_for_status() + + return r.json() + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + else: + return {"version": False} + + +class ModelNameForm(BaseModel): + name: str + + +@app.post("/api/pull") +@app.post("/api/pull/{url_idx}") +async def pull_model( + form_data: ModelNameForm, url_idx: int = 0, user=Depends(get_admin_user) +): + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + r = None + + # Admin should be able to pull models from any source + payload = {**form_data.model_dump(exclude_none=True), "insecure": True} + + return await post_streaming_url(f"{url}/api/pull", json.dumps(payload)) + + +class PushModelForm(BaseModel): + name: str + insecure: Optional[bool] = None + stream: Optional[bool] = None + + +@app.delete("/api/push") +@app.delete("/api/push/{url_idx}") +async def push_model( + form_data: PushModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.name in app.state.MODELS: + url_idx = app.state.MODELS[form_data.name]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.debug(f"url: {url}") + + return await post_streaming_url( + f"{url}/api/push", form_data.model_dump_json(exclude_none=True).encode() + ) + + +class CreateModelForm(BaseModel): + name: str + modelfile: Optional[str] = None + stream: Optional[bool] = None + path: Optional[str] = None + + +@app.post("/api/create") +@app.post("/api/create/{url_idx}") +async def create_model( + form_data: CreateModelForm, url_idx: int = 0, user=Depends(get_admin_user) +): + log.debug(f"form_data: {form_data}") + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + return await post_streaming_url( + f"{url}/api/create", form_data.model_dump_json(exclude_none=True).encode() + ) + + +class CopyModelForm(BaseModel): + source: str + destination: str + + +@app.post("/api/copy") +@app.post("/api/copy/{url_idx}") +async def copy_model( + form_data: CopyModelForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.source in app.state.MODELS: + url_idx = app.state.MODELS[form_data.source]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.source), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + try: + r = requests.request( + method="POST", + url=f"{url}/api/copy", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + log.debug(f"r.text: {r.text}") + + return True + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.delete("/api/delete") +@app.delete("/api/delete/{url_idx}") +async def delete_model( + form_data: ModelNameForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + if form_data.name in app.state.MODELS: + url_idx = app.state.MODELS[form_data.name]["urls"][0] + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + try: + r = requests.request( + method="DELETE", + url=f"{url}/api/delete", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + log.debug(f"r.text: {r.text}") + + return True + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.post("/api/show") +async def show_model_info(form_data: ModelNameForm, user=Depends(get_verified_user)): + if form_data.name not in app.state.MODELS: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.name), + ) + + url_idx = random.choice(app.state.MODELS[form_data.name]["urls"]) + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + try: + r = requests.request( + method="POST", + url=f"{url}/api/show", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + return r.json() + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class GenerateEmbeddingsForm(BaseModel): + model: str + prompt: str + options: Optional[dict] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/embeddings") +@app.post("/api/embeddings/{url_idx}") +async def generate_embeddings( + form_data: GenerateEmbeddingsForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + if url_idx == None: + model = form_data.model + + if ":" not in model: + model = f"{model}:latest" + + if model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + try: + r = requests.request( + method="POST", + url=f"{url}/api/embeddings", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + return r.json() + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +def generate_ollama_embeddings( + form_data: GenerateEmbeddingsForm, + url_idx: Optional[int] = None, +): + + log.info(f"generate_ollama_embeddings {form_data}") + + if url_idx == None: + model = form_data.model + + if ":" not in model: + model = f"{model}:latest" + + if model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + try: + r = requests.request( + method="POST", + url=f"{url}/api/embeddings", + data=form_data.model_dump_json(exclude_none=True).encode(), + ) + r.raise_for_status() + + data = r.json() + + log.info(f"generate_ollama_embeddings {data}") + + if "embedding" in data: + return data["embedding"] + else: + raise "Something went wrong :/" + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise error_detail + + +class GenerateCompletionForm(BaseModel): + model: str + prompt: str + images: Optional[List[str]] = None + format: Optional[str] = None + options: Optional[dict] = None + system: Optional[str] = None + template: Optional[str] = None + context: Optional[str] = None + stream: Optional[bool] = True + raw: Optional[bool] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/generate") +@app.post("/api/generate/{url_idx}") +async def generate_completion( + form_data: GenerateCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + + if url_idx == None: + model = form_data.model + + if ":" not in model: + model = f"{model}:latest" + + if model in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[model]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + return await post_streaming_url( + f"{url}/api/generate", form_data.model_dump_json(exclude_none=True).encode() + ) + + +class ChatMessage(BaseModel): + role: str + content: str + images: Optional[List[str]] = None + + +class GenerateChatCompletionForm(BaseModel): + model: str + messages: List[ChatMessage] + format: Optional[str] = None + options: Optional[dict] = None + template: Optional[str] = None + stream: Optional[bool] = None + keep_alive: Optional[Union[int, str]] = None + + +@app.post("/api/chat") +@app.post("/api/chat/{url_idx}") +async def generate_chat_completion( + form_data: GenerateChatCompletionForm, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + + log.debug( + "form_data.model_dump_json(exclude_none=True).encode(): {0} ".format( + form_data.model_dump_json(exclude_none=True).encode() + ) + ) + + payload = { + **form_data.model_dump(exclude_none=True, exclude=["metadata"]), + } + if "metadata" in payload: + del payload["metadata"] + + model_id = form_data.model + model_info = Models.get_model_by_id(model_id) + + if model_info: + if model_info.base_model_id: + payload["model"] = model_info.base_model_id + + model_info.params = model_info.params.model_dump() + + if model_info.params: + if payload.get("options") is None: + payload["options"] = {} + + if ( + model_info.params.get("mirostat", None) + and payload["options"].get("mirostat") is None + ): + payload["options"]["mirostat"] = model_info.params.get("mirostat", None) + + if ( + model_info.params.get("mirostat_eta", None) + and payload["options"].get("mirostat_eta") is None + ): + payload["options"]["mirostat_eta"] = model_info.params.get( + "mirostat_eta", None + ) + + if ( + model_info.params.get("mirostat_tau", None) + and payload["options"].get("mirostat_tau") is None + ): + payload["options"]["mirostat_tau"] = model_info.params.get( + "mirostat_tau", None + ) + + if ( + model_info.params.get("num_ctx", None) + and payload["options"].get("num_ctx") is None + ): + payload["options"]["num_ctx"] = model_info.params.get("num_ctx", None) + + if ( + model_info.params.get("num_batch", None) + and payload["options"].get("num_batch") is None + ): + payload["options"]["num_batch"] = model_info.params.get( + "num_batch", None + ) + + if ( + model_info.params.get("num_keep", None) + and payload["options"].get("num_keep") is None + ): + payload["options"]["num_keep"] = model_info.params.get("num_keep", None) + + if ( + model_info.params.get("repeat_last_n", None) + and payload["options"].get("repeat_last_n") is None + ): + payload["options"]["repeat_last_n"] = model_info.params.get( + "repeat_last_n", None + ) + + if ( + model_info.params.get("frequency_penalty", None) + and payload["options"].get("frequency_penalty") is None + ): + payload["options"]["repeat_penalty"] = model_info.params.get( + "frequency_penalty", None + ) + + if ( + model_info.params.get("temperature", None) is not None + and payload["options"].get("temperature") is None + ): + payload["options"]["temperature"] = model_info.params.get( + "temperature", None + ) + + if ( + model_info.params.get("seed", None) is not None + and payload["options"].get("seed") is None + ): + payload["options"]["seed"] = model_info.params.get("seed", None) + + if ( + model_info.params.get("stop", None) + and payload["options"].get("stop") is None + ): + payload["options"]["stop"] = ( + [ + bytes(stop, "utf-8").decode("unicode_escape") + for stop in model_info.params["stop"] + ] + if model_info.params.get("stop", None) + else None + ) + + if ( + model_info.params.get("tfs_z", None) + and payload["options"].get("tfs_z") is None + ): + payload["options"]["tfs_z"] = model_info.params.get("tfs_z", None) + + if ( + model_info.params.get("max_tokens", None) + and payload["options"].get("max_tokens") is None + ): + payload["options"]["num_predict"] = model_info.params.get( + "max_tokens", None + ) + + if ( + model_info.params.get("top_k", None) + and payload["options"].get("top_k") is None + ): + payload["options"]["top_k"] = model_info.params.get("top_k", None) + + if ( + model_info.params.get("top_p", None) + and payload["options"].get("top_p") is None + ): + payload["options"]["top_p"] = model_info.params.get("top_p", None) + + if ( + model_info.params.get("use_mmap", None) + and payload["options"].get("use_mmap") is None + ): + payload["options"]["use_mmap"] = model_info.params.get("use_mmap", None) + + if ( + model_info.params.get("use_mlock", None) + and payload["options"].get("use_mlock") is None + ): + payload["options"]["use_mlock"] = model_info.params.get( + "use_mlock", None + ) + + if ( + model_info.params.get("num_thread", None) + and payload["options"].get("num_thread") is None + ): + payload["options"]["num_thread"] = model_info.params.get( + "num_thread", None + ) + + system = model_info.params.get("system", None) + if system: + system = prompt_template( + system, + **( + { + "user_name": user.name, + "user_location": ( + user.info.get("location") if user.info else None + ), + } + if user + else {} + ), + ) + + if payload.get("messages"): + payload["messages"] = add_or_update_system_message( + system, payload["messages"] + ) + + if url_idx == None: + if ":" not in payload["model"]: + payload["model"] = f"{payload['model']}:latest" + + if payload["model"] in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + log.debug(payload) + + return await post_streaming_url(f"{url}/api/chat", json.dumps(payload)) + + +# TODO: we should update this part once Ollama supports other types +class OpenAIChatMessageContent(BaseModel): + type: str + model_config = ConfigDict(extra="allow") + + +class OpenAIChatMessage(BaseModel): + role: str + content: Union[str, OpenAIChatMessageContent] + + model_config = ConfigDict(extra="allow") + + +class OpenAIChatCompletionForm(BaseModel): + model: str + messages: List[OpenAIChatMessage] + + model_config = ConfigDict(extra="allow") + + +@app.post("/v1/chat/completions") +@app.post("/v1/chat/completions/{url_idx}") +async def generate_openai_chat_completion( + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + form_data = OpenAIChatCompletionForm(**form_data) + payload = {**form_data.model_dump(exclude_none=True, exclude=["metadata"])} + + if "metadata" in payload: + del payload["metadata"] + + model_id = form_data.model + model_info = Models.get_model_by_id(model_id) + + if model_info: + if model_info.base_model_id: + payload["model"] = model_info.base_model_id + + model_info.params = model_info.params.model_dump() + + if model_info.params: + payload["temperature"] = model_info.params.get("temperature", None) + payload["top_p"] = model_info.params.get("top_p", None) + payload["max_tokens"] = model_info.params.get("max_tokens", None) + payload["frequency_penalty"] = model_info.params.get( + "frequency_penalty", None + ) + payload["seed"] = model_info.params.get("seed", None) + payload["stop"] = ( + [ + bytes(stop, "utf-8").decode("unicode_escape") + for stop in model_info.params["stop"] + ] + if model_info.params.get("stop", None) + else None + ) + + system = model_info.params.get("system", None) + + if system: + system = prompt_template( + system, + **( + { + "user_name": user.name, + "user_location": ( + user.info.get("location") if user.info else None + ), + } + if user + else {} + ), + ) + # Check if the payload already has a system message + # If not, add a system message to the payload + if payload.get("messages"): + for message in payload["messages"]: + if message.get("role") == "system": + message["content"] = system + message["content"] + break + else: + payload["messages"].insert( + 0, + { + "role": "system", + "content": system, + }, + ) + + if url_idx == None: + if ":" not in payload["model"]: + payload["model"] = f"{payload['model']}:latest" + + if payload["model"] in app.state.MODELS: + url_idx = random.choice(app.state.MODELS[payload["model"]]["urls"]) + else: + raise HTTPException( + status_code=400, + detail=ERROR_MESSAGES.MODEL_NOT_FOUND(form_data.model), + ) + + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + log.info(f"url: {url}") + + return await post_streaming_url( + f"{url}/v1/chat/completions", + json.dumps(payload), + stream=payload.get("stream", False), + ) + + +@app.get("/v1/models") +@app.get("/v1/models/{url_idx}") +async def get_openai_models( + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + if url_idx == None: + models = await get_all_models() + + if app.state.config.ENABLE_MODEL_FILTER: + if user.role == "user": + models["models"] = list( + filter( + lambda model: model["name"] + in app.state.config.MODEL_FILTER_LIST, + models["models"], + ) + ) + + return { + "data": [ + { + "id": model["model"], + "object": "model", + "created": int(time.time()), + "owned_by": "openai", + } + for model in models["models"] + ], + "object": "list", + } + + else: + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + try: + r = requests.request(method="GET", url=f"{url}/api/tags") + r.raise_for_status() + + models = r.json() + + return { + "data": [ + { + "id": model["model"], + "object": "model", + "created": int(time.time()), + "owned_by": "openai", + } + for model in models["models"] + ], + "object": "list", + } + + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"Ollama: {res['error']}" + except: + error_detail = f"Ollama: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +class UrlForm(BaseModel): + url: str + + +class UploadBlobForm(BaseModel): + filename: str + + +def parse_huggingface_url(hf_url): + try: + # Parse the URL + parsed_url = urlparse(hf_url) + + # Get the path and split it into components + path_components = parsed_url.path.split("/") + + # Extract the desired output + user_repo = "/".join(path_components[1:3]) + model_file = path_components[-1] + + return model_file + except ValueError: + return None + + +async def download_file_stream( + ollama_url, file_url, file_path, file_name, chunk_size=1024 * 1024 +): + done = False + + if os.path.exists(file_path): + current_size = os.path.getsize(file_path) + else: + current_size = 0 + + headers = {"Range": f"bytes={current_size}-"} if current_size > 0 else {} + + timeout = aiohttp.ClientTimeout(total=600) # Set the timeout + + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(file_url, headers=headers) as response: + total_size = int(response.headers.get("content-length", 0)) + current_size + + with open(file_path, "ab+") as file: + async for data in response.content.iter_chunked(chunk_size): + current_size += len(data) + file.write(data) + + done = current_size == total_size + progress = round((current_size / total_size) * 100, 2) + + yield f'data: {{"progress": {progress}, "completed": {current_size}, "total": {total_size}}}\n\n' + + if done: + file.seek(0) + hashed = calculate_sha256(file) + file.seek(0) + + url = f"{ollama_url}/api/blobs/sha256:{hashed}" + response = requests.post(url, data=file) + + if response.ok: + res = { + "done": done, + "blob": f"sha256:{hashed}", + "name": file_name, + } + os.remove(file_path) + + yield f"data: {json.dumps(res)}\n\n" + else: + raise "Ollama: Could not create blob, Please try again." + + +# url = "https://huggingface.co/TheBloke/stablelm-zephyr-3b-GGUF/resolve/main/stablelm-zephyr-3b.Q2_K.gguf" +@app.post("/models/download") +@app.post("/models/download/{url_idx}") +async def download_model( + form_data: UrlForm, + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + + allowed_hosts = ["https://huggingface.co/", "https://github.com/"] + + if not any(form_data.url.startswith(host) for host in allowed_hosts): + raise HTTPException( + status_code=400, + detail="Invalid file_url. Only URLs from allowed hosts are permitted.", + ) + + if url_idx == None: + url_idx = 0 + url = app.state.config.OLLAMA_BASE_URLS[url_idx] + + file_name = parse_huggingface_url(form_data.url) + + if file_name: + file_path = f"{UPLOAD_DIR}/{file_name}" + + return StreamingResponse( + download_file_stream(url, form_data.url, file_path, file_name), + ) + else: + return None + + +@app.post("/models/upload") +@app.post("/models/upload/{url_idx}") +def upload_model( + file: UploadFile = File(...), + url_idx: Optional[int] = None, + user=Depends(get_admin_user), +): + if url_idx == None: + url_idx = 0 + ollama_url = app.state.config.OLLAMA_BASE_URLS[url_idx] + + file_path = f"{UPLOAD_DIR}/{file.filename}" + + # Save file in chunks + with open(file_path, "wb+") as f: + for chunk in file.file: + f.write(chunk) + + def file_process_stream(): + nonlocal ollama_url + total_size = os.path.getsize(file_path) + chunk_size = 1024 * 1024 + try: + with open(file_path, "rb") as f: + total = 0 + done = False + + while not done: + chunk = f.read(chunk_size) + if not chunk: + done = True + continue + + total += len(chunk) + progress = round((total / total_size) * 100, 2) + + res = { + "progress": progress, + "total": total_size, + "completed": total, + } + yield f"data: {json.dumps(res)}\n\n" + + if done: + f.seek(0) + hashed = calculate_sha256(f) + f.seek(0) + + url = f"{ollama_url}/api/blobs/sha256:{hashed}" + response = requests.post(url, data=f) + + if response.ok: + res = { + "done": done, + "blob": f"sha256:{hashed}", + "name": file.filename, + } + os.remove(file_path) + yield f"data: {json.dumps(res)}\n\n" + else: + raise Exception( + "Ollama: Could not create blob, Please try again." + ) + + except Exception as e: + res = {"error": str(e)} + yield f"data: {json.dumps(res)}\n\n" + + return StreamingResponse(file_process_stream(), media_type="text/event-stream") diff --git a/backend/apps/openai/main.py b/backend/apps/openai/main.py new file mode 100644 index 0000000000000000000000000000000000000000..c712709a5cca8e00fba54032b0956ba73efbb533 --- /dev/null +++ b/backend/apps/openai/main.py @@ -0,0 +1,579 @@ +from fastapi import FastAPI, Request, Response, HTTPException, Depends +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, JSONResponse, FileResponse + +import requests +import aiohttp +import asyncio +import json +import logging + +from pydantic import BaseModel +from starlette.background import BackgroundTask + +from apps.webui.models.models import Models +from apps.webui.models.users import Users +from constants import ERROR_MESSAGES +from utils.utils import ( + decode_token, + get_verified_user, + get_verified_user, + get_admin_user, +) +from utils.task import prompt_template +from utils.misc import add_or_update_system_message + +from config import ( + SRC_LOG_LEVELS, + ENABLE_OPENAI_API, + AIOHTTP_CLIENT_TIMEOUT, + OPENAI_API_BASE_URLS, + OPENAI_API_KEYS, + CACHE_DIR, + ENABLE_MODEL_FILTER, + MODEL_FILTER_LIST, + AppConfig, +) +from typing import List, Optional + + +import hashlib +from pathlib import Path + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["OPENAI"]) + +app = FastAPI() +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +app.state.config = AppConfig() + +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API +app.state.config.OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS +app.state.config.OPENAI_API_KEYS = OPENAI_API_KEYS + +app.state.MODELS = {} + + +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + response = await call_next(request) + return response + + +@app.get("/config") +async def get_config(user=Depends(get_admin_user)): + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + +class OpenAIConfigForm(BaseModel): + enable_openai_api: Optional[bool] = None + + +@app.post("/config/update") +async def update_config(form_data: OpenAIConfigForm, user=Depends(get_admin_user)): + app.state.config.ENABLE_OPENAI_API = form_data.enable_openai_api + return {"ENABLE_OPENAI_API": app.state.config.ENABLE_OPENAI_API} + + +class UrlsUpdateForm(BaseModel): + urls: List[str] + + +class KeysUpdateForm(BaseModel): + keys: List[str] + + +@app.get("/urls") +async def get_openai_urls(user=Depends(get_admin_user)): + return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS} + + +@app.post("/urls/update") +async def update_openai_urls(form_data: UrlsUpdateForm, user=Depends(get_admin_user)): + await get_all_models() + app.state.config.OPENAI_API_BASE_URLS = form_data.urls + return {"OPENAI_API_BASE_URLS": app.state.config.OPENAI_API_BASE_URLS} + + +@app.get("/keys") +async def get_openai_keys(user=Depends(get_admin_user)): + return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS} + + +@app.post("/keys/update") +async def update_openai_key(form_data: KeysUpdateForm, user=Depends(get_admin_user)): + app.state.config.OPENAI_API_KEYS = form_data.keys + return {"OPENAI_API_KEYS": app.state.config.OPENAI_API_KEYS} + + +@app.post("/audio/speech") +async def speech(request: Request, user=Depends(get_verified_user)): + idx = None + try: + idx = app.state.config.OPENAI_API_BASE_URLS.index("https://api.openai.com/v1") + body = await request.body() + name = hashlib.sha256(body).hexdigest() + + SPEECH_CACHE_DIR = Path(CACHE_DIR).joinpath("./audio/speech/") + SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True) + file_path = SPEECH_CACHE_DIR.joinpath(f"{name}.mp3") + file_body_path = SPEECH_CACHE_DIR.joinpath(f"{name}.json") + + # Check if the file already exists in the cache + if file_path.is_file(): + return FileResponse(file_path) + + headers = {} + headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEYS[idx]}" + headers["Content-Type"] = "application/json" + if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]: + headers["HTTP-Referer"] = "https://openwebui.com/" + headers["X-Title"] = "Open WebUI" + r = None + try: + r = requests.post( + url=f"{app.state.config.OPENAI_API_BASE_URLS[idx]}/audio/speech", + data=body, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + # Save the streaming content to a file + with open(file_path, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + + with open(file_body_path, "w") as f: + json.dump(json.loads(body.decode("utf-8")), f) + + # Return the saved file + return FileResponse(file_path) + + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, detail=error_detail + ) + + except ValueError: + raise HTTPException(status_code=401, detail=ERROR_MESSAGES.OPENAI_NOT_FOUND) + + +async def fetch_url(url, key): + timeout = aiohttp.ClientTimeout(total=5) + try: + headers = {"Authorization": f"Bearer {key}"} + async with aiohttp.ClientSession(timeout=timeout, trust_env=True) as session: + async with session.get(url, headers=headers) as response: + return await response.json() + except Exception as e: + # Handle connection error here + log.error(f"Connection error: {e}") + return None + + +async def cleanup_response( + response: Optional[aiohttp.ClientResponse], + session: Optional[aiohttp.ClientSession], +): + if response: + response.close() + if session: + await session.close() + + +def merge_models_lists(model_lists): + log.debug(f"merge_models_lists {model_lists}") + merged_list = [] + + for idx, models in enumerate(model_lists): + if models is not None and "error" not in models: + merged_list.extend( + [ + { + **model, + "name": model.get("name", model["id"]), + "owned_by": "openai", + "openai": model, + "urlIdx": idx, + } + for model in models + if "api.openai.com" + not in app.state.config.OPENAI_API_BASE_URLS[idx] + or "gpt" in model["id"] + ] + ) + + return merged_list + + +async def get_all_models(raw: bool = False): + log.info("get_all_models()") + + if ( + len(app.state.config.OPENAI_API_KEYS) == 1 + and app.state.config.OPENAI_API_KEYS[0] == "" + ) or not app.state.config.ENABLE_OPENAI_API: + models = {"data": []} + else: + # Check if API KEYS length is same than API URLS length + if len(app.state.config.OPENAI_API_KEYS) != len( + app.state.config.OPENAI_API_BASE_URLS + ): + # if there are more keys than urls, remove the extra keys + if len(app.state.config.OPENAI_API_KEYS) > len( + app.state.config.OPENAI_API_BASE_URLS + ): + app.state.config.OPENAI_API_KEYS = app.state.config.OPENAI_API_KEYS[ + : len(app.state.config.OPENAI_API_BASE_URLS) + ] + # if there are more urls than keys, add empty keys + else: + app.state.config.OPENAI_API_KEYS += [ + "" + for _ in range( + len(app.state.config.OPENAI_API_BASE_URLS) + - len(app.state.config.OPENAI_API_KEYS) + ) + ] + + tasks = [ + fetch_url(f"{url}/models", app.state.config.OPENAI_API_KEYS[idx]) + for idx, url in enumerate(app.state.config.OPENAI_API_BASE_URLS) + ] + + responses = await asyncio.gather(*tasks) + log.debug(f"get_all_models:responses() {responses}") + + if raw: + return responses + + models = { + "data": merge_models_lists( + list( + map( + lambda response: ( + response["data"] + if (response and "data" in response) + else (response if isinstance(response, list) else None) + ), + responses, + ) + ) + ) + } + + log.debug(f"models: {models}") + app.state.MODELS = {model["id"]: model for model in models["data"]} + + return models + + +@app.get("/models") +@app.get("/models/{url_idx}") +async def get_models(url_idx: Optional[int] = None, user=Depends(get_verified_user)): + if url_idx == None: + models = await get_all_models() + if app.state.config.ENABLE_MODEL_FILTER: + if user.role == "user": + models["data"] = list( + filter( + lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, + models["data"], + ) + ) + return models + return models + else: + url = app.state.config.OPENAI_API_BASE_URLS[url_idx] + key = app.state.config.OPENAI_API_KEYS[url_idx] + + headers = {} + headers["Authorization"] = f"Bearer {key}" + headers["Content-Type"] = "application/json" + + r = None + + try: + r = requests.request(method="GET", url=f"{url}/models", headers=headers) + r.raise_for_status() + + response_data = r.json() + if "api.openai.com" in url: + response_data["data"] = list( + filter(lambda model: "gpt" in model["id"], response_data["data"]) + ) + + return response_data + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = r.json() + if "error" in res: + error_detail = f"External: {res['error']}" + except: + error_detail = f"External: {e}" + + raise HTTPException( + status_code=r.status_code if r else 500, + detail=error_detail, + ) + + +@app.post("/chat/completions") +@app.post("/chat/completions/{url_idx}") +async def generate_chat_completion( + form_data: dict, + url_idx: Optional[int] = None, + user=Depends(get_verified_user), +): + idx = 0 + payload = {**form_data} + if "metadata" in payload: + del payload["metadata"] + + model_id = form_data.get("model") + model_info = Models.get_model_by_id(model_id) + + if model_info: + if model_info.base_model_id: + payload["model"] = model_info.base_model_id + + model_info.params = model_info.params.model_dump() + + if model_info.params: + if ( + model_info.params.get("temperature", None) is not None + and payload.get("temperature") is None + ): + payload["temperature"] = float(model_info.params.get("temperature")) + + if model_info.params.get("top_p", None) and payload.get("top_p") is None: + payload["top_p"] = int(model_info.params.get("top_p", None)) + + if ( + model_info.params.get("max_tokens", None) + and payload.get("max_tokens") is None + ): + payload["max_tokens"] = int(model_info.params.get("max_tokens", None)) + + if ( + model_info.params.get("frequency_penalty", None) + and payload.get("frequency_penalty") is None + ): + payload["frequency_penalty"] = int( + model_info.params.get("frequency_penalty", None) + ) + + if ( + model_info.params.get("seed", None) is not None + and payload.get("seed") is None + ): + payload["seed"] = model_info.params.get("seed", None) + + if model_info.params.get("stop", None) and payload.get("stop") is None: + payload["stop"] = ( + [ + bytes(stop, "utf-8").decode("unicode_escape") + for stop in model_info.params["stop"] + ] + if model_info.params.get("stop", None) + else None + ) + + system = model_info.params.get("system", None) + if system: + system = prompt_template( + system, + **( + { + "user_name": user.name, + "user_location": ( + user.info.get("location") if user.info else None + ), + } + if user + else {} + ), + ) + if payload.get("messages"): + payload["messages"] = add_or_update_system_message( + system, payload["messages"] + ) + + else: + pass + + model = app.state.MODELS[payload.get("model")] + idx = model["urlIdx"] + + if "pipeline" in model and model.get("pipeline"): + payload["user"] = { + "name": user.name, + "id": user.id, + "email": user.email, + "role": user.role, + } + + # Check if the model is "gpt-4-vision-preview" and set "max_tokens" to 4000 + # This is a workaround until OpenAI fixes the issue with this model + if payload.get("model") == "gpt-4-vision-preview": + if "max_tokens" not in payload: + payload["max_tokens"] = 4000 + log.debug("Modified payload:", payload) + + # Convert the modified body back to JSON + payload = json.dumps(payload) + + log.debug(payload) + + url = app.state.config.OPENAI_API_BASE_URLS[idx] + key = app.state.config.OPENAI_API_KEYS[idx] + + headers = {} + headers["Authorization"] = f"Bearer {key}" + headers["Content-Type"] = "application/json" + if "openrouter.ai" in app.state.config.OPENAI_API_BASE_URLS[idx]: + headers["HTTP-Referer"] = "https://openwebui.com/" + headers["X-Title"] = "Open WebUI" + + r = None + session = None + streaming = False + + try: + session = aiohttp.ClientSession( + trust_env=True, timeout=aiohttp.ClientTimeout(total=AIOHTTP_CLIENT_TIMEOUT) + ) + r = await session.request( + method="POST", + url=f"{url}/chat/completions", + data=payload, + headers=headers, + ) + + r.raise_for_status() + + # Check if response is SSE + if "text/event-stream" in r.headers.get("Content-Type", ""): + streaming = True + return StreamingResponse( + r.content, + status_code=r.status, + headers=dict(r.headers), + background=BackgroundTask( + cleanup_response, response=r, session=session + ), + ) + else: + response_data = await r.json() + return response_data + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = await r.json() + print(res) + if "error" in res: + error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" + except: + error_detail = f"External: {e}" + raise HTTPException(status_code=r.status if r else 500, detail=error_detail) + finally: + if not streaming and session: + if r: + r.close() + await session.close() + + +@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"]) +async def proxy(path: str, request: Request, user=Depends(get_verified_user)): + idx = 0 + + body = await request.body() + + url = app.state.config.OPENAI_API_BASE_URLS[idx] + key = app.state.config.OPENAI_API_KEYS[idx] + + target_url = f"{url}/{path}" + + headers = {} + headers["Authorization"] = f"Bearer {key}" + headers["Content-Type"] = "application/json" + + r = None + session = None + streaming = False + + try: + session = aiohttp.ClientSession(trust_env=True) + r = await session.request( + method=request.method, + url=target_url, + data=body, + headers=headers, + ) + + r.raise_for_status() + + # Check if response is SSE + if "text/event-stream" in r.headers.get("Content-Type", ""): + streaming = True + return StreamingResponse( + r.content, + status_code=r.status, + headers=dict(r.headers), + background=BackgroundTask( + cleanup_response, response=r, session=session + ), + ) + else: + response_data = await r.json() + return response_data + except Exception as e: + log.exception(e) + error_detail = "Open WebUI: Server Connection Error" + if r is not None: + try: + res = await r.json() + print(res) + if "error" in res: + error_detail = f"External: {res['error']['message'] if 'message' in res['error'] else res['error']}" + except: + error_detail = f"External: {e}" + raise HTTPException(status_code=r.status if r else 500, detail=error_detail) + finally: + if not streaming and session: + if r: + r.close() + await session.close() diff --git a/backend/apps/rag/main.py b/backend/apps/rag/main.py new file mode 100644 index 0000000000000000000000000000000000000000..dc6b8830efec53d1d153ecf165852a1c3820b85a --- /dev/null +++ b/backend/apps/rag/main.py @@ -0,0 +1,1463 @@ +from fastapi import ( + FastAPI, + Depends, + HTTPException, + status, + UploadFile, + File, + Form, +) +from fastapi.middleware.cors import CORSMiddleware +import requests +import os, shutil, logging, re +from datetime import datetime + +from pathlib import Path +from typing import List, Union, Sequence, Iterator, Any + +from chromadb.utils.batch_utils import create_batches +from langchain_core.documents import Document + +from langchain_community.document_loaders import ( + WebBaseLoader, + TextLoader, + PyPDFLoader, + CSVLoader, + BSHTMLLoader, + Docx2txtLoader, + UnstructuredEPubLoader, + UnstructuredWordDocumentLoader, + UnstructuredMarkdownLoader, + UnstructuredXMLLoader, + UnstructuredRSTLoader, + UnstructuredExcelLoader, + UnstructuredPowerPointLoader, + YoutubeLoader, + OutlookMessageLoader, +) +from langchain.text_splitter import RecursiveCharacterTextSplitter + +import validators +import urllib.parse +import socket + + +from pydantic import BaseModel +from typing import Optional +import mimetypes +import uuid +import json + +from apps.webui.models.documents import ( + Documents, + DocumentForm, + DocumentResponse, +) +from apps.webui.models.files import ( + Files, +) + +from apps.rag.utils import ( + get_model_path, + get_embedding_function, + query_doc, + query_doc_with_hybrid_search, + query_collection, + query_collection_with_hybrid_search, +) + +from apps.rag.search.brave import search_brave +from apps.rag.search.google_pse import search_google_pse +from apps.rag.search.main import SearchResult +from apps.rag.search.searxng import search_searxng +from apps.rag.search.serper import search_serper +from apps.rag.search.serpstack import search_serpstack +from apps.rag.search.serply import search_serply +from apps.rag.search.duckduckgo import search_duckduckgo +from apps.rag.search.tavily import search_tavily +from apps.rag.search.jina_search import search_jina + +from utils.misc import ( + calculate_sha256, + calculate_sha256_string, + sanitize_filename, + extract_folders_after_data_docs, +) +from utils.utils import get_verified_user, get_admin_user + +from config import ( + AppConfig, + ENV, + SRC_LOG_LEVELS, + UPLOAD_DIR, + DOCS_DIR, + CONTENT_EXTRACTION_ENGINE, + TIKA_SERVER_URL, + RAG_TOP_K, + RAG_RELEVANCE_THRESHOLD, + RAG_EMBEDDING_ENGINE, + RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, + RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + ENABLE_RAG_HYBRID_SEARCH, + ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + RAG_RERANKING_MODEL, + PDF_EXTRACT_IMAGES, + RAG_RERANKING_MODEL_AUTO_UPDATE, + RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + RAG_OPENAI_API_BASE_URL, + RAG_OPENAI_API_KEY, + DEVICE_TYPE, + CHROMA_CLIENT, + CHUNK_SIZE, + CHUNK_OVERLAP, + RAG_TEMPLATE, + ENABLE_RAG_LOCAL_WEB_FETCH, + YOUTUBE_LOADER_LANGUAGE, + ENABLE_RAG_WEB_SEARCH, + RAG_WEB_SEARCH_ENGINE, + RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + SEARXNG_QUERY_URL, + GOOGLE_PSE_API_KEY, + GOOGLE_PSE_ENGINE_ID, + BRAVE_SEARCH_API_KEY, + SERPSTACK_API_KEY, + SERPSTACK_HTTPS, + SERPER_API_KEY, + SERPLY_API_KEY, + TAVILY_API_KEY, + RAG_WEB_SEARCH_RESULT_COUNT, + RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + RAG_EMBEDDING_OPENAI_BATCH_SIZE, +) + +from constants import ERROR_MESSAGES + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + +app = FastAPI() + +app.state.config = AppConfig() + +app.state.config.TOP_K = RAG_TOP_K +app.state.config.RELEVANCE_THRESHOLD = RAG_RELEVANCE_THRESHOLD + +app.state.config.ENABLE_RAG_HYBRID_SEARCH = ENABLE_RAG_HYBRID_SEARCH +app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( + ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION +) + +app.state.config.CONTENT_EXTRACTION_ENGINE = CONTENT_EXTRACTION_ENGINE +app.state.config.TIKA_SERVER_URL = TIKA_SERVER_URL + +app.state.config.CHUNK_SIZE = CHUNK_SIZE +app.state.config.CHUNK_OVERLAP = CHUNK_OVERLAP + +app.state.config.RAG_EMBEDDING_ENGINE = RAG_EMBEDDING_ENGINE +app.state.config.RAG_EMBEDDING_MODEL = RAG_EMBEDDING_MODEL +app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = RAG_EMBEDDING_OPENAI_BATCH_SIZE +app.state.config.RAG_RERANKING_MODEL = RAG_RERANKING_MODEL +app.state.config.RAG_TEMPLATE = RAG_TEMPLATE + + +app.state.config.OPENAI_API_BASE_URL = RAG_OPENAI_API_BASE_URL +app.state.config.OPENAI_API_KEY = RAG_OPENAI_API_KEY + +app.state.config.PDF_EXTRACT_IMAGES = PDF_EXTRACT_IMAGES + + +app.state.config.YOUTUBE_LOADER_LANGUAGE = YOUTUBE_LOADER_LANGUAGE +app.state.YOUTUBE_LOADER_TRANSLATION = None + + +app.state.config.ENABLE_RAG_WEB_SEARCH = ENABLE_RAG_WEB_SEARCH +app.state.config.RAG_WEB_SEARCH_ENGINE = RAG_WEB_SEARCH_ENGINE +app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = RAG_WEB_SEARCH_DOMAIN_FILTER_LIST + +app.state.config.SEARXNG_QUERY_URL = SEARXNG_QUERY_URL +app.state.config.GOOGLE_PSE_API_KEY = GOOGLE_PSE_API_KEY +app.state.config.GOOGLE_PSE_ENGINE_ID = GOOGLE_PSE_ENGINE_ID +app.state.config.BRAVE_SEARCH_API_KEY = BRAVE_SEARCH_API_KEY +app.state.config.SERPSTACK_API_KEY = SERPSTACK_API_KEY +app.state.config.SERPSTACK_HTTPS = SERPSTACK_HTTPS +app.state.config.SERPER_API_KEY = SERPER_API_KEY +app.state.config.SERPLY_API_KEY = SERPLY_API_KEY +app.state.config.TAVILY_API_KEY = TAVILY_API_KEY +app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT +app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS + + +def update_embedding_model( + embedding_model: str, + update_model: bool = False, +): + if embedding_model and app.state.config.RAG_EMBEDDING_ENGINE == "": + import sentence_transformers + + app.state.sentence_transformer_ef = sentence_transformers.SentenceTransformer( + get_model_path(embedding_model, update_model), + device=DEVICE_TYPE, + trust_remote_code=RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE, + ) + else: + app.state.sentence_transformer_ef = None + + +def update_reranking_model( + reranking_model: str, + update_model: bool = False, +): + if reranking_model: + import sentence_transformers + + app.state.sentence_transformer_rf = sentence_transformers.CrossEncoder( + get_model_path(reranking_model, update_model), + device=DEVICE_TYPE, + trust_remote_code=RAG_RERANKING_MODEL_TRUST_REMOTE_CODE, + ) + else: + app.state.sentence_transformer_rf = None + + +update_embedding_model( + app.state.config.RAG_EMBEDDING_MODEL, + RAG_EMBEDDING_MODEL_AUTO_UPDATE, +) + +update_reranking_model( + app.state.config.RAG_RERANKING_MODEL, + RAG_RERANKING_MODEL_AUTO_UPDATE, +) + + +app.state.EMBEDDING_FUNCTION = get_embedding_function( + app.state.config.RAG_EMBEDDING_ENGINE, + app.state.config.RAG_EMBEDDING_MODEL, + app.state.sentence_transformer_ef, + app.state.config.OPENAI_API_KEY, + app.state.config.OPENAI_API_BASE_URL, + app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, +) + +origins = ["*"] + + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +class CollectionNameForm(BaseModel): + collection_name: Optional[str] = "test" + + +class UrlForm(CollectionNameForm): + url: str + + +class SearchForm(CollectionNameForm): + query: str + + +@app.get("/") +async def get_status(): + return { + "status": True, + "chunk_size": app.state.config.CHUNK_SIZE, + "chunk_overlap": app.state.config.CHUNK_OVERLAP, + "template": app.state.config.RAG_TEMPLATE, + "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE, + "embedding_model": app.state.config.RAG_EMBEDDING_MODEL, + "reranking_model": app.state.config.RAG_RERANKING_MODEL, + "openai_batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, + } + + +@app.get("/embedding") +async def get_embedding_config(user=Depends(get_admin_user)): + return { + "status": True, + "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE, + "embedding_model": app.state.config.RAG_EMBEDDING_MODEL, + "openai_config": { + "url": app.state.config.OPENAI_API_BASE_URL, + "key": app.state.config.OPENAI_API_KEY, + "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, + }, + } + + +@app.get("/reranking") +async def get_reraanking_config(user=Depends(get_admin_user)): + return { + "status": True, + "reranking_model": app.state.config.RAG_RERANKING_MODEL, + } + + +class OpenAIConfigForm(BaseModel): + url: str + key: str + batch_size: Optional[int] = None + + +class EmbeddingModelUpdateForm(BaseModel): + openai_config: Optional[OpenAIConfigForm] = None + embedding_engine: str + embedding_model: str + + +@app.post("/embedding/update") +async def update_embedding_config( + form_data: EmbeddingModelUpdateForm, user=Depends(get_admin_user) +): + log.info( + f"Updating embedding model: {app.state.config.RAG_EMBEDDING_MODEL} to {form_data.embedding_model}" + ) + try: + app.state.config.RAG_EMBEDDING_ENGINE = form_data.embedding_engine + app.state.config.RAG_EMBEDDING_MODEL = form_data.embedding_model + + if app.state.config.RAG_EMBEDDING_ENGINE in ["ollama", "openai"]: + if form_data.openai_config is not None: + app.state.config.OPENAI_API_BASE_URL = form_data.openai_config.url + app.state.config.OPENAI_API_KEY = form_data.openai_config.key + app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE = ( + form_data.openai_config.batch_size + if form_data.openai_config.batch_size + else 1 + ) + + update_embedding_model(app.state.config.RAG_EMBEDDING_MODEL) + + app.state.EMBEDDING_FUNCTION = get_embedding_function( + app.state.config.RAG_EMBEDDING_ENGINE, + app.state.config.RAG_EMBEDDING_MODEL, + app.state.sentence_transformer_ef, + app.state.config.OPENAI_API_KEY, + app.state.config.OPENAI_API_BASE_URL, + app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, + ) + + return { + "status": True, + "embedding_engine": app.state.config.RAG_EMBEDDING_ENGINE, + "embedding_model": app.state.config.RAG_EMBEDDING_MODEL, + "openai_config": { + "url": app.state.config.OPENAI_API_BASE_URL, + "key": app.state.config.OPENAI_API_KEY, + "batch_size": app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, + }, + } + except Exception as e: + log.exception(f"Problem updating embedding model: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +class RerankingModelUpdateForm(BaseModel): + reranking_model: str + + +@app.post("/reranking/update") +async def update_reranking_config( + form_data: RerankingModelUpdateForm, user=Depends(get_admin_user) +): + log.info( + f"Updating reranking model: {app.state.config.RAG_RERANKING_MODEL} to {form_data.reranking_model}" + ) + try: + app.state.config.RAG_RERANKING_MODEL = form_data.reranking_model + + update_reranking_model(app.state.config.RAG_RERANKING_MODEL), True + + return { + "status": True, + "reranking_model": app.state.config.RAG_RERANKING_MODEL, + } + except Exception as e: + log.exception(f"Problem updating reranking model: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +@app.get("/config") +async def get_rag_config(user=Depends(get_admin_user)): + return { + "status": True, + "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES, + "content_extraction": { + "engine": app.state.config.CONTENT_EXTRACTION_ENGINE, + "tika_server_url": app.state.config.TIKA_SERVER_URL, + }, + "chunk": { + "chunk_size": app.state.config.CHUNK_SIZE, + "chunk_overlap": app.state.config.CHUNK_OVERLAP, + }, + "youtube": { + "language": app.state.config.YOUTUBE_LOADER_LANGUAGE, + "translation": app.state.YOUTUBE_LOADER_TRANSLATION, + }, + "web": { + "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "search": { + "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH, + "engine": app.state.config.RAG_WEB_SEARCH_ENGINE, + "searxng_query_url": app.state.config.SEARXNG_QUERY_URL, + "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY, + "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID, + "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY, + "serpstack_api_key": app.state.config.SERPSTACK_API_KEY, + "serpstack_https": app.state.config.SERPSTACK_HTTPS, + "serper_api_key": app.state.config.SERPER_API_KEY, + "serply_api_key": app.state.config.SERPLY_API_KEY, + "tavily_api_key": app.state.config.TAVILY_API_KEY, + "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + }, + }, + } + + +class ContentExtractionConfig(BaseModel): + engine: str = "" + tika_server_url: Optional[str] = None + + +class ChunkParamUpdateForm(BaseModel): + chunk_size: int + chunk_overlap: int + + +class YoutubeLoaderConfig(BaseModel): + language: List[str] + translation: Optional[str] = None + + +class WebSearchConfig(BaseModel): + enabled: bool + engine: Optional[str] = None + searxng_query_url: Optional[str] = None + google_pse_api_key: Optional[str] = None + google_pse_engine_id: Optional[str] = None + brave_search_api_key: Optional[str] = None + serpstack_api_key: Optional[str] = None + serpstack_https: Optional[bool] = None + serper_api_key: Optional[str] = None + serply_api_key: Optional[str] = None + tavily_api_key: Optional[str] = None + result_count: Optional[int] = None + concurrent_requests: Optional[int] = None + + +class WebConfig(BaseModel): + search: WebSearchConfig + web_loader_ssl_verification: Optional[bool] = None + + +class ConfigUpdateForm(BaseModel): + pdf_extract_images: Optional[bool] = None + content_extraction: Optional[ContentExtractionConfig] = None + chunk: Optional[ChunkParamUpdateForm] = None + youtube: Optional[YoutubeLoaderConfig] = None + web: Optional[WebConfig] = None + + +@app.post("/config/update") +async def update_rag_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)): + app.state.config.PDF_EXTRACT_IMAGES = ( + form_data.pdf_extract_images + if form_data.pdf_extract_images is not None + else app.state.config.PDF_EXTRACT_IMAGES + ) + + if form_data.content_extraction is not None: + log.info(f"Updating text settings: {form_data.content_extraction}") + app.state.config.CONTENT_EXTRACTION_ENGINE = form_data.content_extraction.engine + app.state.config.TIKA_SERVER_URL = form_data.content_extraction.tika_server_url + + if form_data.chunk is not None: + app.state.config.CHUNK_SIZE = form_data.chunk.chunk_size + app.state.config.CHUNK_OVERLAP = form_data.chunk.chunk_overlap + + if form_data.youtube is not None: + app.state.config.YOUTUBE_LOADER_LANGUAGE = form_data.youtube.language + app.state.YOUTUBE_LOADER_TRANSLATION = form_data.youtube.translation + + if form_data.web is not None: + app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = ( + form_data.web.web_loader_ssl_verification + ) + + app.state.config.ENABLE_RAG_WEB_SEARCH = form_data.web.search.enabled + app.state.config.RAG_WEB_SEARCH_ENGINE = form_data.web.search.engine + app.state.config.SEARXNG_QUERY_URL = form_data.web.search.searxng_query_url + app.state.config.GOOGLE_PSE_API_KEY = form_data.web.search.google_pse_api_key + app.state.config.GOOGLE_PSE_ENGINE_ID = ( + form_data.web.search.google_pse_engine_id + ) + app.state.config.BRAVE_SEARCH_API_KEY = ( + form_data.web.search.brave_search_api_key + ) + app.state.config.SERPSTACK_API_KEY = form_data.web.search.serpstack_api_key + app.state.config.SERPSTACK_HTTPS = form_data.web.search.serpstack_https + app.state.config.SERPER_API_KEY = form_data.web.search.serper_api_key + app.state.config.SERPLY_API_KEY = form_data.web.search.serply_api_key + app.state.config.TAVILY_API_KEY = form_data.web.search.tavily_api_key + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = form_data.web.search.result_count + app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = ( + form_data.web.search.concurrent_requests + ) + + return { + "status": True, + "pdf_extract_images": app.state.config.PDF_EXTRACT_IMAGES, + "content_extraction": { + "engine": app.state.config.CONTENT_EXTRACTION_ENGINE, + "tika_server_url": app.state.config.TIKA_SERVER_URL, + }, + "chunk": { + "chunk_size": app.state.config.CHUNK_SIZE, + "chunk_overlap": app.state.config.CHUNK_OVERLAP, + }, + "youtube": { + "language": app.state.config.YOUTUBE_LOADER_LANGUAGE, + "translation": app.state.YOUTUBE_LOADER_TRANSLATION, + }, + "web": { + "ssl_verification": app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + "search": { + "enabled": app.state.config.ENABLE_RAG_WEB_SEARCH, + "engine": app.state.config.RAG_WEB_SEARCH_ENGINE, + "searxng_query_url": app.state.config.SEARXNG_QUERY_URL, + "google_pse_api_key": app.state.config.GOOGLE_PSE_API_KEY, + "google_pse_engine_id": app.state.config.GOOGLE_PSE_ENGINE_ID, + "brave_search_api_key": app.state.config.BRAVE_SEARCH_API_KEY, + "serpstack_api_key": app.state.config.SERPSTACK_API_KEY, + "serpstack_https": app.state.config.SERPSTACK_HTTPS, + "serper_api_key": app.state.config.SERPER_API_KEY, + "serply_api_key": app.state.config.SERPLY_API_KEY, + "tavily_api_key": app.state.config.TAVILY_API_KEY, + "result_count": app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + "concurrent_requests": app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + }, + }, + } + + +@app.get("/template") +async def get_rag_template(user=Depends(get_verified_user)): + return { + "status": True, + "template": app.state.config.RAG_TEMPLATE, + } + + +@app.get("/query/settings") +async def get_query_settings(user=Depends(get_admin_user)): + return { + "status": True, + "template": app.state.config.RAG_TEMPLATE, + "k": app.state.config.TOP_K, + "r": app.state.config.RELEVANCE_THRESHOLD, + "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH, + } + + +class QuerySettingsForm(BaseModel): + k: Optional[int] = None + r: Optional[float] = None + template: Optional[str] = None + hybrid: Optional[bool] = None + + +@app.post("/query/settings/update") +async def update_query_settings( + form_data: QuerySettingsForm, user=Depends(get_admin_user) +): + app.state.config.RAG_TEMPLATE = ( + form_data.template if form_data.template else RAG_TEMPLATE + ) + app.state.config.TOP_K = form_data.k if form_data.k else 4 + app.state.config.RELEVANCE_THRESHOLD = form_data.r if form_data.r else 0.0 + app.state.config.ENABLE_RAG_HYBRID_SEARCH = ( + form_data.hybrid if form_data.hybrid else False + ) + return { + "status": True, + "template": app.state.config.RAG_TEMPLATE, + "k": app.state.config.TOP_K, + "r": app.state.config.RELEVANCE_THRESHOLD, + "hybrid": app.state.config.ENABLE_RAG_HYBRID_SEARCH, + } + + +class QueryDocForm(BaseModel): + collection_name: str + query: str + k: Optional[int] = None + r: Optional[float] = None + hybrid: Optional[bool] = None + + +@app.post("/query/doc") +def query_doc_handler( + form_data: QueryDocForm, + user=Depends(get_verified_user), +): + try: + if app.state.config.ENABLE_RAG_HYBRID_SEARCH: + return query_doc_with_hybrid_search( + collection_name=form_data.collection_name, + query=form_data.query, + embedding_function=app.state.EMBEDDING_FUNCTION, + k=form_data.k if form_data.k else app.state.config.TOP_K, + reranking_function=app.state.sentence_transformer_rf, + r=( + form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD + ), + ) + else: + return query_doc( + collection_name=form_data.collection_name, + query=form_data.query, + embedding_function=app.state.EMBEDDING_FUNCTION, + k=form_data.k if form_data.k else app.state.config.TOP_K, + ) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +class QueryCollectionsForm(BaseModel): + collection_names: List[str] + query: str + k: Optional[int] = None + r: Optional[float] = None + hybrid: Optional[bool] = None + + +@app.post("/query/collection") +def query_collection_handler( + form_data: QueryCollectionsForm, + user=Depends(get_verified_user), +): + try: + if app.state.config.ENABLE_RAG_HYBRID_SEARCH: + return query_collection_with_hybrid_search( + collection_names=form_data.collection_names, + query=form_data.query, + embedding_function=app.state.EMBEDDING_FUNCTION, + k=form_data.k if form_data.k else app.state.config.TOP_K, + reranking_function=app.state.sentence_transformer_rf, + r=( + form_data.r if form_data.r else app.state.config.RELEVANCE_THRESHOLD + ), + ) + else: + return query_collection( + collection_names=form_data.collection_names, + query=form_data.query, + embedding_function=app.state.EMBEDDING_FUNCTION, + k=form_data.k if form_data.k else app.state.config.TOP_K, + ) + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +@app.post("/youtube") +def store_youtube_video(form_data: UrlForm, user=Depends(get_verified_user)): + try: + loader = YoutubeLoader.from_youtube_url( + form_data.url, + add_video_info=True, + language=app.state.config.YOUTUBE_LOADER_LANGUAGE, + translation=app.state.YOUTUBE_LOADER_TRANSLATION, + ) + data = loader.load() + + collection_name = form_data.collection_name + if collection_name == "": + collection_name = calculate_sha256_string(form_data.url)[:63] + + store_data_in_vector_db(data, collection_name, overwrite=True) + return { + "status": True, + "collection_name": collection_name, + "filename": form_data.url, + } + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +@app.post("/web") +def store_web(form_data: UrlForm, user=Depends(get_verified_user)): + # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" + try: + loader = get_web_loader( + form_data.url, + verify_ssl=app.state.config.ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION, + ) + data = loader.load() + + collection_name = form_data.collection_name + if collection_name == "": + collection_name = calculate_sha256_string(form_data.url)[:63] + + store_data_in_vector_db(data, collection_name, overwrite=True) + return { + "status": True, + "collection_name": collection_name, + "filename": form_data.url, + } + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +def get_web_loader(url: Union[str, Sequence[str]], verify_ssl: bool = True): + # Check if the URL is valid + if not validate_url(url): + raise ValueError(ERROR_MESSAGES.INVALID_URL) + return SafeWebBaseLoader( + url, + verify_ssl=verify_ssl, + requests_per_second=RAG_WEB_SEARCH_CONCURRENT_REQUESTS, + continue_on_failure=True, + ) + + +def validate_url(url: Union[str, Sequence[str]]): + if isinstance(url, str): + if isinstance(validators.url(url), validators.ValidationError): + raise ValueError(ERROR_MESSAGES.INVALID_URL) + if not ENABLE_RAG_LOCAL_WEB_FETCH: + # Local web fetch is disabled, filter out any URLs that resolve to private IP addresses + parsed_url = urllib.parse.urlparse(url) + # Get IPv4 and IPv6 addresses + ipv4_addresses, ipv6_addresses = resolve_hostname(parsed_url.hostname) + # Check if any of the resolved addresses are private + # This is technically still vulnerable to DNS rebinding attacks, as we don't control WebBaseLoader + for ip in ipv4_addresses: + if validators.ipv4(ip, private=True): + raise ValueError(ERROR_MESSAGES.INVALID_URL) + for ip in ipv6_addresses: + if validators.ipv6(ip, private=True): + raise ValueError(ERROR_MESSAGES.INVALID_URL) + return True + elif isinstance(url, Sequence): + return all(validate_url(u) for u in url) + else: + return False + + +def resolve_hostname(hostname): + # Get address information + addr_info = socket.getaddrinfo(hostname, None) + + # Extract IP addresses from address information + ipv4_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET] + ipv6_addresses = [info[4][0] for info in addr_info if info[0] == socket.AF_INET6] + + return ipv4_addresses, ipv6_addresses + + +def search_web(engine: str, query: str) -> list[SearchResult]: + """Search the web using a search engine and return the results as a list of SearchResult objects. + Will look for a search engine API key in environment variables in the following order: + - SEARXNG_QUERY_URL + - GOOGLE_PSE_API_KEY + GOOGLE_PSE_ENGINE_ID + - BRAVE_SEARCH_API_KEY + - SERPSTACK_API_KEY + - SERPER_API_KEY + - SERPLY_API_KEY + - TAVILY_API_KEY + Args: + query (str): The query to search for + """ + + # TODO: add playwright to search the web + if engine == "searxng": + if app.state.config.SEARXNG_QUERY_URL: + return search_searxng( + app.state.config.SEARXNG_QUERY_URL, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No SEARXNG_QUERY_URL found in environment variables") + elif engine == "google_pse": + if ( + app.state.config.GOOGLE_PSE_API_KEY + and app.state.config.GOOGLE_PSE_ENGINE_ID + ): + return search_google_pse( + app.state.config.GOOGLE_PSE_API_KEY, + app.state.config.GOOGLE_PSE_ENGINE_ID, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception( + "No GOOGLE_PSE_API_KEY or GOOGLE_PSE_ENGINE_ID found in environment variables" + ) + elif engine == "brave": + if app.state.config.BRAVE_SEARCH_API_KEY: + return search_brave( + app.state.config.BRAVE_SEARCH_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No BRAVE_SEARCH_API_KEY found in environment variables") + elif engine == "serpstack": + if app.state.config.SERPSTACK_API_KEY: + return search_serpstack( + app.state.config.SERPSTACK_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + https_enabled=app.state.config.SERPSTACK_HTTPS, + ) + else: + raise Exception("No SERPSTACK_API_KEY found in environment variables") + elif engine == "serper": + if app.state.config.SERPER_API_KEY: + return search_serper( + app.state.config.SERPER_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No SERPER_API_KEY found in environment variables") + elif engine == "serply": + if app.state.config.SERPLY_API_KEY: + return search_serply( + app.state.config.SERPLY_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + else: + raise Exception("No SERPLY_API_KEY found in environment variables") + elif engine == "duckduckgo": + return search_duckduckgo( + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) + elif engine == "tavily": + if app.state.config.TAVILY_API_KEY: + return search_tavily( + app.state.config.TAVILY_API_KEY, + query, + app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + ) + else: + raise Exception("No TAVILY_API_KEY found in environment variables") + elif engine == "jina": + return search_jina(query, app.state.config.RAG_WEB_SEARCH_RESULT_COUNT) + else: + raise Exception("No search engine API key found in environment variables") + + +@app.post("/web/search") +def store_web_search(form_data: SearchForm, user=Depends(get_verified_user)): + try: + logging.info( + f"trying to web search with {app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query}" + ) + web_results = search_web( + app.state.config.RAG_WEB_SEARCH_ENGINE, form_data.query + ) + except Exception as e: + log.exception(e) + + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.WEB_SEARCH_ERROR(e), + ) + + try: + urls = [result.link for result in web_results] + loader = get_web_loader(urls) + data = loader.load() + + collection_name = form_data.collection_name + if collection_name == "": + collection_name = calculate_sha256_string(form_data.query)[:63] + + store_data_in_vector_db(data, collection_name, overwrite=True) + return { + "status": True, + "collection_name": collection_name, + "filenames": urls, + } + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +def store_data_in_vector_db( + data, collection_name, metadata: Optional[dict] = None, overwrite: bool = False +) -> bool: + + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=app.state.config.CHUNK_SIZE, + chunk_overlap=app.state.config.CHUNK_OVERLAP, + add_start_index=True, + ) + + docs = text_splitter.split_documents(data) + + if len(docs) > 0: + log.info(f"store_data_in_vector_db {docs}") + return store_docs_in_vector_db(docs, collection_name, metadata, overwrite), None + else: + raise ValueError(ERROR_MESSAGES.EMPTY_CONTENT) + + +def store_text_in_vector_db( + text, metadata, collection_name, overwrite: bool = False +) -> bool: + text_splitter = RecursiveCharacterTextSplitter( + chunk_size=app.state.config.CHUNK_SIZE, + chunk_overlap=app.state.config.CHUNK_OVERLAP, + add_start_index=True, + ) + docs = text_splitter.create_documents([text], metadatas=[metadata]) + return store_docs_in_vector_db(docs, collection_name, overwrite=overwrite) + + +def store_docs_in_vector_db( + docs, collection_name, metadata: Optional[dict] = None, overwrite: bool = False +) -> bool: + log.info(f"store_docs_in_vector_db {docs} {collection_name}") + + texts = [doc.page_content for doc in docs] + metadatas = [{**doc.metadata, **(metadata if metadata else {})} for doc in docs] + + # ChromaDB does not like datetime formats + # for meta-data so convert them to string. + for metadata in metadatas: + for key, value in metadata.items(): + if isinstance(value, datetime): + metadata[key] = str(value) + + try: + if overwrite: + for collection in CHROMA_CLIENT.list_collections(): + if collection_name == collection.name: + log.info(f"deleting existing collection {collection_name}") + CHROMA_CLIENT.delete_collection(name=collection_name) + + collection = CHROMA_CLIENT.create_collection(name=collection_name) + + embedding_func = get_embedding_function( + app.state.config.RAG_EMBEDDING_ENGINE, + app.state.config.RAG_EMBEDDING_MODEL, + app.state.sentence_transformer_ef, + app.state.config.OPENAI_API_KEY, + app.state.config.OPENAI_API_BASE_URL, + app.state.config.RAG_EMBEDDING_OPENAI_BATCH_SIZE, + ) + + embedding_texts = list(map(lambda x: x.replace("\n", " "), texts)) + embeddings = embedding_func(embedding_texts) + + for batch in create_batches( + api=CHROMA_CLIENT, + ids=[str(uuid.uuid4()) for _ in texts], + metadatas=metadatas, + embeddings=embeddings, + documents=texts, + ): + collection.add(*batch) + + return True + except Exception as e: + if e.__class__.__name__ == "UniqueConstraintError": + return True + + log.exception(e) + + return False + + +class TikaLoader: + def __init__(self, file_path, mime_type=None): + self.file_path = file_path + self.mime_type = mime_type + + def load(self) -> List[Document]: + with open(self.file_path, "rb") as f: + data = f.read() + + if self.mime_type is not None: + headers = {"Content-Type": self.mime_type} + else: + headers = {} + + endpoint = app.state.config.TIKA_SERVER_URL + if not endpoint.endswith("/"): + endpoint += "/" + endpoint += "tika/text" + + r = requests.put(endpoint, data=data, headers=headers) + + if r.ok: + raw_metadata = r.json() + text = raw_metadata.get("X-TIKA:content", "<No text content found>") + + if "Content-Type" in raw_metadata: + headers["Content-Type"] = raw_metadata["Content-Type"] + + log.info("Tika extracted text: %s", text) + + return [Document(page_content=text, metadata=headers)] + else: + raise Exception(f"Error calling Tika: {r.reason}") + + +def get_loader(filename: str, file_content_type: str, file_path: str): + file_ext = filename.split(".")[-1].lower() + known_type = True + + known_source_ext = [ + "go", + "py", + "java", + "sh", + "bat", + "ps1", + "cmd", + "js", + "ts", + "css", + "cpp", + "hpp", + "h", + "c", + "cs", + "sql", + "log", + "ini", + "pl", + "pm", + "r", + "dart", + "dockerfile", + "env", + "php", + "hs", + "hsc", + "lua", + "nginxconf", + "conf", + "m", + "mm", + "plsql", + "perl", + "rb", + "rs", + "db2", + "scala", + "bash", + "swift", + "vue", + "svelte", + "msg", + "ex", + "exs", + "erl", + "tsx", + "jsx", + "hs", + "lhs", + ] + + if ( + app.state.config.CONTENT_EXTRACTION_ENGINE == "tika" + and app.state.config.TIKA_SERVER_URL + ): + if file_ext in known_source_ext or ( + file_content_type and file_content_type.find("text/") >= 0 + ): + loader = TextLoader(file_path, autodetect_encoding=True) + else: + loader = TikaLoader(file_path, file_content_type) + else: + if file_ext == "pdf": + loader = PyPDFLoader( + file_path, extract_images=app.state.config.PDF_EXTRACT_IMAGES + ) + elif file_ext == "csv": + loader = CSVLoader(file_path) + elif file_ext == "rst": + loader = UnstructuredRSTLoader(file_path, mode="elements") + elif file_ext == "xml": + loader = UnstructuredXMLLoader(file_path) + elif file_ext in ["htm", "html"]: + loader = BSHTMLLoader(file_path, open_encoding="unicode_escape") + elif file_ext == "md": + loader = UnstructuredMarkdownLoader(file_path) + elif file_content_type == "application/epub+zip": + loader = UnstructuredEPubLoader(file_path) + elif ( + file_content_type + == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + or file_ext in ["doc", "docx"] + ): + loader = Docx2txtLoader(file_path) + elif file_content_type in [ + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ] or file_ext in ["xls", "xlsx"]: + loader = UnstructuredExcelLoader(file_path) + elif file_content_type in [ + "application/vnd.ms-powerpoint", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ] or file_ext in ["ppt", "pptx"]: + loader = UnstructuredPowerPointLoader(file_path) + elif file_ext == "msg": + loader = OutlookMessageLoader(file_path) + elif file_ext in known_source_ext or ( + file_content_type and file_content_type.find("text/") >= 0 + ): + loader = TextLoader(file_path, autodetect_encoding=True) + else: + loader = TextLoader(file_path, autodetect_encoding=True) + known_type = False + + return loader, known_type + + +@app.post("/doc") +def store_doc( + collection_name: Optional[str] = Form(None), + file: UploadFile = File(...), + user=Depends(get_verified_user), +): + # "https://www.gutenberg.org/files/1727/1727-h/1727-h.htm" + + log.info(f"file.content_type: {file.content_type}") + try: + unsanitized_filename = file.filename + filename = os.path.basename(unsanitized_filename) + + file_path = f"{UPLOAD_DIR}/{filename}" + + contents = file.file.read() + with open(file_path, "wb") as f: + f.write(contents) + f.close() + + f = open(file_path, "rb") + if collection_name == None: + collection_name = calculate_sha256(f)[:63] + f.close() + + loader, known_type = get_loader(filename, file.content_type, file_path) + data = loader.load() + + try: + result = store_data_in_vector_db(data, collection_name) + + if result: + return { + "status": True, + "collection_name": collection_name, + "filename": filename, + "known_type": known_type, + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=e, + ) + except Exception as e: + log.exception(e) + if "No pandoc was found" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +class ProcessDocForm(BaseModel): + file_id: str + collection_name: Optional[str] = None + + +@app.post("/process/doc") +def process_doc( + form_data: ProcessDocForm, + user=Depends(get_verified_user), +): + try: + file = Files.get_file_by_id(form_data.file_id) + file_path = file.meta.get("path", f"{UPLOAD_DIR}/{file.filename}") + + f = open(file_path, "rb") + + collection_name = form_data.collection_name + if collection_name == None: + collection_name = calculate_sha256(f)[:63] + f.close() + + loader, known_type = get_loader( + file.filename, file.meta.get("content_type"), file_path + ) + data = loader.load() + + try: + result = store_data_in_vector_db( + data, + collection_name, + { + "file_id": form_data.file_id, + "name": file.meta.get("name", file.filename), + }, + ) + + if result: + return { + "status": True, + "collection_name": collection_name, + "known_type": known_type, + "filename": file.meta.get("name", file.filename), + } + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=e, + ) + except Exception as e: + log.exception(e) + if "No pandoc was found" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.PANDOC_NOT_INSTALLED, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +class TextRAGForm(BaseModel): + name: str + content: str + collection_name: Optional[str] = None + + +@app.post("/text") +def store_text( + form_data: TextRAGForm, + user=Depends(get_verified_user), +): + + collection_name = form_data.collection_name + if collection_name == None: + collection_name = calculate_sha256_string(form_data.content) + + result = store_text_in_vector_db( + form_data.content, + metadata={"name": form_data.name, "created_by": user.id}, + collection_name=collection_name, + ) + + if result: + return {"status": True, "collection_name": collection_name} + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +@app.get("/scan") +def scan_docs_dir(user=Depends(get_admin_user)): + for path in Path(DOCS_DIR).rglob("./**/*"): + try: + if path.is_file() and not path.name.startswith("."): + tags = extract_folders_after_data_docs(path) + filename = path.name + file_content_type = mimetypes.guess_type(path) + + f = open(path, "rb") + collection_name = calculate_sha256(f)[:63] + f.close() + + loader, known_type = get_loader( + filename, file_content_type[0], str(path) + ) + data = loader.load() + + try: + result = store_data_in_vector_db(data, collection_name) + + if result: + sanitized_filename = sanitize_filename(filename) + doc = Documents.get_doc_by_name(sanitized_filename) + + if doc == None: + doc = Documents.insert_new_doc( + user.id, + DocumentForm( + **{ + "name": sanitized_filename, + "title": filename, + "collection_name": collection_name, + "filename": filename, + "content": ( + json.dumps( + { + "tags": list( + map( + lambda name: {"name": name}, + tags, + ) + ) + } + ) + if len(tags) + else "{}" + ), + } + ), + ) + except Exception as e: + log.exception(e) + pass + + except Exception as e: + log.exception(e) + + return True + + +@app.get("/reset/db") +def reset_vector_db(user=Depends(get_admin_user)): + CHROMA_CLIENT.reset() + + +@app.get("/reset/uploads") +def reset_upload_dir(user=Depends(get_admin_user)) -> bool: + folder = f"{UPLOAD_DIR}" + try: + # Check if the directory exists + if os.path.exists(folder): + # Iterate over all the files and directories in the specified directory + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) # Remove the file or link + elif os.path.isdir(file_path): + shutil.rmtree(file_path) # Remove the directory + except Exception as e: + print(f"Failed to delete {file_path}. Reason: {e}") + else: + print(f"The directory {folder} does not exist") + except Exception as e: + print(f"Failed to process the directory {folder}. Reason: {e}") + + return True + + +@app.get("/reset") +def reset(user=Depends(get_admin_user)) -> bool: + folder = f"{UPLOAD_DIR}" + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + log.error("Failed to delete %s. Reason: %s" % (file_path, e)) + + try: + CHROMA_CLIENT.reset() + except Exception as e: + log.exception(e) + + return True + + +class SafeWebBaseLoader(WebBaseLoader): + """WebBaseLoader with enhanced error handling for URLs.""" + + def lazy_load(self) -> Iterator[Document]: + """Lazy load text from the url(s) in web_path with error handling.""" + for path in self.web_paths: + try: + soup = self._scrape(path, bs_kwargs=self.bs_kwargs) + text = soup.get_text(**self.bs_get_text_kwargs) + + # Build metadata + metadata = {"source": path} + if title := soup.find("title"): + metadata["title"] = title.get_text() + if description := soup.find("meta", attrs={"name": "description"}): + metadata["description"] = description.get( + "content", "No description found." + ) + if html := soup.find("html"): + metadata["language"] = html.get("lang", "No language found.") + + yield Document(page_content=text, metadata=metadata) + except Exception as e: + # Log the error and continue with the next URL + log.error(f"Error loading {path}: {e}") + + +if ENV == "dev": + + @app.get("/ef") + async def get_embeddings(): + return {"result": app.state.EMBEDDING_FUNCTION("hello world")} + + @app.get("/ef/{text}") + async def get_embeddings_text(text: str): + return {"result": app.state.EMBEDDING_FUNCTION(text)} diff --git a/backend/apps/rag/search/brave.py b/backend/apps/rag/search/brave.py new file mode 100644 index 0000000000000000000000000000000000000000..76ad1fb4731d2e572027cec32f65267a3f9f3675 --- /dev/null +++ b/backend/apps/rag/search/brave.py @@ -0,0 +1,42 @@ +import logging +from typing import List, Optional +import requests + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_brave( + api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None +) -> list[SearchResult]: + """Search using Brave's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Brave Search API key + query (str): The query to search for + """ + url = "https://api.search.brave.com/res/v1/web/search" + headers = { + "Accept": "application/json", + "Accept-Encoding": "gzip", + "X-Subscription-Token": api_key, + } + params = {"q": query, "count": count} + + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = json_response.get("web", {}).get("results", []) + if filter_list: + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result["url"], title=result.get("title"), snippet=result.get("snippet") + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/duckduckgo.py b/backend/apps/rag/search/duckduckgo.py new file mode 100644 index 0000000000000000000000000000000000000000..f0cc2a71035d5279d9c184f2c20787d41b16de52 --- /dev/null +++ b/backend/apps/rag/search/duckduckgo.py @@ -0,0 +1,49 @@ +import logging +from typing import List, Optional +from apps.rag.search.main import SearchResult, get_filtered_results +from duckduckgo_search import DDGS +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_duckduckgo( + query: str, count: int, filter_list: Optional[List[str]] = None +) -> list[SearchResult]: + """ + Search using DuckDuckGo's Search API and return the results as a list of SearchResult objects. + Args: + query (str): The query to search for + count (int): The number of results to return + + Returns: + List[SearchResult]: A list of search results + """ + # Use the DDGS context manager to create a DDGS object + with DDGS() as ddgs: + # Use the ddgs.text() method to perform the search + ddgs_gen = ddgs.text( + query, safesearch="moderate", max_results=count, backend="api" + ) + # Check if there are search results + if ddgs_gen: + # Convert the search results into a list + search_results = [r for r in ddgs_gen] + + # Create an empty list to store the SearchResult objects + results = [] + # Iterate over each search result + for result in search_results: + # Create a SearchResult object and append it to the results list + results.append( + SearchResult( + link=result["href"], + title=result.get("title"), + snippet=result.get("body"), + ) + ) + if filter_list: + results = get_filtered_results(results, filter_list) + # Return the list of search results + return results diff --git a/backend/apps/rag/search/google_pse.py b/backend/apps/rag/search/google_pse.py new file mode 100644 index 0000000000000000000000000000000000000000..0c78512e74ef82d391dad426c42e55e3a25a148c --- /dev/null +++ b/backend/apps/rag/search/google_pse.py @@ -0,0 +1,51 @@ +import json +import logging +from typing import List, Optional +import requests + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_google_pse( + api_key: str, + search_engine_id: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, +) -> list[SearchResult]: + """Search using Google's Programmable Search Engine API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Programmable Search Engine API key + search_engine_id (str): A Programmable Search Engine ID + query (str): The query to search for + """ + url = "https://www.googleapis.com/customsearch/v1" + + headers = {"Content-Type": "application/json"} + params = { + "cx": search_engine_id, + "q": query, + "key": api_key, + "num": count, + } + + response = requests.request("GET", url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = json_response.get("items", []) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result["link"], + title=result.get("title"), + snippet=result.get("snippet"), + ) + for result in results + ] diff --git a/backend/apps/rag/search/jina_search.py b/backend/apps/rag/search/jina_search.py new file mode 100644 index 0000000000000000000000000000000000000000..65f9ad68fe7984fa0568ac6b32367a1cf8632c26 --- /dev/null +++ b/backend/apps/rag/search/jina_search.py @@ -0,0 +1,41 @@ +import logging +import requests +from yarl import URL + +from apps.rag.search.main import SearchResult +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_jina(query: str, count: int) -> list[SearchResult]: + """ + Search using Jina's Search API and return the results as a list of SearchResult objects. + Args: + query (str): The query to search for + count (int): The number of results to return + + Returns: + List[SearchResult]: A list of search results + """ + jina_search_endpoint = "https://s.jina.ai/" + headers = { + "Accept": "application/json", + } + url = str(URL(jina_search_endpoint + query)) + response = requests.get(url, headers=headers) + response.raise_for_status() + data = response.json() + + results = [] + for result in data["data"][:count]: + results.append( + SearchResult( + link=result["url"], + title=result.get("title"), + snippet=result.get("content"), + ) + ) + + return results diff --git a/backend/apps/rag/search/main.py b/backend/apps/rag/search/main.py new file mode 100644 index 0000000000000000000000000000000000000000..49056f1fd1ec74f2e5cea0aade8e9d170f7b1c58 --- /dev/null +++ b/backend/apps/rag/search/main.py @@ -0,0 +1,20 @@ +from typing import Optional +from urllib.parse import urlparse +from pydantic import BaseModel + + +def get_filtered_results(results, filter_list): + if not filter_list: + return results + filtered_results = [] + for result in results: + domain = urlparse(result["url"]).netloc + if any(domain.endswith(filtered_domain) for filtered_domain in filter_list): + filtered_results.append(result) + return filtered_results + + +class SearchResult(BaseModel): + link: str + title: Optional[str] + snippet: Optional[str] diff --git a/backend/apps/rag/search/searxng.py b/backend/apps/rag/search/searxng.py new file mode 100644 index 0000000000000000000000000000000000000000..6e545e994e8659c63a82cf0fce48b4724e042d84 --- /dev/null +++ b/backend/apps/rag/search/searxng.py @@ -0,0 +1,92 @@ +import logging +import requests + +from typing import List, Optional + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_searxng( + query_url: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + **kwargs, +) -> List[SearchResult]: + """ + Search a SearXNG instance for a given query and return the results as a list of SearchResult objects. + + The function allows passing additional parameters such as language or time_range to tailor the search result. + + Args: + query_url (str): The base URL of the SearXNG server. + query (str): The search term or question to find in the SearXNG database. + count (int): The maximum number of results to retrieve from the search. + + Keyword Args: + language (str): Language filter for the search results; e.g., "en-US". Defaults to an empty string. + safesearch (int): Safe search filter for safer web results; 0 = off, 1 = moderate, 2 = strict. Defaults to 1 (moderate). + time_range (str): Time range for filtering results by date; e.g., "2023-04-05..today" or "all-time". Defaults to ''. + categories: (Optional[List[str]]): Specific categories within which the search should be performed, defaulting to an empty string if not provided. + + Returns: + List[SearchResult]: A list of SearchResults sorted by relevance score in descending order. + + Raise: + requests.exceptions.RequestException: If a request error occurs during the search process. + """ + + # Default values for optional parameters are provided as empty strings or None when not specified. + language = kwargs.get("language", "en-US") + safesearch = kwargs.get("safesearch", "1") + time_range = kwargs.get("time_range", "") + categories = "".join(kwargs.get("categories", [])) + + params = { + "q": query, + "format": "json", + "pageno": 1, + "safesearch": safesearch, + "language": language, + "time_range": time_range, + "categories": categories, + "theme": "simple", + "image_proxy": 0, + } + + # Legacy query format + if "<query>" in query_url: + # Strip all query parameters from the URL + query_url = query_url.split("?")[0] + + log.debug(f"searching {query_url}") + + response = requests.get( + query_url, + headers={ + "User-Agent": "Open WebUI (https://github.com/open-webui/open-webui) RAG Bot", + "Accept": "text/html", + "Accept-Encoding": "gzip, deflate", + "Accept-Language": "en-US,en;q=0.5", + "Connection": "keep-alive", + }, + params=params, + ) + + response.raise_for_status() # Raise an exception for HTTP errors. + + json_response = response.json() + results = json_response.get("results", []) + sorted_results = sorted(results, key=lambda x: x.get("score", 0), reverse=True) + if filter_list: + sorted_results = get_filtered_results(sorted_results, filter_list) + return [ + SearchResult( + link=result["url"], title=result.get("title"), snippet=result.get("content") + ) + for result in sorted_results[:count] + ] diff --git a/backend/apps/rag/search/serper.py b/backend/apps/rag/search/serper.py new file mode 100644 index 0000000000000000000000000000000000000000..b278a4df15a1322a03dad373518043cf2c003eab --- /dev/null +++ b/backend/apps/rag/search/serper.py @@ -0,0 +1,43 @@ +import json +import logging +from typing import List, Optional +import requests + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_serper( + api_key: str, query: str, count: int, filter_list: Optional[List[str]] = None +) -> list[SearchResult]: + """Search using serper.dev's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serper.dev API key + query (str): The query to search for + """ + url = "https://google.serper.dev/search" + + payload = json.dumps({"q": query}) + headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} + + response = requests.request("POST", url, headers=headers, data=payload) + response.raise_for_status() + + json_response = response.json() + results = sorted( + json_response.get("organic", []), key=lambda x: x.get("position", 0) + ) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result["link"], + title=result.get("title"), + snippet=result.get("description"), + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/serply.py b/backend/apps/rag/search/serply.py new file mode 100644 index 0000000000000000000000000000000000000000..24b249b739425b9ad941c336f3daa76f46272297 --- /dev/null +++ b/backend/apps/rag/search/serply.py @@ -0,0 +1,70 @@ +import json +import logging +from typing import List, Optional +import requests +from urllib.parse import urlencode + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_serply( + api_key: str, + query: str, + count: int, + hl: str = "us", + limit: int = 10, + device_type: str = "desktop", + proxy_location: str = "US", + filter_list: Optional[List[str]] = None, +) -> list[SearchResult]: + """Search using serper.dev's API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serply.io API key + query (str): The query to search for + hl (str): Host Language code to display results in (reference https://developers.google.com/custom-search/docs/xml_results?hl=en#wsInterfaceLanguages) + limit (int): The maximum number of results to return [10-100, defaults to 10] + """ + log.info("Searching with Serply") + + url = "https://api.serply.io/v1/search/" + + query_payload = { + "q": query, + "language": "en", + "num": limit, + "gl": proxy_location.upper(), + "hl": hl.lower(), + } + + url = f"{url}{urlencode(query_payload)}" + headers = { + "X-API-KEY": api_key, + "X-User-Agent": device_type, + "User-Agent": "open-webui", + "X-Proxy-Location": proxy_location, + } + + response = requests.request("GET", url, headers=headers) + response.raise_for_status() + + json_response = response.json() + log.info(f"results from serply search: {json_response}") + + results = sorted( + json_response.get("results", []), key=lambda x: x.get("realPosition", 0) + ) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result["link"], + title=result.get("title"), + snippet=result.get("description"), + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/serpstack.py b/backend/apps/rag/search/serpstack.py new file mode 100644 index 0000000000000000000000000000000000000000..64b0f117d906414714d56470271ca13a0a9d4bb9 --- /dev/null +++ b/backend/apps/rag/search/serpstack.py @@ -0,0 +1,49 @@ +import json +import logging +from typing import List, Optional +import requests + +from apps.rag.search.main import SearchResult, get_filtered_results +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_serpstack( + api_key: str, + query: str, + count: int, + filter_list: Optional[List[str]] = None, + https_enabled: bool = True, +) -> list[SearchResult]: + """Search using serpstack.com's and return the results as a list of SearchResult objects. + + Args: + api_key (str): A serpstack.com API key + query (str): The query to search for + https_enabled (bool): Whether to use HTTPS or HTTP for the API request + """ + url = f"{'https' if https_enabled else 'http'}://api.serpstack.com/search" + + headers = {"Content-Type": "application/json"} + params = { + "access_key": api_key, + "query": query, + } + + response = requests.request("POST", url, headers=headers, params=params) + response.raise_for_status() + + json_response = response.json() + results = sorted( + json_response.get("organic_results", []), key=lambda x: x.get("position", 0) + ) + if filter_list: + results = get_filtered_results(results, filter_list) + return [ + SearchResult( + link=result["url"], title=result.get("title"), snippet=result.get("snippet") + ) + for result in results[:count] + ] diff --git a/backend/apps/rag/search/tavily.py b/backend/apps/rag/search/tavily.py new file mode 100644 index 0000000000000000000000000000000000000000..b15d6ef9d5b55aa2eee6f77212e6561f1fbaf4fa --- /dev/null +++ b/backend/apps/rag/search/tavily.py @@ -0,0 +1,39 @@ +import logging + +import requests + +from apps.rag.search.main import SearchResult +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_tavily(api_key: str, query: str, count: int) -> list[SearchResult]: + """Search using Tavily's Search API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Tavily Search API key + query (str): The query to search for + + Returns: + List[SearchResult]: A list of search results + """ + url = "https://api.tavily.com/search" + data = {"query": query, "api_key": api_key} + + response = requests.post(url, json=data) + response.raise_for_status() + + json_response = response.json() + + raw_search_results = json_response.get("results", []) + + return [ + SearchResult( + link=result["url"], + title=result.get("title", ""), + snippet=result.get("content"), + ) + for result in raw_search_results[:count] + ] diff --git a/backend/apps/rag/search/testdata/brave.json b/backend/apps/rag/search/testdata/brave.json new file mode 100644 index 0000000000000000000000000000000000000000..38487390d9067236e8d5bd6afcedfd08ae9e8368 --- /dev/null +++ b/backend/apps/rag/search/testdata/brave.json @@ -0,0 +1,998 @@ +{ + "query": { + "original": "python", + "show_strict_warning": false, + "is_navigational": true, + "is_news_breaking": false, + "spellcheck_off": true, + "country": "us", + "bad_results": false, + "should_fallback": false, + "postal_code": "", + "city": "", + "header_country": "", + "more_results_available": true, + "state": "" + }, + "mixed": { + "type": "mixed", + "main": [ + { + "type": "web", + "index": 0, + "all": false + }, + { + "type": "web", + "index": 1, + "all": false + }, + { + "type": "news", + "all": true + }, + { + "type": "web", + "index": 2, + "all": false + }, + { + "type": "videos", + "all": true + }, + { + "type": "web", + "index": 3, + "all": false + }, + { + "type": "web", + "index": 4, + "all": false + }, + { + "type": "web", + "index": 5, + "all": false + }, + { + "type": "web", + "index": 6, + "all": false + }, + { + "type": "web", + "index": 7, + "all": false + }, + { + "type": "web", + "index": 8, + "all": false + }, + { + "type": "web", + "index": 9, + "all": false + }, + { + "type": "web", + "index": 10, + "all": false + }, + { + "type": "web", + "index": 11, + "all": false + }, + { + "type": "web", + "index": 12, + "all": false + }, + { + "type": "web", + "index": 13, + "all": false + }, + { + "type": "web", + "index": 14, + "all": false + }, + { + "type": "web", + "index": 15, + "all": false + }, + { + "type": "web", + "index": 16, + "all": false + }, + { + "type": "web", + "index": 17, + "all": false + }, + { + "type": "web", + "index": 18, + "all": false + }, + { + "type": "web", + "index": 19, + "all": false + } + ], + "top": [], + "side": [] + }, + "news": { + "type": "news", + "results": [ + { + "title": "Google lays off staff from Flutter, Dart and Python teams weeks before its developer conference | TechCrunch", + "url": "https://techcrunch.com/2024/05/01/google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference/", + "is_source_local": false, + "is_source_both": false, + "description": "Google told TechCrunch that Flutter will have new updates to share at I/O this year.", + "page_age": "2024-05-02T17:40:05", + "family_friendly": true, + "meta_url": { + "scheme": "https", + "netloc": "techcrunch.com", + "hostname": "techcrunch.com", + "favicon": "https://imgs.search.brave.com/N6VSEVahheQOb7lqfb47dhUOB4XD-6sfQOP94sCe3Oo/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGI5Njk0Yzlk/YWM3ZWMwZjg1MTM1/NmIyMWEyNzBjZDZj/ZDQyNmFlNGU0NDRi/MDgyYjQwOGU0Y2Qy/ZWMwNWQ2ZC90ZWNo/Y3J1bmNoLmNvbS8", + "path": "› 2024 › 05 › 01 › google-lays-off-staff-from-flutter-dart-python-weeks-before-its-developer-conference" + }, + "breaking": false, + "thumbnail": { + "src": "https://imgs.search.brave.com/gCI5UG8muOEOZDAx9vpu6L6r6R00mD7jOF08-biFoyQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly90ZWNo/Y3J1bmNoLmNvbS93/cC1jb250ZW50L3Vw/bG9hZHMvMjAxOC8x/MS9HZXR0eUltYWdl/cy0xMDAyNDg0NzQ2/LmpwZz9yZXNpemU9/MTIwMCw4MDA" + }, + "age": "3 days ago", + "extra_snippets": [ + "Ahead of Google’s annual I/O developer conference in May, the tech giant has laid off staff across key teams like Flutter, Dart, Python and others, according to reports from affected employees shared on social media. Google confirmed the layoffs to TechCrunch, but not the specific teams, roles or how many people were let go.", + "In a separate post on Reddit, another commenter noted the Python team affected by the layoffs were those who managed the internal Python runtimes and toolchains and worked with OSS Python. Included in this group were “multiple current and former core devs and steering council members,” they said.", + "Meanwhile, others shared on Y Combinator’s Hacker News, where a Python team member detailed their specific duties on the technical front and noted that, for years, much of the work was done with fewer than 10 people. Another Hacker News commenter said their early years on the Python team were spent paying down internal technical debt accumulated from not having a strong Python strategy.", + "CNBC reports that a total of 200 people were let go across Google’s “Core” teams, which included those working on Python, app platforms, and other engineering roles. Some jobs were being shifted to India and Mexico, it said, citing internal documents." + ] + } + ], + "mutated_by_goggles": false + }, + "type": "search", + "videos": { + "type": "videos", + "results": [ + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=b093aqAZiPU", + "title": "👩💻 Python for Beginners Tutorial - YouTube", + "description": "In this step-by-step Python for beginner's tutorial, learn how you can get started programming in Python. In this video, I assume that you are completely new...", + "age": "March 25, 2021", + "page_age": "2021-03-25T10:00:08", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/tZI4Do4_EYcTCsD_MvE3Jx8FzjIXwIJ5ZuKhwiWTyZs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9i/MDkzYXFBWmlQVS9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=rfscVS0vtbw", + "title": "Learn Python - Full Course for Beginners [Tutorial] - YouTube", + "description": "This course will give you a full introduction into all of the core concepts in python. Follow along with the videos and you'll be a python programmer in no t...", + "age": "July 11, 2018", + "page_age": "2018-07-11T18:00:42", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/65zkx_kPU_zJb-4nmvvY-q5-ZZwzceChz-N00V8cqvk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9y/ZnNjVlMwdnRidy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=_uQrJ0TkZlc", + "title": "Python Tutorial - Python Full Course for Beginners - YouTube", + "description": "Become a Python pro! 🚀 This comprehensive tutorial takes you from beginner to hero, covering the basics, machine learning, and web development projects.🚀 W...", + "age": "February 18, 2019", + "page_age": "2019-02-18T15:00:08", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Djiv1pXLq1ClqBSE_86jQnEYR8bW8UJP6Cs7LrgyQzQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9f/dVFySjBUa1psYy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=wRKgzC-MhIc", + "title": "[] and {} vs list() and dict(), which is better?", + "description": "Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on YouTube.", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Hw9ep2Pio13X1VZjRw_h9R2VH_XvZFOuGlQJVnVkeq0/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS93/UktnekMtTWhJYy9o/cWRlZmF1bHQuanBn" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=LWdsF79H1Pg", + "title": "print() vs. return in Python Functions - YouTube", + "description": "In this video, you will learn the differences between the return statement and the print function when they are used inside Python functions. We will see an ...", + "age": "June 11, 2022", + "page_age": "2022-06-11T21:33:26", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/ebglnr5_jwHHpvon3WU-5hzt0eHdTZSVGg3Ts6R38xY/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9M/V2RzRjc5SDFQZy9t/YXhyZXNkZWZhdWx0/LmpwZw" + } + }, + { + "type": "video_result", + "url": "https://www.youtube.com/watch?v=AovxLr8jUH4", + "title": "Python Tutorial for Beginners 5 - Python print() and input() Function ...", + "description": "In this Video I am going to show How to use print() Function and input() Function in Python. In python The print() function is used to print the specified ...", + "age": "August 28, 2018", + "page_age": "2018-08-28T20:11:09", + "video": {}, + "meta_url": { + "scheme": "https", + "netloc": "youtube.com", + "hostname": "www.youtube.com", + "favicon": "https://imgs.search.brave.com/Ux4Hee4evZhvjuTKwtapBycOGjGDci2Gvn2pbSzvbC0/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTkyZTZiMWU3/YzU3Nzc5YjExYzUy/N2VhZTIxOWNlYjM5/ZGVjN2MyZDY4Nzdh/ZDYzMTYxNmI5N2Rk/Y2Q3N2FkNy93d3cu/eW91dHViZS5jb20v", + "path": "› watch" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/nCoLEcWkKtiecprWbS6nufwGCaSbPH7o0-sMeIkFmjI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9pLnl0/aW1nLmNvbS92aS9B/b3Z4THI4alVINC9o/cWRlZmF1bHQuanBn" + } + } + ], + "mutated_by_goggles": false + }, + "web": { + "type": "search", + "results": [ + { + "title": "Welcome to Python.org", + "url": "https://www.python.org", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the <strong>Python</strong> Programming Language", + "page_age": "2023-09-09T15:55:05", + "profile": { + "name": "Python", + "url": "https://www.python.org", + "long_name": "python.org", + "img": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "python.org", + "hostname": "www.python.org", + "favicon": "https://imgs.search.brave.com/vBaRH-v6oPS4csO4cdvuKhZ7-xDVvydin3oe3zXYxAI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNTJjMzZjNDBj/MmIzODgwMGUyOTRj/Y2E5MjM3YjRkYTZj/YWI1Yzk1NTlmYTgw/ZDBjNzM0MGMxZjQz/YWFjNTczYy93d3cu/cHl0aG9uLm9yZy8", + "path": "" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/GGfNfe5rxJ8QWEoxXniSLc0-POLU3qPyTIpuqPdbmXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cHl0aG9uLm9yZy9z/dGF0aWMvb3Blbmdy/YXBoLWljb24tMjAw/eDIwMC5wbmc", + "original": "https://www.python.org/static/opengraph-icon-200x200.png", + "logo": false + }, + "age": "September 9, 2023", + "cluster_type": "generic", + "cluster": [ + { + "title": "Downloads", + "url": "https://www.python.org/downloads/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the <strong>Python</strong> Programming Language", + "family_friendly": true + }, + { + "title": "Macos", + "url": "https://www.python.org/downloads/macos/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the <strong>Python</strong> Programming Language", + "family_friendly": true + }, + { + "title": "Windows", + "url": "https://www.python.org/downloads/windows/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the <strong>Python</strong> Programming Language", + "family_friendly": true + }, + { + "title": "Getting Started", + "url": "https://www.python.org/about/gettingstarted/", + "is_source_local": false, + "is_source_both": false, + "description": "The official home of the <strong>Python</strong> Programming Language", + "family_friendly": true + } + ], + "extra_snippets": [ + "Calculations are simple with Python, and expression syntax is straightforward: the operators +, -, * and / work as expected; parentheses () can be used for grouping. More about simple math functions in Python 3.", + "The core of extensible programming is defining functions. Python allows mandatory and optional arguments, keyword arguments, and even arbitrary argument lists. More about defining functions in Python 3", + "Lists (known as arrays in other languages) are one of the compound data types that Python understands. Lists can be indexed, sliced and manipulated with other built-in functions. More about lists in Python 3", + "# Python 3: Simple output (with Unicode) >>> print(\"Hello, I'm Python!\") Hello, I'm Python! # Input, assignment >>> name = input('What is your name?\\n') >>> print('Hi, %s.' % name) What is your name? Python Hi, Python." + ] + }, + { + "title": "Python (programming language) - Wikipedia", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "is_source_local": false, + "is_source_both": false, + "description": "<strong>Python</strong> is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. <strong>Python</strong> is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), ...", + "page_age": "2024-05-01T12:54:03", + "profile": { + "name": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "long_name": "en.wikipedia.org", + "img": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "en.wikipedia.org", + "hostname": "en.wikipedia.org", + "favicon": "https://imgs.search.brave.com/0kxnVOiqv-faZvOJc7zpym4Zin1CTs1f1svfNZSzmfU/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNjQwNGZhZWY0/ZTQ1YWUzYzQ3MDUw/MmMzMGY3NTQ0ZjNj/NDUwMDk5ZTI3MWRk/NWYyNTM4N2UwOTE0/NTI3ZDQzNy9lbi53/aWtpcGVkaWEub3Jn/Lw", + "path": "› wiki › Python_(programming_language)" + }, + "age": "4 days ago", + "extra_snippets": [ + "Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a \"batteries included\" language due to its comprehensive standard library.", + "Guido van Rossum began working on Python in the late 1980s as a successor to the ABC programming language and first released it in 1991 as Python 0.9.0. Python 2.0 was released in 2000. Python 3.0, released in 2008, was a major revision not completely backward-compatible with earlier versions. Python 2.7.18, released in 2020, was the last release of Python 2.", + "Python was invented in the late 1980s by Guido van Rossum at Centrum Wiskunde & Informatica (CWI) in the Netherlands as a successor to the ABC programming language, which was inspired by SETL, capable of exception handling and interfacing with the Amoeba operating system.", + "Python consistently ranks as one of the most popular programming languages, and has gained widespread use in the machine learning community." + ] + }, + { + "title": "Python Tutorial", + "url": "https://www.w3schools.com/python/", + "is_source_local": false, + "is_source_both": false, + "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.", + "page_age": "2017-12-07T00:00:00", + "profile": { + "name": "W3Schools", + "url": "https://www.w3schools.com/python/", + "long_name": "w3schools.com", + "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "w3schools.com", + "hostname": "www.w3schools.com", + "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", + "path": "› python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", + "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", + "logo": true + }, + "age": "December 7, 2017", + "extra_snippets": [ + "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", + "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", + "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", + "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" + ] + }, + { + "title": "Online Python - IDE, Editor, Compiler, Interpreter", + "url": "https://www.online-python.com/", + "is_source_local": false, + "is_source_both": false, + "description": "Build and Run your <strong>Python</strong> code instantly. Online-<strong>Python</strong> is a quick and easy tool that helps you to build, compile, test your <strong>python</strong> programs.", + "profile": { + "name": "Online-python", + "url": "https://www.online-python.com/", + "long_name": "online-python.com", + "img": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "online-python.com", + "hostname": "www.online-python.com", + "favicon": "https://imgs.search.brave.com/kfaEvapwHxSsRObO52-I-otYFPHpG1h7UXJyUqDM2Ec/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZGYxODdjNWQ0/NjZjZTNiMjk5NDY1/MWI5MTgyYjU3Y2Q3/MTI3NGM5MjUzY2Fi/OGQ3MTQ4MmIxMTQx/ZTcxNWFhMC93d3cu/b25saW5lLXB5dGhv/bi5jb20v", + "path": "" + }, + "extra_snippets": [ + "Build, run, and share Python code online for free with the help of online-integrated python's development environment (IDE). It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local.", + "It is one of the most efficient, dependable, and potent online compilers for the Python programming language. It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice.", + "It is not necessary for you to bother about establishing a Python environment in your local. Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button!", + "Now You can immediately execute the Python code in the web browser of your choice. Using this Python editor is simple and quick to get up and running with. Simply type in the programme, and then press the RUN button! The code can be saved online by choosing the SHARE option, which also gives you the ability to access your code from any location providing you have internet access." + ] + }, + { + "title": "Python · GitHub", + "url": "https://github.com/python", + "is_source_local": false, + "is_source_both": false, + "description": "Repositories related to the <strong>Python</strong> Programming language - <strong>Python</strong>", + "page_age": "2023-03-06T00:00:00", + "profile": { + "name": "GitHub", + "url": "https://github.com/python", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/POoaRfu_7gfp-D_O3qMNJrwDqJNbiDu1HuBpNJ_MpVQ/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9hdmF0/YXJzLmdpdGh1YnVz/ZXJjb250ZW50LmNv/bS91LzE1MjU5ODE_/cz0yMDAmYW1wO3Y9/NA", + "original": "https://avatars.githubusercontent.com/u/1525981?s=200&v=4", + "logo": false + }, + "age": "March 6, 2023", + "extra_snippets": ["Configuration for Python planets (e.g. http://planetpython.org)"] + }, + { + "title": "Online Python Compiler (Interpreter)", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "is_source_local": false, + "is_source_both": false, + "description": "Write and run <strong>Python</strong> code using our online compiler (interpreter). You can use <strong>Python</strong> Shell like IDLE, and take inputs from the user in our <strong>Python</strong> compiler.", + "page_age": "2020-06-02T00:00:00", + "profile": { + "name": "Programiz", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "long_name": "programiz.com", + "img": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "programiz.com", + "hostname": "www.programiz.com", + "favicon": "https://imgs.search.brave.com/ozj4JFayZ3Fs5c9eTp7M5g12azQ_Hblgu4dpTuHRz6U/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMGJlN2U1YjVi/Y2M3ZDU5OGMwMWNi/M2Q3YjhjOTM1ZTFk/Y2NkZjE4NGQwOGIx/MTQ4NjI2YmNhODVj/MzFkMmJhYy93d3cu/cHJvZ3JhbWl6LmNv/bS8", + "path": "› python-programming › online-compiler" + }, + "age": "June 2, 2020", + "extra_snippets": [ + "Python Online Compiler Online R Compiler SQL Online Editor Online HTML/CSS Editor Online Java Compiler C Online Compiler C++ Online Compiler C# Online Compiler JavaScript Online Compiler Online GoLang Compiler Online PHP Compiler Online Swift Compiler Online Rust Compiler", + "# Online Python compiler (interpreter) to run Python online. # Write Python 3 code in this online editor and run it. print(\"Try programiz.pro\")" + ] + }, + { + "title": "Python Developer", + "url": "https://twitter.com/Python_Dv/status/1786763460992544791", + "is_source_local": false, + "is_source_both": false, + "description": "<strong>Python</strong> Developer", + "page_age": "2024-05-04T14:30:03", + "profile": { + "name": "X", + "url": "https://twitter.com/Python_Dv/status/1786763460992544791", + "long_name": "twitter.com", + "img": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "twitter.com", + "hostname": "twitter.com", + "favicon": "https://imgs.search.brave.com/Zq483bGX0GnSgym-1P7iyOyEDX3PkDZSNT8m56F862A/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2MxOTUxNzhj/OTY1ZTQ3N2I0MjJk/MTY5NGM0MTRlYWVi/MjU1YWE2NDUwYmQ2/YTA2MDFhMDlkZDEx/NTAzZGNiNi90d2l0/dGVyLmNvbS8", + "path": "› Python_Dv › status › 1786763460992544791" + }, + "age": "20 hours ago" + }, + { + "title": "input table name? - python script - KNIME Extensions - KNIME Community Forum", + "url": "https://forum.knime.com/t/input-table-name-python-script/78978", + "is_source_local": false, + "is_source_both": false, + "description": "Hi, when running a <strong>python</strong> script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? Best wishes, Dario", + "page_age": "2024-05-04T09:20:44", + "profile": { + "name": "Knime", + "url": "https://forum.knime.com/t/input-table-name-python-script/78978", + "long_name": "forum.knime.com", + "img": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "article", + "meta_url": { + "scheme": "https", + "netloc": "forum.knime.com", + "hostname": "forum.knime.com", + "favicon": "https://imgs.search.brave.com/WQoOhAD5i6uEhJ-qXvlWMJwbGA52f2Ycc_ns36EK698/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTAxNzMxNjFl/MzJjNzU5NzRkOTMz/Mjg4NDU2OWUxM2Rj/YzVkOGM3MzIwNzI2/YTY1NzYxNzA1MDE5/NzQzOWU3NC9mb3J1/bS5rbmltZS5jb20v", + "path": " › knime extensions" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/DtEl38dcvuM1kGfhN0T5HfOrsMJcztWNyriLvtDJmKI/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9mb3J1/bS1jZG4ua25pbWUu/Y29tL3VwbG9hZHMv/ZGVmYXVsdC9vcmln/aW5hbC8zWC9lLzYv/ZTY0M2M2NzFlNzAz/MDg2MjkwMWY2YzJh/OWFjOWI5ZmEwM2M3/ZjMwZi5wbmc", + "original": "https://forum-cdn.knime.com/uploads/default/original/3X/e/6/e643c671e7030862901f6c2a9ac9b9fa03c7f30f.png", + "logo": false + }, + "age": "1 day ago", + "extra_snippets": [ + "Hi, when running a python script node, I get the error seen on the screenshot Same happens with this code too: The script input is output from the csv reader node. How can I get the right name for that table? …" + ] + }, + { + "title": "What does the Double Star operator mean in Python? - GeeksforGeeks", + "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", + "is_source_local": false, + "is_source_both": false, + "description": "A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.", + "page_age": "2023-03-14T17:15:04", + "profile": { + "name": "GeeksforGeeks", + "url": "https://www.geeksforgeeks.org/what-does-the-double-star-operator-mean-in-python/", + "long_name": "geeksforgeeks.org", + "img": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "article", + "meta_url": { + "scheme": "https", + "netloc": "geeksforgeeks.org", + "hostname": "www.geeksforgeeks.org", + "favicon": "https://imgs.search.brave.com/fhzcfv5xltx6-YBvJI9RZgS7xZo0dPNaASsrB8YOsCs/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjBhOGQ3MmNi/ZWE5N2EwMmZjYzA1/ZTI0ZTFhMGUyMTE0/MGM0ZTBmMWZlM2Y2/Yzk2ODMxZTRhYTBi/NDdjYTE0OS93d3cu/Z2Vla3Nmb3JnZWVr/cy5vcmcv", + "path": "› what-does-the-double-star-operator-mean-in-python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/GcR-j_dLbyHkbHEI3ffLMi6xpXGhF_2Z8POIoqtokhM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9tZWRp/YS5nZWVrc2Zvcmdl/ZWtzLm9yZy93cC1j/b250ZW50L3VwbG9h/ZHMvZ2ZnXzIwMFgy/MDAtMTAweDEwMC5w/bmc", + "original": "https://media.geeksforgeeks.org/wp-content/uploads/gfg_200X200-100x100.png", + "logo": false + }, + "age": "March 14, 2023", + "extra_snippets": [ + "Difference between / vs. // operator in Python", + "Double Star or (**) is one of the Arithmetic Operator (Like +, -, *, **, /, //, %) in Python Language. It is also known as Power Operator.", + "The time complexity of the given Python program is O(n), where n is the number of key-value pairs in the input dictionary.", + "Inplace Operators in Python | Set 2 (ixor(), iand(), ipow(),…)" + ] + }, + { + "title": "r/Python", + "url": "https://www.reddit.com/r/Python/", + "is_source_local": false, + "is_source_both": false, + "description": "The official <strong>Python</strong> community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the <strong>Python</strong> programming language. --- If you have questions or are new to <strong>Python</strong> use r/LearnPython", + "page_age": "2022-12-30T16:25:02", + "profile": { + "name": "Reddit", + "url": "https://www.reddit.com/r/Python/", + "long_name": "reddit.com", + "img": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "reddit.com", + "hostname": "www.reddit.com", + "favicon": "https://imgs.search.brave.com/mAZYEK9Wi13WLDUge7XZ8YuDTwm6DP6gBjvz1GdYZVY/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvN2ZiNTU0M2Nj/MTFhZjRiYWViZDlk/MjJiMjBjMzFjMDRk/Y2IzYWI0MGI0MjVk/OGY5NzQzOGQ5NzQ5/NWJhMWI0NC93d3cu/cmVkZGl0LmNvbS8", + "path": "› r › Python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/zWd10t3zg34ciHiAB-K5WWK3h_H4LedeDot9BVX7Ydo/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9zdHls/ZXMucmVkZGl0bWVk/aWEuY29tL3Q1XzJx/aDB5L3N0eWxlcy9j/b21tdW5pdHlJY29u/X2NpZmVobDR4dDdu/YzEucG5n", + "original": "https://styles.redditmedia.com/t5_2qh0y/styles/communityIcon_cifehl4xt7nc1.png", + "logo": false + }, + "age": "December 30, 2022", + "extra_snippets": [ + "r/Python: The official Python community for Reddit! Stay up to date with the latest news, packages, and meta information relating to the Python…", + "By default, Python allows you to import and use anything, anywhere. Over time, this results in modules that were intended to be separate getting tightly coupled together, and domain boundaries breaking down. We experienced this first-hand at a unicorn startup, where the eng team paused development for over a year in an attempt to split up packages into independent services.", + "Hello r/Python! It's time to share what you've been working on! Whether it's a work-in-progress, a completed masterpiece, or just a rough idea, let us know what you're up to!", + "Whether it's your job, your hobby, or your passion project, all Python-related work is welcome here." + ] + }, + { + "title": "GitHub - python/cpython: The Python programming language", + "url": "https://github.com/python/cpython", + "is_source_local": false, + "is_source_both": false, + "description": "The <strong>Python</strong> programming language. Contribute to <strong>python</strong>/cpython development by creating an account on GitHub.", + "page_age": "2022-10-29T00:00:00", + "profile": { + "name": "GitHub", + "url": "https://github.com/python/cpython", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "software", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› python › cpython" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/BJbWFRUqgP-tKIyGK9ByXjuYjHO2mtYigUOEFNz_gXk/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS82/MTY5YmJkNTQ0YzAy/NDg0MGU4NDdjYTU1/YTU3ZGZmMDA2ZDAw/YWQ1NDIzOTFmYTQ3/YmJjODg3OWM0NWYw/MTZhL3B5dGhvbi9j/cHl0aG9u", + "original": "https://opengraph.githubassets.com/6169bbd544c024840e847ca55a57dff006d00ad542391fa47bbc8879c45f016a/python/cpython", + "logo": false + }, + "age": "October 29, 2022", + "extra_snippets": [ + "You can pass many options to the configure script; run ./configure --help to find out more. On macOS case-insensitive file systems and on Cygwin, the executable is called python.exe; elsewhere it's just python.", + "Building a complete Python installation requires the use of various additional third-party libraries, depending on your build platform and configure options. Not all standard library modules are buildable or useable on all platforms. Refer to the Install dependencies section of the Developer Guide for current detailed information on dependencies for various Linux distributions and macOS.", + "To get an optimized build of Python, configure --enable-optimizations before you run make. This sets the default make targets up to enable Profile Guided Optimization (PGO) and may be used to auto-enable Link Time Optimization (LTO) on some platforms. For more details, see the sections below.", + "Copyright © 2001-2024 Python Software Foundation. All rights reserved." + ] + }, + { + "title": "5. Data Structures — Python 3.12.3 documentation", + "url": "https://docs.python.org/3/tutorial/datastructures.html", + "is_source_local": false, + "is_source_both": false, + "description": "This chapter describes some things you’ve learned about already in more detail, and adds some new things as well. More on Lists: The list data type has some more methods. Here are all of the method...", + "page_age": "2023-07-04T00:00:00", + "profile": { + "name": "Python documentation", + "url": "https://docs.python.org/3/tutorial/datastructures.html", + "long_name": "docs.python.org", + "img": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "docs.python.org", + "hostname": "docs.python.org", + "favicon": "https://imgs.search.brave.com/F5Ym7eSElhGdGUFKLRxDj9Z_tc180ldpeMvQ2Q6ARbA/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTUzOTFjOGVi/YTcyOTVmODA3ODIy/YjE2NzFjY2ViMjhl/NzRlY2JhYTc5YjNm/ZjhmODAyZWI2OGUw/ZjU4NDVlNy9kb2Nz/LnB5dGhvbi5vcmcv", + "path": "› 3 › tutorial › datastructures.html" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/Y7GrMRF8WorDIMLuOl97XC8ltYpoOCqNwWF2pQIIKls/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9kb2Nz/LnB5dGhvbi5vcmcv/My9fc3RhdGljL29n/LWltYWdlLnBuZw", + "original": "https://docs.python.org/3/_static/og-image.png", + "logo": false + }, + "age": "July 4, 2023", + "extra_snippets": [ + "You might have noticed that methods like insert, remove or sort that only modify the list have no return value printed – they return the default None. [1] This is a design principle for all mutable data structures in Python.", + "We saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types — list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.", + "Python also includes a data type for sets. A set is an unordered collection with no duplicate elements. Basic uses include membership testing and eliminating duplicate entries. Set objects also support mathematical operations like union, intersection, difference, and symmetric difference.", + "Another useful data type built into Python is the dictionary (see Mapping Types — dict). Dictionaries are sometimes found in other languages as “associative memories” or “associative arrays”. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys." + ] + }, + { + "title": "Something wrong with python packages / AUR Issues, Discussion & PKGBUILD Requests / Arch Linux Forums", + "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", + "is_source_local": false, + "is_source_both": false, + "description": "Big <strong>Python</strong> updates require <strong>Python</strong> packages to be rebuild. For some reason they didn't think a bump that made it necessary to rebuild half the official repo was a news post.", + "page_age": "2024-05-04T08:30:02", + "profile": { + "name": "Archlinux", + "url": "https://bbs.archlinux.org/viewtopic.php?id=295466", + "long_name": "bbs.archlinux.org", + "img": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "bbs.archlinux.org", + "hostname": "bbs.archlinux.org", + "favicon": "https://imgs.search.brave.com/3au9oqkzSri_aLEec3jo-0bFgLuICkydrWfjFcC8lkI/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvNWNkODM1MWJl/ZmJhMzkzNzYzMDkz/NmEyMWMxNjI5MjNk/NGJmZjFhNTBlZDNl/Mzk5MzJjOGZkYjZl/MjNmY2IzNS9iYnMu/YXJjaGxpbnV4Lm9y/Zy8", + "path": "› viewtopic.php" + }, + "age": "1 day ago", + "extra_snippets": [ + "Traceback (most recent call last): File \"/usr/lib/python3.12/importlib/metadata/__init__.py\", line 397, in from_name return next(cls.discover(name=name)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ StopIteration During handling of the above exception, another exception occurred: Traceback (most recent call last): File \"/usr/bin/informant\", line 33, in <module> sys.exit(load_entry_point('informant==0.5.0', 'console_scripts', 'informant')()) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File \"/usr/bin/informant\", line 22, in importlib_load_entry_point for entry_point in distribution(dis" + ] + }, + { + "title": "Introduction to Python", + "url": "https://www.w3schools.com/python/python_intro.asp", + "is_source_local": false, + "is_source_both": false, + "description": "W3Schools offers free online tutorials, references and exercises in all the major languages of the web. Covering popular subjects like HTML, CSS, JavaScript, <strong>Python</strong>, SQL, Java, and many, many more.", + "profile": { + "name": "W3Schools", + "url": "https://www.w3schools.com/python/python_intro.asp", + "long_name": "w3schools.com", + "img": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "w3schools.com", + "hostname": "www.w3schools.com", + "favicon": "https://imgs.search.brave.com/JwO5r7z3HTBkU29vgNH_4rrSWLf2M4-8FMWNvbxrKX8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjVlMGVkZDVj/ZGMyZWRmMzAwODRi/ZDAwZGE4NWI3NmU4/MjRhNjEzOGFhZWY3/ZGViMjY1OWY2ZDYw/YTZiOGUyZS93d3cu/dzNzY2hvb2xzLmNv/bS8", + "path": "› python › python_intro.asp" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/EMfp8dodbJehmj0yCJh8317RHuaumsddnHI4bujvFcg/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dzNzY2hvb2xzLmNv/bS9pbWFnZXMvdzNz/Y2hvb2xzX2xvZ29f/NDM2XzIucG5n", + "original": "https://www.w3schools.com/images/w3schools_logo_436_2.png", + "logo": true + }, + "extra_snippets": [ + "Well organized and easy to understand Web building tutorials with lots of examples of how to use HTML, CSS, JavaScript, SQL, Python, PHP, Bootstrap, Java, XML and more.", + "HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS R TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI GO KOTLIN SASS VUE DSA GEN AI SCIPY AWS CYBERSECURITY DATA SCIENCE", + "Python Variables Variable Names Assign Multiple Values Output Variables Global Variables Variable Exercises Python Data Types Python Numbers Python Casting Python Strings", + "Python Strings Slicing Strings Modify Strings Concatenate Strings Format Strings Escape Characters String Methods String Exercises Python Booleans Python Operators Python Lists" + ] + }, + { + "title": "bug: AUR package wants to use python but does not find any preset version · Issue #1740 · asdf-vm/asdf", + "url": "https://github.com/asdf-vm/asdf/issues/1740", + "is_source_local": false, + "is_source_both": false, + "description": "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0...", + "page_age": "2024-05-04T06:45:04", + "profile": { + "name": "GitHub", + "url": "https://github.com/asdf-vm/asdf/issues/1740", + "long_name": "github.com", + "img": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "software", + "meta_url": { + "scheme": "https", + "netloc": "github.com", + "hostname": "github.com", + "favicon": "https://imgs.search.brave.com/v8685zI4XInM0zxlNI2s7oE_2Sb-EL7lAy81WXbkQD8/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYWQyNWM1NjA5/ZjZmZjNlYzI2MDNk/N2VkNmJhYjE2MzZl/MDY5ZTMxMDUzZmY1/NmU3NWIzNWVmMjk0/NTBjMjJjZi9naXRo/dWIuY29tLw", + "path": "› asdf-vm › asdf › issues › 1740" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/KrLW5s_2n4jyP8XLbc3ZPVBaLD963tQgWzG9EWPZlQs/rs:fit:200:200:1/g:ce/aHR0cHM6Ly9vcGVu/Z3JhcGguZ2l0aHVi/YXNzZXRzLmNvbS81/MTE0ZTdkOGIwODM2/YmQ2MTY3NzQ1ZGI4/MmZjMGE3OGUyMjcw/MGFlY2ZjMWZkODBl/MDYzZTNiN2ZjOWNj/NzYyL2FzZGYtdm0v/YXNkZi9pc3N1ZXMv/MTc0MA", + "original": "https://opengraph.githubassets.com/5114e7d8b0836bd6167745db82fc0a78e22700aecfc1fd80e063e3b7fc9cc762/asdf-vm/asdf/issues/1740", + "logo": false + }, + "age": "1 day ago", + "extra_snippets": [ + "==> Starting build()... No preset version installed for command python Please install a version by running one of the following: asdf install python 3.8 or add one of the following versions in your config file at /home/ferret/.tool-versions python 3.11.0 python 3.12.1 python 3.12.3 ==> ERROR: A failure occurred in build(). Aborting...", + "-> error making: tlpui-exit status 4 -> Failed to install the following packages. Manual intervention is required: tlpui - exit status 4 ferret@FX505DT in ~ $ cat /home/ferret/.tool-versions nodejs 21.6.0 python 3.12.3 ferret@FX505DT in ~ $ python -V Python 3.12.3 ferret@FX505DT in ~ $ which python /home/ferret/.asdf/shims/python", + "Describe the Bug I am not sure why this is happening, I am trying to install tlpui from AUR and it fails, here are some logs to help: ==> Making package: tlpui 2:1.6.5-1 (Mi 10 apr 2024 23:19:15 +0300) ==> Retrieving sources... -> Found ..." + ] + }, + { + "title": "What are python.exe and python3.exe, and why do they appear to point to App Installer? | Windows 11 Forum", + "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", + "is_source_local": false, + "is_source_both": false, + "description": "I was looking at App execution aliases (Settings > Apps > Advanced app settings > App execution aliases) on my new computer -- my first Windows 11 computer. Why are <strong>python</strong>.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP...", + "page_age": "2024-05-03T17:30:04", + "profile": { + "name": "Windows 11 Forum", + "url": "https://www.elevenforum.com/t/what-are-python-exe-and-python3-exe-and-why-do-they-appear-to-point-to-app-installer.24886/", + "long_name": "elevenforum.com", + "img": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "elevenforum.com", + "hostname": "www.elevenforum.com", + "favicon": "https://imgs.search.brave.com/XVRAYMEj6Im8i7jV5RxeTwpiRPtY9IWg4wRIuh-WhEw/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvZjk5MDZkMDIw/M2U1OWIwNjM5Y2U1/M2U2NzNiNzVkNTA5/NzA5OTI1ZTFmOTc4/MzU3OTlhYzU5OTVi/ZGNjNTY4MS93d3cu/ZWxldmVuZm9ydW0u/Y29tLw", + "path": " › windows support forums › apps and software" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/DVoFcE6d_-lx3BVGNS-RZK_lZzxQ8VhwZVf3AVqEJFA/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/ZWxldmVuZm9ydW0u/Y29tL2RhdGEvYXNz/ZXRzL2xvZ28vbWV0/YTEtMjAxLnBuZw", + "original": "https://www.elevenforum.com/data/assets/logo/meta1-201.png", + "logo": true + }, + "age": "2 days ago", + "extra_snippets": [ + "Why are python.exe and python3.exe listed as App Installer? I assume that App Installer refers to installation of Microsoft Store / UWP apps, but if that's the case, then why are they called python.exe and python3.exe? Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App?", + "Or are python.exe and python3.exe simply serving as aliases / pointers pointing to App Installer, which is itself a Microsoft Store App? I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python.", + "I wish to soon install Python, along with an integrated development editor (IDE), on my machine, so that I can code in Python. But is a Python interpreter already on my computer as suggested, if obliquely, by the presence of python.exe and python3.exe? I kind of doubt it." + ] + }, + { + "title": "How to Watermark Your Images Using Python OpenCV in ...", + "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", + "is_source_local": false, + "is_source_both": false, + "description": "Medium is an open platform where readers find dynamic thinking, and where expert and undiscovered voices can share their writing on any topic.", + "page_age": "2024-05-03T14:05:06", + "profile": { + "name": "Medium", + "url": "https://medium.com/@daily_data_prep/how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1", + "long_name": "medium.com", + "img": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "medium.com", + "hostname": "medium.com", + "favicon": "https://imgs.search.brave.com/qvE2kIQCiAsnPv2C6P9xM5J2VVWdm55g-A-2Q_yIJ0g/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvOTZhYmQ1N2Q4/NDg4ZDcyODIyMDZi/MzFmOWNhNjE3Y2E4/Y2YzMThjNjljNDIx/ZjllZmNhYTcwODhl/YTcwNDEzYy9tZWRp/dW0uY29tLw", + "path": "› @daily_data_prep › how-to-watermark-your-images-using-python-opencv-in-bulk-e472085389a1" + }, + "age": "2 days ago" + }, + { + "title": "Increment and Decrement Operators in Python?", + "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", + "is_source_local": false, + "is_source_both": false, + "description": "Increment and Decrement Operators in <strong>Python</strong> - <strong>Python</strong> does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python ...", + "page_age": "2023-08-23T00:00:00", + "profile": { + "name": "Tutorialspoint", + "url": "https://www.tutorialspoint.com/increment-and-decrement-operators-in-python", + "long_name": "tutorialspoint.com", + "img": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "tutorialspoint.com", + "hostname": "www.tutorialspoint.com", + "favicon": "https://imgs.search.brave.com/Wt8BSkivPlFwcU5yBtf7YzuvTuRExyd_502cdABCS5c/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYjcyYjAzYmVl/ODU4MzZiMjJiYTFh/MjJhZDNmNWE4YzA5/MDgyYTZhMDg3NTYw/M2NiY2NiZTUxN2I5/MjU1MWFmMS93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tLw", + "path": "› increment-and-decrement-operators-in-python" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/ddG5vyZGLVudvecEbQJPeG8tGuaZ7g3Xz6Gyjdl5WA8/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/dHV0b3JpYWxzcG9p/bnQuY29tL2ltYWdl/cy90cF9sb2dvXzQz/Ni5wbmc", + "original": "https://www.tutorialspoint.com/images/tp_logo_436.png", + "logo": true + }, + "age": "August 23, 2023", + "extra_snippets": [ + "Increment and Decrement Operators in Python - Python does not have unary increment/decrement operator (++/--). Instead to increment a value, usea += 1to decrement a value, use −a -= 1Example>>> a = 0 >>> >>> #Increment >>> a +=1 >>> >>> #Decrement >>> a -= 1 >>> >>> #value of a >>> a 0Python does not provide multiple ways to do the same thing", + "So what above statement means in python is: create an object of type int having value 1 and give the name a to it. The object is an instance of int having value 1 and the name a refers to it. The assigned name a and the object to which it refers are distinct.", + "Python does not provide multiple ways to do the same thing .", + "However, be careful if you are coming from a language like C, Python doesn’t have \"variables\" in the sense that C does, instead python uses names and objects and in python integers (int’s) are immutable." + ] + }, + { + "title": "Gumroad – How not to suck at Python / SideFX Houdini | CG Persia", + "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", + "is_source_local": false, + "is_source_both": false, + "description": "Info: This course is made for artists or TD (technical director) willing to learn <strong>Python</strong> to improve their workflows inside SideFX Houdini, get faster in production and develop all the tools you always wished you had.", + "page_age": "2024-05-03T08:35:03", + "profile": { + "name": "Cgpersia", + "url": "https://cgpersia.com/2024/05/gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html", + "long_name": "cgpersia.com", + "img": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "cgpersia.com", + "hostname": "cgpersia.com", + "favicon": "https://imgs.search.brave.com/VjyaopAm-M9sWvM7n-KnGZ3T5swIOwwE80iF5QVqQPg/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvYmE0MzQ4NmI2/NjFhMTA1ZDBiN2Iw/ZWNiNDUxNjUwYjdh/MGE5ZjQ0ZjIxNzll/NmVkZDE2YzYyMDBh/NDNiMDgwMy9jZ3Bl/cnNpYS5jb20v", + "path": "› 2024 › 05 › gumroad-how-not-to-suck-at-python-sidefx-houdini-195370.html" + }, + "age": "2 days ago", + "extra_snippets": [ + "Posted in: 2D, CG Releases, Downloads, Learning, Tutorials, Videos. Tagged: Gumroad, Python, Sidefx. Leave a Comment", + "01 – Python – Fundamentals Get the Fundamentals of python before starting the fun stuff ! 02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools !", + "02 – Python Construction Part02 digging further into python concepts 03 – Houdini – Python Basics Applying some basic python in Houdini and starting to make tools ! 04 – Houdini – Python Intermediate Applying some more advanced python in Houdini to make tools ! 05 – Houdini – Python Expert Using QtDesigner in combinaison with Houdini Python/Pyside to create advanced tools." + ] + }, + { + "title": "How to install Python: The complete Python programmer’s guide", + "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", + "is_source_local": false, + "is_source_both": false, + "description": "An easy guide on how set up your operating system so you can program in <strong>Python</strong>, and how to update or uninstall it. For Linux, Windows, and macOS.", + "page_age": "2024-05-02T07:30:02", + "profile": { + "name": "Pluralsight", + "url": "https://www.pluralsight.com/resources/blog/software-development/python-installation-guide", + "long_name": "pluralsight.com", + "img": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw" + }, + "language": "en", + "family_friendly": true, + "type": "search_result", + "subtype": "generic", + "meta_url": { + "scheme": "https", + "netloc": "pluralsight.com", + "hostname": "www.pluralsight.com", + "favicon": "https://imgs.search.brave.com/zvwQNSVu9-jR2CRlNcsTzxjaXKPlXNuh-Jo9-0yA1OE/rs:fit:32:32:1/g:ce/aHR0cDovL2Zhdmlj/b25zLnNlYXJjaC5i/cmF2ZS5jb20vaWNv/bnMvMTNkNWQyNjk3/M2Q0NzYyMmUyNDc3/ZjYwMWFlZDI5YTI4/ODhmYzc2MDkzMjAy/MjNkMWY1MDE3NTQw/MzI5NWVkZS93d3cu/cGx1cmFsc2lnaHQu/Y29tLw", + "path": " › blog › blog" + }, + "thumbnail": { + "src": "https://imgs.search.brave.com/xrv5PHH2Bzmq2rcIYzk__8h5RqCj6kS3I6SGCNw5dZM/rs:fit:200:200:1/g:ce/aHR0cHM6Ly93d3cu/cGx1cmFsc2lnaHQu/Y29tL2NvbnRlbnQv/ZGFtL3BzL2ltYWdl/cy9yZXNvdXJjZS1j/ZW50ZXIvYmxvZy9o/ZWFkZXItaGVyby1p/bWFnZXMvUHl0aG9u/LndlYnA", + "original": "https://www.pluralsight.com/content/dam/ps/images/resource-center/blog/header-hero-images/Python.webp", + "logo": false + }, + "age": "3 days ago", + "extra_snippets": [ + "Whether it’s your first time programming or you’re a seasoned programmer, you’ll have to install or update Python every now and then --- or if necessary, uninstall it. In this article, you'll learn how to do just that.", + "Some systems come with Python, so to start off, we’ll first check to see if it’s installed on your system before we proceed. To do that, we’ll need to open a terminal. Since you might be new to programming, let’s go over how to open a terminal for Linux, Windows, and macOS.", + "Before we dive into setting up your system so you can program in Python, let’s talk terminal basics and benefits.", + "However, let’s focus on why we need it for working with Python. We use a terminal, or command line, to:" + ] + } + ], + "family_friendly": true + } +} diff --git a/backend/apps/rag/search/testdata/google_pse.json b/backend/apps/rag/search/testdata/google_pse.json new file mode 100644 index 0000000000000000000000000000000000000000..15da9729cde30eee86d0302f662c5ca37d71f65b --- /dev/null +++ b/backend/apps/rag/search/testdata/google_pse.json @@ -0,0 +1,442 @@ +{ + "kind": "customsearch#search", + "url": { + "type": "application/json", + "template": "https://www.googleapis.com/customsearch/v1?q={searchTerms}&num={count?}&start={startIndex?}&lr={language?}&safe={safe?}&cx={cx?}&sort={sort?}&filter={filter?}&gl={gl?}&cr={cr?}&googlehost={googleHost?}&c2coff={disableCnTwTranslation?}&hq={hq?}&hl={hl?}&siteSearch={siteSearch?}&siteSearchFilter={siteSearchFilter?}&exactTerms={exactTerms?}&excludeTerms={excludeTerms?}&linkSite={linkSite?}&orTerms={orTerms?}&dateRestrict={dateRestrict?}&lowRange={lowRange?}&highRange={highRange?}&searchType={searchType}&fileType={fileType?}&rights={rights?}&imgSize={imgSize?}&imgType={imgType?}&imgColorType={imgColorType?}&imgDominantColor={imgDominantColor?}&alt=json" + }, + "queries": { + "request": [ + { + "title": "Google Custom Search - lectures", + "totalResults": "2450000000", + "searchTerms": "lectures", + "count": 10, + "startIndex": 1, + "inputEncoding": "utf8", + "outputEncoding": "utf8", + "safe": "off", + "cx": "0473ef98502d44e18" + } + ], + "nextPage": [ + { + "title": "Google Custom Search - lectures", + "totalResults": "2450000000", + "searchTerms": "lectures", + "count": 10, + "startIndex": 11, + "inputEncoding": "utf8", + "outputEncoding": "utf8", + "safe": "off", + "cx": "0473ef98502d44e18" + } + ] + }, + "context": { + "title": "LLM Search" + }, + "searchInformation": { + "searchTime": 0.445959, + "formattedSearchTime": "0.45", + "totalResults": "2450000000", + "formattedTotalResults": "2,450,000,000" + }, + "items": [ + { + "kind": "customsearch#result", + "title": "The Feynman Lectures on Physics", + "htmlTitle": "The Feynman \u003cb\u003eLectures\u003c/b\u003e on Physics", + "link": "https://www.feynmanlectures.caltech.edu/", + "displayLink": "www.feynmanlectures.caltech.edu", + "snippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", + "htmlSnippet": "This edition has been designed for ease of reading on devices of any size or shape; text, figures and equations can all be zoomed without degradation.", + "cacheId": "CyXMWYWs9UEJ", + "formattedUrl": "https://www.feynmanlectures.caltech.edu/", + "htmlFormattedUrl": "https://www.feynman\u003cb\u003electures\u003c/b\u003e.caltech.edu/", + "pagemap": { + "metatags": [ + { + "viewport": "width=device-width, initial-scale=1.0" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Video Lectures", + "htmlTitle": "Video \u003cb\u003eLectures\u003c/b\u003e", + "link": "https://www.reddit.com/r/lectures/", + "displayLink": "www.reddit.com", + "snippet": "r/lectures: This subreddit is all about video lectures, talks and interesting public speeches. The topics include mathematics, physics, computer…", + "htmlSnippet": "r/\u003cb\u003electures\u003c/b\u003e: This subreddit is all about video \u003cb\u003electures\u003c/b\u003e, talks and interesting public speeches. The topics include mathematics, physics, computer…", + "formattedUrl": "https://www.reddit.com/r/lectures/", + "htmlFormattedUrl": "https://www.reddit.com/r/\u003cb\u003electures\u003c/b\u003e/", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTZtOjhfkgUKQbL3DZxe5F6OVsgeDNffleObjJ7n9RllKQTSsimax7VIaY&s", + "width": "192", + "height": "192" + } + ], + "metatags": [ + { + "og:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", + "theme-color": "#000000", + "og:image:width": "256", + "og:type": "website", + "twitter:card": "summary", + "twitter:title": "r/lectures", + "og:site_name": "Reddit", + "og:title": "r/lectures", + "og:image:height": "256", + "bingbot": "noarchive", + "msapplication-navbutton-color": "#000000", + "og:description": "This subreddit is all about video lectures, talks and interesting public speeches.\n\nThe topics include mathematics, physics, computer science, programming, engineering, biology, medicine, economics, politics, social sciences, and any other subjects!", + "twitter:image": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png", + "apple-mobile-web-app-status-bar-style": "black", + "twitter:site": "@reddit", + "viewport": "width=device-width, initial-scale=1, viewport-fit=cover", + "apple-mobile-web-app-capable": "yes", + "og:ttl": "600", + "og:url": "https://www.reddit.com/r/lectures/" + } + ], + "cse_image": [ + { + "src": "https://www.redditstatic.com/shreddit/assets/favicon/192x192.png" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lectures & Discussions | Flint Institute of Arts", + "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e & Discussions | Flint Institute of Arts", + "link": "https://flintarts.org/events/lectures", + "displayLink": "flintarts.org", + "snippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", + "htmlSnippet": "It will trace the intricate relationship between jewelry, attire, and the expression of personal identity, social hierarchy, and spiritual belief systems that ...", + "cacheId": "jvpb9DxrfxoJ", + "formattedUrl": "https://flintarts.org/events/lectures", + "htmlFormattedUrl": "https://flintarts.org/events/\u003cb\u003electures\u003c/b\u003e", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS23tMtAeNhJbOWdGxShYsmnyzFdzOC9Hb7lRykA9Pw72z1IlKTkjTdZw&s", + "width": "447", + "height": "113" + } + ], + "metatags": [ + { + "og:image": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg", + "og:type": "website", + "viewport": "width=device-width, initial-scale=1", + "og:title": "Lectures & Discussions | Flint Institute of Arts", + "og:description": "The Flint Institute of Arts is the second largest art museum in Michigan and one of the largest museum art schools in the nation." + } + ], + "cse_image": [ + { + "src": "https://flintarts.org/uploads/images/page-headers/_headerImage/nightshot.jpg" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Mandel Lectures | Mandel Center for the Humanities ... - Waltham", + "htmlTitle": "Mandel \u003cb\u003eLectures\u003c/b\u003e | Mandel Center for the Humanities ... - Waltham", + "link": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "displayLink": "www.brandeis.edu", + "snippet": "Past Lectures · Lecture 1: \"Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction\" · Lecture 2: \"Solidarity in Sound: Grassroots ...", + "htmlSnippet": "Past \u003cb\u003eLectures\u003c/b\u003e · \u003cb\u003eLecture\u003c/b\u003e 1: "Invisible Music: The Sonic Idea of Black Revolution From Captivity to Reconstruction" · \u003cb\u003eLecture\u003c/b\u003e 2: "Solidarity in Sound: Grassroots ...", + "cacheId": "cQLOZr0kgEEJ", + "formattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "htmlFormattedUrl": "https://www.brandeis.edu/mandel-center-humanities/mandel-\u003cb\u003electures\u003c/b\u003e.html", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQWlU7bcJ5pIHk7RBCk2QKE-48ejF7hyPV0pr-20_cBt2BGdfKtiYXBuyw&s", + "width": "275", + "height": "183" + } + ], + "metatags": [ + { + "og:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba", + "twitter:card": "summary_large_image", + "viewport": "width=device-width,initial-scale=1,minimum-scale=1", + "og:title": "Mandel Lectures in the Humanities", + "og:url": "https://www.brandeis.edu/mandel-center-humanities/mandel-lectures.html", + "og:description": "Annual Lecture Series", + "twitter:image": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" + } + ], + "cse_image": [ + { + "src": "https://www.brandeis.edu/mandel-center-humanities/events/events-images/mlhzumba" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Brian Douglas - YouTube", + "htmlTitle": "Brian Douglas - YouTube", + "link": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "displayLink": "www.youtube.com", + "snippet": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it.", + "htmlSnippet": "Welcome to Control Systems \u003cb\u003eLectures\u003c/b\u003e! This collection of videos is intended to supplement a first year controls class, not replace it.", + "cacheId": "NEROyBHolL0J", + "formattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "htmlFormattedUrl": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "pagemap": { + "hcard": [ + { + "fn": "Brian Douglas", + "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" + } + ], + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR7G0CeCBz_wVTZgjnhEr2QbiKP7f3uYzKitZYn74Mi32cDmVxvsegJoLI&s", + "width": "225", + "height": "225" + } + ], + "imageobject": [ + { + "width": "900", + "url": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "height": "900" + } + ], + "person": [ + { + "name": "Brian Douglas", + "url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg" + } + ], + "metatags": [ + { + "apple-itunes-app": "app-id=544007664, app-argument=https://m.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?referring_app=com.apple.mobilesafari-smartbanner, affiliate-data=ct=smart_app_banner_polymer&pt=9008", + "og:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "twitter:app:url:iphone": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:id:googleplay": "com.google.android.youtube", + "theme-color": "rgb(255, 255, 255)", + "og:image:width": "900", + "twitter:card": "summary", + "og:site_name": "YouTube", + "twitter:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:url:ipad": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "al:android:package": "com.google.android.youtube", + "twitter:app:name:googleplay": "YouTube", + "al:ios:url": "vnd.youtube://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:id:iphone": "544007664", + "og:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", + "al:ios:app_store_id": "544007664", + "twitter:image": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj", + "twitter:site": "@youtube", + "og:type": "profile", + "twitter:title": "Brian Douglas", + "al:ios:app_name": "YouTube", + "og:title": "Brian Douglas", + "og:image:height": "900", + "twitter:app:id:ipad": "544007664", + "al:web:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", + "al:android:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg?feature=applinks", + "fb:app_id": "87741124305", + "twitter:app:url:googleplay": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "twitter:app:name:ipad": "YouTube", + "viewport": "width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no,", + "twitter:description": "Welcome to Control Systems Lectures! This collection of videos is intended to supplement a first year controls class, not replace it. My goal is to take specific concepts in controls and expand on them in order to provide an intuitive understanding which will ultimately make you a better controls engineer. \n\nI'm glad you made it to my channel and I hope you find it useful.\n\nShoot me a message at controlsystemlectures@gmail.com, leave a comment or question and I'll get back to you if I can. Don't forget to subscribe!\n \nTwitter: @BrianBDouglas for engineering tweets and announcement of new videos.\nWebpage: http://engineeringmedia.com\n\nHere is the hardware/software I use: http://www.youtube.com/watch?v=m-M5_mIyHe4\n\nHere's a list of my favorite references: http://bit.ly/2skvmWd\n\n--Brian", + "og:url": "https://www.youtube.com/channel/UCq0imsn84ShAe9PBOFnoIrg", + "al:android:app_name": "YouTube", + "twitter:app:name:iphone": "YouTube" + } + ], + "cse_image": [ + { + "src": "https://yt3.googleusercontent.com/ytc/AIdro_nLo68wetImbwGUYP3stve_iKmAEccjhqB-q4o79xdInN4=s900-c-k-c0x00ffffff-no-rj" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lecture - Wikipedia", + "htmlTitle": "\u003cb\u003eLecture\u003c/b\u003e - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Lecture", + "displayLink": "en.wikipedia.org", + "snippet": "Lecture ... For the academic rank, see Lecturer. A lecture (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", + "htmlSnippet": "\u003cb\u003eLecture\u003c/b\u003e ... For the academic rank, see \u003cb\u003eLecturer\u003c/b\u003e. A \u003cb\u003electure\u003c/b\u003e (from Latin: lēctūra 'reading') is an oral presentation intended to present information or teach people ...", + "cacheId": "d9Pjta02fmgJ", + "formattedUrl": "https://en.wikipedia.org/wiki/Lecture", + "htmlFormattedUrl": "https://en.wikipedia.org/wiki/Lecture", + "pagemap": { + "metatags": [ + { + "referrer": "origin", + "og:image": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/26/ADFA_Lecture_Theatres.jpg/1200px-ADFA_Lecture_Theatres.jpg", + "theme-color": "#eaecf0", + "og:image:width": "1200", + "og:type": "website", + "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0", + "og:title": "Lecture - Wikipedia", + "og:image:height": "799", + "format-detection": "telephone=no" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Mount Wilson Observatory | Lectures", + "htmlTitle": "Mount Wilson Observatory | \u003cb\u003eLectures\u003c/b\u003e", + "link": "https://www.mtwilson.edu/lectures/", + "displayLink": "www.mtwilson.edu", + "snippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", + "htmlSnippet": "Talks & Telescopes: August 24, 2024 – Panel: The Triumph of Hubble ... Compelling talks followed by picnicking and convivial stargazing through both the big ...", + "cacheId": "wdXI0azqx5UJ", + "formattedUrl": "https://www.mtwilson.edu/lectures/", + "htmlFormattedUrl": "https://www.mtwilson.edu/\u003cb\u003electures\u003c/b\u003e/", + "pagemap": { + "metatags": [ + { + "viewport": "width=device-width,initial-scale=1,user-scalable=no" + } + ], + "webpage": [ + { + "image": "http://www.mtwilson.edu/wp-content/uploads/2016/09/Logo.jpg", + "url": "https://www.facebook.com/WilsonObs" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Lectures | NBER", + "htmlTitle": "\u003cb\u003eLectures\u003c/b\u003e | NBER", + "link": "https://www.nber.org/research/lectures", + "displayLink": "www.nber.org", + "snippet": "Results 1 - 50 of 354 ... Among featured events at the NBER Summer Institute are the Martin Feldstein Lecture, which examines a current issue involving economic ...", + "htmlSnippet": "Results 1 - 50 of 354 \u003cb\u003e...\u003c/b\u003e Among featured events at the NBER Summer Institute are the Martin Feldstein \u003cb\u003eLecture\u003c/b\u003e, which examines a current issue involving economic ...", + "cacheId": "CvvP3U3nb44J", + "formattedUrl": "https://www.nber.org/research/lectures", + "htmlFormattedUrl": "https://www.nber.org/research/\u003cb\u003electures\u003c/b\u003e", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTmeViEZyV1YmFEFLhcA6WdgAG3v3RV6tB93ncyxSJ5JPst_p2aWrL7D1k&s", + "width": "310", + "height": "163" + } + ], + "metatags": [ + { + "og:image": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg", + "og:site_name": "NBER", + "handheldfriendly": "true", + "viewport": "width=device-width, initial-scale=1.0", + "og:title": "Lectures", + "mobileoptimized": "width", + "og:url": "https://www.nber.org/research/lectures" + } + ], + "cse_image": [ + { + "src": "https://www.nber.org/sites/default/files/2022-06/NBER-FB-Share-Tile-1200.jpg" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", + "htmlTitle": "STUDENTS CANNOT ACCESS RECORDED LECTURES ... - Solved", + "link": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/td-p/190358", + "displayLink": "community.canvaslms.com", + "snippet": "Mar 19, 2020 ... I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", + "htmlSnippet": "Mar 19, 2020 \u003cb\u003e...\u003c/b\u003e I believe the issue is that students were not invited. Are you trying to capture your screen? If not, there is an option to just record your web ...", + "cacheId": "wqrynQXX61sJ", + "formattedUrl": "https://community.canvaslms.com/t5/Canvas...LECTURES/td-p/190358", + "htmlFormattedUrl": "https://community.canvaslms.com/t5/Canvas...\u003cb\u003eLECTURES\u003c/b\u003e/td-p/190358", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRUqXau3N8LfKgSD7OJOvV7xzGarLKRU-ckWXy1ZQ1p4CLPsedvLKmLMhk&s", + "width": "310", + "height": "163" + } + ], + "metatags": [ + { + "og:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png", + "og:type": "article", + "article:section": "Canvas Question Forum", + "article:published_time": "2020-03-19T15:50:03.409Z", + "og:site_name": "Instructure Community", + "article:modified_time": "2020-03-19T13:55:53-07:00", + "viewport": "width=device-width, initial-scale=1.0, user-scalable=yes", + "og:title": "STUDENTS CANNOT ACCESS RECORDED LECTURES", + "og:url": "https://community.canvaslms.com/t5/Canvas-Question-Forum/STUDENTS-CANNOT-ACCESS-RECORDED-LECTURES/m-p/190358#M93667", + "og:description": "I can access and see my recorded lectures but my students can't. They have an error message when they try to open the recorded presentation or notes.", + "article:author": "https://community.canvaslms.com/t5/user/viewprofilepage/user-id/794287", + "twitter:image": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" + } + ], + "cse_image": [ + { + "src": "https://community.canvaslms.com/html/@6A1FDD4D5FF35E4BBB4083A1022FA0DB/assets/CommunityPreview23.png" + } + ] + } + }, + { + "kind": "customsearch#result", + "title": "Public Lecture Series - Sam Fox School of Design & Visual Arts", + "htmlTitle": "Public \u003cb\u003eLecture\u003c/b\u003e Series - Sam Fox School of Design & Visual Arts", + "link": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "displayLink": "samfoxschool.wustl.edu", + "snippet": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", + "htmlSnippet": "The Sam Fox School's Spring 2024 Public \u003cb\u003eLecture\u003c/b\u003e Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like ...", + "cacheId": "B-cgQG0j6tUJ", + "formattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "htmlFormattedUrl": "https://samfoxschool.wustl.edu/calendar/series/2-public-lecture-series", + "pagemap": { + "cse_thumbnail": [ + { + "src": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQSmHaGianm-64m-qauYjkPK_Q0JKWe-7yom4m1ogFYTmpWArA7k6dmk0sR&s", + "width": "307", + "height": "164" + } + ], + "website": [ + { + "name": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis" + } + ], + "metatags": [ + { + "og:image": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg", + "og:type": "website", + "og:site_name": "Sam Fox School of Design & Visual Arts — Washington University in St. Louis", + "viewport": "width=device-width, initial-scale=1.0", + "og:title": "Public Lecture Series - Sam Fox School of Design & Visual Arts — Washington University in St. Louis", + "csrf-token": "jBQsfZGY3RH8NVs0-KVDBYB-2N2kib4UYZHYdrShfTdLkvzfSvGeOaMrRKTRdYBPRKzdcGIuP7zwm9etqX_uvg", + "csrf-param": "authenticity_token", + "og:description": "The Sam Fox School's Spring 2024 Public Lecture Series highlights design and art as catalysts for change. Renowned speakers will delve into themes like social equity, resilient cities, and the impact of emerging technologies on contemporary life. Speakers include artists, architects, designers, and critics of the highest caliber, widely recognized for their research-based practices and multidisciplinary approaches to their fields." + } + ], + "cse_image": [ + { + "src": "https://dvsp0hlm0xrn3.cloudfront.net/assets/default_og_image-44e73dee4b9d1e2c6a6295901371270c8ec5899eaed48ee8167a9b12f1b0f8b3.jpg" + } + ] + } + } + ] +} diff --git a/backend/apps/rag/search/testdata/searxng.json b/backend/apps/rag/search/testdata/searxng.json new file mode 100644 index 0000000000000000000000000000000000000000..0e6952baa807842cf130bd0232eab6fe55f1ffba --- /dev/null +++ b/backend/apps/rag/search/testdata/searxng.json @@ -0,0 +1,476 @@ +{ + "query": "python", + "number_of_results": 116000000, + "results": [ + { + "url": "https://www.python.org/", + "title": "Welcome to Python.org", + "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Learn how to get started, download the latest version, access documentation, find jobs, and join the Python community.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [1, 1, 1], + "score": 9.0, + "category": "general" + }, + { + "url": "https://wiki.nerdvpn.de/wiki/Python_(programming_language)", + "title": "Python (programming language) - Wikipedia", + "content": "Python is a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming.", + "engine": "bing", + "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python_(programming_language)", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [4, 3, 2], + "score": 3.25, + "category": "general" + }, + { + "url": "https://docs.python.org/3/tutorial/index.html", + "title": "The Python Tutorial \u2014 Python 3.12.3 documentation", + "content": "3 days ago \u00b7 Python is an easy to learn, powerful programming language. It has efficient high-level data structures and a simple but effective approach to object-oriented programming. Python\u2019s elegant syntax and dynamic typing, together with its interpreted nature, make it an ideal language for scripting and rapid application development in many \u2026", + "engine": "bing", + "parsed_url": ["https", "docs.python.org", "/3/tutorial/index.html", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [5, 5, 3], + "score": 2.2, + "category": "general" + }, + { + "url": "https://www.python.org/downloads/", + "title": "Download Python | Python.org", + "content": "Python is a popular programming language for various purposes. Find the latest version of Python for different operating systems, download release notes, and learn about the development process.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/downloads/", "", "", ""], + "template": "default.html", + "engines": ["bing", "duckduckgo"], + "positions": [2, 2], + "score": 2.0, + "category": "general" + }, + { + "url": "https://www.python.org/about/gettingstarted/", + "title": "Python For Beginners | Python.org", + "content": "Learn the basics of Python, a popular and easy-to-use programming language, from installing it to using it for various purposes. Find out how to access online documentation, tutorials, books, code samples, and more resources to help you get started with Python.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/about/gettingstarted/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [9, 4, 4], + "score": 1.8333333333333333, + "category": "general" + }, + { + "url": "https://www.python.org/shell/", + "title": "Welcome to Python.org", + "content": "Python is a versatile and easy-to-use programming language that lets you work quickly. Learn more about Python, download the latest version, access documentation, find jobs, and join the community.", + "engine": "bing", + "parsed_url": ["https", "www.python.org", "/shell/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [3, 10, 8], + "score": 1.675, + "category": "general" + }, + { + "url": "https://realpython.com/", + "title": "Python Tutorials \u2013 Real Python", + "content": "Real Python offers comprehensive and up-to-date tutorials, books, and courses for Python developers of all skill levels. Whether you want to learn Python basics, web development, data science, machine learning, or more, you can find clear and practical guides and code examples here.", + "engine": "bing", + "parsed_url": ["https", "realpython.com", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "qwant", "duckduckgo"], + "positions": [6, 6, 5], + "score": 1.6, + "category": "general" + }, + { + "url": "https://wiki.nerdvpn.de/wiki/Python", + "title": "Python", + "content": "Topics referred to by the same term", + "engine": "wikipedia", + "parsed_url": ["https", "wiki.nerdvpn.de", "/wiki/Python", "", "", ""], + "template": "default.html", + "engines": ["wikipedia"], + "positions": [1], + "score": 1.0, + "category": "general" + }, + { + "title": "Online Python - IDE, Editor, Compiler, Interpreter", + "content": "Online Python IDE is a free online tool that lets you write, execute, and share Python code in the web browser. Learn about Python, its features, and its popularity as a general-purpose programming language for web development, data science, and more.", + "url": "https://www.online-python.com/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.online-python.com", "/", "", "", ""], + "template": "default.html", + "engines": ["qwant", "duckduckgo"], + "positions": [8, 6], + "score": 0.5833333333333333, + "category": "general" + }, + { + "url": "https://micropython.org/", + "title": "MicroPython - Python for microcontrollers", + "content": "MicroPython is a full Python compiler and runtime that runs on the bare-metal. You get an interactive prompt (the REPL) to execute commands immediately, along ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "micropython.org", "/", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [1], + "score": 1.0, + "category": "general" + }, + { + "url": "https://dictionary.cambridge.org/uk/dictionary/english/python", + "title": "PYTHON | \u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432 \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456\u0439 \u043c\u043e\u0432\u0456 - Cambridge Dictionary", + "content": "Apr 17, 2024 \u2014 \u0412\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f PYTHON: 1. a very large snake that kills animals for food by wrapping itself around them and crushing them\u2026. \u0414\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f \u0431\u0456\u043b\u044c\u0448\u0435.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "dictionary.cambridge.org", + "/uk/dictionary/english/python", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [2], + "score": 0.5, + "category": "general" + }, + { + "url": "https://www.codetoday.co.uk/code", + "title": "Web-based Python Editor (with Turtle graphics)", + "content": "Quick way of starting to write Python code, including drawing with Turtle, provided by CodeToday using Trinket.io Ideal for young children to start ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "www.codetoday.co.uk", "/code", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [3], + "score": 0.3333333333333333, + "category": "general" + }, + { + "url": "https://snapcraft.io/docs/python-plugin", + "title": "The python plugin | Snapcraft documentation", + "content": "The python plugin can be used by either Python 2 or Python 3 based parts using a setup.py script for building the project, or using a package published to ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "snapcraft.io", "/docs/python-plugin", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [4], + "score": 0.25, + "category": "general" + }, + { + "url": "https://www.developer-tech.com/categories/developer-languages/developer-languages-python/", + "title": "Latest Python Developer News", + "content": "Python's status as the primary language for AI and machine learning projects, from its extensive data-handling capabilities to its flexibility and ...", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "www.developer-tech.com", + "/categories/developer-languages/developer-languages-python/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [5], + "score": 0.2, + "category": "general" + }, + { + "url": "https://subjectguides.york.ac.uk/coding/python", + "title": "Coding: a Practical Guide - Python - Subject Guides", + "content": "Python is a coding language used for a wide range of things, including working with data, building systems and software, and even creating games.", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "subjectguides.york.ac.uk", "/coding/python", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [6], + "score": 0.16666666666666666, + "category": "general" + }, + { + "url": "https://hub.salford.ac.uk/psytech/python/getting-started-python/", + "title": "Getting Started - Python - Salford PsyTech Home - The Hub", + "content": "Python in itself is a very friendly programming language, when we get to grips with writing code, once you grasp the logic, it will become very intuitive.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "hub.salford.ac.uk", + "/psytech/python/getting-started-python/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [7], + "score": 0.14285714285714285, + "category": "general" + }, + { + "url": "https://snapcraft.io/docs/python-apps", + "title": "Python apps | Snapcraft documentation", + "content": "Snapcraft can be used to package and distribute Python applications in a way that enables convenient installation by users. The process of creating a snap ...", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "snapcraft.io", "/docs/python-apps", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [8], + "score": 0.125, + "category": "general" + }, + { + "url": "https://anvil.works/", + "title": "Anvil | Build Web Apps with Nothing but Python", + "content": "Anvil is a free Python-based drag-and-drop web app builder.\u200eSign Up \u00b7 \u200eSign in \u00b7 \u200ePricing \u00b7 \u200eForum", + "img_src": null, + "engine": "google", + "parsed_url": ["https", "anvil.works", "/", "", "", ""], + "template": "default.html", + "engines": ["google"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "url": "https://docs.python.org/", + "title": "Python 3.12.3 documentation", + "content": "3 days ago \u00b7 This is the official documentation for Python 3.12.3. Documentation sections: What's new in Python 3.12? Or all \"What's new\" documents since Python 2.0. Tutorial. Start here: a tour of Python's syntax and features. Library reference. Standard library and builtins. Language reference.", + "engine": "bing", + "parsed_url": ["https", "docs.python.org", "/", "", "", ""], + "template": "default.html", + "engines": ["bing", "duckduckgo"], + "positions": [7, 13], + "score": 0.43956043956043955, + "category": "general" + }, + { + "title": "How to Use Python: Your First Steps - Real Python", + "content": "Learn the basics of Python syntax, installation, error handling, and more in this tutorial. You'll also code your first Python program and test your knowledge with a quiz.", + "url": "https://realpython.com/python-first-steps/", + "engine": "duckduckgo", + "parsed_url": ["https", "realpython.com", "/python-first-steps/", "", "", ""], + "template": "default.html", + "engines": ["qwant", "duckduckgo"], + "positions": [14, 7], + "score": 0.42857142857142855, + "category": "general" + }, + { + "title": "The Python Tutorial \u2014 Python 3.11.8 documentation", + "content": "This tutorial introduces the reader informally to the basic concepts and features of the Python language and system. It helps to have a Python interpreter handy for hands-on experience, but all examples are self-contained, so the tutorial can be read off-line as well. For a description of standard objects and modules, see The Python Standard ...", + "url": "https://docs.python.org/3.11/tutorial/", + "engine": "duckduckgo", + "parsed_url": ["https", "docs.python.org", "/3.11/tutorial/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [7], + "score": 0.14285714285714285, + "category": "general" + }, + { + "url": "https://realpython.com/python-introduction/", + "title": "Introduction to Python 3 \u2013 Real Python", + "content": "Python programming language, including a brief history of the development of Python and reasons why you might select Python as your language of choice.", + "engine": "bing", + "parsed_url": ["https", "realpython.com", "/python-introduction/", "", "", ""], + "template": "default.html", + "engines": ["bing"], + "positions": [8], + "score": 0.125, + "category": "general" + }, + { + "title": "Our Documentation | Python.org", + "content": "Find online or download Python's documentation, tutorials, and guides for beginners and advanced users. Learn how to port from Python 2 to Python 3, contribute to Python, and access Python videos and books.", + "url": "https://www.python.org/doc/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/doc/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "title": "Welcome to Python.org", + "url": "http://www.get-python.org/shell/", + "content": "The mission of the Python Software Foundation is to promote, protect, and advance the Python programming language, and to support and facilitate the growth of a diverse and international community of Python programmers. Learn more. Become a Member Donate to the PSF.", + "engine": "qwant", + "parsed_url": ["http", "www.get-python.org", "/shell/", "", "", ""], + "template": "default.html", + "engines": ["qwant"], + "positions": [9], + "score": 0.1111111111111111, + "category": "general" + }, + { + "title": "About Python\u2122 | Python.org", + "content": "Python is a powerful, fast, and versatile programming language that runs on various platforms and is easy to learn. Learn how to get started, explore the applications, and join the community of Python programmers and users.", + "url": "https://www.python.org/about/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/about/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [11], + "score": 0.09090909090909091, + "category": "general" + }, + { + "title": "Online Python Compiler (Interpreter) - Programiz", + "content": "Write and run Python code using this online tool. You can use Python Shell like IDLE, and take inputs from the user in our Python compiler.", + "url": "https://www.programiz.com/python-programming/online-compiler/", + "engine": "duckduckgo", + "parsed_url": [ + "https", + "www.programiz.com", + "/python-programming/online-compiler/", + "", + "", + "" + ], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [12], + "score": 0.08333333333333333, + "category": "general" + }, + { + "title": "Welcome to Python.org", + "content": "Python is a versatile and powerful language that lets you work quickly and integrate systems more effectively. Download the latest version, read the documentation, find jobs, events, success stories, and more on Python.org.", + "url": "https://www.python.org/?downloads", + "engine": "duckduckgo", + "parsed_url": ["https", "www.python.org", "/", "", "downloads", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [15], + "score": 0.06666666666666667, + "category": "general" + }, + { + "url": "https://www.matillion.com/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", + "title": "The Importance of Python and its Growing Influence on ...", + "content": "Jan 30, 2024 \u2014 The synergy of low-code functionality with Python's versatility empowers data professionals to orchestrate complex transformations seamlessly.", + "img_src": null, + "engine": "google", + "parsed_url": [ + "https", + "www.matillion.com", + "/blog/the-importance-of-python-and-its-growing-influence-on-data-productivty-a-matillion-perspective", + "", + "", + "" + ], + "template": "default.html", + "engines": ["google"], + "positions": [10], + "score": 0.1, + "category": "general" + }, + { + "title": "BeginnersGuide - Python Wiki", + "content": "This is the program that reads Python programs and carries out their instructions; you need it before you can do any Python programming. Mac and Linux distributions may include an outdated version of Python (Python 2), but you should install an updated one (Python 3). See BeginnersGuide/Download for instructions to download the correct version ...", + "url": "https://wiki.python.org/moin/BeginnersGuide", + "engine": "duckduckgo", + "parsed_url": ["https", "wiki.python.org", "/moin/BeginnersGuide", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [16], + "score": 0.0625, + "category": "general" + }, + { + "title": "Learn Python - Free Interactive Python Tutorial", + "content": "Learn Python from scratch or improve your skills with this website that offers tutorials, exercises, tests and certification. Explore topics such as basics, data science, advanced features and more with DataCamp.", + "url": "https://www.learnpython.org/", + "engine": "duckduckgo", + "parsed_url": ["https", "www.learnpython.org", "/", "", "", ""], + "template": "default.html", + "engines": ["duckduckgo"], + "positions": [17], + "score": 0.058823529411764705, + "category": "general" + } + ], + "answers": [], + "corrections": [], + "infoboxes": [ + { + "infobox": "Python", + "id": "https://en.wikipedia.org/wiki/Python_(programming_language)", + "content": "general-purpose programming language", + "img_src": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/.PY_file_recreation.png/500px-.PY_file_recreation.png", + "urls": [ + { + "title": "Official website", + "url": "https://www.python.org/", + "official": true + }, + { + "title": "Wikipedia (en)", + "url": "https://en.wikipedia.org/wiki/Python_(programming_language)" + }, + { + "title": "Wikidata", + "url": "http://www.wikidata.org/entity/Q28865" + } + ], + "attributes": [ + { + "label": "Inception", + "value": "Wednesday, February 20, 1991", + "entity": "P571" + }, + { + "label": "Developer", + "value": "Python Software Foundation, Guido van Rossum", + "entity": "P178" + }, + { + "label": "Copyright license", + "value": "Python Software Foundation License", + "entity": "P275" + }, + { + "label": "Programmed in", + "value": "C, Python", + "entity": "P277" + }, + { + "label": "Software version identifier", + "value": "3.12.3, 3.13.0a6", + "entity": "P348" + } + ], + "engine": "wikidata", + "engines": ["wikidata"] + } + ], + "suggestions": [ + "python turtle", + "micro python tutorial", + "python docs", + "python compiler", + "snapcraft python", + "micropython vs python", + "python online", + "python download" + ], + "unresponsive_engines": [] +} diff --git a/backend/apps/rag/search/testdata/serper.json b/backend/apps/rag/search/testdata/serper.json new file mode 100644 index 0000000000000000000000000000000000000000..b269eaf5b34fe64234ba6e7ffb27fd3fbbaa3fe0 --- /dev/null +++ b/backend/apps/rag/search/testdata/serper.json @@ -0,0 +1,190 @@ +{ + "searchParameters": { + "q": "apple inc", + "gl": "us", + "hl": "en", + "autocorrect": true, + "page": 1, + "type": "search" + }, + "knowledgeGraph": { + "title": "Apple", + "type": "Technology company", + "website": "http://www.apple.com/", + "imageUrl": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQwGQRv5TjjkycpctY66mOg_e2-npacrmjAb6_jAWhzlzkFE3OTjxyzbA&s=0", + "description": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, California, United States.", + "descriptionSource": "Wikipedia", + "descriptionLink": "https://en.wikipedia.org/wiki/Apple_Inc.", + "attributes": { + "Headquarters": "Cupertino, CA", + "CEO": "Tim Cook (Aug 24, 2011–)", + "Founded": "April 1, 1976, Los Altos, CA", + "Sales": "1 (800) 692-7753", + "Products": "iPhone, Apple Watch, iPad, and more", + "Founders": "Steve Jobs, Steve Wozniak, and Ronald Wayne", + "Subsidiaries": "Apple Store, Beats Electronics, Beddit, and more" + } + }, + "organic": [ + { + "title": "Apple", + "link": "https://www.apple.com/", + "snippet": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "sitelinks": [ + { + "title": "Support", + "link": "https://support.apple.com/" + }, + { + "title": "iPhone", + "link": "https://www.apple.com/iphone/" + }, + { + "title": "Apple makes business better.", + "link": "https://www.apple.com/business/" + }, + { + "title": "Mac", + "link": "https://www.apple.com/mac/" + } + ], + "position": 1 + }, + { + "title": "Apple Inc. - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Apple_Inc.", + "snippet": "Apple Inc. is an American multinational technology company specializing in consumer electronics, software and online services headquartered in Cupertino, ...", + "attributes": { + "Products": "AirPods; Apple Watch; iPad; iPhone; Mac", + "Founders": "Steve Jobs; Steve Wozniak; Ronald Wayne", + "Founded": "April 1, 1976; 46 years ago in Los Altos, California, U.S", + "Industry": "Consumer electronics; Software services; Online services" + }, + "sitelinks": [ + { + "title": "History", + "link": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." + }, + { + "title": "Timeline of Apple Inc. products", + "link": "https://en.wikipedia.org/wiki/Timeline_of_Apple_Inc._products" + }, + { + "title": "List of software by Apple Inc.", + "link": "https://en.wikipedia.org/wiki/List_of_software_by_Apple_Inc." + }, + { + "title": "Apple Store", + "link": "https://en.wikipedia.org/wiki/Apple_Store" + } + ], + "position": 2 + }, + { + "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", + "link": "https://www.britannica.com/topic/Apple-Inc", + "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal computers, smartphones, tablet computers, computer peripherals, ...", + "date": "Aug 31, 2022", + "attributes": { + "Related People": "Steve Jobs Steve Wozniak Jony Ive Tim Cook Angela Ahrendts", + "Date": "1976 - present", + "Areas Of Involvement": "peripheral device" + }, + "position": 3 + }, + { + "title": "AAPL: Apple Inc Stock Price Quote - NASDAQ GS - Bloomberg.com", + "link": "https://www.bloomberg.com/quote/AAPL:US", + "snippet": "Stock analysis for Apple Inc (AAPL:NASDAQ GS) including stock price, stock chart, company news, key statistics, fundamentals and company profile.", + "position": 4 + }, + { + "title": "Apple Inc. (AAPL) Company Profile & Facts - Yahoo Finance", + "link": "https://finance.yahoo.com/quote/AAPL/profile/", + "snippet": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. It also sells various related ...", + "position": 5 + }, + { + "title": "AAPL | Apple Inc. Stock Price & News - WSJ", + "link": "https://www.wsj.com/market-data/quotes/AAPL", + "snippet": "Apple, Inc. engages in the design, manufacture, and sale of smartphones, personal computers, tablets, wearables and accessories, and other varieties of ...", + "position": 6 + }, + { + "title": "Apple Inc Company Profile - Apple Inc Overview - GlobalData", + "link": "https://www.globaldata.com/company-profile/apple-inc/", + "snippet": "Apple Inc (Apple) designs, manufactures, and markets smartphones, tablets, personal computers (PCs), portable and wearable devices. The company also offers ...", + "position": 7 + }, + { + "title": "Apple Inc (AAPL) Stock Price & News - Google Finance", + "link": "https://www.google.com/finance/quote/AAPL:NASDAQ?hl=en", + "snippet": "Get the latest Apple Inc (AAPL) real-time quote, historical performance, charts, and other financial information to help you make more informed trading and ...", + "position": 8 + } + ], + "peopleAlsoAsk": [ + { + "question": "What does Apple Inc mean?", + "snippet": "Apple Inc., formerly Apple Computer, Inc., American manufacturer of personal\ncomputers, smartphones, tablet computers, computer peripherals, and computer\nsoftware. It was the first successful personal computer company and the\npopularizer of the graphical user interface.\nAug 31, 2022", + "title": "Apple Inc. | History, Products, Headquarters, & Facts | Britannica", + "link": "https://www.britannica.com/topic/Apple-Inc" + }, + { + "question": "Is Apple and Apple Inc same?", + "snippet": "Apple was founded as Apple Computer Company on April 1, 1976, by Steve Jobs,\nSteve Wozniak and Ronald Wayne to develop and sell Wozniak's Apple I personal\ncomputer. It was incorporated by Jobs and Wozniak as Apple Computer, Inc.", + "title": "Apple Inc. - Wikipedia", + "link": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "question": "Who owns Apple Inc?", + "snippet": "Apple Inc. is owned by two main institutional investors (Vanguard Group and\nBlackRock, Inc). While its major individual shareholders comprise people like\nArt Levinson, Tim Cook, Bruce Sewell, Al Gore, Johny Sroujli, and others.", + "title": "Who Owns Apple In 2022? - FourWeekMBA", + "link": "https://fourweekmba.com/who-owns-apple/" + }, + { + "question": "What products does Apple Inc offer?", + "snippet": "APPLE FOOTER\nStore.\nMac.\niPad.\niPhone.\nWatch.\nAirPods.\nTV & Home.\nAirTag.", + "title": "More items...", + "link": "https://www.apple.com/business/" + } + ], + "relatedSearches": [ + { + "query": "Who invented the iPhone" + }, + { + "query": "Apple Inc competitors" + }, + { + "query": "Apple iPad" + }, + { + "query": "iPhones" + }, + { + "query": "Apple Inc us" + }, + { + "query": "Apple company history" + }, + { + "query": "Apple Store" + }, + { + "query": "Apple customer service" + }, + { + "query": "Apple Watch" + }, + { + "query": "Apple Inc Industry" + }, + { + "query": "Apple Inc registered address" + }, + { + "query": "Apple Inc Bloomberg" + } + ] +} diff --git a/backend/apps/rag/search/testdata/serply.json b/backend/apps/rag/search/testdata/serply.json new file mode 100644 index 0000000000000000000000000000000000000000..0fc2a31e4d63cefba8aa96cad147208d596060c4 --- /dev/null +++ b/backend/apps/rag/search/testdata/serply.json @@ -0,0 +1,206 @@ +{ + "ads": [], + "ads_count": 0, + "answers": [], + "results": [ + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "subdomains": [ + { + "title": "Support", + "link": "https://support.apple.com/", + "description": "SupportContact - iPhone Support - Billing and Subscriptions - Apple Repair" + }, + { + "title": "Store", + "link": "https://www.apple.com/store", + "description": "StoreShop iPhone - Shop iPad - App Store - Shop Mac - ..." + }, + { + "title": "Mac", + "link": "https://www.apple.com/mac/", + "description": "MacMacBook Air - MacBook Pro - iMac - Compare Mac models - Mac mini" + }, + { + "title": "iPad", + "link": "https://www.apple.com/ipad/", + "description": "iPadShop iPad - iPad Pro - iPad Air - Compare iPad models - ..." + }, + { + "title": "Watch", + "link": "https://www.apple.com/watch/", + "description": "WatchShop Apple Watch - Series 9 - SE - Ultra 2 - Nike - Hermès - ..." + } + ], + "realPosition": 1 + }, + { + "title": "Apple", + "link": "https://www.apple.com/", + "description": "Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, ...", + "additional_links": [ + { + "text": "AppleApplehttps://www.apple.com", + "href": "https://www.apple.com/" + } + ], + "cite": {}, + "realPosition": 2 + }, + { + "title": "Apple Inc.", + "link": "https://en.wikipedia.org/wiki/Apple_Inc.", + "description": "Apple Inc. (formerly Apple Computer, Inc.) is an American multinational corporation and technology company headquartered in Cupertino, California, ...", + "additional_links": [ + { + "text": "Apple Inc.Wikipediahttps://en.wikipedia.org › wiki › Apple_Inc", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "", + "href": "https://en.wikipedia.org/wiki/Apple_Inc." + }, + { + "text": "History", + "href": "https://en.wikipedia.org/wiki/History_of_Apple_Inc." + }, + { + "text": "List of Apple products", + "href": "https://en.wikipedia.org/wiki/List_of_Apple_products" + }, + { + "text": "Litigation involving Apple Inc.", + "href": "https://en.wikipedia.org/wiki/Litigation_involving_Apple_Inc." + }, + { + "text": "Apple Park", + "href": "https://en.wikipedia.org/wiki/Apple_Park" + } + ], + "cite": { + "domain": "https://en.wikipedia.org › wiki › Apple_Inc", + "span": " › wiki › Apple_Inc" + }, + "realPosition": 3 + }, + { + "title": "Apple Inc. (AAPL) Company Profile & Facts", + "link": "https://finance.yahoo.com/quote/AAPL/profile/", + "description": "Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables, and accessories worldwide. The company offers iPhone, a line ...", + "additional_links": [ + { + "text": "Apple Inc. (AAPL) Company Profile & FactsYahoo Financehttps://finance.yahoo.com › quote › AAPL › profile", + "href": "https://finance.yahoo.com/quote/AAPL/profile/" + } + ], + "cite": { + "domain": "https://finance.yahoo.com › quote › AAPL › profile", + "span": " › quote › AAPL › profile" + }, + "realPosition": 4 + }, + { + "title": "Apple Inc - Company Profile and News", + "link": "https://www.bloomberg.com/profile/company/AAPL:US", + "description": "Apple Inc. Apple Inc. designs, manufactures, and markets smartphones, personal computers, tablets, wearables and accessories, and sells a variety of related ...", + "additional_links": [ + { + "text": "Apple Inc - Company Profile and NewsBloomberghttps://www.bloomberg.com › company › AAPL:US", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + }, + { + "text": "", + "href": "https://www.bloomberg.com/profile/company/AAPL:US" + } + ], + "cite": { + "domain": "https://www.bloomberg.com › company › AAPL:US", + "span": " › company › AAPL:US" + }, + "realPosition": 5 + }, + { + "title": "Apple Inc. | History, Products, Headquarters, & Facts", + "link": "https://www.britannica.com/money/Apple-Inc", + "description": "May 22, 2024 — Apple Inc. is an American multinational technology company that revolutionized the technology sector through its innovation of computer ...", + "additional_links": [ + { + "text": "Apple Inc. | History, Products, Headquarters, & FactsBritannicahttps://www.britannica.com › money › Apple-Inc", + "href": "https://www.britannica.com/money/Apple-Inc" + }, + { + "text": "", + "href": "https://www.britannica.com/money/Apple-Inc" + } + ], + "cite": { + "domain": "https://www.britannica.com › money › Apple-Inc", + "span": " › money › Apple-Inc" + }, + "realPosition": 6 + } + ], + "shopping_ads": [], + "places": [ + { + "title": "Apple Inc." + }, + { + "title": "Apple Inc" + }, + { + "title": "Apple Inc" + } + ], + "related_searches": { + "images": [], + "text": [ + { + "title": "apple inc full form", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+full+form&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhPEAE" + }, + { + "title": "apple company history", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+company+history&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhOEAE" + }, + { + "title": "apple store", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Store&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhQEAE" + }, + { + "title": "apple id", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+id&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhSEAE" + }, + { + "title": "apple inc industry", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+Inc+industry&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhREAE" + }, + { + "title": "apple login", + "link": "https://www.google.com/search?sca_esv=6b6df170a5c9891b&sca_upv=1&q=Apple+login&sa=X&ved=2ahUKEwjLxuSJwM-GAxUHODQIHYuJBhgQ1QJ6BAhTEAE" + } + ] + }, + "image_results": [], + "carousel": [], + "total": 2450000000, + "knowledge_graph": "", + "related_questions": [ + "What does the Apple Inc do?", + "Why did Apple change to Apple Inc?", + "Who owns Apple Inc.?", + "What is Apple Inc best known for?" + ], + "carousel_count": 0, + "ts": 2.491065263748169, + "device_type": null +} diff --git a/backend/apps/rag/search/testdata/serpstack.json b/backend/apps/rag/search/testdata/serpstack.json new file mode 100644 index 0000000000000000000000000000000000000000..a82f689d8b2293586d6b94974e018f74e49d1013 --- /dev/null +++ b/backend/apps/rag/search/testdata/serpstack.json @@ -0,0 +1,276 @@ +{ + "request": { + "success": true, + "total_time_taken": 3.4, + "processed_timestamp": 1714968442, + "search_url": "http://www.google.com/search?q=mcdonalds\u0026gl=us\u0026hl=en\u0026safe=0\u0026num=10" + }, + "search_parameters": { + "engine": "google", + "type": "web", + "device": "desktop", + "auto_location": "1", + "google_domain": "google.com", + "gl": "us", + "hl": "en", + "safe": "0", + "news_type": "all", + "exclude_autocorrected_results": "0", + "images_color": "any", + "page": "1", + "num": "10", + "output": "json", + "csv_fields": "search_parameters.query,organic_results.position,organic_results.title,organic_results.url,organic_results.domain", + "query": "mcdonalds", + "action": "search", + "access_key": "aac48e007e15c532bb94ffb34532a4b2", + "error": {} + }, + "search_information": { + "total_results": 1170000000, + "time_taken_displayed": 0.49, + "detected_location": {}, + "did_you_mean": {}, + "no_results_for_original_query": false, + "showing_results_for": {} + }, + "organic_results": [ + { + "position": 1, + "title": "Our Full McDonald\u0027s Food Menu", + "snippet": "", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/full-menu.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a full-menu" + }, + { + "position": 2, + "title": "McDonald\u0027s", + "snippet": "McDonald\u0027s is the world\u0027s largest fast food restaurant chain, serving over 69 million customers daily in over 100 countries in more than 40,000 outlets as of\u00a0...", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://en.wikipedia.org/wiki/McDonald%27s", + "domain": "en.wikipedia.org", + "displayed_url": "https://en.wikipedia.org \u203a wiki \u203a McDonald\u0027s" + }, + { + "position": 3, + "title": "Restaurants Near Me: Nearby McDonald\u0027s Locations", + "snippet": "", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/restaurant-locator.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a restaurant-locator" + }, + { + "position": 4, + "title": "Download the McDonald\u0027s App: Deals, Promotions \u0026 ...", + "snippet": "Download the McDonald\u0027s app for Mobile Order \u0026 Pay, exclusive deals and coupons, menu information and special promotions.", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://www.mcdonalds.com/us/en-us/download-app.html", + "domain": "www.mcdonalds.com", + "displayed_url": "https://www.mcdonalds.com \u203a en-us \u203a download-app" + }, + { + "position": 5, + "title": "McDonald\u0027s Restaurant Careers in the US", + "snippet": "McDonald\u0027s restaurant jobs are one-of-a-kind \u2013 just like you. Restaurants are hiring across all levels, from Crew team to Management. Apply today!", + "prerender": false, + "cached_page_url": {}, + "related_pages_url": {}, + "url": "https://jobs.mchire.com/", + "domain": "jobs.mchire.com", + "displayed_url": "https://jobs.mchire.com" + } + ], + "inline_images": [ + { + "image_url": "https://serpstack-assets.apilayer.net/2418910010831954152.png", + "title": "" + } + ], + "local_results": [ + { + "position": 1, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + }, + { + "position": 2, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + }, + { + "position": 3, + "title": "McDonald\u0027s", + "coordinates": { + "latitude": 0, + "longitude": 0 + }, + "address": "", + "rating": 0, + "reviews": 0, + "type": "", + "price": {}, + "url": 0 + } + ], + "top_stories": [ + { + "block_position": 1, + "title": "Menu nutrition", + "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=mcdonald%27s+double+quarter+pounder+with+cheese\u0026stick=H4sIAAAAAAAAAONgFuLUz9U3ME-vLDBX4tVP1zc0TCsuNE0ytjTTUs5OttJPy89P0c9NzSuNLyjKL8tMSS2yAvNS80qKMlOLF7Hq5ian5Ocl5qSoFyuk5Jcm5aQqFJYmFpWkFikU5JfmATUolGeWZCgkZ6SmFqcCAM4ilJtxAAAA\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qri56BAh0EAM", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + }, + { + "block_position": 2, + "title": "Profiles", + "url": "https://www.instagram.com/McDonalds", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + }, + { + "block_position": 3, + "title": "People also search for", + "url": "/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-L5Wg8IL2sxPFxxcDEhVbocy-LJPZIvZySijw0ho2hfZ-KtV-sSEEJ9lw7JuEkXHDnRK5y4Dm8aqbiLwugbLbslwjG3hO_gpDTFZK2VoUGZPy2nrmOBCy0G3PoOfoiEtct2GSZlUz0uufG-xP8emtNzQKQpvjkAm5Zmi57iVZueiD62upz7-x2N3dAbwtm6FkInAPRw1yR91zuT7F3lEaPblTW3LaRwCDC0bvaRCh9x4N9zHgY1OOQa_rzts2jf5WpXcuw4Y%3D\u0026q=Burger+King\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4Qs9oBKAB6BAhzEAI", + "source": "", + "uploaded": "", + "uploaded_utc": "2024-05-06T04:07:22.082Z" + } + ], + "related_questions": [ + { + "question": "What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?What\u0027s a number 7 at McDonald\u0027s?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?Why is McDonald\u0027s changing their name?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?What is the oldest still running Mcdonalds?", + "answer": "", + "title": "", + "displayed_url": "" + }, + { + "question": "Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?Why is McDonald\u0027s now WcDonald\u0027s?", + "answer": "", + "title": "", + "displayed_url": "" + } + ], + "knowledge_graph": { + "title": "", + "type": "Fast-food restaurant company", + "image_urls": ["https://serpstack-assets.apilayer.net/2418910010831954152.png"], + "description": "McDonald\u0027s Corporation is an American multinational fast food chain, founded in 1940 as a restaurant operated by Richard and Maurice McDonald, in San Bernardino, California, United States.", + "source": { + "name": "Wikipedia", + "url": "https://en.wikipedia.org/wiki/McDonald\u0027s" + }, + "people_also_search_for": [], + "known_attributes": [ + { + "attribute": "kc:/business/business_operation:founder", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg", + "name": "Founder: ", + "value": "Ray Kroc" + }, + { + "attribute": "kc:/organization/organization:ceo", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHUQAg", + "name": "CEO: ", + "value": "Chris Kempczinski (Nov 1, 2019\u2013)" + }, + { + "attribute": "kc:/business/employer:revenue", + "link": "", + "name": "Revenue: ", + "value": "25.49\u00a0billion USD (2023)" + }, + { + "attribute": "kc:/organization/organization:founded", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Des+Plaines\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm_yqLtI_DBi5PXGOtg_Z3qrzzEP6mcih1nN7h5A7v6OefnEJiC7a8dBR-v9LxlRubfyR6vlMr3fZ3TmVKWwz9FRpvZb1eYNt-RM7KIDKQlwGEIgINvzhxjUrv6uxSmceduzxd8W7Pkz71XGwxF0F8OlSzHlx\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECG4QAg", + "name": "Founded: ", + "value": "April 15, 1955, Des Plaines, IL" + }, + { + "attribute": "kc:/organization/organization:headquarters", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chicago\u0026si=ACC90nyvvWro6QmnyY1IfSdgk5wwjB1r8BGd_IWRjXqmKPQqm-46AEJ_kJbUIEvsvEEZqteiYJvXVXs2ScRNDvFFpjfeAaW3dxtpTGCgcsf5RMdi6IdzOdtjJMN3ZaFwqZOmdi7tC6r0Mh1O9bnP3HrVDB9hH02m7aA6f70dCAfTdpOFnGxDU6wVMAI5MxWBE3wTugtUDOK-\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHYQAg", + "name": "Headquarters: ", + "value": "Chicago, IL" + }, + { + "attribute": "kc:/organization/organization:president", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Chris+Kempczinski\u0026si=ACC90nwLLwns5sISZcdzuISy7t-NHozt8Cbt6G3WNQfC9ekAgKFbjdEFCDgxLbt57EDZGosYDGiZuq1AcBhA6IhTOSZxfVSySuGQ3VDwmmTA7Z93n3K3596jAuZH9VVv5h8PyvKJSuGuSsQWviJTl3eKj2UL1ZIWuDgkjyVMnC47rN7j0G9PlHRCCLdQF7VDQ1gubTiC4onXqLRBTbwAj6a--PD6Jv_NoA%3D%3D\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHEQAg", + "name": "President: ", + "value": "Chris Kempczinski" + } + ], + "website": "https://www.mcdonalds.com/us/en-us.html", + "profiles": [ + { + "name": "Instagram", + "url": "https://www.instagram.com/McDonalds" + }, + { + "name": "X (Twitter)", + "url": "https://twitter.com/McDonalds" + }, + { + "name": "Facebook", + "url": "https://www.facebook.com/McDonaldsUS" + }, + { + "name": "YouTube", + "url": "https://www.youtube.com/user/McDonaldsUS" + }, + { + "name": "Pinterest", + "url": "https://www.pinterest.com/mcdonalds" + } + ], + "founded": "April 15, 1955, Des Plaines, IL", + "headquarters": "Chicago, IL", + "founders": [ + { + "name": "Ray Kroc", + "link": "http://www.google.com/search?safe=0\u0026sca_esv=c9c7fd42856085e2\u0026sca_upv=1\u0026gl=us\u0026hl=en\u0026q=Ray+Kroc\u0026si=ACC90nzx_D3_zUKRnpAjmO0UBLNxnt7EyN4YYdru6U3bxLI-LxARWRdbk5SkoY2sDn5Qq7yOmqYGei6qZ7sfJhsjZXBPgjMlLbS7824rpJOm69GzqVWMdoNIZiFX2T4A2td14sZOn4a1BexZLtZXHU7NZdF6VsWbGMVuiSYtXdev7uaUjEJKumiwlqTAATTebOriYTEBuSzC\u0026sa=X\u0026ved=2ahUKEwjF55alk_iFAxXlamwGHbqgAs4QmxMoAHoECHgQAg" + } + ] + } +} diff --git a/backend/apps/rag/utils.py b/backend/apps/rag/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fde89b0697414a54a5fdabf4cdfb853c12e11c70 --- /dev/null +++ b/backend/apps/rag/utils.py @@ -0,0 +1,493 @@ +import os +import logging +import requests + +from typing import List, Union + +from apps.ollama.main import ( + generate_ollama_embeddings, + GenerateEmbeddingsForm, +) + +from huggingface_hub import snapshot_download + +from langchain_core.documents import Document +from langchain_community.retrievers import BM25Retriever +from langchain.retrievers import ( + ContextualCompressionRetriever, + EnsembleRetriever, +) + +from typing import Optional + +from utils.misc import get_last_user_message, add_or_update_system_message +from config import SRC_LOG_LEVELS, CHROMA_CLIENT + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def query_doc( + collection_name: str, + query: str, + embedding_function, + k: int, +): + try: + collection = CHROMA_CLIENT.get_collection(name=collection_name) + query_embeddings = embedding_function(query) + + result = collection.query( + query_embeddings=[query_embeddings], + n_results=k, + ) + + log.info(f"query_doc:result {result}") + return result + except Exception as e: + raise e + + +def query_doc_with_hybrid_search( + collection_name: str, + query: str, + embedding_function, + k: int, + reranking_function, + r: float, +): + try: + collection = CHROMA_CLIENT.get_collection(name=collection_name) + documents = collection.get() # get all documents + + bm25_retriever = BM25Retriever.from_texts( + texts=documents.get("documents"), + metadatas=documents.get("metadatas"), + ) + bm25_retriever.k = k + + chroma_retriever = ChromaRetriever( + collection=collection, + embedding_function=embedding_function, + top_n=k, + ) + + ensemble_retriever = EnsembleRetriever( + retrievers=[bm25_retriever, chroma_retriever], weights=[0.5, 0.5] + ) + + compressor = RerankCompressor( + embedding_function=embedding_function, + top_n=k, + reranking_function=reranking_function, + r_score=r, + ) + + compression_retriever = ContextualCompressionRetriever( + base_compressor=compressor, base_retriever=ensemble_retriever + ) + + result = compression_retriever.invoke(query) + result = { + "distances": [[d.metadata.get("score") for d in result]], + "documents": [[d.page_content for d in result]], + "metadatas": [[d.metadata for d in result]], + } + + log.info(f"query_doc_with_hybrid_search:result {result}") + return result + except Exception as e: + raise e + + +def merge_and_sort_query_results(query_results, k, reverse=False): + # Initialize lists to store combined data + combined_distances = [] + combined_documents = [] + combined_metadatas = [] + + for data in query_results: + combined_distances.extend(data["distances"][0]) + combined_documents.extend(data["documents"][0]) + combined_metadatas.extend(data["metadatas"][0]) + + # Create a list of tuples (distance, document, metadata) + combined = list(zip(combined_distances, combined_documents, combined_metadatas)) + + # Sort the list based on distances + combined.sort(key=lambda x: x[0], reverse=reverse) + + # We don't have anything :-( + if not combined: + sorted_distances = [] + sorted_documents = [] + sorted_metadatas = [] + else: + # Unzip the sorted list + sorted_distances, sorted_documents, sorted_metadatas = zip(*combined) + + # Slicing the lists to include only k elements + sorted_distances = list(sorted_distances)[:k] + sorted_documents = list(sorted_documents)[:k] + sorted_metadatas = list(sorted_metadatas)[:k] + + # Create the output dictionary + result = { + "distances": [sorted_distances], + "documents": [sorted_documents], + "metadatas": [sorted_metadatas], + } + + return result + + +def query_collection( + collection_names: List[str], + query: str, + embedding_function, + k: int, +): + results = [] + for collection_name in collection_names: + try: + result = query_doc( + collection_name=collection_name, + query=query, + k=k, + embedding_function=embedding_function, + ) + results.append(result) + except: + pass + return merge_and_sort_query_results(results, k=k) + + +def query_collection_with_hybrid_search( + collection_names: List[str], + query: str, + embedding_function, + k: int, + reranking_function, + r: float, +): + results = [] + for collection_name in collection_names: + try: + result = query_doc_with_hybrid_search( + collection_name=collection_name, + query=query, + embedding_function=embedding_function, + k=k, + reranking_function=reranking_function, + r=r, + ) + results.append(result) + except: + pass + return merge_and_sort_query_results(results, k=k, reverse=True) + + +def rag_template(template: str, context: str, query: str): + template = template.replace("[context]", context) + template = template.replace("[query]", query) + return template + + +def get_embedding_function( + embedding_engine, + embedding_model, + embedding_function, + openai_key, + openai_url, + batch_size, +): + if embedding_engine == "": + return lambda query: embedding_function.encode(query).tolist() + elif embedding_engine in ["ollama", "openai"]: + if embedding_engine == "ollama": + func = lambda query: generate_ollama_embeddings( + GenerateEmbeddingsForm( + **{ + "model": embedding_model, + "prompt": query, + } + ) + ) + elif embedding_engine == "openai": + func = lambda query: generate_openai_embeddings( + model=embedding_model, + text=query, + key=openai_key, + url=openai_url, + ) + + def generate_multiple(query, f): + if isinstance(query, list): + if embedding_engine == "openai": + embeddings = [] + for i in range(0, len(query), batch_size): + embeddings.extend(f(query[i : i + batch_size])) + return embeddings + else: + return [f(q) for q in query] + else: + return f(query) + + return lambda query: generate_multiple(query, func) + + +def get_rag_context( + files, + messages, + embedding_function, + k, + reranking_function, + r, + hybrid_search, +): + log.debug(f"files: {files} {messages} {embedding_function} {reranking_function}") + query = get_last_user_message(messages) + + extracted_collections = [] + relevant_contexts = [] + + for file in files: + context = None + + collection_names = ( + file["collection_names"] + if file["type"] == "collection" + else [file["collection_name"]] + ) + + collection_names = set(collection_names).difference(extracted_collections) + if not collection_names: + log.debug(f"skipping {file} as it has already been extracted") + continue + + try: + if file["type"] == "text": + context = file["content"] + else: + if hybrid_search: + context = query_collection_with_hybrid_search( + collection_names=collection_names, + query=query, + embedding_function=embedding_function, + k=k, + reranking_function=reranking_function, + r=r, + ) + else: + context = query_collection( + collection_names=collection_names, + query=query, + embedding_function=embedding_function, + k=k, + ) + except Exception as e: + log.exception(e) + context = None + + if context: + relevant_contexts.append({**context, "source": file}) + + extracted_collections.extend(collection_names) + + contexts = [] + citations = [] + + for context in relevant_contexts: + try: + if "documents" in context: + contexts.append( + "\n\n".join( + [text for text in context["documents"][0] if text is not None] + ) + ) + + if "metadatas" in context: + citations.append( + { + "source": context["source"], + "document": context["documents"][0], + "metadata": context["metadatas"][0], + } + ) + except Exception as e: + log.exception(e) + + return contexts, citations + + +def get_model_path(model: str, update_model: bool = False): + # Construct huggingface_hub kwargs with local_files_only to return the snapshot path + cache_dir = os.getenv("SENTENCE_TRANSFORMERS_HOME") + + local_files_only = not update_model + + snapshot_kwargs = { + "cache_dir": cache_dir, + "local_files_only": local_files_only, + } + + log.debug(f"model: {model}") + log.debug(f"snapshot_kwargs: {snapshot_kwargs}") + + # Inspiration from upstream sentence_transformers + if ( + os.path.exists(model) + or ("\\" in model or model.count("/") > 1) + and local_files_only + ): + # If fully qualified path exists, return input, else set repo_id + return model + elif "/" not in model: + # Set valid repo_id for model short-name + model = "sentence-transformers" + "/" + model + + snapshot_kwargs["repo_id"] = model + + # Attempt to query the huggingface_hub library to determine the local path and/or to update + try: + model_repo_path = snapshot_download(**snapshot_kwargs) + log.debug(f"model_repo_path: {model_repo_path}") + return model_repo_path + except Exception as e: + log.exception(f"Cannot determine model snapshot path: {e}") + return model + + +def generate_openai_embeddings( + model: str, + text: Union[str, list[str]], + key: str, + url: str = "https://api.openai.com/v1", +): + if isinstance(text, list): + embeddings = generate_openai_batch_embeddings(model, text, key, url) + else: + embeddings = generate_openai_batch_embeddings(model, [text], key, url) + + return embeddings[0] if isinstance(text, str) else embeddings + + +def generate_openai_batch_embeddings( + model: str, texts: list[str], key: str, url: str = "https://api.openai.com/v1" +) -> Optional[list[list[float]]]: + try: + r = requests.post( + f"{url}/embeddings", + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {key}", + }, + json={"input": texts, "model": model}, + ) + r.raise_for_status() + data = r.json() + if "data" in data: + return [elem["embedding"] for elem in data["data"]] + else: + raise "Something went wrong :/" + except Exception as e: + print(e) + return None + + +from typing import Any + +from langchain_core.retrievers import BaseRetriever +from langchain_core.callbacks import CallbackManagerForRetrieverRun + + +class ChromaRetriever(BaseRetriever): + collection: Any + embedding_function: Any + top_n: int + + def _get_relevant_documents( + self, + query: str, + *, + run_manager: CallbackManagerForRetrieverRun, + ) -> List[Document]: + query_embeddings = self.embedding_function(query) + + results = self.collection.query( + query_embeddings=[query_embeddings], + n_results=self.top_n, + ) + + ids = results["ids"][0] + metadatas = results["metadatas"][0] + documents = results["documents"][0] + + results = [] + for idx in range(len(ids)): + results.append( + Document( + metadata=metadatas[idx], + page_content=documents[idx], + ) + ) + return results + + +import operator + +from typing import Optional, Sequence + +from langchain_core.documents import BaseDocumentCompressor, Document +from langchain_core.callbacks import Callbacks +from langchain_core.pydantic_v1 import Extra + + +class RerankCompressor(BaseDocumentCompressor): + embedding_function: Any + top_n: int + reranking_function: Any + r_score: float + + class Config: + extra = Extra.forbid + arbitrary_types_allowed = True + + def compress_documents( + self, + documents: Sequence[Document], + query: str, + callbacks: Optional[Callbacks] = None, + ) -> Sequence[Document]: + reranking = self.reranking_function is not None + + if reranking: + scores = self.reranking_function.predict( + [(query, doc.page_content) for doc in documents] + ) + else: + from sentence_transformers import util + + query_embedding = self.embedding_function(query) + document_embedding = self.embedding_function( + [doc.page_content for doc in documents] + ) + scores = util.cos_sim(query_embedding, document_embedding)[0] + + docs_with_scores = list(zip(documents, scores.tolist())) + if self.r_score: + docs_with_scores = [ + (d, s) for d, s in docs_with_scores if s >= self.r_score + ] + + result = sorted(docs_with_scores, key=operator.itemgetter(1), reverse=True) + final_results = [] + for doc, doc_score in result[: self.top_n]: + metadata = doc.metadata + metadata["score"] = doc_score + doc = Document( + page_content=doc.page_content, + metadata=metadata, + ) + final_results.append(doc) + return final_results diff --git a/backend/apps/socket/main.py b/backend/apps/socket/main.py new file mode 100644 index 0000000000000000000000000000000000000000..18ce7a6072f64d1783534f5d5103d05f62cc6dcb --- /dev/null +++ b/backend/apps/socket/main.py @@ -0,0 +1,170 @@ +import socketio +import asyncio + + +from apps.webui.models.users import Users +from utils.utils import decode_token + +sio = socketio.AsyncServer(cors_allowed_origins=[], async_mode="asgi") +app = socketio.ASGIApp(sio, socketio_path="/ws/socket.io") + +# Dictionary to maintain the user pool + +SESSION_POOL = {} +USER_POOL = {} +USAGE_POOL = {} +# Timeout duration in seconds +TIMEOUT_DURATION = 3 + + +@sio.event +async def connect(sid, environ, auth): + user = None + if auth and "token" in auth: + data = decode_token(auth["token"]) + + if data is not None and "id" in data: + user = Users.get_user_by_id(data["id"]) + + if user: + SESSION_POOL[sid] = user.id + if user.id in USER_POOL: + USER_POOL[user.id].append(sid) + else: + USER_POOL[user.id] = [sid] + + print(f"user {user.name}({user.id}) connected with session ID {sid}") + + await sio.emit("user-count", {"count": len(set(USER_POOL))}) + await sio.emit("usage", {"models": get_models_in_use()}) + + +@sio.on("user-join") +async def user_join(sid, data): + print("user-join", sid, data) + + auth = data["auth"] if "auth" in data else None + + if auth and "token" in auth: + data = decode_token(auth["token"]) + + if data is not None and "id" in data: + user = Users.get_user_by_id(data["id"]) + + if user: + + SESSION_POOL[sid] = user.id + if user.id in USER_POOL: + USER_POOL[user.id].append(sid) + else: + USER_POOL[user.id] = [sid] + + print(f"user {user.name}({user.id}) connected with session ID {sid}") + + await sio.emit("user-count", {"count": len(set(USER_POOL))}) + + +@sio.on("user-count") +async def user_count(sid): + await sio.emit("user-count", {"count": len(set(USER_POOL))}) + + +def get_models_in_use(): + # Aggregate all models in use + models_in_use = [] + for model_id, data in USAGE_POOL.items(): + models_in_use.append(model_id) + + return models_in_use + + +@sio.on("usage") +async def usage(sid, data): + + model_id = data["model"] + + # Cancel previous callback if there is one + if model_id in USAGE_POOL: + USAGE_POOL[model_id]["callback"].cancel() + + # Store the new usage data and task + + if model_id in USAGE_POOL: + USAGE_POOL[model_id]["sids"].append(sid) + USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"])) + + else: + USAGE_POOL[model_id] = {"sids": [sid]} + + # Schedule a task to remove the usage data after TIMEOUT_DURATION + USAGE_POOL[model_id]["callback"] = asyncio.create_task( + remove_after_timeout(sid, model_id) + ) + + # Broadcast the usage data to all clients + await sio.emit("usage", {"models": get_models_in_use()}) + + +async def remove_after_timeout(sid, model_id): + try: + await asyncio.sleep(TIMEOUT_DURATION) + if model_id in USAGE_POOL: + print(USAGE_POOL[model_id]["sids"]) + USAGE_POOL[model_id]["sids"].remove(sid) + USAGE_POOL[model_id]["sids"] = list(set(USAGE_POOL[model_id]["sids"])) + + if len(USAGE_POOL[model_id]["sids"]) == 0: + del USAGE_POOL[model_id] + + # Broadcast the usage data to all clients + await sio.emit("usage", {"models": get_models_in_use()}) + except asyncio.CancelledError: + # Task was cancelled due to new 'usage' event + pass + + +@sio.event +async def disconnect(sid): + if sid in SESSION_POOL: + user_id = SESSION_POOL[sid] + del SESSION_POOL[sid] + + USER_POOL[user_id].remove(sid) + + if len(USER_POOL[user_id]) == 0: + del USER_POOL[user_id] + + await sio.emit("user-count", {"count": len(USER_POOL)}) + else: + print(f"Unknown session ID {sid} disconnected") + + +async def get_event_emitter(request_info): + async def __event_emitter__(event_data): + await sio.emit( + "chat-events", + { + "chat_id": request_info["chat_id"], + "message_id": request_info["message_id"], + "data": event_data, + }, + to=request_info["session_id"], + ) + + return __event_emitter__ + + +async def get_event_call(request_info): + async def __event_call__(event_data): + response = await sio.call( + "chat-events", + { + "chat_id": request_info["chat_id"], + "message_id": request_info["message_id"], + "data": event_data, + }, + to=request_info["session_id"], + ) + return response + + return __event_call__ diff --git a/backend/apps/webui/internal/db.py b/backend/apps/webui/internal/db.py new file mode 100644 index 0000000000000000000000000000000000000000..fbe287e185f3a7011f2c73ef894781cf2512a783 --- /dev/null +++ b/backend/apps/webui/internal/db.py @@ -0,0 +1,110 @@ +import os +import logging +import json +from contextlib import contextmanager + +from peewee_migrate import Router +from apps.webui.internal.wrappers import register_connection + +from typing import Optional, Any +from typing_extensions import Self + +from sqlalchemy import create_engine, types, Dialect +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker, scoped_session +from sqlalchemy.sql.type_api import _T + +from config import SRC_LOG_LEVELS, DATA_DIR, DATABASE_URL, BACKEND_DIR + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["DB"]) + + +class JSONField(types.TypeDecorator): + impl = types.Text + cache_ok = True + + def process_bind_param(self, value: Optional[_T], dialect: Dialect) -> Any: + return json.dumps(value) + + def process_result_value(self, value: Optional[_T], dialect: Dialect) -> Any: + if value is not None: + return json.loads(value) + + def copy(self, **kw: Any) -> Self: + return JSONField(self.impl.length) + + def db_value(self, value): + return json.dumps(value) + + def python_value(self, value): + if value is not None: + return json.loads(value) + + +# Check if the file exists +if os.path.exists(f"{DATA_DIR}/ollama.db"): + # Rename the file + os.rename(f"{DATA_DIR}/ollama.db", f"{DATA_DIR}/webui.db") + log.info("Database migrated from Ollama-WebUI successfully.") +else: + pass + + +# Workaround to handle the peewee migration +# This is required to ensure the peewee migration is handled before the alembic migration +def handle_peewee_migration(DATABASE_URL): + try: + # Replace the postgresql:// with postgres:// and %40 with @ in the DATABASE_URL + db = register_connection( + DATABASE_URL.replace("postgresql://", "postgres://").replace("%40", "@") + ) + migrate_dir = BACKEND_DIR / "apps" / "webui" / "internal" / "migrations" + router = Router(db, logger=log, migrate_dir=migrate_dir) + router.run() + db.close() + + # check if db connection has been closed + + except Exception as e: + log.error(f"Failed to initialize the database connection: {e}") + raise + + finally: + # Properly closing the database connection + if db and not db.is_closed(): + db.close() + + # Assert if db connection has been closed + assert db.is_closed(), "Database connection is still open." + + +handle_peewee_migration(DATABASE_URL) + + +SQLALCHEMY_DATABASE_URL = DATABASE_URL +if "sqlite" in SQLALCHEMY_DATABASE_URL: + engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} + ) +else: + engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_pre_ping=True) + + +SessionLocal = sessionmaker( + autocommit=False, autoflush=False, bind=engine, expire_on_commit=False +) +Base = declarative_base() +Session = scoped_session(SessionLocal) + + +# Dependency +def get_session(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +get_db = contextmanager(get_session) diff --git a/backend/apps/webui/internal/migrations/001_initial_schema.py b/backend/apps/webui/internal/migrations/001_initial_schema.py new file mode 100644 index 0000000000000000000000000000000000000000..93f278f15b842306c6d7e3367c696272c5e9da69 --- /dev/null +++ b/backend/apps/webui/internal/migrations/001_initial_schema.py @@ -0,0 +1,254 @@ +"""Peewee migrations -- 001_initial_schema.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # We perform different migrations for SQLite and other databases + # This is because SQLite is very loose with enforcing its schema, and trying to migrate other databases like SQLite + # will require per-database SQL queries. + # Instead, we assume that because external DB support was added at a later date, it is safe to assume a newer base + # schema instead of trying to migrate from an older schema. + if isinstance(database, pw.SqliteDatabase): + migrate_sqlite(migrator, database, fake=fake) + else: + migrate_external(migrator, database, fake=fake) + + +def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Auth(pw.Model): + id = pw.CharField(max_length=255, unique=True) + email = pw.CharField(max_length=255) + password = pw.CharField(max_length=255) + active = pw.BooleanField() + + class Meta: + table_name = "auth" + + @migrator.create_model + class Chat(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.CharField() + chat = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "chat" + + @migrator.create_model + class ChatIdTag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + tag_name = pw.CharField(max_length=255) + chat_id = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "chatidtag" + + @migrator.create_model + class Document(pw.Model): + id = pw.AutoField() + collection_name = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255, unique=True) + title = pw.CharField() + filename = pw.CharField() + content = pw.TextField(null=True) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "document" + + @migrator.create_model + class Modelfile(pw.Model): + id = pw.AutoField() + tag_name = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + modelfile = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "modelfile" + + @migrator.create_model + class Prompt(pw.Model): + id = pw.AutoField() + command = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.CharField() + content = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "prompt" + + @migrator.create_model + class Tag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + data = pw.TextField(null=True) + + class Meta: + table_name = "tag" + + @migrator.create_model + class User(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + email = pw.CharField(max_length=255) + role = pw.CharField(max_length=255) + profile_image_url = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "user" + + +def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Auth(pw.Model): + id = pw.CharField(max_length=255, unique=True) + email = pw.CharField(max_length=255) + password = pw.TextField() + active = pw.BooleanField() + + class Meta: + table_name = "auth" + + @migrator.create_model + class Chat(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.TextField() + chat = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "chat" + + @migrator.create_model + class ChatIdTag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + tag_name = pw.CharField(max_length=255) + chat_id = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "chatidtag" + + @migrator.create_model + class Document(pw.Model): + id = pw.AutoField() + collection_name = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255, unique=True) + title = pw.TextField() + filename = pw.TextField() + content = pw.TextField(null=True) + user_id = pw.CharField(max_length=255) + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "document" + + @migrator.create_model + class Modelfile(pw.Model): + id = pw.AutoField() + tag_name = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + modelfile = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "modelfile" + + @migrator.create_model + class Prompt(pw.Model): + id = pw.AutoField() + command = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + title = pw.TextField() + content = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "prompt" + + @migrator.create_model + class Tag(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + user_id = pw.CharField(max_length=255) + data = pw.TextField(null=True) + + class Meta: + table_name = "tag" + + @migrator.create_model + class User(pw.Model): + id = pw.CharField(max_length=255, unique=True) + name = pw.CharField(max_length=255) + email = pw.CharField(max_length=255) + role = pw.CharField(max_length=255) + profile_image_url = pw.TextField() + timestamp = pw.BigIntegerField() + + class Meta: + table_name = "user" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("user") + + migrator.remove_model("tag") + + migrator.remove_model("prompt") + + migrator.remove_model("modelfile") + + migrator.remove_model("document") + + migrator.remove_model("chatidtag") + + migrator.remove_model("chat") + + migrator.remove_model("auth") diff --git a/backend/apps/webui/internal/migrations/002_add_local_sharing.py b/backend/apps/webui/internal/migrations/002_add_local_sharing.py new file mode 100644 index 0000000000000000000000000000000000000000..e93501aeec522fc102a3ce26112b2edd0e518455 --- /dev/null +++ b/backend/apps/webui/internal/migrations/002_add_local_sharing.py @@ -0,0 +1,48 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + "chat", share_id=pw.CharField(max_length=255, null=True, unique=True) + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("chat", "share_id") diff --git a/backend/apps/webui/internal/migrations/003_add_auth_api_key.py b/backend/apps/webui/internal/migrations/003_add_auth_api_key.py new file mode 100644 index 0000000000000000000000000000000000000000..07144f3aca6688a960f7036bcd2c20470da0881c --- /dev/null +++ b/backend/apps/webui/internal/migrations/003_add_auth_api_key.py @@ -0,0 +1,48 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + "user", api_key=pw.CharField(max_length=255, null=True, unique=True) + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("user", "api_key") diff --git a/backend/apps/webui/internal/migrations/004_add_archived.py b/backend/apps/webui/internal/migrations/004_add_archived.py new file mode 100644 index 0000000000000000000000000000000000000000..d01c06b4e665a61c709a8c662387e0c0755efa9a --- /dev/null +++ b/backend/apps/webui/internal/migrations/004_add_archived.py @@ -0,0 +1,46 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields("chat", archived=pw.BooleanField(default=False)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("chat", "archived") diff --git a/backend/apps/webui/internal/migrations/005_add_updated_at.py b/backend/apps/webui/internal/migrations/005_add_updated_at.py new file mode 100644 index 0000000000000000000000000000000000000000..950866ef024e80fa1b1af6e296d89feb50a5f5f9 --- /dev/null +++ b/backend/apps/webui/internal/migrations/005_add_updated_at.py @@ -0,0 +1,130 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + migrate_sqlite(migrator, database, fake=fake) + else: + migrate_external(migrator, database, fake=fake) + + +def migrate_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + # Adding fields created_at and updated_at to the 'chat' table + migrator.add_fields( + "chat", + created_at=pw.DateTimeField(null=True), # Allow null for transition + updated_at=pw.DateTimeField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql( + "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL" + ) + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields("chat", "timestamp") + + # Update the fields to be not null now that they are populated + migrator.change_fields( + "chat", + created_at=pw.DateTimeField(null=False), + updated_at=pw.DateTimeField(null=False), + ) + + +def migrate_external(migrator: Migrator, database: pw.Database, *, fake=False): + # Adding fields created_at and updated_at to the 'chat' table + migrator.add_fields( + "chat", + created_at=pw.BigIntegerField(null=True), # Allow null for transition + updated_at=pw.BigIntegerField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql( + "UPDATE chat SET created_at = timestamp, updated_at = timestamp WHERE timestamp IS NOT NULL" + ) + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields("chat", "timestamp") + + # Update the fields to be not null now that they are populated + migrator.change_fields( + "chat", + created_at=pw.BigIntegerField(null=False), + updated_at=pw.BigIntegerField(null=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + rollback_sqlite(migrator, database, fake=fake) + else: + rollback_external(migrator, database, fake=fake) + + +def rollback_sqlite(migrator: Migrator, database: pw.Database, *, fake=False): + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields("chat", timestamp=pw.DateTimeField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql("UPDATE chat SET timestamp = created_at") + + # Remove the created_at and updated_at fields + migrator.remove_fields("chat", "created_at", "updated_at") + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields("chat", timestamp=pw.DateTimeField(null=False)) + + +def rollback_external(migrator: Migrator, database: pw.Database, *, fake=False): + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields("chat", timestamp=pw.BigIntegerField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql("UPDATE chat SET timestamp = created_at") + + # Remove the created_at and updated_at fields + migrator.remove_fields("chat", "created_at", "updated_at") + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields("chat", timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py b/backend/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py new file mode 100644 index 0000000000000000000000000000000000000000..caca14d323e1fad148e7e14bae207c7e1b8896a9 --- /dev/null +++ b/backend/apps/webui/internal/migrations/006_migrate_timestamps_and_charfields.py @@ -0,0 +1,130 @@ +"""Peewee migrations -- 006_migrate_timestamps_and_charfields.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Alter the tables with timestamps + migrator.change_fields( + "chatidtag", + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + "document", + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + "modelfile", + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + "prompt", + timestamp=pw.BigIntegerField(), + ) + migrator.change_fields( + "user", + timestamp=pw.BigIntegerField(), + ) + # Alter the tables with varchar to text where necessary + migrator.change_fields( + "auth", + password=pw.TextField(), + ) + migrator.change_fields( + "chat", + title=pw.TextField(), + ) + migrator.change_fields( + "document", + title=pw.TextField(), + filename=pw.TextField(), + ) + migrator.change_fields( + "prompt", + title=pw.TextField(), + ) + migrator.change_fields( + "user", + profile_image_url=pw.TextField(), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + if isinstance(database, pw.SqliteDatabase): + # Alter the tables with timestamps + migrator.change_fields( + "chatidtag", + timestamp=pw.DateField(), + ) + migrator.change_fields( + "document", + timestamp=pw.DateField(), + ) + migrator.change_fields( + "modelfile", + timestamp=pw.DateField(), + ) + migrator.change_fields( + "prompt", + timestamp=pw.DateField(), + ) + migrator.change_fields( + "user", + timestamp=pw.DateField(), + ) + migrator.change_fields( + "auth", + password=pw.CharField(max_length=255), + ) + migrator.change_fields( + "chat", + title=pw.CharField(), + ) + migrator.change_fields( + "document", + title=pw.CharField(), + filename=pw.CharField(), + ) + migrator.change_fields( + "prompt", + title=pw.CharField(), + ) + migrator.change_fields( + "user", + profile_image_url=pw.CharField(), + ) diff --git a/backend/apps/webui/internal/migrations/007_add_user_last_active_at.py b/backend/apps/webui/internal/migrations/007_add_user_last_active_at.py new file mode 100644 index 0000000000000000000000000000000000000000..dd176ba73e51b15f74f45b839d0eb8e72fc63ecf --- /dev/null +++ b/backend/apps/webui/internal/migrations/007_add_user_last_active_at.py @@ -0,0 +1,79 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields created_at and updated_at to the 'user' table + migrator.add_fields( + "user", + created_at=pw.BigIntegerField(null=True), # Allow null for transition + updated_at=pw.BigIntegerField(null=True), # Allow null for transition + last_active_at=pw.BigIntegerField(null=True), # Allow null for transition + ) + + # Populate the new fields from an existing 'timestamp' field + migrator.sql( + 'UPDATE "user" SET created_at = timestamp, updated_at = timestamp, last_active_at = timestamp WHERE timestamp IS NOT NULL' + ) + + # Now that the data has been copied, remove the original 'timestamp' field + migrator.remove_fields("user", "timestamp") + + # Update the fields to be not null now that they are populated + migrator.change_fields( + "user", + created_at=pw.BigIntegerField(null=False), + updated_at=pw.BigIntegerField(null=False), + last_active_at=pw.BigIntegerField(null=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Recreate the timestamp field initially allowing null values for safe transition + migrator.add_fields("user", timestamp=pw.BigIntegerField(null=True)) + + # Copy the earliest created_at date back into the new timestamp field + # This assumes created_at was originally a copy of timestamp + migrator.sql('UPDATE "user" SET timestamp = created_at') + + # Remove the created_at and updated_at fields + migrator.remove_fields("user", "created_at", "updated_at", "last_active_at") + + # Finally, alter the timestamp field to not allow nulls if that was the original setting + migrator.change_fields("user", timestamp=pw.BigIntegerField(null=False)) diff --git a/backend/apps/webui/internal/migrations/008_add_memory.py b/backend/apps/webui/internal/migrations/008_add_memory.py new file mode 100644 index 0000000000000000000000000000000000000000..9307aa4d5c933cf0ee98c0932ab0e48bc4cecbc6 --- /dev/null +++ b/backend/apps/webui/internal/migrations/008_add_memory.py @@ -0,0 +1,53 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + @migrator.create_model + class Memory(pw.Model): + id = pw.CharField(max_length=255, unique=True) + user_id = pw.CharField(max_length=255) + content = pw.TextField(null=False) + updated_at = pw.BigIntegerField(null=False) + created_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "memory" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("memory") diff --git a/backend/apps/webui/internal/migrations/009_add_models.py b/backend/apps/webui/internal/migrations/009_add_models.py new file mode 100644 index 0000000000000000000000000000000000000000..548ec7cdcabbc620f8c2c79b65709a6b08d9a11c --- /dev/null +++ b/backend/apps/webui/internal/migrations/009_add_models.py @@ -0,0 +1,61 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Model(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + base_model_id = pw.TextField(null=True) + + name = pw.TextField() + + meta = pw.TextField() + params = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "model" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("model") diff --git a/backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py b/backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py new file mode 100644 index 0000000000000000000000000000000000000000..2ef814c06b046d4a5281d2486a41222ca7ac87ad --- /dev/null +++ b/backend/apps/webui/internal/migrations/010_migrate_modelfiles_to_models.py @@ -0,0 +1,130 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator +import json + +from utils.misc import parse_ollama_modelfile + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Fetch data from 'modelfile' table and insert into 'model' table + migrate_modelfile_to_model(migrator, database) + # Drop the 'modelfile' table + migrator.remove_model("modelfile") + + +def migrate_modelfile_to_model(migrator: Migrator, database: pw.Database): + ModelFile = migrator.orm["modelfile"] + Model = migrator.orm["model"] + + modelfiles = ModelFile.select() + + for modelfile in modelfiles: + # Extract and transform data in Python + + modelfile.modelfile = json.loads(modelfile.modelfile) + meta = json.dumps( + { + "description": modelfile.modelfile.get("desc"), + "profile_image_url": modelfile.modelfile.get("imageUrl"), + "ollama": {"modelfile": modelfile.modelfile.get("content")}, + "suggestion_prompts": modelfile.modelfile.get("suggestionPrompts"), + "categories": modelfile.modelfile.get("categories"), + "user": {**modelfile.modelfile.get("user", {}), "community": True}, + } + ) + + info = parse_ollama_modelfile(modelfile.modelfile.get("content")) + + # Insert the processed data into the 'model' table + Model.create( + id=f"ollama-{modelfile.tag_name}", + user_id=modelfile.user_id, + base_model_id=info.get("base_model_id"), + name=modelfile.modelfile.get("title"), + meta=meta, + params=json.dumps(info.get("params", {})), + created_at=modelfile.timestamp, + updated_at=modelfile.timestamp, + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + recreate_modelfile_table(migrator, database) + move_data_back_to_modelfile(migrator, database) + migrator.remove_model("model") + + +def recreate_modelfile_table(migrator: Migrator, database: pw.Database): + query = """ + CREATE TABLE IF NOT EXISTS modelfile ( + user_id TEXT, + tag_name TEXT, + modelfile JSON, + timestamp BIGINT + ) + """ + migrator.sql(query) + + +def move_data_back_to_modelfile(migrator: Migrator, database: pw.Database): + Model = migrator.orm["model"] + Modelfile = migrator.orm["modelfile"] + + models = Model.select() + + for model in models: + # Extract and transform data in Python + meta = json.loads(model.meta) + + modelfile_data = { + "title": model.name, + "desc": meta.get("description"), + "imageUrl": meta.get("profile_image_url"), + "content": meta.get("ollama", {}).get("modelfile"), + "suggestionPrompts": meta.get("suggestion_prompts"), + "categories": meta.get("categories"), + "user": {k: v for k, v in meta.get("user", {}).items() if k != "community"}, + } + + # Insert the processed data back into the 'modelfile' table + Modelfile.create( + user_id=model.user_id, + tag_name=model.id, + modelfile=modelfile_data, + timestamp=model.created_at, + ) diff --git a/backend/apps/webui/internal/migrations/011_add_user_settings.py b/backend/apps/webui/internal/migrations/011_add_user_settings.py new file mode 100644 index 0000000000000000000000000000000000000000..a1620dcadae41891922e324a9a6b152b81ea0ec4 --- /dev/null +++ b/backend/apps/webui/internal/migrations/011_add_user_settings.py @@ -0,0 +1,48 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields settings to the 'user' table + migrator.add_fields("user", settings=pw.TextField(null=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Remove the settings field + migrator.remove_fields("user", "settings") diff --git a/backend/apps/webui/internal/migrations/012_add_tools.py b/backend/apps/webui/internal/migrations/012_add_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..4a68eea552e4fb9b5d5f64f55e7b7966f342435b --- /dev/null +++ b/backend/apps/webui/internal/migrations/012_add_tools.py @@ -0,0 +1,61 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Tool(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + + name = pw.TextField() + content = pw.TextField() + specs = pw.TextField() + + meta = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "tool" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("tool") diff --git a/backend/apps/webui/internal/migrations/013_add_user_info.py b/backend/apps/webui/internal/migrations/013_add_user_info.py new file mode 100644 index 0000000000000000000000000000000000000000..0f68669cca869fcd5537d1a0600ed45155056437 --- /dev/null +++ b/backend/apps/webui/internal/migrations/013_add_user_info.py @@ -0,0 +1,48 @@ +"""Peewee migrations -- 002_add_local_sharing.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + # Adding fields info to the 'user' table + migrator.add_fields("user", info=pw.TextField(null=True)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + # Remove the settings field + migrator.remove_fields("user", "info") diff --git a/backend/apps/webui/internal/migrations/014_add_files.py b/backend/apps/webui/internal/migrations/014_add_files.py new file mode 100644 index 0000000000000000000000000000000000000000..5e1acf0ad8b9510b8d090c6458f292a124dd73cd --- /dev/null +++ b/backend/apps/webui/internal/migrations/014_add_files.py @@ -0,0 +1,55 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class File(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + filename = pw.TextField() + meta = pw.TextField() + created_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "file" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("file") diff --git a/backend/apps/webui/internal/migrations/015_add_functions.py b/backend/apps/webui/internal/migrations/015_add_functions.py new file mode 100644 index 0000000000000000000000000000000000000000..8316a9333bad45eccf7d6708016ef5c45b208360 --- /dev/null +++ b/backend/apps/webui/internal/migrations/015_add_functions.py @@ -0,0 +1,61 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + @migrator.create_model + class Function(pw.Model): + id = pw.TextField(unique=True) + user_id = pw.TextField() + + name = pw.TextField() + type = pw.TextField() + + content = pw.TextField() + meta = pw.TextField() + + created_at = pw.BigIntegerField(null=False) + updated_at = pw.BigIntegerField(null=False) + + class Meta: + table_name = "function" + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_model("function") diff --git a/backend/apps/webui/internal/migrations/016_add_valves_and_is_active.py b/backend/apps/webui/internal/migrations/016_add_valves_and_is_active.py new file mode 100644 index 0000000000000000000000000000000000000000..e3af521b7e841ee01b226ad867ae509e42e5eb4f --- /dev/null +++ b/backend/apps/webui/internal/migrations/016_add_valves_and_is_active.py @@ -0,0 +1,50 @@ +"""Peewee migrations -- 009_add_models.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields("tool", valves=pw.TextField(null=True)) + migrator.add_fields("function", valves=pw.TextField(null=True)) + migrator.add_fields("function", is_active=pw.BooleanField(default=False)) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("tool", "valves") + migrator.remove_fields("function", "valves") + migrator.remove_fields("function", "is_active") diff --git a/backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py b/backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py new file mode 100644 index 0000000000000000000000000000000000000000..eaa3fa5fe54bd37691593ba6fc2840b8653d534d --- /dev/null +++ b/backend/apps/webui/internal/migrations/017_add_user_oauth_sub.py @@ -0,0 +1,45 @@ +"""Peewee migrations -- 017_add_user_oauth_sub.py. +Some examples (model - class or model name):: + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + "user", + oauth_sub=pw.TextField(null=True, unique=True), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("user", "oauth_sub") diff --git a/backend/apps/webui/internal/migrations/018_add_function_is_global.py b/backend/apps/webui/internal/migrations/018_add_function_is_global.py new file mode 100644 index 0000000000000000000000000000000000000000..04cdab705986a36227a7f73e800d6739f00033ce --- /dev/null +++ b/backend/apps/webui/internal/migrations/018_add_function_is_global.py @@ -0,0 +1,49 @@ +"""Peewee migrations -- 017_add_user_oauth_sub.py. + +Some examples (model - class or model name):: + + > Model = migrator.orm['table_name'] # Return model in current state by name + > Model = migrator.ModelClass # Return model in current state by name + + > migrator.sql(sql) # Run custom SQL + > migrator.run(func, *args, **kwargs) # Run python function with the given args + > migrator.create_model(Model) # Create a model (could be used as decorator) + > migrator.remove_model(model, cascade=True) # Remove a model + > migrator.add_fields(model, **fields) # Add fields to a model + > migrator.change_fields(model, **fields) # Change fields + > migrator.remove_fields(model, *field_names, cascade=True) + > migrator.rename_field(model, old_field_name, new_field_name) + > migrator.rename_table(model, new_table_name) + > migrator.add_index(model, *col_names, unique=False) + > migrator.add_not_null(model, *field_names) + > migrator.add_default(model, field_name, default) + > migrator.add_constraint(model, name, sql) + > migrator.drop_index(model, *col_names) + > migrator.drop_not_null(model, *field_names) + > migrator.drop_constraints(model, *constraints) + +""" + +from contextlib import suppress + +import peewee as pw +from peewee_migrate import Migrator + + +with suppress(ImportError): + import playhouse.postgres_ext as pw_pext + + +def migrate(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your migrations here.""" + + migrator.add_fields( + "function", + is_global=pw.BooleanField(default=False), + ) + + +def rollback(migrator: Migrator, database: pw.Database, *, fake=False): + """Write your rollback migrations here.""" + + migrator.remove_fields("function", "is_global") diff --git a/backend/apps/webui/internal/wrappers.py b/backend/apps/webui/internal/wrappers.py new file mode 100644 index 0000000000000000000000000000000000000000..2b5551ce2ba833fa57480b19a84984f93673260e --- /dev/null +++ b/backend/apps/webui/internal/wrappers.py @@ -0,0 +1,72 @@ +from contextvars import ContextVar +from peewee import * +from peewee import PostgresqlDatabase, InterfaceError as PeeWeeInterfaceError + +import logging +from playhouse.db_url import connect, parse +from playhouse.shortcuts import ReconnectMixin + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["DB"]) + +db_state_default = {"closed": None, "conn": None, "ctx": None, "transactions": None} +db_state = ContextVar("db_state", default=db_state_default.copy()) + + +class PeeweeConnectionState(object): + def __init__(self, **kwargs): + super().__setattr__("_state", db_state) + super().__init__(**kwargs) + + def __setattr__(self, name, value): + self._state.get()[name] = value + + def __getattr__(self, name): + value = self._state.get()[name] + return value + + +class CustomReconnectMixin(ReconnectMixin): + reconnect_errors = ( + # psycopg2 + (OperationalError, "termin"), + (InterfaceError, "closed"), + # peewee + (PeeWeeInterfaceError, "closed"), + ) + + +class ReconnectingPostgresqlDatabase(CustomReconnectMixin, PostgresqlDatabase): + pass + + +def register_connection(db_url): + db = connect(db_url) + if isinstance(db, PostgresqlDatabase): + # Enable autoconnect for SQLite databases, managed by Peewee + db.autoconnect = True + db.reuse_if_open = True + log.info("Connected to PostgreSQL database") + + # Get the connection details + connection = parse(db_url) + + # Use our custom database class that supports reconnection + db = ReconnectingPostgresqlDatabase( + connection["database"], + user=connection["user"], + password=connection["password"], + host=connection["host"], + port=connection["port"], + ) + db.connect(reuse_if_open=True) + elif isinstance(db, SqliteDatabase): + # Enable autoconnect for SQLite databases, managed by Peewee + db.autoconnect = True + db.reuse_if_open = True + log.info("Connected to SQLite database") + else: + raise ValueError("Unsupported database connection") + return db diff --git a/backend/apps/webui/main.py b/backend/apps/webui/main.py new file mode 100644 index 0000000000000000000000000000000000000000..848c798c574d2372eeab1fe75917d6f1164b3907 --- /dev/null +++ b/backend/apps/webui/main.py @@ -0,0 +1,484 @@ +from fastapi import FastAPI, Depends +from fastapi.routing import APIRoute +from fastapi.responses import StreamingResponse +from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware +from sqlalchemy.orm import Session +from apps.webui.routers import ( + auths, + users, + chats, + documents, + tools, + models, + prompts, + configs, + memories, + utils, + files, + functions, +) +from apps.webui.models.functions import Functions +from apps.webui.models.models import Models +from apps.webui.utils import load_function_module_by_id + +from utils.misc import stream_message_template +from utils.task import prompt_template + + +from config import ( + WEBUI_BUILD_HASH, + SHOW_ADMIN_DETAILS, + ADMIN_EMAIL, + WEBUI_AUTH, + DEFAULT_MODELS, + DEFAULT_PROMPT_SUGGESTIONS, + DEFAULT_USER_ROLE, + ENABLE_SIGNUP, + ENABLE_LOGIN_FORM, + USER_PERMISSIONS, + WEBHOOK_URL, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, + JWT_EXPIRES_IN, + WEBUI_BANNERS, + ENABLE_COMMUNITY_SHARING, + OAUTH_PROVIDERS, + AppConfig, + OAUTH_USERNAME_CLAIM, + OAUTH_PICTURE_CLAIM, +) + +from apps.socket.main import get_event_call, get_event_emitter + +import inspect +import uuid +import time +import json + +from typing import Iterator, Generator, AsyncGenerator, Optional +from pydantic import BaseModel + +app = FastAPI() + +origins = ["*"] + +app.state.config = AppConfig() + +app.state.config.ENABLE_SIGNUP = ENABLE_SIGNUP +app.state.config.ENABLE_LOGIN_FORM = ENABLE_LOGIN_FORM +app.state.config.JWT_EXPIRES_IN = JWT_EXPIRES_IN +app.state.AUTH_TRUSTED_EMAIL_HEADER = WEBUI_AUTH_TRUSTED_EMAIL_HEADER +app.state.AUTH_TRUSTED_NAME_HEADER = WEBUI_AUTH_TRUSTED_NAME_HEADER + + +app.state.config.SHOW_ADMIN_DETAILS = SHOW_ADMIN_DETAILS +app.state.config.ADMIN_EMAIL = ADMIN_EMAIL + + +app.state.config.DEFAULT_MODELS = DEFAULT_MODELS +app.state.config.DEFAULT_PROMPT_SUGGESTIONS = DEFAULT_PROMPT_SUGGESTIONS +app.state.config.DEFAULT_USER_ROLE = DEFAULT_USER_ROLE +app.state.config.USER_PERMISSIONS = USER_PERMISSIONS +app.state.config.WEBHOOK_URL = WEBHOOK_URL +app.state.config.BANNERS = WEBUI_BANNERS + +app.state.config.ENABLE_COMMUNITY_SHARING = ENABLE_COMMUNITY_SHARING + +app.state.config.OAUTH_USERNAME_CLAIM = OAUTH_USERNAME_CLAIM +app.state.config.OAUTH_PICTURE_CLAIM = OAUTH_PICTURE_CLAIM + +app.state.MODELS = {} +app.state.TOOLS = {} +app.state.FUNCTIONS = {} + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +app.include_router(configs.router, prefix="/configs", tags=["configs"]) +app.include_router(auths.router, prefix="/auths", tags=["auths"]) +app.include_router(users.router, prefix="/users", tags=["users"]) +app.include_router(chats.router, prefix="/chats", tags=["chats"]) + +app.include_router(documents.router, prefix="/documents", tags=["documents"]) +app.include_router(models.router, prefix="/models", tags=["models"]) +app.include_router(prompts.router, prefix="/prompts", tags=["prompts"]) + +app.include_router(memories.router, prefix="/memories", tags=["memories"]) +app.include_router(files.router, prefix="/files", tags=["files"]) +app.include_router(tools.router, prefix="/tools", tags=["tools"]) +app.include_router(functions.router, prefix="/functions", tags=["functions"]) + +app.include_router(utils.router, prefix="/utils", tags=["utils"]) + + +@app.get("/") +async def get_status(): + return { + "status": True, + "auth": WEBUI_AUTH, + "default_models": app.state.config.DEFAULT_MODELS, + "default_prompt_suggestions": app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + } + + +async def get_pipe_models(): + pipes = Functions.get_functions_by_type("pipe", active_only=True) + pipe_models = [] + + for pipe in pipes: + # Check if function is already loaded + if pipe.id not in app.state.FUNCTIONS: + function_module, function_type, frontmatter = load_function_module_by_id( + pipe.id + ) + app.state.FUNCTIONS[pipe.id] = function_module + else: + function_module = app.state.FUNCTIONS[pipe.id] + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(pipe.id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + # Check if function is a manifold + if hasattr(function_module, "type"): + if function_module.type == "manifold": + manifold_pipes = [] + + # Check if pipes is a function or a list + if callable(function_module.pipes): + manifold_pipes = function_module.pipes() + else: + manifold_pipes = function_module.pipes + + for p in manifold_pipes: + manifold_pipe_id = f'{pipe.id}.{p["id"]}' + manifold_pipe_name = p["name"] + + if hasattr(function_module, "name"): + manifold_pipe_name = ( + f"{function_module.name}{manifold_pipe_name}" + ) + + pipe_flag = {"type": pipe.type} + if hasattr(function_module, "ChatValves"): + pipe_flag["valves_spec"] = function_module.ChatValves.schema() + + pipe_models.append( + { + "id": manifold_pipe_id, + "name": manifold_pipe_name, + "object": "model", + "created": pipe.created_at, + "owned_by": "openai", + "pipe": pipe_flag, + } + ) + else: + pipe_flag = {"type": "pipe"} + if hasattr(function_module, "ChatValves"): + pipe_flag["valves_spec"] = function_module.ChatValves.schema() + + pipe_models.append( + { + "id": pipe.id, + "name": pipe.name, + "object": "model", + "created": pipe.created_at, + "owned_by": "openai", + "pipe": pipe_flag, + } + ) + + return pipe_models + + +async def generate_function_chat_completion(form_data, user): + model_id = form_data.get("model") + model_info = Models.get_model_by_id(model_id) + + metadata = None + if "metadata" in form_data: + metadata = form_data["metadata"] + del form_data["metadata"] + + __event_emitter__ = None + __event_call__ = None + __task__ = None + + if metadata: + if ( + metadata.get("session_id") + and metadata.get("chat_id") + and metadata.get("message_id") + ): + __event_emitter__ = await get_event_emitter(metadata) + __event_call__ = await get_event_call(metadata) + + if metadata.get("task"): + __task__ = metadata.get("task") + + if model_info: + if model_info.base_model_id: + form_data["model"] = model_info.base_model_id + + model_info.params = model_info.params.model_dump() + + if model_info.params: + if model_info.params.get("temperature", None) is not None: + form_data["temperature"] = float(model_info.params.get("temperature")) + + if model_info.params.get("top_p", None): + form_data["top_p"] = int(model_info.params.get("top_p", None)) + + if model_info.params.get("max_tokens", None): + form_data["max_tokens"] = int(model_info.params.get("max_tokens", None)) + + if model_info.params.get("frequency_penalty", None): + form_data["frequency_penalty"] = int( + model_info.params.get("frequency_penalty", None) + ) + + if model_info.params.get("seed", None): + form_data["seed"] = model_info.params.get("seed", None) + + if model_info.params.get("stop", None): + form_data["stop"] = ( + [ + bytes(stop, "utf-8").decode("unicode_escape") + for stop in model_info.params["stop"] + ] + if model_info.params.get("stop", None) + else None + ) + + system = model_info.params.get("system", None) + if system: + system = prompt_template( + system, + **( + { + "user_name": user.name, + "user_location": ( + user.info.get("location") if user.info else None + ), + } + if user + else {} + ), + ) + # Check if the payload already has a system message + # If not, add a system message to the payload + if form_data.get("messages"): + for message in form_data["messages"]: + if message.get("role") == "system": + message["content"] = system + message["content"] + break + else: + form_data["messages"].insert( + 0, + { + "role": "system", + "content": system, + }, + ) + + else: + pass + + async def job(): + pipe_id = form_data["model"] + if "." in pipe_id: + pipe_id, sub_pipe_id = pipe_id.split(".", 1) + print(pipe_id) + + # Check if function is already loaded + if pipe_id not in app.state.FUNCTIONS: + function_module, function_type, frontmatter = load_function_module_by_id( + pipe_id + ) + app.state.FUNCTIONS[pipe_id] = function_module + else: + function_module = app.state.FUNCTIONS[pipe_id] + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + + valves = Functions.get_function_valves_by_id(pipe_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + pipe = function_module.pipe + + # Get the signature of the function + sig = inspect.signature(pipe) + params = {"body": form_data} + + if "__user__" in sig.parameters: + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(function_module, "UserValves"): + __user__["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id(pipe_id, user.id) + ) + except Exception as e: + print(e) + + params = {**params, "__user__": __user__} + + if "__event_emitter__" in sig.parameters: + params = {**params, "__event_emitter__": __event_emitter__} + + if "__event_call__" in sig.parameters: + params = {**params, "__event_call__": __event_call__} + + if "__task__" in sig.parameters: + params = {**params, "__task__": __task__} + + if form_data["stream"]: + + async def stream_content(): + try: + if inspect.iscoroutinefunction(pipe): + res = await pipe(**params) + else: + res = pipe(**params) + + # Directly return if the response is a StreamingResponse + if isinstance(res, StreamingResponse): + async for data in res.body_iterator: + yield data + return + if isinstance(res, dict): + yield f"data: {json.dumps(res)}\n\n" + return + + except Exception as e: + print(f"Error: {e}") + yield f"data: {json.dumps({'error': {'detail':str(e)}})}\n\n" + return + + if isinstance(res, str): + message = stream_message_template(form_data["model"], res) + yield f"data: {json.dumps(message)}\n\n" + + if isinstance(res, Iterator): + for line in res: + if isinstance(line, BaseModel): + line = line.model_dump_json() + line = f"data: {line}" + if isinstance(line, dict): + line = f"data: {json.dumps(line)}" + + try: + line = line.decode("utf-8") + except: + pass + + if line.startswith("data:"): + yield f"{line}\n\n" + else: + line = stream_message_template(form_data["model"], line) + yield f"data: {json.dumps(line)}\n\n" + + if isinstance(res, str) or isinstance(res, Generator): + finish_message = { + "id": f"{form_data['model']}-{str(uuid.uuid4())}", + "object": "chat.completion.chunk", + "created": int(time.time()), + "model": form_data["model"], + "choices": [ + { + "index": 0, + "delta": {}, + "logprobs": None, + "finish_reason": "stop", + } + ], + } + + yield f"data: {json.dumps(finish_message)}\n\n" + yield f"data: [DONE]" + + if isinstance(res, AsyncGenerator): + async for line in res: + if isinstance(line, BaseModel): + line = line.model_dump_json() + line = f"data: {line}" + if isinstance(line, dict): + line = f"data: {json.dumps(line)}" + + try: + line = line.decode("utf-8") + except: + pass + + if line.startswith("data:"): + yield f"{line}\n\n" + else: + line = stream_message_template(form_data["model"], line) + yield f"data: {json.dumps(line)}\n\n" + + return StreamingResponse(stream_content(), media_type="text/event-stream") + else: + + try: + if inspect.iscoroutinefunction(pipe): + res = await pipe(**params) + else: + res = pipe(**params) + + if isinstance(res, StreamingResponse): + return res + except Exception as e: + print(f"Error: {e}") + return {"error": {"detail": str(e)}} + + if isinstance(res, dict): + return res + elif isinstance(res, BaseModel): + return res.model_dump() + else: + message = "" + if isinstance(res, str): + message = res + elif isinstance(res, Generator): + for stream in res: + message = f"{message}{stream}" + elif isinstance(res, AsyncGenerator): + async for stream in res: + message = f"{message}{stream}" + + return { + "id": f"{form_data['model']}-{str(uuid.uuid4())}", + "object": "chat.completion", + "created": int(time.time()), + "model": form_data["model"], + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": message, + }, + "logprobs": None, + "finish_reason": "stop", + } + ], + } + + return await job() diff --git a/backend/apps/webui/models/auths.py b/backend/apps/webui/models/auths.py new file mode 100644 index 0000000000000000000000000000000000000000..bcea4a367ae2d328fd69e770363fcc400083c866 --- /dev/null +++ b/backend/apps/webui/models/auths.py @@ -0,0 +1,207 @@ +from pydantic import BaseModel +from typing import Optional +import uuid +import logging +from sqlalchemy import String, Column, Boolean, Text + +from apps.webui.models.users import UserModel, Users +from utils.utils import verify_password + +from apps.webui.internal.db import Base, get_db + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# DB MODEL +#################### + + +class Auth(Base): + __tablename__ = "auth" + + id = Column(String, primary_key=True) + email = Column(String) + password = Column(Text) + active = Column(Boolean) + + +class AuthModel(BaseModel): + id: str + email: str + password: str + active: bool = True + + +#################### +# Forms +#################### + + +class Token(BaseModel): + token: str + token_type: str + + +class ApiKey(BaseModel): + api_key: Optional[str] = None + + +class UserResponse(BaseModel): + id: str + email: str + name: str + role: str + profile_image_url: str + + +class SigninResponse(Token, UserResponse): + pass + + +class SigninForm(BaseModel): + email: str + password: str + + +class ProfileImageUrlForm(BaseModel): + profile_image_url: str + + +class UpdateProfileForm(BaseModel): + profile_image_url: str + name: str + + +class UpdatePasswordForm(BaseModel): + password: str + new_password: str + + +class SignupForm(BaseModel): + name: str + email: str + password: str + profile_image_url: Optional[str] = "/user.png" + + +class AddUserForm(SignupForm): + role: Optional[str] = "pending" + + +class AuthsTable: + + def insert_new_auth( + self, + email: str, + password: str, + name: str, + profile_image_url: str = "/user.png", + role: str = "pending", + oauth_sub: Optional[str] = None, + ) -> Optional[UserModel]: + with get_db() as db: + + log.info("insert_new_auth") + + id = str(uuid.uuid4()) + + auth = AuthModel( + **{"id": id, "email": email, "password": password, "active": True} + ) + result = Auth(**auth.model_dump()) + db.add(result) + + user = Users.insert_new_user( + id, name, email, profile_image_url, role, oauth_sub + ) + + db.commit() + db.refresh(result) + + if result and user: + return user + else: + return None + + def authenticate_user(self, email: str, password: str) -> Optional[UserModel]: + log.info(f"authenticate_user: {email}") + try: + with get_db() as db: + + auth = db.query(Auth).filter_by(email=email, active=True).first() + if auth: + if verify_password(password, auth.password): + user = Users.get_user_by_id(auth.id) + return user + else: + return None + else: + return None + except: + return None + + def authenticate_user_by_api_key(self, api_key: str) -> Optional[UserModel]: + log.info(f"authenticate_user_by_api_key: {api_key}") + # if no api_key, return None + if not api_key: + return None + + try: + user = Users.get_user_by_api_key(api_key) + return user if user else None + except: + return False + + def authenticate_user_by_trusted_header(self, email: str) -> Optional[UserModel]: + log.info(f"authenticate_user_by_trusted_header: {email}") + try: + with get_db() as db: + auth = db.query(Auth).filter_by(email=email, active=True).first() + if auth: + user = Users.get_user_by_id(auth.id) + return user + except: + return None + + def update_user_password_by_id(self, id: str, new_password: str) -> bool: + try: + with get_db() as db: + result = ( + db.query(Auth).filter_by(id=id).update({"password": new_password}) + ) + db.commit() + return True if result == 1 else False + except: + return False + + def update_email_by_id(self, id: str, email: str) -> bool: + try: + with get_db() as db: + result = db.query(Auth).filter_by(id=id).update({"email": email}) + db.commit() + return True if result == 1 else False + except: + return False + + def delete_auth_by_id(self, id: str) -> bool: + try: + with get_db() as db: + + # Delete User + result = Users.delete_user_by_id(id) + + if result: + db.query(Auth).filter_by(id=id).delete() + db.commit() + + return True + else: + return False + except: + return False + + +Auths = AuthsTable() diff --git a/backend/apps/webui/models/chats.py b/backend/apps/webui/models/chats.py new file mode 100644 index 0000000000000000000000000000000000000000..abde4f2b31996bd59d38de964e8a5f81b6aabee0 --- /dev/null +++ b/backend/apps/webui/models/chats.py @@ -0,0 +1,406 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Union, Optional + +import json +import uuid +import time + +from sqlalchemy import Column, String, BigInteger, Boolean, Text + +from apps.webui.internal.db import Base, get_db + + +#################### +# Chat DB Schema +#################### + + +class Chat(Base): + __tablename__ = "chat" + + id = Column(String, primary_key=True) + user_id = Column(String) + title = Column(Text) + chat = Column(Text) # Save Chat JSON as Text + + created_at = Column(BigInteger) + updated_at = Column(BigInteger) + + share_id = Column(Text, unique=True, nullable=True) + archived = Column(Boolean, default=False) + + +class ChatModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + id: str + user_id: str + title: str + chat: str + + created_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + + share_id: Optional[str] = None + archived: bool = False + + +#################### +# Forms +#################### + + +class ChatForm(BaseModel): + chat: dict + + +class ChatTitleForm(BaseModel): + title: str + + +class ChatResponse(BaseModel): + id: str + user_id: str + title: str + chat: dict + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + share_id: Optional[str] = None # id of the chat to be shared + archived: bool + + +class ChatTitleIdResponse(BaseModel): + id: str + title: str + updated_at: int + created_at: int + + +class ChatTable: + + def insert_new_chat(self, user_id: str, form_data: ChatForm) -> Optional[ChatModel]: + with get_db() as db: + + id = str(uuid.uuid4()) + chat = ChatModel( + **{ + "id": id, + "user_id": user_id, + "title": ( + form_data.chat["title"] + if "title" in form_data.chat + else "New Chat" + ), + "chat": json.dumps(form_data.chat), + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + + result = Chat(**chat.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + return ChatModel.model_validate(result) if result else None + + def update_chat_by_id(self, id: str, chat: dict) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat_obj = db.get(Chat, id) + chat_obj.chat = json.dumps(chat) + chat_obj.title = chat["title"] if "title" in chat else "New Chat" + chat_obj.updated_at = int(time.time()) + db.commit() + db.refresh(chat_obj) + + return ChatModel.model_validate(chat_obj) + except Exception as e: + return None + + def insert_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: + with get_db() as db: + + # Get the existing chat to share + chat = db.get(Chat, chat_id) + # Check if the chat is already shared + if chat.share_id: + return self.get_chat_by_id_and_user_id(chat.share_id, "shared") + # Create a new chat with the same data, but with a new ID + shared_chat = ChatModel( + **{ + "id": str(uuid.uuid4()), + "user_id": f"shared-{chat_id}", + "title": chat.title, + "chat": chat.chat, + "created_at": chat.created_at, + "updated_at": int(time.time()), + } + ) + shared_result = Chat(**shared_chat.model_dump()) + db.add(shared_result) + db.commit() + db.refresh(shared_result) + + # Update the original chat with the share_id + result = ( + db.query(Chat) + .filter_by(id=chat_id) + .update({"share_id": shared_chat.id}) + ) + db.commit() + return shared_chat if (shared_result and result) else None + + def update_shared_chat_by_chat_id(self, chat_id: str) -> Optional[ChatModel]: + try: + with get_db() as db: + + print("update_shared_chat_by_id") + chat = db.get(Chat, chat_id) + print(chat) + chat.title = chat.title + chat.chat = chat.chat + db.commit() + db.refresh(chat) + + return self.get_chat_by_id(chat.share_id) + except: + return None + + def delete_shared_chat_by_chat_id(self, chat_id: str) -> bool: + try: + with get_db() as db: + + db.query(Chat).filter_by(user_id=f"shared-{chat_id}").delete() + db.commit() + + return True + except: + return False + + def update_chat_share_id_by_id( + self, id: str, share_id: Optional[str] + ) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat = db.get(Chat, id) + chat.share_id = share_id + db.commit() + db.refresh(chat) + return ChatModel.model_validate(chat) + except: + return None + + def toggle_chat_archive_by_id(self, id: str) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat = db.get(Chat, id) + chat.archived = not chat.archived + db.commit() + db.refresh(chat) + return ChatModel.model_validate(chat) + except: + return None + + def archive_all_chats_by_user_id(self, user_id: str) -> bool: + try: + with get_db() as db: + db.query(Chat).filter_by(user_id=user_id).update({"archived": True}) + db.commit() + return True + except: + return False + + def get_archived_chat_list_by_user_id( + self, user_id: str, skip: int = 0, limit: int = 50 + ) -> List[ChatModel]: + with get_db() as db: + + all_chats = ( + db.query(Chat) + .filter_by(user_id=user_id, archived=True) + .order_by(Chat.updated_at.desc()) + # .limit(limit).offset(skip) + .all() + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def get_chat_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + skip: int = 0, + limit: int = 50, + ) -> List[ChatModel]: + with get_db() as db: + query = db.query(Chat).filter_by(user_id=user_id) + if not include_archived: + query = query.filter_by(archived=False) + all_chats = ( + query.order_by(Chat.updated_at.desc()) + # .limit(limit).offset(skip) + .all() + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def get_chat_title_id_list_by_user_id( + self, + user_id: str, + include_archived: bool = False, + skip: int = 0, + limit: int = 50, + ) -> List[ChatTitleIdResponse]: + with get_db() as db: + query = db.query(Chat).filter_by(user_id=user_id) + if not include_archived: + query = query.filter_by(archived=False) + + all_chats = ( + query.order_by(Chat.updated_at.desc()) + # limit cols + .with_entities( + Chat.id, Chat.title, Chat.updated_at, Chat.created_at + ).all() + ) + # result has to be destrctured from sqlalchemy `row` and mapped to a dict since the `ChatModel`is not the returned dataclass. + return [ + ChatTitleIdResponse.model_validate( + { + "id": chat[0], + "title": chat[1], + "updated_at": chat[2], + "created_at": chat[3], + } + ) + for chat in all_chats + ] + + def get_chat_list_by_chat_ids( + self, chat_ids: List[str], skip: int = 0, limit: int = 50 + ) -> List[ChatModel]: + with get_db() as db: + all_chats = ( + db.query(Chat) + .filter(Chat.id.in_(chat_ids)) + .filter_by(archived=False) + .order_by(Chat.updated_at.desc()) + .all() + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def get_chat_by_id(self, id: str) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat = db.get(Chat, id) + return ChatModel.model_validate(chat) + except: + return None + + def get_chat_by_share_id(self, id: str) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat = db.query(Chat).filter_by(share_id=id).first() + + if chat: + return self.get_chat_by_id(id) + else: + return None + except Exception as e: + return None + + def get_chat_by_id_and_user_id(self, id: str, user_id: str) -> Optional[ChatModel]: + try: + with get_db() as db: + + chat = db.query(Chat).filter_by(id=id, user_id=user_id).first() + return ChatModel.model_validate(chat) + except: + return None + + def get_chats(self, skip: int = 0, limit: int = 50) -> List[ChatModel]: + with get_db() as db: + + all_chats = ( + db.query(Chat) + # .limit(limit).offset(skip) + .order_by(Chat.updated_at.desc()) + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def get_chats_by_user_id(self, user_id: str) -> List[ChatModel]: + with get_db() as db: + + all_chats = ( + db.query(Chat) + .filter_by(user_id=user_id) + .order_by(Chat.updated_at.desc()) + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def get_archived_chats_by_user_id(self, user_id: str) -> List[ChatModel]: + with get_db() as db: + + all_chats = ( + db.query(Chat) + .filter_by(user_id=user_id, archived=True) + .order_by(Chat.updated_at.desc()) + ) + return [ChatModel.model_validate(chat) for chat in all_chats] + + def delete_chat_by_id(self, id: str) -> bool: + try: + with get_db() as db: + + db.query(Chat).filter_by(id=id).delete() + db.commit() + + return True and self.delete_shared_chat_by_chat_id(id) + except: + return False + + def delete_chat_by_id_and_user_id(self, id: str, user_id: str) -> bool: + try: + with get_db() as db: + + db.query(Chat).filter_by(id=id, user_id=user_id).delete() + db.commit() + + return True and self.delete_shared_chat_by_chat_id(id) + except: + return False + + def delete_chats_by_user_id(self, user_id: str) -> bool: + try: + + with get_db() as db: + + self.delete_shared_chats_by_user_id(user_id) + + db.query(Chat).filter_by(user_id=user_id).delete() + db.commit() + + return True + except: + return False + + def delete_shared_chats_by_user_id(self, user_id: str) -> bool: + try: + + with get_db() as db: + + chats_by_user = db.query(Chat).filter_by(user_id=user_id).all() + shared_chat_ids = [f"shared-{chat.id}" for chat in chats_by_user] + + db.query(Chat).filter(Chat.user_id.in_(shared_chat_ids)).delete() + db.commit() + + return True + except: + return False + + +Chats = ChatTable() diff --git a/backend/apps/webui/models/documents.py b/backend/apps/webui/models/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..ac8655da9ce4ee2fd50299aaaf1a9a6bdba86b58 --- /dev/null +++ b/backend/apps/webui/models/documents.py @@ -0,0 +1,167 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Optional +import time +import logging + +from sqlalchemy import String, Column, BigInteger, Text + +from apps.webui.internal.db import Base, get_db + +import json + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Documents DB Schema +#################### + + +class Document(Base): + __tablename__ = "document" + + collection_name = Column(String, primary_key=True) + name = Column(String, unique=True) + title = Column(Text) + filename = Column(Text) + content = Column(Text, nullable=True) + user_id = Column(String) + timestamp = Column(BigInteger) + + +class DocumentModel(BaseModel): + model_config = ConfigDict(from_attributes=True) + + collection_name: str + name: str + title: str + filename: str + content: Optional[str] = None + user_id: str + timestamp: int # timestamp in epoch + + +#################### +# Forms +#################### + + +class DocumentResponse(BaseModel): + collection_name: str + name: str + title: str + filename: str + content: Optional[dict] = None + user_id: str + timestamp: int # timestamp in epoch + + +class DocumentUpdateForm(BaseModel): + name: str + title: str + + +class DocumentForm(DocumentUpdateForm): + collection_name: str + filename: str + content: Optional[str] = None + + +class DocumentsTable: + + def insert_new_doc( + self, user_id: str, form_data: DocumentForm + ) -> Optional[DocumentModel]: + with get_db() as db: + + document = DocumentModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "timestamp": int(time.time()), + } + ) + + try: + result = Document(**document.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return DocumentModel.model_validate(result) + else: + return None + except: + return None + + def get_doc_by_name(self, name: str) -> Optional[DocumentModel]: + try: + with get_db() as db: + + document = db.query(Document).filter_by(name=name).first() + return DocumentModel.model_validate(document) if document else None + except: + return None + + def get_docs(self) -> List[DocumentModel]: + with get_db() as db: + + return [ + DocumentModel.model_validate(doc) for doc in db.query(Document).all() + ] + + def update_doc_by_name( + self, name: str, form_data: DocumentUpdateForm + ) -> Optional[DocumentModel]: + try: + with get_db() as db: + + db.query(Document).filter_by(name=name).update( + { + "title": form_data.title, + "name": form_data.name, + "timestamp": int(time.time()), + } + ) + db.commit() + return self.get_doc_by_name(form_data.name) + except Exception as e: + log.exception(e) + return None + + def update_doc_content_by_name( + self, name: str, updated: dict + ) -> Optional[DocumentModel]: + try: + doc = self.get_doc_by_name(name) + doc_content = json.loads(doc.content if doc.content else "{}") + doc_content = {**doc_content, **updated} + + with get_db() as db: + + db.query(Document).filter_by(name=name).update( + { + "content": json.dumps(doc_content), + "timestamp": int(time.time()), + } + ) + db.commit() + return self.get_doc_by_name(name) + except Exception as e: + log.exception(e) + return None + + def delete_doc_by_name(self, name: str) -> bool: + try: + with get_db() as db: + + db.query(Document).filter_by(name=name).delete() + db.commit() + return True + except: + return False + + +Documents = DocumentsTable() diff --git a/backend/apps/webui/models/files.py b/backend/apps/webui/models/files.py new file mode 100644 index 0000000000000000000000000000000000000000..16272f24ad11f9fda4047f2c2721123cdea3a2aa --- /dev/null +++ b/backend/apps/webui/models/files.py @@ -0,0 +1,126 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Union, Optional +import time +import logging + +from sqlalchemy import Column, String, BigInteger, Text + +from apps.webui.internal.db import JSONField, Base, get_db + +import json + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Files DB Schema +#################### + + +class File(Base): + __tablename__ = "file" + + id = Column(String, primary_key=True) + user_id = Column(String) + filename = Column(Text) + meta = Column(JSONField) + created_at = Column(BigInteger) + + +class FileModel(BaseModel): + id: str + user_id: str + filename: str + meta: dict + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class FileModelResponse(BaseModel): + id: str + user_id: str + filename: str + meta: dict + created_at: int # timestamp in epoch + + +class FileForm(BaseModel): + id: str + filename: str + meta: dict = {} + + +class FilesTable: + + def insert_new_file(self, user_id: str, form_data: FileForm) -> Optional[FileModel]: + with get_db() as db: + + file = FileModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "created_at": int(time.time()), + } + ) + + try: + result = File(**file.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return FileModel.model_validate(result) + else: + return None + except Exception as e: + print(f"Error creating tool: {e}") + return None + + def get_file_by_id(self, id: str) -> Optional[FileModel]: + with get_db() as db: + + try: + file = db.get(File, id) + return FileModel.model_validate(file) + except: + return None + + def get_files(self) -> List[FileModel]: + with get_db() as db: + + return [FileModel.model_validate(file) for file in db.query(File).all()] + + def delete_file_by_id(self, id: str) -> bool: + + with get_db() as db: + + try: + db.query(File).filter_by(id=id).delete() + db.commit() + + return True + except: + return False + + def delete_all_files(self) -> bool: + + with get_db() as db: + + try: + db.query(File).delete() + db.commit() + + return True + except: + return False + + +Files = FilesTable() diff --git a/backend/apps/webui/models/functions.py b/backend/apps/webui/models/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..cb73da69449482a35264e688748422323880583b --- /dev/null +++ b/backend/apps/webui/models/functions.py @@ -0,0 +1,288 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Union, Optional +import time +import logging + +from sqlalchemy import Column, String, Text, BigInteger, Boolean + +from apps.webui.internal.db import JSONField, Base, get_db +from apps.webui.models.users import Users + +import json +import copy + + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Functions DB Schema +#################### + + +class Function(Base): + __tablename__ = "function" + + id = Column(String, primary_key=True) + user_id = Column(String) + name = Column(Text) + type = Column(Text) + content = Column(Text) + meta = Column(JSONField) + valves = Column(JSONField) + is_active = Column(Boolean) + is_global = Column(Boolean) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class FunctionMeta(BaseModel): + description: Optional[str] = None + manifest: Optional[dict] = {} + + +class FunctionModel(BaseModel): + id: str + user_id: str + name: str + type: str + content: str + meta: FunctionMeta + is_active: bool = False + is_global: bool = False + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class FunctionResponse(BaseModel): + id: str + user_id: str + type: str + name: str + meta: FunctionMeta + is_active: bool + is_global: bool + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class FunctionForm(BaseModel): + id: str + name: str + content: str + meta: FunctionMeta + + +class FunctionValves(BaseModel): + valves: Optional[dict] = None + + +class FunctionsTable: + + def insert_new_function( + self, user_id: str, type: str, form_data: FunctionForm + ) -> Optional[FunctionModel]: + + function = FunctionModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "type": type, + "updated_at": int(time.time()), + "created_at": int(time.time()), + } + ) + + try: + with get_db() as db: + result = Function(**function.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return FunctionModel.model_validate(result) + else: + return None + except Exception as e: + print(f"Error creating tool: {e}") + return None + + def get_function_by_id(self, id: str) -> Optional[FunctionModel]: + try: + with get_db() as db: + + function = db.get(Function, id) + return FunctionModel.model_validate(function) + except: + return None + + def get_functions(self, active_only=False) -> List[FunctionModel]: + with get_db() as db: + + if active_only: + return [ + FunctionModel.model_validate(function) + for function in db.query(Function).filter_by(is_active=True).all() + ] + else: + return [ + FunctionModel.model_validate(function) + for function in db.query(Function).all() + ] + + def get_functions_by_type( + self, type: str, active_only=False + ) -> List[FunctionModel]: + with get_db() as db: + + if active_only: + return [ + FunctionModel.model_validate(function) + for function in db.query(Function) + .filter_by(type=type, is_active=True) + .all() + ] + else: + return [ + FunctionModel.model_validate(function) + for function in db.query(Function).filter_by(type=type).all() + ] + + def get_global_filter_functions(self) -> List[FunctionModel]: + with get_db() as db: + + return [ + FunctionModel.model_validate(function) + for function in db.query(Function) + .filter_by(type="filter", is_active=True, is_global=True) + .all() + ] + + def get_global_action_functions(self) -> List[FunctionModel]: + with get_db() as db: + return [ + FunctionModel.model_validate(function) + for function in db.query(Function) + .filter_by(type="action", is_active=True, is_global=True) + .all() + ] + + def get_function_valves_by_id(self, id: str) -> Optional[dict]: + with get_db() as db: + + try: + function = db.get(Function, id) + return function.valves if function.valves else {} + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_function_valves_by_id( + self, id: str, valves: dict + ) -> Optional[FunctionValves]: + with get_db() as db: + + try: + function = db.get(Function, id) + function.valves = valves + function.updated_at = int(time.time()) + db.commit() + db.refresh(function) + return self.get_function_by_id(id) + except: + return None + + def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str + ) -> Optional[dict]: + + try: + user = Users.get_user_by_id(user_id) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "functions" and "valves" settings + if "functions" not in user_settings: + user_settings["functions"] = {} + if "valves" not in user_settings["functions"]: + user_settings["functions"]["valves"] = {} + + return user_settings["functions"]["valves"].get(id, {}) + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict + ) -> Optional[dict]: + + try: + user = Users.get_user_by_id(user_id) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "functions" and "valves" settings + if "functions" not in user_settings: + user_settings["functions"] = {} + if "valves" not in user_settings["functions"]: + user_settings["functions"]["valves"] = {} + + user_settings["functions"]["valves"][id] = valves + + # Update the user settings in the database + Users.update_user_by_id(user_id, {"settings": user_settings}) + + return user_settings["functions"]["valves"][id] + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_function_by_id(self, id: str, updated: dict) -> Optional[FunctionModel]: + with get_db() as db: + + try: + db.query(Function).filter_by(id=id).update( + { + **updated, + "updated_at": int(time.time()), + } + ) + db.commit() + return self.get_function_by_id(id) + except: + return None + + def deactivate_all_functions(self) -> Optional[bool]: + with get_db() as db: + + try: + db.query(Function).update( + { + "is_active": False, + "updated_at": int(time.time()), + } + ) + db.commit() + return True + except: + return None + + def delete_function_by_id(self, id: str) -> bool: + with get_db() as db: + try: + db.query(Function).filter_by(id=id).delete() + db.commit() + + return True + except: + return False + + +Functions = FunctionsTable() diff --git a/backend/apps/webui/models/memories.py b/backend/apps/webui/models/memories.py new file mode 100644 index 0000000000000000000000000000000000000000..02d4b6924986465612fd2d32be0499ffd3ff3f93 --- /dev/null +++ b/backend/apps/webui/models/memories.py @@ -0,0 +1,148 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Union, Optional + +from sqlalchemy import Column, String, BigInteger, Text + +from apps.webui.internal.db import Base, get_db + +import time +import uuid + +#################### +# Memory DB Schema +#################### + + +class Memory(Base): + __tablename__ = "memory" + + id = Column(String, primary_key=True) + user_id = Column(String) + content = Column(Text) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class MemoryModel(BaseModel): + id: str + user_id: str + content: str + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class MemoriesTable: + + def insert_new_memory( + self, + user_id: str, + content: str, + ) -> Optional[MemoryModel]: + + with get_db() as db: + id = str(uuid.uuid4()) + + memory = MemoryModel( + **{ + "id": id, + "user_id": user_id, + "content": content, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + result = Memory(**memory.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return MemoryModel.model_validate(result) + else: + return None + + def update_memory_by_id( + self, + id: str, + content: str, + ) -> Optional[MemoryModel]: + with get_db() as db: + + try: + db.query(Memory).filter_by(id=id).update( + {"content": content, "updated_at": int(time.time())} + ) + db.commit() + return self.get_memory_by_id(id) + except: + return None + + def get_memories(self) -> List[MemoryModel]: + with get_db() as db: + + try: + memories = db.query(Memory).all() + return [MemoryModel.model_validate(memory) for memory in memories] + except: + return None + + def get_memories_by_user_id(self, user_id: str) -> List[MemoryModel]: + with get_db() as db: + + try: + memories = db.query(Memory).filter_by(user_id=user_id).all() + return [MemoryModel.model_validate(memory) for memory in memories] + except: + return None + + def get_memory_by_id(self, id: str) -> Optional[MemoryModel]: + with get_db() as db: + + try: + memory = db.get(Memory, id) + return MemoryModel.model_validate(memory) + except: + return None + + def delete_memory_by_id(self, id: str) -> bool: + with get_db() as db: + + try: + db.query(Memory).filter_by(id=id).delete() + db.commit() + + return True + + except: + return False + + def delete_memories_by_user_id(self, user_id: str) -> bool: + with get_db() as db: + + try: + db.query(Memory).filter_by(user_id=user_id).delete() + db.commit() + + return True + except: + return False + + def delete_memory_by_id_and_user_id(self, id: str, user_id: str) -> bool: + with get_db() as db: + + try: + db.query(Memory).filter_by(id=id, user_id=user_id).delete() + db.commit() + + return True + except: + return False + + +Memories = MemoriesTable() diff --git a/backend/apps/webui/models/models.py b/backend/apps/webui/models/models.py new file mode 100644 index 0000000000000000000000000000000000000000..3b128c7d6751c9213e84942f35127b727eb0cac1 --- /dev/null +++ b/backend/apps/webui/models/models.py @@ -0,0 +1,190 @@ +import json +import logging +from typing import Optional + +from pydantic import BaseModel, ConfigDict +from sqlalchemy import String, Column, BigInteger, Text + +from apps.webui.internal.db import Base, JSONField, get_db + +from typing import List, Union, Optional +from config import SRC_LOG_LEVELS + +import time + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + + +#################### +# Models DB Schema +#################### + + +# ModelParams is a model for the data stored in the params field of the Model table +class ModelParams(BaseModel): + model_config = ConfigDict(extra="allow") + pass + + +# ModelMeta is a model for the data stored in the meta field of the Model table +class ModelMeta(BaseModel): + profile_image_url: Optional[str] = "/static/favicon.png" + + description: Optional[str] = None + """ + User-facing description of the model. + """ + + capabilities: Optional[dict] = None + + model_config = ConfigDict(extra="allow") + + pass + + +class Model(Base): + __tablename__ = "model" + + id = Column(Text, primary_key=True) + """ + The model's id as used in the API. If set to an existing model, it will override the model. + """ + user_id = Column(Text) + + base_model_id = Column(Text, nullable=True) + """ + An optional pointer to the actual model that should be used when proxying requests. + """ + + name = Column(Text) + """ + The human-readable display name of the model. + """ + + params = Column(JSONField) + """ + Holds a JSON encoded blob of parameters, see `ModelParams`. + """ + + meta = Column(JSONField) + """ + Holds a JSON encoded blob of metadata, see `ModelMeta`. + """ + + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class ModelModel(BaseModel): + id: str + user_id: str + base_model_id: Optional[str] = None + + name: str + params: ModelParams + meta: ModelMeta + + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ModelResponse(BaseModel): + id: str + name: str + meta: ModelMeta + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class ModelForm(BaseModel): + id: str + base_model_id: Optional[str] = None + name: str + meta: ModelMeta + params: ModelParams + + +class ModelsTable: + + def insert_new_model( + self, form_data: ModelForm, user_id: str + ) -> Optional[ModelModel]: + model = ModelModel( + **{ + **form_data.model_dump(), + "user_id": user_id, + "created_at": int(time.time()), + "updated_at": int(time.time()), + } + ) + try: + + with get_db() as db: + + result = Model(**model.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + + if result: + return ModelModel.model_validate(result) + else: + return None + except Exception as e: + print(e) + return None + + def get_all_models(self) -> List[ModelModel]: + with get_db() as db: + + return [ModelModel.model_validate(model) for model in db.query(Model).all()] + + def get_model_by_id(self, id: str) -> Optional[ModelModel]: + try: + with get_db() as db: + + model = db.get(Model, id) + return ModelModel.model_validate(model) + except: + return None + + def update_model_by_id(self, id: str, model: ModelForm) -> Optional[ModelModel]: + try: + with get_db() as db: + # update only the fields that are present in the model + result = ( + db.query(Model) + .filter_by(id=id) + .update(model.model_dump(exclude={"id"}, exclude_none=True)) + ) + db.commit() + + model = db.get(Model, id) + db.refresh(model) + return ModelModel.model_validate(model) + except Exception as e: + print(e) + + return None + + def delete_model_by_id(self, id: str) -> bool: + try: + with get_db() as db: + + db.query(Model).filter_by(id=id).delete() + db.commit() + + return True + except: + return False + + +Models = ModelsTable() diff --git a/backend/apps/webui/models/prompts.py b/backend/apps/webui/models/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..b8467b63164f537a6e669c5c078a95eb2d162339 --- /dev/null +++ b/backend/apps/webui/models/prompts.py @@ -0,0 +1,119 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Optional +import time + +from sqlalchemy import String, Column, BigInteger, Text + +from apps.webui.internal.db import Base, get_db + +import json + +#################### +# Prompts DB Schema +#################### + + +class Prompt(Base): + __tablename__ = "prompt" + + command = Column(String, primary_key=True) + user_id = Column(String) + title = Column(Text) + content = Column(Text) + timestamp = Column(BigInteger) + + +class PromptModel(BaseModel): + command: str + user_id: str + title: str + content: str + timestamp: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class PromptForm(BaseModel): + command: str + title: str + content: str + + +class PromptsTable: + + def insert_new_prompt( + self, user_id: str, form_data: PromptForm + ) -> Optional[PromptModel]: + prompt = PromptModel( + **{ + "user_id": user_id, + "command": form_data.command, + "title": form_data.title, + "content": form_data.content, + "timestamp": int(time.time()), + } + ) + + try: + with get_db() as db: + + result = Prompt(**prompt.dict()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return PromptModel.model_validate(result) + else: + return None + except Exception as e: + return None + + def get_prompt_by_command(self, command: str) -> Optional[PromptModel]: + try: + with get_db() as db: + + prompt = db.query(Prompt).filter_by(command=command).first() + return PromptModel.model_validate(prompt) + except: + return None + + def get_prompts(self) -> List[PromptModel]: + with get_db() as db: + + return [ + PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all() + ] + + def update_prompt_by_command( + self, command: str, form_data: PromptForm + ) -> Optional[PromptModel]: + try: + with get_db() as db: + + prompt = db.query(Prompt).filter_by(command=command).first() + prompt.title = form_data.title + prompt.content = form_data.content + prompt.timestamp = int(time.time()) + db.commit() + return PromptModel.model_validate(prompt) + except: + return None + + def delete_prompt_by_command(self, command: str) -> bool: + try: + with get_db() as db: + + db.query(Prompt).filter_by(command=command).delete() + db.commit() + + return True + except: + return False + + +Prompts = PromptsTable() diff --git a/backend/apps/webui/models/tags.py b/backend/apps/webui/models/tags.py new file mode 100644 index 0000000000000000000000000000000000000000..7285b6fe245fc39099040d0a4f568017eee09797 --- /dev/null +++ b/backend/apps/webui/models/tags.py @@ -0,0 +1,272 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Optional + +import json +import uuid +import time +import logging + +from sqlalchemy import String, Column, BigInteger, Text + +from apps.webui.internal.db import Base, get_db + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Tag DB Schema +#################### + + +class Tag(Base): + __tablename__ = "tag" + + id = Column(String, primary_key=True) + name = Column(String) + user_id = Column(String) + data = Column(Text, nullable=True) + + +class ChatIdTag(Base): + __tablename__ = "chatidtag" + + id = Column(String, primary_key=True) + tag_name = Column(String) + chat_id = Column(String) + user_id = Column(String) + timestamp = Column(BigInteger) + + +class TagModel(BaseModel): + id: str + name: str + user_id: str + data: Optional[str] = None + + model_config = ConfigDict(from_attributes=True) + + +class ChatIdTagModel(BaseModel): + id: str + tag_name: str + chat_id: str + user_id: str + timestamp: int + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ChatIdTagForm(BaseModel): + tag_name: str + chat_id: str + + +class TagChatIdsResponse(BaseModel): + chat_ids: List[str] + + +class ChatTagsResponse(BaseModel): + tags: List[str] + + +class TagTable: + + def insert_new_tag(self, name: str, user_id: str) -> Optional[TagModel]: + with get_db() as db: + + id = str(uuid.uuid4()) + tag = TagModel(**{"id": id, "user_id": user_id, "name": name}) + try: + result = Tag(**tag.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return TagModel.model_validate(result) + else: + return None + except Exception as e: + return None + + def get_tag_by_name_and_user_id( + self, name: str, user_id: str + ) -> Optional[TagModel]: + try: + with get_db() as db: + tag = db.query(Tag).filter_by(name=name, user_id=user_id).first() + return TagModel.model_validate(tag) + except Exception as e: + return None + + def add_tag_to_chat( + self, user_id: str, form_data: ChatIdTagForm + ) -> Optional[ChatIdTagModel]: + tag = self.get_tag_by_name_and_user_id(form_data.tag_name, user_id) + if tag == None: + tag = self.insert_new_tag(form_data.tag_name, user_id) + + id = str(uuid.uuid4()) + chatIdTag = ChatIdTagModel( + **{ + "id": id, + "user_id": user_id, + "chat_id": form_data.chat_id, + "tag_name": tag.name, + "timestamp": int(time.time()), + } + ) + try: + with get_db() as db: + result = ChatIdTag(**chatIdTag.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return ChatIdTagModel.model_validate(result) + else: + return None + except: + return None + + def get_tags_by_user_id(self, user_id: str) -> List[TagModel]: + with get_db() as db: + tag_names = [ + chat_id_tag.tag_name + for chat_id_tag in ( + db.query(ChatIdTag) + .filter_by(user_id=user_id) + .order_by(ChatIdTag.timestamp.desc()) + .all() + ) + ] + + return [ + TagModel.model_validate(tag) + for tag in ( + db.query(Tag) + .filter_by(user_id=user_id) + .filter(Tag.name.in_(tag_names)) + .all() + ) + ] + + def get_tags_by_chat_id_and_user_id( + self, chat_id: str, user_id: str + ) -> List[TagModel]: + with get_db() as db: + + tag_names = [ + chat_id_tag.tag_name + for chat_id_tag in ( + db.query(ChatIdTag) + .filter_by(user_id=user_id, chat_id=chat_id) + .order_by(ChatIdTag.timestamp.desc()) + .all() + ) + ] + + return [ + TagModel.model_validate(tag) + for tag in ( + db.query(Tag) + .filter_by(user_id=user_id) + .filter(Tag.name.in_(tag_names)) + .all() + ) + ] + + def get_chat_ids_by_tag_name_and_user_id( + self, tag_name: str, user_id: str + ) -> List[ChatIdTagModel]: + with get_db() as db: + + return [ + ChatIdTagModel.model_validate(chat_id_tag) + for chat_id_tag in ( + db.query(ChatIdTag) + .filter_by(user_id=user_id, tag_name=tag_name) + .order_by(ChatIdTag.timestamp.desc()) + .all() + ) + ] + + def count_chat_ids_by_tag_name_and_user_id( + self, tag_name: str, user_id: str + ) -> int: + with get_db() as db: + + return ( + db.query(ChatIdTag) + .filter_by(tag_name=tag_name, user_id=user_id) + .count() + ) + + def delete_tag_by_tag_name_and_user_id(self, tag_name: str, user_id: str) -> bool: + try: + with get_db() as db: + res = ( + db.query(ChatIdTag) + .filter_by(tag_name=tag_name, user_id=user_id) + .delete() + ) + log.debug(f"res: {res}") + db.commit() + + tag_count = self.count_chat_ids_by_tag_name_and_user_id( + tag_name, user_id + ) + if tag_count == 0: + # Remove tag item from Tag col as well + db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete() + db.commit() + return True + except Exception as e: + log.error(f"delete_tag: {e}") + return False + + def delete_tag_by_tag_name_and_chat_id_and_user_id( + self, tag_name: str, chat_id: str, user_id: str + ) -> bool: + try: + with get_db() as db: + + res = ( + db.query(ChatIdTag) + .filter_by(tag_name=tag_name, chat_id=chat_id, user_id=user_id) + .delete() + ) + log.debug(f"res: {res}") + db.commit() + + tag_count = self.count_chat_ids_by_tag_name_and_user_id( + tag_name, user_id + ) + if tag_count == 0: + # Remove tag item from Tag col as well + db.query(Tag).filter_by(name=tag_name, user_id=user_id).delete() + db.commit() + + return True + except Exception as e: + log.error(f"delete_tag: {e}") + return False + + def delete_tags_by_chat_id_and_user_id(self, chat_id: str, user_id: str) -> bool: + tags = self.get_tags_by_chat_id_and_user_id(chat_id, user_id) + + for tag in tags: + self.delete_tag_by_tag_name_and_chat_id_and_user_id( + tag.tag_name, chat_id, user_id + ) + + return True + + +Tags = TagTable() diff --git a/backend/apps/webui/models/tools.py b/backend/apps/webui/models/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..685ce6fcfbd3b721482a4bf62888402fc73a9137 --- /dev/null +++ b/backend/apps/webui/models/tools.py @@ -0,0 +1,213 @@ +from pydantic import BaseModel, ConfigDict +from typing import List, Optional +import time +import logging +from sqlalchemy import String, Column, BigInteger, Text + +from apps.webui.internal.db import Base, JSONField, get_db +from apps.webui.models.users import Users + +import json +import copy + + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +#################### +# Tools DB Schema +#################### + + +class Tool(Base): + __tablename__ = "tool" + + id = Column(String, primary_key=True) + user_id = Column(String) + name = Column(Text) + content = Column(Text) + specs = Column(JSONField) + meta = Column(JSONField) + valves = Column(JSONField) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + +class ToolMeta(BaseModel): + description: Optional[str] = None + manifest: Optional[dict] = {} + + +class ToolModel(BaseModel): + id: str + user_id: str + name: str + content: str + specs: List[dict] + meta: ToolMeta + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class ToolResponse(BaseModel): + id: str + user_id: str + name: str + meta: ToolMeta + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + +class ToolForm(BaseModel): + id: str + name: str + content: str + meta: ToolMeta + + +class ToolValves(BaseModel): + valves: Optional[dict] = None + + +class ToolsTable: + + def insert_new_tool( + self, user_id: str, form_data: ToolForm, specs: List[dict] + ) -> Optional[ToolModel]: + + with get_db() as db: + + tool = ToolModel( + **{ + **form_data.model_dump(), + "specs": specs, + "user_id": user_id, + "updated_at": int(time.time()), + "created_at": int(time.time()), + } + ) + + try: + result = Tool(**tool.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return ToolModel.model_validate(result) + else: + return None + except Exception as e: + print(f"Error creating tool: {e}") + return None + + def get_tool_by_id(self, id: str) -> Optional[ToolModel]: + try: + with get_db() as db: + + tool = db.get(Tool, id) + return ToolModel.model_validate(tool) + except: + return None + + def get_tools(self) -> List[ToolModel]: + with get_db() as db: + return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()] + + def get_tool_valves_by_id(self, id: str) -> Optional[dict]: + try: + with get_db() as db: + + tool = db.get(Tool, id) + return tool.valves if tool.valves else {} + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_tool_valves_by_id(self, id: str, valves: dict) -> Optional[ToolValves]: + try: + with get_db() as db: + + db.query(Tool).filter_by(id=id).update( + {"valves": valves, "updated_at": int(time.time())} + ) + db.commit() + return self.get_tool_by_id(id) + except: + return None + + def get_user_valves_by_id_and_user_id( + self, id: str, user_id: str + ) -> Optional[dict]: + try: + user = Users.get_user_by_id(user_id) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "tools" and "valves" settings + if "tools" not in user_settings: + user_settings["tools"] = {} + if "valves" not in user_settings["tools"]: + user_settings["tools"]["valves"] = {} + + return user_settings["tools"]["valves"].get(id, {}) + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_user_valves_by_id_and_user_id( + self, id: str, user_id: str, valves: dict + ) -> Optional[dict]: + try: + user = Users.get_user_by_id(user_id) + user_settings = user.settings.model_dump() if user.settings else {} + + # Check if user has "tools" and "valves" settings + if "tools" not in user_settings: + user_settings["tools"] = {} + if "valves" not in user_settings["tools"]: + user_settings["tools"]["valves"] = {} + + user_settings["tools"]["valves"][id] = valves + + # Update the user settings in the database + Users.update_user_by_id(user_id, {"settings": user_settings}) + + return user_settings["tools"]["valves"][id] + except Exception as e: + print(f"An error occurred: {e}") + return None + + def update_tool_by_id(self, id: str, updated: dict) -> Optional[ToolModel]: + try: + with get_db() as db: + db.query(Tool).filter_by(id=id).update( + {**updated, "updated_at": int(time.time())} + ) + db.commit() + + tool = db.query(Tool).get(id) + db.refresh(tool) + return ToolModel.model_validate(tool) + except: + return None + + def delete_tool_by_id(self, id: str) -> bool: + try: + with get_db() as db: + db.query(Tool).filter_by(id=id).delete() + db.commit() + + return True + except: + return False + + +Tools = ToolsTable() diff --git a/backend/apps/webui/models/users.py b/backend/apps/webui/models/users.py new file mode 100644 index 0000000000000000000000000000000000000000..2f30cda0230292c86ab1a6fb89010ae9d419b610 --- /dev/null +++ b/backend/apps/webui/models/users.py @@ -0,0 +1,269 @@ +from pydantic import BaseModel, ConfigDict, parse_obj_as +from typing import List, Union, Optional +import time + +from sqlalchemy import String, Column, BigInteger, Text + +from utils.misc import get_gravatar_url + +from apps.webui.internal.db import Base, JSONField, Session, get_db +from apps.webui.models.chats import Chats + +#################### +# User DB Schema +#################### + + +class User(Base): + __tablename__ = "user" + + id = Column(String, primary_key=True) + name = Column(String) + email = Column(String) + role = Column(String) + profile_image_url = Column(Text) + + last_active_at = Column(BigInteger) + updated_at = Column(BigInteger) + created_at = Column(BigInteger) + + api_key = Column(String, nullable=True, unique=True) + settings = Column(JSONField, nullable=True) + info = Column(JSONField, nullable=True) + + oauth_sub = Column(Text, unique=True) + + +class UserSettings(BaseModel): + ui: Optional[dict] = {} + model_config = ConfigDict(extra="allow") + pass + + +class UserModel(BaseModel): + id: str + name: str + email: str + role: str = "pending" + profile_image_url: str + + last_active_at: int # timestamp in epoch + updated_at: int # timestamp in epoch + created_at: int # timestamp in epoch + + api_key: Optional[str] = None + settings: Optional[UserSettings] = None + info: Optional[dict] = None + + oauth_sub: Optional[str] = None + + model_config = ConfigDict(from_attributes=True) + + +#################### +# Forms +#################### + + +class UserRoleUpdateForm(BaseModel): + id: str + role: str + + +class UserUpdateForm(BaseModel): + name: str + email: str + profile_image_url: str + password: Optional[str] = None + + +class UsersTable: + + def insert_new_user( + self, + id: str, + name: str, + email: str, + profile_image_url: str = "/user.png", + role: str = "pending", + oauth_sub: Optional[str] = None, + ) -> Optional[UserModel]: + with get_db() as db: + user = UserModel( + **{ + "id": id, + "name": name, + "email": email, + "role": role, + "profile_image_url": profile_image_url, + "last_active_at": int(time.time()), + "created_at": int(time.time()), + "updated_at": int(time.time()), + "oauth_sub": oauth_sub, + } + ) + result = User(**user.model_dump()) + db.add(result) + db.commit() + db.refresh(result) + if result: + return user + else: + return None + + def get_user_by_id(self, id: str) -> Optional[UserModel]: + try: + with get_db() as db: + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except Exception as e: + return None + + def get_user_by_api_key(self, api_key: str) -> Optional[UserModel]: + try: + with get_db() as db: + + user = db.query(User).filter_by(api_key=api_key).first() + return UserModel.model_validate(user) + except: + return None + + def get_user_by_email(self, email: str) -> Optional[UserModel]: + try: + with get_db() as db: + + user = db.query(User).filter_by(email=email).first() + return UserModel.model_validate(user) + except: + return None + + def get_user_by_oauth_sub(self, sub: str) -> Optional[UserModel]: + try: + with get_db() as db: + + user = db.query(User).filter_by(oauth_sub=sub).first() + return UserModel.model_validate(user) + except: + return None + + def get_users(self, skip: int = 0, limit: int = 50) -> List[UserModel]: + with get_db() as db: + users = ( + db.query(User) + # .offset(skip).limit(limit) + .all() + ) + return [UserModel.model_validate(user) for user in users] + + def get_num_users(self) -> Optional[int]: + with get_db() as db: + return db.query(User).count() + + def get_first_user(self) -> UserModel: + try: + with get_db() as db: + user = db.query(User).order_by(User.created_at).first() + return UserModel.model_validate(user) + except: + return None + + def update_user_role_by_id(self, id: str, role: str) -> Optional[UserModel]: + try: + with get_db() as db: + db.query(User).filter_by(id=id).update({"role": role}) + db.commit() + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except: + return None + + def update_user_profile_image_url_by_id( + self, id: str, profile_image_url: str + ) -> Optional[UserModel]: + try: + with get_db() as db: + db.query(User).filter_by(id=id).update( + {"profile_image_url": profile_image_url} + ) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except: + return None + + def update_user_last_active_by_id(self, id: str) -> Optional[UserModel]: + try: + with get_db() as db: + + db.query(User).filter_by(id=id).update( + {"last_active_at": int(time.time())} + ) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except: + return None + + def update_user_oauth_sub_by_id( + self, id: str, oauth_sub: str + ) -> Optional[UserModel]: + try: + with get_db() as db: + db.query(User).filter_by(id=id).update({"oauth_sub": oauth_sub}) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + except: + return None + + def update_user_by_id(self, id: str, updated: dict) -> Optional[UserModel]: + try: + with get_db() as db: + db.query(User).filter_by(id=id).update(updated) + db.commit() + + user = db.query(User).filter_by(id=id).first() + return UserModel.model_validate(user) + # return UserModel(**user.dict()) + except Exception as e: + return None + + def delete_user_by_id(self, id: str) -> bool: + try: + # Delete User Chats + result = Chats.delete_chats_by_user_id(id) + + if result: + with get_db() as db: + # Delete User + db.query(User).filter_by(id=id).delete() + db.commit() + + return True + else: + return False + except: + return False + + def update_user_api_key_by_id(self, id: str, api_key: str) -> str: + try: + with get_db() as db: + result = db.query(User).filter_by(id=id).update({"api_key": api_key}) + db.commit() + return True if result == 1 else False + except: + return False + + def get_user_api_key_by_id(self, id: str) -> Optional[str]: + try: + with get_db() as db: + user = db.query(User).filter_by(id=id).first() + return user.api_key + except Exception as e: + return None + + +Users = UsersTable() diff --git a/backend/apps/webui/routers/auths.py b/backend/apps/webui/routers/auths.py new file mode 100644 index 0000000000000000000000000000000000000000..e2d6a5036ffb4cf4528203e3a2df37343c7c44e9 --- /dev/null +++ b/backend/apps/webui/routers/auths.py @@ -0,0 +1,429 @@ +import logging + +from fastapi import Request, UploadFile, File +from fastapi import Depends, HTTPException, status +from fastapi.responses import Response + +from fastapi import APIRouter +from pydantic import BaseModel +import re +import uuid +import csv + +from apps.webui.models.auths import ( + SigninForm, + SignupForm, + AddUserForm, + UpdateProfileForm, + UpdatePasswordForm, + UserResponse, + SigninResponse, + Auths, + ApiKey, +) +from apps.webui.models.users import Users + +from utils.utils import ( + get_password_hash, + get_current_user, + get_admin_user, + create_token, + create_api_key, +) +from utils.misc import parse_duration, validate_email_format +from utils.webhook import post_webhook +from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES +from config import ( + WEBUI_AUTH, + WEBUI_AUTH_TRUSTED_EMAIL_HEADER, + WEBUI_AUTH_TRUSTED_NAME_HEADER, +) + +router = APIRouter() + +############################ +# GetSessionUser +############################ + + +@router.get("/", response_model=UserResponse) +async def get_session_user( + request: Request, response: Response, user=Depends(get_current_user) +): + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN), + ) + + # Set the cookie token + response.set_cookie( + key="token", + value=token, + httponly=True, # Ensures the cookie is not accessible via JavaScript + ) + + return { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + "profile_image_url": user.profile_image_url, + } + + +############################ +# Update Profile +############################ + + +@router.post("/update/profile", response_model=UserResponse) +async def update_profile( + form_data: UpdateProfileForm, session_user=Depends(get_current_user) +): + if session_user: + user = Users.update_user_by_id( + session_user.id, + {"profile_image_url": form_data.profile_image_url, "name": form_data.name}, + ) + if user: + return user + else: + raise HTTPException(400, detail=ERROR_MESSAGES.DEFAULT()) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# Update Password +############################ + + +@router.post("/update/password", response_model=bool) +async def update_password( + form_data: UpdatePasswordForm, session_user=Depends(get_current_user) +): + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: + raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + if session_user: + user = Auths.authenticate_user(session_user.email, form_data.password) + + if user: + hashed = get_password_hash(form_data.new_password) + return Auths.update_user_password_by_id(user.id, hashed) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_PASSWORD) + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# SignIn +############################ + + +@router.post("/signin", response_model=SigninResponse) +async def signin(request: Request, response: Response, form_data: SigninForm): + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER: + if WEBUI_AUTH_TRUSTED_EMAIL_HEADER not in request.headers: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_TRUSTED_HEADER) + + trusted_email = request.headers[WEBUI_AUTH_TRUSTED_EMAIL_HEADER].lower() + trusted_name = trusted_email + if WEBUI_AUTH_TRUSTED_NAME_HEADER: + trusted_name = request.headers.get( + WEBUI_AUTH_TRUSTED_NAME_HEADER, trusted_email + ) + if not Users.get_user_by_email(trusted_email.lower()): + await signup( + request, + response, + SignupForm( + email=trusted_email, password=str(uuid.uuid4()), name=trusted_name + ), + ) + user = Auths.authenticate_user_by_trusted_header(trusted_email) + elif WEBUI_AUTH == False: + admin_email = "admin@localhost" + admin_password = "admin" + + if Users.get_user_by_email(admin_email.lower()): + user = Auths.authenticate_user(admin_email.lower(), admin_password) + else: + if Users.get_num_users() != 0: + raise HTTPException(400, detail=ERROR_MESSAGES.EXISTING_USERS) + + await signup( + request, + response, + SignupForm(email=admin_email, password=admin_password, name="User"), + ) + + user = Auths.authenticate_user(admin_email.lower(), admin_password) + else: + user = Auths.authenticate_user(form_data.email.lower(), form_data.password) + + if user: + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN), + ) + + # Set the cookie token + response.set_cookie( + key="token", + value=token, + httponly=True, # Ensures the cookie is not accessible via JavaScript + ) + + return { + "token": token, + "token_type": "Bearer", + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + "profile_image_url": user.profile_image_url, + } + else: + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + +############################ +# SignUp +############################ + + +@router.post("/signup", response_model=SigninResponse) +async def signup(request: Request, response: Response, form_data: SignupForm): + if not request.app.state.config.ENABLE_SIGNUP and WEBUI_AUTH: + raise HTTPException( + status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) + + if not validate_email_format(form_data.email.lower()): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT + ) + + if Users.get_user_by_email(form_data.email.lower()): + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + try: + role = ( + "admin" + if Users.get_num_users() == 0 + else request.app.state.config.DEFAULT_USER_ROLE + ) + hashed = get_password_hash(form_data.password) + user = Auths.insert_new_auth( + form_data.email.lower(), + hashed, + form_data.name, + form_data.profile_image_url, + role, + ) + + if user: + token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(request.app.state.config.JWT_EXPIRES_IN), + ) + # response.set_cookie(key='token', value=token, httponly=True) + + # Set the cookie token + response.set_cookie( + key="token", + value=token, + httponly=True, # Ensures the cookie is not accessible via JavaScript + ) + + if request.app.state.config.WEBHOOK_URL: + post_webhook( + request.app.state.config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + "action": "signup", + "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + "user": user.model_dump_json(exclude_none=True), + }, + ) + + return { + "token": token, + "token_type": "Bearer", + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + "profile_image_url": user.profile_image_url, + } + else: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + except Exception as err: + raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + + +############################ +# AddUser +############################ + + +@router.post("/add", response_model=SigninResponse) +async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)): + + if not validate_email_format(form_data.email.lower()): + raise HTTPException( + status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT + ) + + if Users.get_user_by_email(form_data.email.lower()): + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + try: + + print(form_data) + hashed = get_password_hash(form_data.password) + user = Auths.insert_new_auth( + form_data.email.lower(), + hashed, + form_data.name, + form_data.profile_image_url, + form_data.role, + ) + + if user: + token = create_token(data={"id": user.id}) + return { + "token": token, + "token_type": "Bearer", + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + "profile_image_url": user.profile_image_url, + } + else: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR) + except Exception as err: + raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err)) + + +############################ +# GetAdminDetails +############################ + + +@router.get("/admin/details") +async def get_admin_details(request: Request, user=Depends(get_current_user)): + if request.app.state.config.SHOW_ADMIN_DETAILS: + admin_email = request.app.state.config.ADMIN_EMAIL + admin_name = None + + print(admin_email, admin_name) + + if admin_email: + admin = Users.get_user_by_email(admin_email) + if admin: + admin_name = admin.name + else: + admin = Users.get_first_user() + if admin: + admin_email = admin.email + admin_name = admin.name + + return { + "name": admin_name, + "email": admin_email, + } + else: + raise HTTPException(400, detail=ERROR_MESSAGES.ACTION_PROHIBITED) + + +############################ +# ToggleSignUp +############################ + + +@router.get("/admin/config") +async def get_admin_config(request: Request, user=Depends(get_admin_user)): + return { + "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, + "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP, + "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, + "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, + "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, + } + + +class AdminConfig(BaseModel): + SHOW_ADMIN_DETAILS: bool + ENABLE_SIGNUP: bool + DEFAULT_USER_ROLE: str + JWT_EXPIRES_IN: str + ENABLE_COMMUNITY_SHARING: bool + + +@router.post("/admin/config") +async def update_admin_config( + request: Request, form_data: AdminConfig, user=Depends(get_admin_user) +): + request.app.state.config.SHOW_ADMIN_DETAILS = form_data.SHOW_ADMIN_DETAILS + request.app.state.config.ENABLE_SIGNUP = form_data.ENABLE_SIGNUP + + if form_data.DEFAULT_USER_ROLE in ["pending", "user", "admin"]: + request.app.state.config.DEFAULT_USER_ROLE = form_data.DEFAULT_USER_ROLE + + pattern = r"^(-1|0|(-?\d+(\.\d+)?)(ms|s|m|h|d|w))$" + + # Check if the input string matches the pattern + if re.match(pattern, form_data.JWT_EXPIRES_IN): + request.app.state.config.JWT_EXPIRES_IN = form_data.JWT_EXPIRES_IN + + request.app.state.config.ENABLE_COMMUNITY_SHARING = ( + form_data.ENABLE_COMMUNITY_SHARING + ) + + return { + "SHOW_ADMIN_DETAILS": request.app.state.config.SHOW_ADMIN_DETAILS, + "ENABLE_SIGNUP": request.app.state.config.ENABLE_SIGNUP, + "DEFAULT_USER_ROLE": request.app.state.config.DEFAULT_USER_ROLE, + "JWT_EXPIRES_IN": request.app.state.config.JWT_EXPIRES_IN, + "ENABLE_COMMUNITY_SHARING": request.app.state.config.ENABLE_COMMUNITY_SHARING, + } + + +############################ +# API Key +############################ + + +# create api key +@router.post("/api_key", response_model=ApiKey) +async def create_api_key_(user=Depends(get_current_user)): + api_key = create_api_key() + success = Users.update_user_api_key_by_id(user.id, api_key) + if success: + return { + "api_key": api_key, + } + else: + raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_API_KEY_ERROR) + + +# delete api key +@router.delete("/api_key", response_model=bool) +async def delete_api_key(user=Depends(get_current_user)): + success = Users.update_user_api_key_by_id(user.id, None) + return success + + +# get api key +@router.get("/api_key", response_model=ApiKey) +async def get_api_key(user=Depends(get_current_user)): + api_key = Users.get_user_api_key_by_id(user.id) + if api_key: + return { + "api_key": api_key, + } + else: + raise HTTPException(404, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND) diff --git a/backend/apps/webui/routers/chats.py b/backend/apps/webui/routers/chats.py new file mode 100644 index 0000000000000000000000000000000000000000..80308a451b95a79bc146dd2aa1f3a32181113e21 --- /dev/null +++ b/backend/apps/webui/routers/chats.py @@ -0,0 +1,483 @@ +from fastapi import Depends, Request, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional +from utils.utils import get_verified_user, get_admin_user +from fastapi import APIRouter +from pydantic import BaseModel +import json +import logging + +from apps.webui.models.users import Users +from apps.webui.models.chats import ( + ChatModel, + ChatResponse, + ChatTitleForm, + ChatForm, + ChatTitleIdResponse, + Chats, +) + + +from apps.webui.models.tags import ( + TagModel, + ChatIdTagModel, + ChatIdTagForm, + ChatTagsResponse, + Tags, +) + +from constants import ERROR_MESSAGES + +from config import SRC_LOG_LEVELS, ENABLE_ADMIN_EXPORT + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + +############################ +# GetChatList +############################ + + +@router.get("/", response_model=List[ChatTitleIdResponse]) +@router.get("/list", response_model=List[ChatTitleIdResponse]) +async def get_session_user_chat_list( + user=Depends(get_verified_user), skip: int = 0, limit: int = 50 +): + return Chats.get_chat_title_id_list_by_user_id(user.id, skip=skip, limit=limit) + + +############################ +# DeleteAllChats +############################ + + +@router.delete("/", response_model=bool) +async def delete_all_user_chats(request: Request, user=Depends(get_verified_user)): + + if ( + user.role == "user" + and not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"] + ): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = Chats.delete_chats_by_user_id(user.id) + return result + + +############################ +# GetUserChatList +############################ + + +@router.get("/list/user/{user_id}", response_model=List[ChatTitleIdResponse]) +async def get_user_chat_list_by_user_id( + user_id: str, + user=Depends(get_admin_user), + skip: int = 0, + limit: int = 50, +): + return Chats.get_chat_list_by_user_id( + user_id, include_archived=True, skip=skip, limit=limit + ) + + +############################ +# CreateNewChat +############################ + + +@router.post("/new", response_model=Optional[ChatResponse]) +async def create_new_chat(form_data: ChatForm, user=Depends(get_verified_user)): + try: + chat = Chats.insert_new_chat(user.id, form_data) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# GetChats +############################ + + +@router.get("/all", response_model=List[ChatResponse]) +async def get_user_chats(user=Depends(get_verified_user)): + return [ + ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + for chat in Chats.get_chats_by_user_id(user.id) + ] + + +############################ +# GetArchivedChats +############################ + + +@router.get("/all/archived", response_model=List[ChatResponse]) +async def get_user_archived_chats(user=Depends(get_verified_user)): + return [ + ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + for chat in Chats.get_archived_chats_by_user_id(user.id) + ] + + +############################ +# GetAllChatsInDB +############################ + + +@router.get("/all/db", response_model=List[ChatResponse]) +async def get_all_user_chats_in_db(user=Depends(get_admin_user)): + if not ENABLE_ADMIN_EXPORT: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return [ + ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + for chat in Chats.get_chats() + ] + + +############################ +# GetArchivedChats +############################ + + +@router.get("/archived", response_model=List[ChatTitleIdResponse]) +async def get_archived_session_user_chat_list( + user=Depends(get_verified_user), skip: int = 0, limit: int = 50 +): + return Chats.get_archived_chat_list_by_user_id(user.id, skip, limit) + + +############################ +# ArchiveAllChats +############################ + + +@router.post("/archive/all", response_model=bool) +async def archive_all_chats(user=Depends(get_verified_user)): + return Chats.archive_all_chats_by_user_id(user.id) + + +############################ +# GetSharedChatById +############################ + + +@router.get("/share/{share_id}", response_model=Optional[ChatResponse]) +async def get_shared_chat_by_id(share_id: str, user=Depends(get_verified_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: + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# GetChatsByTags +############################ + + +class TagNameForm(BaseModel): + name: str + skip: Optional[int] = 0 + limit: Optional[int] = 50 + + +@router.post("/tags", response_model=List[ChatTitleIdResponse]) +async def get_user_chat_list_by_tag_name( + form_data: TagNameForm, user=Depends(get_verified_user) +): + + chat_ids = [ + chat_id_tag.chat_id + for chat_id_tag in Tags.get_chat_ids_by_tag_name_and_user_id( + form_data.name, user.id + ) + ] + + chats = Chats.get_chat_list_by_chat_ids(chat_ids, form_data.skip, form_data.limit) + + if len(chats) == 0: + Tags.delete_tag_by_tag_name_and_user_id(form_data.name, user.id) + + return chats + + +############################ +# GetAllTags +############################ + + +@router.get("/tags/all", response_model=List[TagModel]) +async def get_all_tags(user=Depends(get_verified_user)): + try: + tags = Tags.get_tags_by_user_id(user.id) + return tags + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# GetChatById +############################ + + +@router.get("/{id}", response_model=Optional[ChatResponse]) +async def get_chat_by_id(id: str, user=Depends(get_verified_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + + if chat: + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# UpdateChatById +############################ + + +@router.post("/{id}", response_model=Optional[ChatResponse]) +async def update_chat_by_id( + id: str, form_data: ChatForm, user=Depends(get_verified_user) +): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + updated_chat = {**json.loads(chat.chat), **form_data.chat} + + chat = Chats.update_chat_by_id(id, updated_chat) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# DeleteChatById +############################ + + +@router.delete("/{id}", response_model=bool) +async def delete_chat_by_id(request: Request, id: str, user=Depends(get_verified_user)): + + if user.role == "admin": + result = Chats.delete_chat_by_id(id) + return result + else: + if not request.app.state.config.USER_PERMISSIONS["chat"]["deletion"]: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + result = Chats.delete_chat_by_id_and_user_id(id, user.id) + return result + + +############################ +# CloneChat +############################ + + +@router.get("/{id}/clone", response_model=Optional[ChatResponse]) +async def clone_chat_by_id(id: str, user=Depends(get_verified_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + + chat_body = json.loads(chat.chat) + updated_chat = { + **chat_body, + "originalChatId": chat.id, + "branchPointMessageId": chat_body["history"]["currentId"], + "title": f"Clone of {chat.title}", + } + + chat = Chats.insert_new_chat(user.id, ChatForm(**{"chat": updated_chat})) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# ArchiveChat +############################ + + +@router.get("/{id}/archive", response_model=Optional[ChatResponse]) +async def archive_chat_by_id(id: str, user=Depends(get_verified_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + chat = Chats.toggle_chat_archive_by_id(id) + return ChatResponse(**{**chat.model_dump(), "chat": json.loads(chat.chat)}) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# ShareChatById +############################ + + +@router.post("/{id}/share", response_model=Optional[ChatResponse]) +async def share_chat_by_id(id: str, user=Depends(get_verified_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + if chat.share_id: + shared_chat = Chats.update_shared_chat_by_chat_id(chat.id) + return ChatResponse( + **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} + ) + + shared_chat = Chats.insert_shared_chat_by_chat_id(chat.id) + if not shared_chat: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + return ChatResponse( + **{**shared_chat.model_dump(), "chat": json.loads(shared_chat.chat)} + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# DeletedSharedChatById +############################ + + +@router.delete("/{id}/share", response_model=Optional[bool]) +async def delete_shared_chat_by_id(id: str, user=Depends(get_verified_user)): + chat = Chats.get_chat_by_id_and_user_id(id, user.id) + if chat: + if not chat.share_id: + return False + + result = Chats.delete_shared_chat_by_chat_id(id) + update_result = Chats.update_chat_share_id_by_id(id, None) + + return result and update_result != None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# GetChatTagsById +############################ + + +@router.get("/{id}/tags", response_model=List[TagModel]) +async def get_chat_tags_by_id(id: str, user=Depends(get_verified_user)): + tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id) + + if tags != None: + return tags + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# AddChatTagById +############################ + + +@router.post("/{id}/tags", response_model=Optional[ChatIdTagModel]) +async def add_chat_tag_by_id( + id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user) +): + tags = Tags.get_tags_by_chat_id_and_user_id(id, user.id) + + if form_data.tag_name not in tags: + tag = Tags.add_tag_to_chat(user.id, form_data) + + if tag: + return tag + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.DEFAULT() + ) + + +############################ +# DeleteChatTagById +############################ + + +@router.delete("/{id}/tags", response_model=Optional[bool]) +async def delete_chat_tag_by_id( + id: str, form_data: ChatIdTagForm, user=Depends(get_verified_user) +): + result = Tags.delete_tag_by_tag_name_and_chat_id_and_user_id( + form_data.tag_name, id, user.id + ) + + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) + + +############################ +# DeleteAllChatTagsById +############################ + + +@router.delete("/{id}/tags/all", response_model=Optional[bool]) +async def delete_all_chat_tags_by_id(id: str, user=Depends(get_verified_user)): + result = Tags.delete_tags_by_chat_id_and_user_id(id, user.id) + + if result: + return result + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND + ) diff --git a/backend/apps/webui/routers/configs.py b/backend/apps/webui/routers/configs.py new file mode 100644 index 0000000000000000000000000000000000000000..39e435013541d40c1f058be8b783595d08515bf4 --- /dev/null +++ b/backend/apps/webui/routers/configs.py @@ -0,0 +1,89 @@ +from fastapi import Response, Request +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union + +from fastapi import APIRouter +from pydantic import BaseModel +import time +import uuid + +from config import BannerModel + +from apps.webui.models.users import Users + +from utils.utils import ( + get_password_hash, + get_verified_user, + get_admin_user, + create_token, +) +from utils.misc import get_gravatar_url, validate_email_format +from constants import ERROR_MESSAGES + +router = APIRouter() + + +class SetDefaultModelsForm(BaseModel): + models: str + + +class PromptSuggestion(BaseModel): + title: List[str] + content: str + + +class SetDefaultSuggestionsForm(BaseModel): + suggestions: List[PromptSuggestion] + + +############################ +# SetDefaultModels +############################ + + +@router.post("/default/models", response_model=str) +async def set_global_default_models( + request: Request, form_data: SetDefaultModelsForm, user=Depends(get_admin_user) +): + request.app.state.config.DEFAULT_MODELS = form_data.models + return request.app.state.config.DEFAULT_MODELS + + +@router.post("/default/suggestions", response_model=List[PromptSuggestion]) +async def set_global_default_suggestions( + request: Request, + form_data: SetDefaultSuggestionsForm, + user=Depends(get_admin_user), +): + data = form_data.model_dump() + request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS = data["suggestions"] + return request.app.state.config.DEFAULT_PROMPT_SUGGESTIONS + + +############################ +# SetBanners +############################ + + +class SetBannersForm(BaseModel): + banners: List[BannerModel] + + +@router.post("/banners", response_model=List[BannerModel]) +async def set_banners( + request: Request, + form_data: SetBannersForm, + user=Depends(get_admin_user), +): + data = form_data.model_dump() + request.app.state.config.BANNERS = data["banners"] + return request.app.state.config.BANNERS + + +@router.get("/banners", response_model=List[BannerModel]) +async def get_banners( + request: Request, + user=Depends(get_verified_user), +): + return request.app.state.config.BANNERS diff --git a/backend/apps/webui/routers/documents.py b/backend/apps/webui/routers/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..2299b2fee3d5fd8e9a65c15ab564e05f2c256039 --- /dev/null +++ b/backend/apps/webui/routers/documents.py @@ -0,0 +1,160 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.webui.models.documents import ( + Documents, + DocumentForm, + DocumentUpdateForm, + DocumentModel, + DocumentResponse, +) + +from utils.utils import get_verified_user, get_admin_user +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetDocuments +############################ + + +@router.get("/", response_model=List[DocumentResponse]) +async def get_documents(user=Depends(get_verified_user)): + docs = [ + DocumentResponse( + **{ + **doc.model_dump(), + "content": json.loads(doc.content if doc.content else "{}"), + } + ) + for doc in Documents.get_docs() + ] + return docs + + +############################ +# CreateNewDoc +############################ + + +@router.post("/create", response_model=Optional[DocumentResponse]) +async def create_new_doc(form_data: DocumentForm, user=Depends(get_admin_user)): + doc = Documents.get_doc_by_name(form_data.name) + if doc == None: + doc = Documents.insert_new_doc(user.id, form_data) + + if doc: + return DocumentResponse( + **{ + **doc.model_dump(), + "content": json.loads(doc.content if doc.content else "{}"), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.FILE_EXISTS, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NAME_TAG_TAKEN, + ) + + +############################ +# GetDocByName +############################ + + +@router.get("/doc", response_model=Optional[DocumentResponse]) +async def get_doc_by_name(name: str, user=Depends(get_verified_user)): + doc = Documents.get_doc_by_name(name) + + if doc: + return DocumentResponse( + **{ + **doc.model_dump(), + "content": json.loads(doc.content if doc.content else "{}"), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# TagDocByName +############################ + + +class TagItem(BaseModel): + name: str + + +class TagDocumentForm(BaseModel): + name: str + tags: List[dict] + + +@router.post("/doc/tags", response_model=Optional[DocumentResponse]) +async def tag_doc_by_name(form_data: TagDocumentForm, user=Depends(get_verified_user)): + doc = Documents.update_doc_content_by_name(form_data.name, {"tags": form_data.tags}) + + if doc: + return DocumentResponse( + **{ + **doc.model_dump(), + "content": json.loads(doc.content if doc.content else "{}"), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateDocByName +############################ + + +@router.post("/doc/update", response_model=Optional[DocumentResponse]) +async def update_doc_by_name( + name: str, + form_data: DocumentUpdateForm, + user=Depends(get_admin_user), +): + doc = Documents.update_doc_by_name(name, form_data) + if doc: + return DocumentResponse( + **{ + **doc.model_dump(), + "content": json.loads(doc.content if doc.content else "{}"), + } + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.NAME_TAG_TAKEN, + ) + + +############################ +# DeleteDocByName +############################ + + +@router.delete("/doc/delete", response_model=bool) +async def delete_doc_by_name(name: str, user=Depends(get_admin_user)): + result = Documents.delete_doc_by_name(name) + return result diff --git a/backend/apps/webui/routers/files.py b/backend/apps/webui/routers/files.py new file mode 100644 index 0000000000000000000000000000000000000000..99fb923a12ccbb034c80979dc28f8fceec323a51 --- /dev/null +++ b/backend/apps/webui/routers/files.py @@ -0,0 +1,241 @@ +from fastapi import ( + Depends, + FastAPI, + HTTPException, + status, + Request, + UploadFile, + File, + Form, +) + + +from datetime import datetime, timedelta +from typing import List, Union, Optional +from pathlib import Path + +from fastapi import APIRouter +from fastapi.responses import StreamingResponse, JSONResponse, FileResponse + +from pydantic import BaseModel +import json + +from apps.webui.models.files import ( + Files, + FileForm, + FileModel, + FileModelResponse, +) +from utils.utils import get_verified_user, get_admin_user +from constants import ERROR_MESSAGES + +from importlib import util +import os +import uuid +import os, shutil, logging, re + + +from config import SRC_LOG_LEVELS, UPLOAD_DIR + + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + + +router = APIRouter() + +############################ +# Upload File +############################ + + +@router.post("/") +def upload_file(file: UploadFile = File(...), user=Depends(get_verified_user)): + log.info(f"file.content_type: {file.content_type}") + try: + unsanitized_filename = file.filename + filename = os.path.basename(unsanitized_filename) + + # replace filename with uuid + id = str(uuid.uuid4()) + name = filename + filename = f"{id}_{filename}" + file_path = f"{UPLOAD_DIR}/{filename}" + + contents = file.file.read() + with open(file_path, "wb") as f: + f.write(contents) + f.close() + + file = Files.insert_new_file( + user.id, + FileForm( + **{ + "id": id, + "filename": filename, + "meta": { + "name": name, + "content_type": file.content_type, + "size": len(contents), + "path": file_path, + }, + } + ), + ) + + if file: + return file + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error uploading file"), + ) + + except Exception as e: + log.exception(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# List Files +############################ + + +@router.get("/", response_model=List[FileModel]) +async def list_files(user=Depends(get_verified_user)): + files = Files.get_files() + return files + + +############################ +# Delete All Files +############################ + + +@router.delete("/all") +async def delete_all_files(user=Depends(get_admin_user)): + result = Files.delete_all_files() + + if result: + folder = f"{UPLOAD_DIR}" + try: + # Check if the directory exists + if os.path.exists(folder): + # Iterate over all the files and directories in the specified directory + for filename in os.listdir(folder): + file_path = os.path.join(folder, filename) + try: + if os.path.isfile(file_path) or os.path.islink(file_path): + os.unlink(file_path) # Remove the file or link + elif os.path.isdir(file_path): + shutil.rmtree(file_path) # Remove the directory + except Exception as e: + print(f"Failed to delete {file_path}. Reason: {e}") + else: + print(f"The directory {folder} does not exist") + except Exception as e: + print(f"Failed to process the directory {folder}. Reason: {e}") + + return {"message": "All files deleted successfully"} + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error deleting files"), + ) + + +############################ +# Get File By Id +############################ + + +@router.get("/{id}", response_model=Optional[FileModel]) +async def get_file_by_id(id: str, user=Depends(get_verified_user)): + file = Files.get_file_by_id(id) + + if file: + return file + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Get File Content By Id +############################ + + +@router.get("/{id}/content", response_model=Optional[FileModel]) +async def get_file_content_by_id(id: str, user=Depends(get_verified_user)): + file = Files.get_file_by_id(id) + + if file: + file_path = Path(file.meta["path"]) + + # Check if the file already exists in the cache + if file_path.is_file(): + print(f"file_path: {file_path}") + return FileResponse(file_path) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get("/{id}/content/{file_name}", response_model=Optional[FileModel]) +async def get_file_content_by_id(id: str, user=Depends(get_verified_user)): + file = Files.get_file_by_id(id) + + if file: + file_path = Path(file.meta["path"]) + + # Check if the file already exists in the cache + if file_path.is_file(): + print(f"file_path: {file_path}") + return FileResponse(file_path) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# Delete File By Id +############################ + + +@router.delete("/{id}") +async def delete_file_by_id(id: str, user=Depends(get_verified_user)): + file = Files.get_file_by_id(id) + + if file: + result = Files.delete_file_by_id(id) + if result: + return {"message": "File deleted successfully"} + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error deleting file"), + ) + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/apps/webui/routers/functions.py b/backend/apps/webui/routers/functions.py new file mode 100644 index 0000000000000000000000000000000000000000..eb5216b202b0858b87cfcda16fc1ed9e4c3cd96c --- /dev/null +++ b/backend/apps/webui/routers/functions.py @@ -0,0 +1,426 @@ +from fastapi import Depends, FastAPI, HTTPException, status, Request +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.webui.models.functions import ( + Functions, + FunctionForm, + FunctionModel, + FunctionResponse, +) +from apps.webui.utils import load_function_module_by_id +from utils.utils import get_verified_user, get_admin_user +from constants import ERROR_MESSAGES + +from importlib import util +import os +from pathlib import Path + +from config import DATA_DIR, CACHE_DIR, FUNCTIONS_DIR + + +router = APIRouter() + +############################ +# GetFunctions +############################ + + +@router.get("/", response_model=List[FunctionResponse]) +async def get_functions(user=Depends(get_verified_user)): + return Functions.get_functions() + + +############################ +# ExportFunctions +############################ + + +@router.get("/export", response_model=List[FunctionModel]) +async def get_functions(user=Depends(get_admin_user)): + return Functions.get_functions() + + +############################ +# CreateNewFunction +############################ + + +@router.post("/create", response_model=Optional[FunctionResponse]) +async def create_new_function( + request: Request, form_data: FunctionForm, user=Depends(get_admin_user) +): + if not form_data.id.isidentifier(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only alphanumeric characters and underscores are allowed in the id", + ) + + form_data.id = form_data.id.lower() + + function = Functions.get_function_by_id(form_data.id) + if function == None: + function_path = os.path.join(FUNCTIONS_DIR, f"{form_data.id}.py") + try: + with open(function_path, "w") as function_file: + function_file.write(form_data.content) + + function_module, function_type, frontmatter = load_function_module_by_id( + form_data.id + ) + form_data.meta.manifest = frontmatter + + FUNCTIONS = request.app.state.FUNCTIONS + FUNCTIONS[form_data.id] = function_module + + function = Functions.insert_new_function(user.id, function_type, form_data) + + function_cache_dir = Path(CACHE_DIR) / "functions" / form_data.id + function_cache_dir.mkdir(parents=True, exist_ok=True) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error creating function"), + ) + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# GetFunctionById +############################ + + +@router.get("/id/{id}", response_model=Optional[FunctionModel]) +async def get_function_by_id(id: str, user=Depends(get_admin_user)): + function = Functions.get_function_by_id(id) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ToggleFunctionById +############################ + + +@router.post("/id/{id}/toggle", response_model=Optional[FunctionModel]) +async def toggle_function_by_id(id: str, user=Depends(get_admin_user)): + function = Functions.get_function_by_id(id) + if function: + function = Functions.update_function_by_id( + id, {"is_active": not function.is_active} + ) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ToggleGlobalById +############################ + + +@router.post("/id/{id}/toggle/global", response_model=Optional[FunctionModel]) +async def toggle_global_by_id(id: str, user=Depends(get_admin_user)): + function = Functions.get_function_by_id(id) + if function: + function = Functions.update_function_by_id( + id, {"is_global": not function.is_global} + ) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateFunctionById +############################ + + +@router.post("/id/{id}/update", response_model=Optional[FunctionModel]) +async def update_function_by_id( + request: Request, id: str, form_data: FunctionForm, user=Depends(get_admin_user) +): + function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") + + try: + with open(function_path, "w") as function_file: + function_file.write(form_data.content) + + function_module, function_type, frontmatter = load_function_module_by_id(id) + form_data.meta.manifest = frontmatter + + FUNCTIONS = request.app.state.FUNCTIONS + FUNCTIONS[id] = function_module + + updated = {**form_data.model_dump(exclude={"id"}), "type": function_type} + print(updated) + + function = Functions.update_function_by_id(id, updated) + + if function: + return function + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating function"), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteFunctionById +############################ + + +@router.delete("/id/{id}/delete", response_model=bool) +async def delete_function_by_id( + request: Request, id: str, user=Depends(get_admin_user) +): + result = Functions.delete_function_by_id(id) + + if result: + FUNCTIONS = request.app.state.FUNCTIONS + if id in FUNCTIONS: + del FUNCTIONS[id] + + # delete the function file + function_path = os.path.join(FUNCTIONS_DIR, f"{id}.py") + try: + os.remove(function_path) + except: + pass + + return result + + +############################ +# GetFunctionValves +############################ + + +@router.get("/id/{id}/valves", response_model=Optional[dict]) +async def get_function_valves_by_id(id: str, user=Depends(get_admin_user)): + function = Functions.get_function_by_id(id) + if function: + try: + valves = Functions.get_function_valves_by_id(id) + return valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# GetFunctionValvesSpec +############################ + + +@router.get("/id/{id}/valves/spec", response_model=Optional[dict]) +async def get_function_valves_spec_by_id( + request: Request, id: str, user=Depends(get_admin_user) +): + function = Functions.get_function_by_id(id) + if function: + if id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[id] + else: + function_module, function_type, frontmatter = load_function_module_by_id(id) + request.app.state.FUNCTIONS[id] = function_module + + if hasattr(function_module, "Valves"): + Valves = function_module.Valves + return Valves.schema() + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateFunctionValves +############################ + + +@router.post("/id/{id}/valves/update", response_model=Optional[dict]) +async def update_function_valves_by_id( + request: Request, id: str, form_data: dict, user=Depends(get_admin_user) +): + function = Functions.get_function_by_id(id) + if function: + + if id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[id] + else: + function_module, function_type, frontmatter = load_function_module_by_id(id) + request.app.state.FUNCTIONS[id] = function_module + + if hasattr(function_module, "Valves"): + Valves = function_module.Valves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + valves = Valves(**form_data) + Functions.update_function_valves_by_id(id, valves.model_dump()) + return valves.model_dump() + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# FunctionUserValves +############################ + + +@router.get("/id/{id}/valves/user", response_model=Optional[dict]) +async def get_function_user_valves_by_id(id: str, user=Depends(get_verified_user)): + function = Functions.get_function_by_id(id) + if function: + try: + user_valves = Functions.get_user_valves_by_id_and_user_id(id, user.id) + return user_valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict]) +async def get_function_user_valves_spec_by_id( + request: Request, id: str, user=Depends(get_verified_user) +): + function = Functions.get_function_by_id(id) + if function: + if id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[id] + else: + function_module, function_type, frontmatter = load_function_module_by_id(id) + request.app.state.FUNCTIONS[id] = function_module + + if hasattr(function_module, "UserValves"): + UserValves = function_module.UserValves + return UserValves.schema() + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.post("/id/{id}/valves/user/update", response_model=Optional[dict]) +async def update_function_user_valves_by_id( + request: Request, id: str, form_data: dict, user=Depends(get_verified_user) +): + function = Functions.get_function_by_id(id) + + if function: + if id in request.app.state.FUNCTIONS: + function_module = request.app.state.FUNCTIONS[id] + else: + function_module, function_type, frontmatter = load_function_module_by_id(id) + request.app.state.FUNCTIONS[id] = function_module + + if hasattr(function_module, "UserValves"): + UserValves = function_module.UserValves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + user_valves = UserValves(**form_data) + Functions.update_user_valves_by_id_and_user_id( + id, user.id, user_valves.model_dump() + ) + return user_valves.model_dump() + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/apps/webui/routers/memories.py b/backend/apps/webui/routers/memories.py new file mode 100644 index 0000000000000000000000000000000000000000..2c473ebe8f6389f3512841f0020abab5d4dc9187 --- /dev/null +++ b/backend/apps/webui/routers/memories.py @@ -0,0 +1,180 @@ +from fastapi import Response, Request +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import logging + +from apps.webui.models.memories import Memories, MemoryModel + +from utils.utils import get_verified_user +from constants import ERROR_MESSAGES + +from config import SRC_LOG_LEVELS, CHROMA_CLIENT + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + + +@router.get("/ef") +async def get_embeddings(request: Request): + return {"result": request.app.state.EMBEDDING_FUNCTION("hello world")} + + +############################ +# GetMemories +############################ + + +@router.get("/", response_model=List[MemoryModel]) +async def get_memories(user=Depends(get_verified_user)): + return Memories.get_memories_by_user_id(user.id) + + +############################ +# AddMemory +############################ + + +class AddMemoryForm(BaseModel): + content: str + + +class MemoryUpdateModel(BaseModel): + content: Optional[str] = None + + +@router.post("/add", response_model=Optional[MemoryModel]) +async def add_memory( + request: Request, + form_data: AddMemoryForm, + user=Depends(get_verified_user), +): + memory = Memories.insert_new_memory(user.id, form_data.content) + memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content) + + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + collection.upsert( + documents=[memory.content], + ids=[memory.id], + embeddings=[memory_embedding], + metadatas=[{"created_at": memory.created_at}], + ) + + return memory + + +@router.post("/{memory_id}/update", response_model=Optional[MemoryModel]) +async def update_memory_by_id( + memory_id: str, + request: Request, + form_data: MemoryUpdateModel, + user=Depends(get_verified_user), +): + memory = Memories.update_memory_by_id(memory_id, form_data.content) + if memory is None: + raise HTTPException(status_code=404, detail="Memory not found") + + if form_data.content is not None: + memory_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content) + collection = CHROMA_CLIENT.get_or_create_collection( + name=f"user-memory-{user.id}" + ) + collection.upsert( + documents=[form_data.content], + ids=[memory.id], + embeddings=[memory_embedding], + metadatas=[ + {"created_at": memory.created_at, "updated_at": memory.updated_at} + ], + ) + + return memory + + +############################ +# QueryMemory +############################ + + +class QueryMemoryForm(BaseModel): + content: str + k: Optional[int] = 1 + + +@router.post("/query") +async def query_memory( + request: Request, form_data: QueryMemoryForm, user=Depends(get_verified_user) +): + query_embedding = request.app.state.EMBEDDING_FUNCTION(form_data.content) + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + + results = collection.query( + query_embeddings=[query_embedding], + n_results=form_data.k, # how many results to return + ) + + return results + + +############################ +# ResetMemoryFromVectorDB +############################ +@router.get("/reset", response_model=bool) +async def reset_memory_from_vector_db( + request: Request, user=Depends(get_verified_user) +): + CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}") + collection = CHROMA_CLIENT.get_or_create_collection(name=f"user-memory-{user.id}") + + memories = Memories.get_memories_by_user_id(user.id) + for memory in memories: + memory_embedding = request.app.state.EMBEDDING_FUNCTION(memory.content) + collection.upsert( + documents=[memory.content], + ids=[memory.id], + embeddings=[memory_embedding], + ) + return True + + +############################ +# DeleteMemoriesByUserId +############################ + + +@router.delete("/user", response_model=bool) +async def delete_memory_by_user_id(user=Depends(get_verified_user)): + result = Memories.delete_memories_by_user_id(user.id) + + if result: + try: + CHROMA_CLIENT.delete_collection(f"user-memory-{user.id}") + except Exception as e: + log.error(e) + return True + + return False + + +############################ +# DeleteMemoryById +############################ + + +@router.delete("/{memory_id}", response_model=bool) +async def delete_memory_by_id(memory_id: str, user=Depends(get_verified_user)): + result = Memories.delete_memory_by_id_and_user_id(memory_id, user.id) + + if result: + collection = CHROMA_CLIENT.get_or_create_collection( + name=f"user-memory-{user.id}" + ) + collection.delete(ids=[memory_id]) + return True + + return False diff --git a/backend/apps/webui/routers/models.py b/backend/apps/webui/routers/models.py new file mode 100644 index 0000000000000000000000000000000000000000..eeae9e1c41a7608b5fb256bb46d9e5de25af1738 --- /dev/null +++ b/backend/apps/webui/routers/models.py @@ -0,0 +1,113 @@ +from fastapi import Depends, FastAPI, HTTPException, status, Request +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.webui.models.models import Models, ModelModel, ModelForm, ModelResponse + +from utils.utils import get_verified_user, get_admin_user +from constants import ERROR_MESSAGES + +router = APIRouter() + +########################### +# getModels +########################### + + +@router.get("/", response_model=List[ModelResponse]) +async def get_models(user=Depends(get_verified_user)): + return Models.get_all_models() + + +############################ +# AddNewModel +############################ + + +@router.post("/add", response_model=Optional[ModelModel]) +async def add_new_model( + request: Request, + form_data: ModelForm, + user=Depends(get_admin_user), +): + if form_data.id in request.app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.MODEL_ID_TAKEN, + ) + else: + model = Models.insert_new_model(form_data, user.id) + + if model: + return model + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# GetModelById +############################ + + +@router.get("/", response_model=Optional[ModelModel]) +async def get_model_by_id(id: str, user=Depends(get_verified_user)): + model = Models.get_model_by_id(id) + + if model: + return model + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateModelById +############################ + + +@router.post("/update", response_model=Optional[ModelModel]) +async def update_model_by_id( + request: Request, + id: str, + form_data: ModelForm, + user=Depends(get_admin_user), +): + model = Models.get_model_by_id(id) + if model: + model = Models.update_model_by_id(id, form_data) + return model + else: + if form_data.id in request.app.state.MODELS: + model = Models.insert_new_model(form_data, user.id) + if model: + return model + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.DEFAULT(), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + +############################ +# DeleteModelById +############################ + + +@router.delete("/delete", response_model=bool) +async def delete_model_by_id(id: str, user=Depends(get_admin_user)): + result = Models.delete_model_by_id(id) + return result diff --git a/backend/apps/webui/routers/prompts.py b/backend/apps/webui/routers/prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..c674590e95998979f8f7749901fa9a699b9cb8f6 --- /dev/null +++ b/backend/apps/webui/routers/prompts.py @@ -0,0 +1,96 @@ +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.webui.models.prompts import Prompts, PromptForm, PromptModel + +from utils.utils import get_verified_user, get_admin_user +from constants import ERROR_MESSAGES + +router = APIRouter() + +############################ +# GetPrompts +############################ + + +@router.get("/", response_model=List[PromptModel]) +async def get_prompts(user=Depends(get_verified_user)): + return Prompts.get_prompts() + + +############################ +# CreateNewPrompt +############################ + + +@router.post("/create", response_model=Optional[PromptModel]) +async def create_new_prompt(form_data: PromptForm, user=Depends(get_admin_user)): + prompt = Prompts.get_prompt_by_command(form_data.command) + if prompt == None: + prompt = Prompts.insert_new_prompt(user.id, form_data) + + if prompt: + return prompt + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.COMMAND_TAKEN, + ) + + +############################ +# GetPromptByCommand +############################ + + +@router.get("/command/{command}", response_model=Optional[PromptModel]) +async def get_prompt_by_command(command: str, user=Depends(get_verified_user)): + prompt = Prompts.get_prompt_by_command(f"/{command}") + + if prompt: + return prompt + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdatePromptByCommand +############################ + + +@router.post("/command/{command}/update", response_model=Optional[PromptModel]) +async def update_prompt_by_command( + command: str, + form_data: PromptForm, + user=Depends(get_admin_user), +): + prompt = Prompts.update_prompt_by_command(f"/{command}", form_data) + if prompt: + return prompt + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + + +############################ +# DeletePromptByCommand +############################ + + +@router.delete("/command/{command}/delete", response_model=bool) +async def delete_prompt_by_command(command: str, user=Depends(get_admin_user)): + result = Prompts.delete_prompt_by_command(f"/{command}") + return result diff --git a/backend/apps/webui/routers/tools.py b/backend/apps/webui/routers/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..ea9db8180b4276bacf03c60a0a624eab80b8e677 --- /dev/null +++ b/backend/apps/webui/routers/tools.py @@ -0,0 +1,379 @@ +from fastapi import Depends, FastAPI, HTTPException, status, Request +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import json + +from apps.webui.models.users import Users +from apps.webui.models.tools import Tools, ToolForm, ToolModel, ToolResponse +from apps.webui.utils import load_toolkit_module_by_id + +from utils.utils import get_admin_user, get_verified_user +from utils.tools import get_tools_specs +from constants import ERROR_MESSAGES + +from importlib import util +import os +from pathlib import Path + +from config import DATA_DIR, CACHE_DIR + + +TOOLS_DIR = f"{DATA_DIR}/tools" +os.makedirs(TOOLS_DIR, exist_ok=True) + + +router = APIRouter() + +############################ +# GetToolkits +############################ + + +@router.get("/", response_model=List[ToolResponse]) +async def get_toolkits(user=Depends(get_verified_user)): + toolkits = [toolkit for toolkit in Tools.get_tools()] + return toolkits + + +############################ +# ExportToolKits +############################ + + +@router.get("/export", response_model=List[ToolModel]) +async def get_toolkits(user=Depends(get_admin_user)): + toolkits = [toolkit for toolkit in Tools.get_tools()] + return toolkits + + +############################ +# CreateNewToolKit +############################ + + +@router.post("/create", response_model=Optional[ToolResponse]) +async def create_new_toolkit( + request: Request, + form_data: ToolForm, + user=Depends(get_admin_user), +): + if not form_data.id.isidentifier(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only alphanumeric characters and underscores are allowed in the id", + ) + + form_data.id = form_data.id.lower() + + toolkit = Tools.get_tool_by_id(form_data.id) + if toolkit == None: + toolkit_path = os.path.join(TOOLS_DIR, f"{form_data.id}.py") + try: + with open(toolkit_path, "w") as tool_file: + tool_file.write(form_data.content) + + toolkit_module, frontmatter = load_toolkit_module_by_id(form_data.id) + form_data.meta.manifest = frontmatter + + TOOLS = request.app.state.TOOLS + TOOLS[form_data.id] = toolkit_module + + specs = get_tools_specs(TOOLS[form_data.id]) + toolkit = Tools.insert_new_tool(user.id, form_data, specs) + + tool_cache_dir = Path(CACHE_DIR) / "tools" / form_data.id + tool_cache_dir.mkdir(parents=True, exist_ok=True) + + if toolkit: + return toolkit + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error creating toolkit"), + ) + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.ID_TAKEN, + ) + + +############################ +# GetToolkitById +############################ + + +@router.get("/id/{id}", response_model=Optional[ToolModel]) +async def get_toolkit_by_id(id: str, user=Depends(get_admin_user)): + toolkit = Tools.get_tool_by_id(id) + + if toolkit: + return toolkit + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateToolkitById +############################ + + +@router.post("/id/{id}/update", response_model=Optional[ToolModel]) +async def update_toolkit_by_id( + request: Request, + id: str, + form_data: ToolForm, + user=Depends(get_admin_user), +): + toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") + + try: + with open(toolkit_path, "w") as tool_file: + tool_file.write(form_data.content) + + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + form_data.meta.manifest = frontmatter + + TOOLS = request.app.state.TOOLS + TOOLS[id] = toolkit_module + + specs = get_tools_specs(TOOLS[id]) + + updated = { + **form_data.model_dump(exclude={"id"}), + "specs": specs, + } + + print(updated) + toolkit = Tools.update_tool_by_id(id, updated) + + if toolkit: + return toolkit + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT("Error updating toolkit"), + ) + + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + + +############################ +# DeleteToolkitById +############################ + + +@router.delete("/id/{id}/delete", response_model=bool) +async def delete_toolkit_by_id(request: Request, id: str, user=Depends(get_admin_user)): + result = Tools.delete_tool_by_id(id) + + if result: + TOOLS = request.app.state.TOOLS + if id in TOOLS: + del TOOLS[id] + + # delete the toolkit file + toolkit_path = os.path.join(TOOLS_DIR, f"{id}.py") + os.remove(toolkit_path) + + return result + + +############################ +# GetToolValves +############################ + + +@router.get("/id/{id}/valves", response_model=Optional[dict]) +async def get_toolkit_valves_by_id(id: str, user=Depends(get_admin_user)): + toolkit = Tools.get_tool_by_id(id) + if toolkit: + try: + valves = Tools.get_tool_valves_by_id(id) + return valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# GetToolValvesSpec +############################ + + +@router.get("/id/{id}/valves/spec", response_model=Optional[dict]) +async def get_toolkit_valves_spec_by_id( + request: Request, id: str, user=Depends(get_admin_user) +): + toolkit = Tools.get_tool_by_id(id) + if toolkit: + if id in request.app.state.TOOLS: + toolkit_module = request.app.state.TOOLS[id] + else: + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + request.app.state.TOOLS[id] = toolkit_module + + if hasattr(toolkit_module, "Valves"): + Valves = toolkit_module.Valves + return Valves.schema() + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# UpdateToolValves +############################ + + +@router.post("/id/{id}/valves/update", response_model=Optional[dict]) +async def update_toolkit_valves_by_id( + request: Request, id: str, form_data: dict, user=Depends(get_admin_user) +): + toolkit = Tools.get_tool_by_id(id) + if toolkit: + if id in request.app.state.TOOLS: + toolkit_module = request.app.state.TOOLS[id] + else: + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + request.app.state.TOOLS[id] = toolkit_module + + if hasattr(toolkit_module, "Valves"): + Valves = toolkit_module.Valves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + valves = Valves(**form_data) + Tools.update_tool_valves_by_id(id, valves.model_dump()) + return valves.model_dump() + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +############################ +# ToolUserValves +############################ + + +@router.get("/id/{id}/valves/user", response_model=Optional[dict]) +async def get_toolkit_user_valves_by_id(id: str, user=Depends(get_verified_user)): + toolkit = Tools.get_tool_by_id(id) + if toolkit: + try: + user_valves = Tools.get_user_valves_by_id_and_user_id(id, user.id) + return user_valves + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.get("/id/{id}/valves/user/spec", response_model=Optional[dict]) +async def get_toolkit_user_valves_spec_by_id( + request: Request, id: str, user=Depends(get_verified_user) +): + toolkit = Tools.get_tool_by_id(id) + if toolkit: + if id in request.app.state.TOOLS: + toolkit_module = request.app.state.TOOLS[id] + else: + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + request.app.state.TOOLS[id] = toolkit_module + + if hasattr(toolkit_module, "UserValves"): + UserValves = toolkit_module.UserValves + return UserValves.schema() + return None + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + +@router.post("/id/{id}/valves/user/update", response_model=Optional[dict]) +async def update_toolkit_user_valves_by_id( + request: Request, id: str, form_data: dict, user=Depends(get_verified_user) +): + toolkit = Tools.get_tool_by_id(id) + + if toolkit: + if id in request.app.state.TOOLS: + toolkit_module = request.app.state.TOOLS[id] + else: + toolkit_module, frontmatter = load_toolkit_module_by_id(id) + request.app.state.TOOLS[id] = toolkit_module + + if hasattr(toolkit_module, "UserValves"): + UserValves = toolkit_module.UserValves + + try: + form_data = {k: v for k, v in form_data.items() if v is not None} + user_valves = UserValves(**form_data) + Tools.update_user_valves_by_id_and_user_id( + id, user.id, user_valves.model_dump() + ) + return user_valves.model_dump() + except Exception as e: + print(e) + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(e), + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.NOT_FOUND, + ) diff --git a/backend/apps/webui/routers/users.py b/backend/apps/webui/routers/users.py new file mode 100644 index 0000000000000000000000000000000000000000..9627f0b06779bd2486ee41cb846583ec613ae3c1 --- /dev/null +++ b/backend/apps/webui/routers/users.py @@ -0,0 +1,273 @@ +from fastapi import Response, Request +from fastapi import Depends, FastAPI, HTTPException, status +from datetime import datetime, timedelta +from typing import List, Union, Optional + +from fastapi import APIRouter +from pydantic import BaseModel +import time +import uuid +import logging + +from apps.webui.models.users import ( + UserModel, + UserUpdateForm, + UserRoleUpdateForm, + UserSettings, + Users, +) +from apps.webui.models.auths import Auths +from apps.webui.models.chats import Chats + +from utils.utils import ( + get_verified_user, + get_password_hash, + get_current_user, + get_admin_user, +) +from constants import ERROR_MESSAGES + +from config import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MODELS"]) + +router = APIRouter() + +############################ +# GetUsers +############################ + + +@router.get("/", response_model=List[UserModel]) +async def get_users(skip: int = 0, limit: int = 50, user=Depends(get_admin_user)): + return Users.get_users(skip, limit) + + +############################ +# User Permissions +############################ + + +@router.get("/permissions/user") +async def get_user_permissions(request: Request, user=Depends(get_admin_user)): + return request.app.state.config.USER_PERMISSIONS + + +@router.post("/permissions/user") +async def update_user_permissions( + request: Request, form_data: dict, user=Depends(get_admin_user) +): + request.app.state.config.USER_PERMISSIONS = form_data + return request.app.state.config.USER_PERMISSIONS + + +############################ +# UpdateUserRole +############################ + + +@router.post("/update/role", response_model=Optional[UserModel]) +async def update_user_role(form_data: UserRoleUpdateForm, user=Depends(get_admin_user)): + + if user.id != form_data.id and form_data.id != Users.get_first_user().id: + return Users.update_user_role_by_id(form_data.id, form_data.role) + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) + + +############################ +# GetUserSettingsBySessionUser +############################ + + +@router.get("/user/settings", response_model=Optional[UserSettings]) +async def get_user_settings_by_session_user(user=Depends(get_verified_user)): + user = Users.get_user_by_id(user.id) + if user: + return user.settings + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# UpdateUserSettingsBySessionUser +############################ + + +@router.post("/user/settings/update", response_model=UserSettings) +async def update_user_settings_by_session_user( + form_data: UserSettings, user=Depends(get_verified_user) +): + user = Users.update_user_by_id(user.id, {"settings": form_data.model_dump()}) + if user: + return user.settings + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserInfoBySessionUser +############################ + + +@router.get("/user/info", response_model=Optional[dict]) +async def get_user_info_by_session_user(user=Depends(get_verified_user)): + user = Users.get_user_by_id(user.id) + if user: + return user.info + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# UpdateUserInfoBySessionUser +############################ + + +@router.post("/user/info/update", response_model=Optional[dict]) +async def update_user_info_by_session_user( + form_data: dict, user=Depends(get_verified_user) +): + user = Users.get_user_by_id(user.id) + if user: + if user.info is None: + user.info = {} + + user = Users.update_user_by_id(user.id, {"info": {**user.info, **form_data}}) + if user: + return user.info + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# GetUserById +############################ + + +class UserResponse(BaseModel): + name: str + profile_image_url: str + + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user_by_id(user_id: str, user=Depends(get_verified_user)): + + # Check if user_id is a shared chat + # If it is, get the user_id from the chat + if user_id.startswith("shared-"): + chat_id = user_id.replace("shared-", "") + chat = Chats.get_chat_by_id(chat_id) + if chat: + user_id = chat.user_id + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + user = Users.get_user_by_id(user_id) + + if user: + return UserResponse(name=user.name, profile_image_url=user.profile_image_url) + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# UpdateUserById +############################ + + +@router.post("/{user_id}/update", response_model=Optional[UserModel]) +async def update_user_by_id( + user_id: str, + form_data: UserUpdateForm, + session_user=Depends(get_admin_user), +): + user = Users.get_user_by_id(user_id) + + if user: + if form_data.email.lower() != user.email: + email_user = Users.get_user_by_email(form_data.email.lower()) + if email_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.EMAIL_TAKEN, + ) + + if form_data.password: + hashed = get_password_hash(form_data.password) + log.debug(f"hashed: {hashed}") + Auths.update_user_password_by_id(user_id, hashed) + + Auths.update_email_by_id(user_id, form_data.email.lower()) + updated_user = Users.update_user_by_id( + user_id, + { + "name": form_data.name, + "email": form_data.email.lower(), + "profile_image_url": form_data.profile_image_url, + }, + ) + + if updated_user: + return updated_user + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DEFAULT(), + ) + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.USER_NOT_FOUND, + ) + + +############################ +# DeleteUserById +############################ + + +@router.delete("/{user_id}", response_model=bool) +async def delete_user_by_id(user_id: str, user=Depends(get_admin_user)): + if user.id != user_id: + result = Auths.delete_auth_by_id(user_id) + + if result: + return True + + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=ERROR_MESSAGES.DELETE_USER_ERROR, + ) + + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=ERROR_MESSAGES.ACTION_PROHIBITED, + ) diff --git a/backend/apps/webui/routers/utils.py b/backend/apps/webui/routers/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..780ed6b43e71b7c2fc0560d4c4ead5e4f47f81e6 --- /dev/null +++ b/backend/apps/webui/routers/utils.py @@ -0,0 +1,135 @@ +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 black + + +from utils.utils import get_admin_user +from utils.misc import calculate_sha256, get_gravatar_url + +from config import OLLAMA_BASE_URLS, DATA_DIR, UPLOAD_DIR, ENABLE_ADMIN_EXPORT +from constants import ERROR_MESSAGES +from typing import List + +router = APIRouter() + + +@router.get("/gravatar") +async def get_gravatar( + email: str, +): + return get_gravatar_url(email) + + +class CodeFormatRequest(BaseModel): + code: str + + +@router.post("/code/format") +async def format_code(request: CodeFormatRequest): + try: + formatted_code = black.format_str(request.code, mode=black.Mode()) + return {"code": formatted_code} + except black.NothingChanged: + return {"code": request.code} + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +class MarkdownForm(BaseModel): + md: str + + +@router.post("/markdown") +async def get_html_from_markdown( + form_data: MarkdownForm, +): + 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)): + if not ENABLE_ADMIN_EXPORT: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + from apps.webui.internal.db import engine + + if engine.name != "sqlite": + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=ERROR_MESSAGES.DB_NOT_SQLITE, + ) + return FileResponse( + engine.url.database, + media_type="application/octet-stream", + filename="webui.db", + ) + + +@router.get("/litellm/config") +async def download_litellm_config_yaml(user=Depends(get_admin_user)): + return FileResponse( + f"{DATA_DIR}/litellm/config.yaml", + media_type="application/octet-stream", + filename="config.yaml", + ) diff --git a/backend/apps/webui/utils.py b/backend/apps/webui/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..96d2b29ebfa67a5d2d27feb43b8434041cbc04e2 --- /dev/null +++ b/backend/apps/webui/utils.py @@ -0,0 +1,90 @@ +from importlib import util +import os +import re + +from config import TOOLS_DIR, FUNCTIONS_DIR + + +def extract_frontmatter(file_path): + """ + Extract frontmatter as a dictionary from the specified file path. + """ + frontmatter = {} + frontmatter_started = False + frontmatter_ended = False + frontmatter_pattern = re.compile(r"^\s*([a-z_]+):\s*(.*)\s*$", re.IGNORECASE) + + try: + with open(file_path, "r", encoding="utf-8") as file: + first_line = file.readline() + if first_line.strip() != '"""': + # The file doesn't start with triple quotes + return {} + + frontmatter_started = True + + for line in file: + if '"""' in line: + if frontmatter_started: + frontmatter_ended = True + break + + if frontmatter_started and not frontmatter_ended: + match = frontmatter_pattern.match(line) + if match: + key, value = match.groups() + frontmatter[key.strip()] = value.strip() + + except FileNotFoundError: + print(f"Error: The file {file_path} does not exist.") + return {} + except Exception as e: + print(f"An error occurred: {e}") + return {} + + return frontmatter + + +def load_toolkit_module_by_id(toolkit_id): + toolkit_path = os.path.join(TOOLS_DIR, f"{toolkit_id}.py") + spec = util.spec_from_file_location(toolkit_id, toolkit_path) + module = util.module_from_spec(spec) + frontmatter = extract_frontmatter(toolkit_path) + + try: + spec.loader.exec_module(module) + print(f"Loaded module: {module.__name__}") + if hasattr(module, "Tools"): + return module.Tools(), frontmatter + else: + raise Exception("No Tools class found") + except Exception as e: + print(f"Error loading module: {toolkit_id}") + # Move the file to the error folder + os.rename(toolkit_path, f"{toolkit_path}.error") + raise e + + +def load_function_module_by_id(function_id): + function_path = os.path.join(FUNCTIONS_DIR, f"{function_id}.py") + + spec = util.spec_from_file_location(function_id, function_path) + module = util.module_from_spec(spec) + frontmatter = extract_frontmatter(function_path) + + try: + spec.loader.exec_module(module) + print(f"Loaded module: {module.__name__}") + if hasattr(module, "Pipe"): + return module.Pipe(), "pipe", frontmatter + elif hasattr(module, "Filter"): + return module.Filter(), "filter", frontmatter + elif hasattr(module, "Action"): + return module.Action(), "action", frontmatter + else: + raise Exception("No Function class found") + except Exception as e: + print(f"Error loading module: {function_id}") + # Move the file to the error folder + os.rename(function_path, f"{function_path}.error") + raise e diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000000000000000000000000000000000000..395720c188817912f7aa4af069c8aae90a22076c --- /dev/null +++ b/backend/config.py @@ -0,0 +1,1382 @@ +import os +import sys +import logging +import importlib.metadata +import pkgutil +import chromadb +from chromadb import Settings +from bs4 import BeautifulSoup +from typing import TypeVar, Generic +from pydantic import BaseModel +from typing import Optional + +from pathlib import Path +import json +import yaml + +import markdown +import requests +import shutil + +from constants import ERROR_MESSAGES + +#################################### +# Load .env file +#################################### + +BACKEND_DIR = Path(__file__).parent # the path containing this file +BASE_DIR = BACKEND_DIR.parent # the path containing the backend/ + +print(BASE_DIR) + +try: + from dotenv import load_dotenv, find_dotenv + + load_dotenv(find_dotenv(str(BASE_DIR / ".env"))) +except ImportError: + print("dotenv not installed, skipping...") + + +#################################### +# LOGGING +#################################### + +log_levels = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] + +GLOBAL_LOG_LEVEL = os.environ.get("GLOBAL_LOG_LEVEL", "").upper() +if GLOBAL_LOG_LEVEL in log_levels: + logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL, force=True) +else: + GLOBAL_LOG_LEVEL = "INFO" + +log = logging.getLogger(__name__) +log.info(f"GLOBAL_LOG_LEVEL: {GLOBAL_LOG_LEVEL}") + +log_sources = [ + "AUDIO", + "COMFYUI", + "CONFIG", + "DB", + "IMAGES", + "MAIN", + "MODELS", + "OLLAMA", + "OPENAI", + "RAG", + "WEBHOOK", +] + +SRC_LOG_LEVELS = {} + +for source in log_sources: + log_env_var = source + "_LOG_LEVEL" + SRC_LOG_LEVELS[source] = os.environ.get(log_env_var, "").upper() + if SRC_LOG_LEVELS[source] not in log_levels: + SRC_LOG_LEVELS[source] = GLOBAL_LOG_LEVEL + log.info(f"{log_env_var}: {SRC_LOG_LEVELS[source]}") + +log.setLevel(SRC_LOG_LEVELS["CONFIG"]) + + +class EndpointFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + return record.getMessage().find("/health") == -1 + + +# Filter out /endpoint +logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) + + +WEBUI_NAME = os.environ.get("WEBUI_NAME", "Open WebUI") +if WEBUI_NAME != "Open WebUI": + WEBUI_NAME += " (Open WebUI)" + +WEBUI_URL = os.environ.get("WEBUI_URL", "http://localhost:3000") + +WEBUI_FAVICON_URL = "https://openwebui.com/favicon.png" + + +#################################### +# ENV (dev,test,prod) +#################################### + +ENV = os.environ.get("ENV", "dev") + +try: + PACKAGE_DATA = json.loads((BASE_DIR / "package.json").read_text()) +except: + try: + PACKAGE_DATA = {"version": importlib.metadata.version("open-webui")} + except importlib.metadata.PackageNotFoundError: + PACKAGE_DATA = {"version": "0.0.0"} + +VERSION = PACKAGE_DATA["version"] + + +# Function to parse each section +def parse_section(section): + items = [] + for li in section.find_all("li"): + # Extract raw HTML string + raw_html = str(li) + + # Extract text without HTML tags + text = li.get_text(separator=" ", strip=True) + + # Split into title and content + parts = text.split(": ", 1) + title = parts[0].strip() if len(parts) > 1 else "" + content = parts[1].strip() if len(parts) > 1 else text + + items.append({"title": title, "content": content, "raw": raw_html}) + return items + + +try: + changelog_path = BASE_DIR / "CHANGELOG.md" + with open(str(changelog_path.absolute()), "r", encoding="utf8") as file: + changelog_content = file.read() + +except: + changelog_content = (pkgutil.get_data("open_webui", "CHANGELOG.md") or b"").decode() + + +# Convert markdown content to HTML +html_content = markdown.markdown(changelog_content) + +# Parse the HTML content +soup = BeautifulSoup(html_content, "html.parser") + +# Initialize JSON structure +changelog_json = {} + +# Iterate over each version +for version in soup.find_all("h2"): + version_number = version.get_text().strip().split(" - ")[0][1:-1] # Remove brackets + date = version.get_text().strip().split(" - ")[1] + + version_data = {"date": date} + + # Find the next sibling that is a h3 tag (section title) + current = version.find_next_sibling() + + while current and current.name != "h2": + if current.name == "h3": + section_title = current.get_text().lower() # e.g., "added", "fixed" + section_items = parse_section(current.find_next_sibling("ul")) + version_data[section_title] = section_items + + # Move to the next element + current = current.find_next_sibling() + + changelog_json[version_number] = version_data + + +CHANGELOG = changelog_json + + +#################################### +# SAFE_MODE +#################################### + +SAFE_MODE = os.environ.get("SAFE_MODE", "false").lower() == "true" + +#################################### +# WEBUI_BUILD_HASH +#################################### + +WEBUI_BUILD_HASH = os.environ.get("WEBUI_BUILD_HASH", "dev-build") + +#################################### +# DATA/FRONTEND BUILD DIR +#################################### + +DATA_DIR = Path(os.getenv("DATA_DIR", BACKEND_DIR / "data")).resolve() +FRONTEND_BUILD_DIR = Path(os.getenv("FRONTEND_BUILD_DIR", BASE_DIR / "build")).resolve() + +RESET_CONFIG_ON_START = ( + os.environ.get("RESET_CONFIG_ON_START", "False").lower() == "true" +) +if RESET_CONFIG_ON_START: + try: + os.remove(f"{DATA_DIR}/config.json") + with open(f"{DATA_DIR}/config.json", "w") as f: + f.write("{}") + except: + pass + +try: + CONFIG_DATA = json.loads((DATA_DIR / "config.json").read_text()) +except: + CONFIG_DATA = {} + + +#################################### +# Config helpers +#################################### + + +def save_config(): + try: + with open(f"{DATA_DIR}/config.json", "w") as f: + json.dump(CONFIG_DATA, f, indent="\t") + except Exception as e: + log.exception(e) + + +def get_config_value(config_path: str): + path_parts = config_path.split(".") + cur_config = CONFIG_DATA + for key in path_parts: + if key in cur_config: + cur_config = cur_config[key] + else: + return None + return cur_config + + +T = TypeVar("T") + + +class PersistentConfig(Generic[T]): + def __init__(self, env_name: str, config_path: str, env_value: T): + self.env_name = env_name + self.config_path = config_path + self.env_value = env_value + self.config_value = get_config_value(config_path) + if self.config_value is not None: + log.info(f"'{env_name}' loaded from config.json") + self.value = self.config_value + else: + self.value = env_value + + def __str__(self): + return str(self.value) + + @property + def __dict__(self): + raise TypeError( + "PersistentConfig object cannot be converted to dict, use config_get or .value instead." + ) + + def __getattribute__(self, item): + if item == "__dict__": + raise TypeError( + "PersistentConfig object cannot be converted to dict, use config_get or .value instead." + ) + return super().__getattribute__(item) + + def save(self): + # Don't save if the value is the same as the env value and the config value + if self.env_value == self.value: + if self.config_value == self.value: + return + log.info(f"Saving '{self.env_name}' to config.json") + path_parts = self.config_path.split(".") + config = CONFIG_DATA + for key in path_parts[:-1]: + if key not in config: + config[key] = {} + config = config[key] + config[path_parts[-1]] = self.value + save_config() + self.config_value = self.value + + +class AppConfig: + _state: dict[str, PersistentConfig] + + def __init__(self): + super().__setattr__("_state", {}) + + def __setattr__(self, key, value): + if isinstance(value, PersistentConfig): + self._state[key] = value + else: + self._state[key].value = value + self._state[key].save() + + def __getattr__(self, key): + return self._state[key].value + + +#################################### +# WEBUI_AUTH (Required for security) +#################################### + +WEBUI_AUTH = os.environ.get("WEBUI_AUTH", "True").lower() == "true" +WEBUI_AUTH_TRUSTED_EMAIL_HEADER = os.environ.get( + "WEBUI_AUTH_TRUSTED_EMAIL_HEADER", None +) +WEBUI_AUTH_TRUSTED_NAME_HEADER = os.environ.get("WEBUI_AUTH_TRUSTED_NAME_HEADER", None) +JWT_EXPIRES_IN = PersistentConfig( + "JWT_EXPIRES_IN", "auth.jwt_expiry", os.environ.get("JWT_EXPIRES_IN", "-1") +) + +#################################### +# OAuth config +#################################### + +ENABLE_OAUTH_SIGNUP = PersistentConfig( + "ENABLE_OAUTH_SIGNUP", + "oauth.enable_signup", + os.environ.get("ENABLE_OAUTH_SIGNUP", "False").lower() == "true", +) + +OAUTH_MERGE_ACCOUNTS_BY_EMAIL = PersistentConfig( + "OAUTH_MERGE_ACCOUNTS_BY_EMAIL", + "oauth.merge_accounts_by_email", + os.environ.get("OAUTH_MERGE_ACCOUNTS_BY_EMAIL", "False").lower() == "true", +) + +OAUTH_PROVIDERS = {} + +GOOGLE_CLIENT_ID = PersistentConfig( + "GOOGLE_CLIENT_ID", + "oauth.google.client_id", + os.environ.get("GOOGLE_CLIENT_ID", ""), +) + +GOOGLE_CLIENT_SECRET = PersistentConfig( + "GOOGLE_CLIENT_SECRET", + "oauth.google.client_secret", + os.environ.get("GOOGLE_CLIENT_SECRET", ""), +) + +GOOGLE_OAUTH_SCOPE = PersistentConfig( + "GOOGLE_OAUTH_SCOPE", + "oauth.google.scope", + os.environ.get("GOOGLE_OAUTH_SCOPE", "openid email profile"), +) + +MICROSOFT_CLIENT_ID = PersistentConfig( + "MICROSOFT_CLIENT_ID", + "oauth.microsoft.client_id", + os.environ.get("MICROSOFT_CLIENT_ID", ""), +) + +MICROSOFT_CLIENT_SECRET = PersistentConfig( + "MICROSOFT_CLIENT_SECRET", + "oauth.microsoft.client_secret", + os.environ.get("MICROSOFT_CLIENT_SECRET", ""), +) + +MICROSOFT_CLIENT_TENANT_ID = PersistentConfig( + "MICROSOFT_CLIENT_TENANT_ID", + "oauth.microsoft.tenant_id", + os.environ.get("MICROSOFT_CLIENT_TENANT_ID", ""), +) + +MICROSOFT_OAUTH_SCOPE = PersistentConfig( + "MICROSOFT_OAUTH_SCOPE", + "oauth.microsoft.scope", + os.environ.get("MICROSOFT_OAUTH_SCOPE", "openid email profile"), +) + +OAUTH_CLIENT_ID = PersistentConfig( + "OAUTH_CLIENT_ID", + "oauth.oidc.client_id", + os.environ.get("OAUTH_CLIENT_ID", ""), +) + +OAUTH_CLIENT_SECRET = PersistentConfig( + "OAUTH_CLIENT_SECRET", + "oauth.oidc.client_secret", + os.environ.get("OAUTH_CLIENT_SECRET", ""), +) + +OPENID_PROVIDER_URL = PersistentConfig( + "OPENID_PROVIDER_URL", + "oauth.oidc.provider_url", + os.environ.get("OPENID_PROVIDER_URL", ""), +) + +OAUTH_SCOPES = PersistentConfig( + "OAUTH_SCOPES", + "oauth.oidc.scopes", + os.environ.get("OAUTH_SCOPES", "openid email profile"), +) + +OAUTH_PROVIDER_NAME = PersistentConfig( + "OAUTH_PROVIDER_NAME", + "oauth.oidc.provider_name", + os.environ.get("OAUTH_PROVIDER_NAME", "SSO"), +) + +OAUTH_USERNAME_CLAIM = PersistentConfig( + "OAUTH_USERNAME_CLAIM", + "oauth.oidc.username_claim", + os.environ.get("OAUTH_USERNAME_CLAIM", "name"), +) + +OAUTH_PICTURE_CLAIM = PersistentConfig( + "OAUTH_USERNAME_CLAIM", + "oauth.oidc.avatar_claim", + os.environ.get("OAUTH_PICTURE_CLAIM", "picture"), +) + + +def load_oauth_providers(): + OAUTH_PROVIDERS.clear() + if GOOGLE_CLIENT_ID.value and GOOGLE_CLIENT_SECRET.value: + OAUTH_PROVIDERS["google"] = { + "client_id": GOOGLE_CLIENT_ID.value, + "client_secret": GOOGLE_CLIENT_SECRET.value, + "server_metadata_url": "https://accounts.google.com/.well-known/openid-configuration", + "scope": GOOGLE_OAUTH_SCOPE.value, + } + + if ( + MICROSOFT_CLIENT_ID.value + and MICROSOFT_CLIENT_SECRET.value + and MICROSOFT_CLIENT_TENANT_ID.value + ): + OAUTH_PROVIDERS["microsoft"] = { + "client_id": MICROSOFT_CLIENT_ID.value, + "client_secret": MICROSOFT_CLIENT_SECRET.value, + "server_metadata_url": f"https://login.microsoftonline.com/{MICROSOFT_CLIENT_TENANT_ID.value}/v2.0/.well-known/openid-configuration", + "scope": MICROSOFT_OAUTH_SCOPE.value, + } + + if ( + OAUTH_CLIENT_ID.value + and OAUTH_CLIENT_SECRET.value + and OPENID_PROVIDER_URL.value + ): + OAUTH_PROVIDERS["oidc"] = { + "client_id": OAUTH_CLIENT_ID.value, + "client_secret": OAUTH_CLIENT_SECRET.value, + "server_metadata_url": OPENID_PROVIDER_URL.value, + "scope": OAUTH_SCOPES.value, + "name": OAUTH_PROVIDER_NAME.value, + } + + +load_oauth_providers() + +#################################### +# Static DIR +#################################### + +STATIC_DIR = Path(os.getenv("STATIC_DIR", BACKEND_DIR / "static")).resolve() + +frontend_favicon = FRONTEND_BUILD_DIR / "static" / "favicon.png" + +if frontend_favicon.exists(): + try: + shutil.copyfile(frontend_favicon, STATIC_DIR / "favicon.png") + except Exception as e: + logging.error(f"An error occurred: {e}") +else: + logging.warning(f"Frontend favicon not found at {frontend_favicon}") + +frontend_splash = FRONTEND_BUILD_DIR / "static" / "splash.png" + +if frontend_splash.exists(): + try: + shutil.copyfile(frontend_splash, STATIC_DIR / "splash.png") + except Exception as e: + logging.error(f"An error occurred: {e}") +else: + logging.warning(f"Frontend splash not found at {frontend_splash}") + + +#################################### +# CUSTOM_NAME +#################################### + +CUSTOM_NAME = os.environ.get("CUSTOM_NAME", "") + +if CUSTOM_NAME: + try: + r = requests.get(f"https://api.openwebui.com/api/v1/custom/{CUSTOM_NAME}") + data = r.json() + if r.ok: + if "logo" in data: + WEBUI_FAVICON_URL = url = ( + f"https://api.openwebui.com{data['logo']}" + if data["logo"][0] == "/" + else data["logo"] + ) + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f"{STATIC_DIR}/favicon.png", "wb") as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + if "splash" in data: + url = ( + f"https://api.openwebui.com{data['splash']}" + if data["splash"][0] == "/" + else data["splash"] + ) + + r = requests.get(url, stream=True) + if r.status_code == 200: + with open(f"{STATIC_DIR}/splash.png", "wb") as f: + r.raw.decode_content = True + shutil.copyfileobj(r.raw, f) + + WEBUI_NAME = data["name"] + except Exception as e: + log.exception(e) + pass + + +#################################### +# File Upload DIR +#################################### + +UPLOAD_DIR = f"{DATA_DIR}/uploads" +Path(UPLOAD_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# Cache DIR +#################################### + +CACHE_DIR = f"{DATA_DIR}/cache" +Path(CACHE_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# Docs DIR +#################################### + +DOCS_DIR = os.getenv("DOCS_DIR", f"{DATA_DIR}/docs") +Path(DOCS_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# Tools DIR +#################################### + +TOOLS_DIR = os.getenv("TOOLS_DIR", f"{DATA_DIR}/tools") +Path(TOOLS_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# Functions DIR +#################################### + +FUNCTIONS_DIR = os.getenv("FUNCTIONS_DIR", f"{DATA_DIR}/functions") +Path(FUNCTIONS_DIR).mkdir(parents=True, exist_ok=True) + + +#################################### +# LITELLM_CONFIG +#################################### + + +def create_config_file(file_path): + directory = os.path.dirname(file_path) + + # Check if directory exists, if not, create it + if not os.path.exists(directory): + os.makedirs(directory) + + # Data to write into the YAML file + config_data = { + "general_settings": {}, + "litellm_settings": {}, + "model_list": [], + "router_settings": {}, + } + + # Write data to YAML file + with open(file_path, "w") as file: + yaml.dump(config_data, file) + + +LITELLM_CONFIG_PATH = f"{DATA_DIR}/litellm/config.yaml" + +# if not os.path.exists(LITELLM_CONFIG_PATH): +# log.info("Config file doesn't exist. Creating...") +# create_config_file(LITELLM_CONFIG_PATH) +# log.info("Config file created successfully.") + + +#################################### +# OLLAMA_BASE_URL +#################################### + + +ENABLE_OLLAMA_API = PersistentConfig( + "ENABLE_OLLAMA_API", + "ollama.enable", + os.environ.get("ENABLE_OLLAMA_API", "True").lower() == "true", +) + +OLLAMA_API_BASE_URL = os.environ.get( + "OLLAMA_API_BASE_URL", "http://localhost:11434/api" +) + +OLLAMA_BASE_URL = os.environ.get("OLLAMA_BASE_URL", "") +AIOHTTP_CLIENT_TIMEOUT = os.environ.get("AIOHTTP_CLIENT_TIMEOUT", "") + +if AIOHTTP_CLIENT_TIMEOUT == "": + AIOHTTP_CLIENT_TIMEOUT = None +else: + try: + AIOHTTP_CLIENT_TIMEOUT = int(AIOHTTP_CLIENT_TIMEOUT) + except: + AIOHTTP_CLIENT_TIMEOUT = 300 + + +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 = ( + OLLAMA_API_BASE_URL[:-4] + if OLLAMA_API_BASE_URL.endswith("/api") + else OLLAMA_API_BASE_URL + ) + +if ENV == "prod": + 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" + + +OLLAMA_BASE_URLS = os.environ.get("OLLAMA_BASE_URLS", "") +OLLAMA_BASE_URLS = OLLAMA_BASE_URLS if OLLAMA_BASE_URLS != "" else OLLAMA_BASE_URL + +OLLAMA_BASE_URLS = [url.strip() for url in OLLAMA_BASE_URLS.split(";")] +OLLAMA_BASE_URLS = PersistentConfig( + "OLLAMA_BASE_URLS", "ollama.base_urls", OLLAMA_BASE_URLS +) + +#################################### +# OPENAI_API +#################################### + + +ENABLE_OPENAI_API = PersistentConfig( + "ENABLE_OPENAI_API", + "openai.enable", + os.environ.get("ENABLE_OPENAI_API", "True").lower() == "true", +) + + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") +OPENAI_API_BASE_URL = os.environ.get("OPENAI_API_BASE_URL", "") + + +if OPENAI_API_BASE_URL == "": + OPENAI_API_BASE_URL = "https://api.openai.com/v1" + +OPENAI_API_KEYS = os.environ.get("OPENAI_API_KEYS", "") +OPENAI_API_KEYS = OPENAI_API_KEYS if OPENAI_API_KEYS != "" else OPENAI_API_KEY + +OPENAI_API_KEYS = [url.strip() for url in OPENAI_API_KEYS.split(";")] +OPENAI_API_KEYS = PersistentConfig( + "OPENAI_API_KEYS", "openai.api_keys", OPENAI_API_KEYS +) + +OPENAI_API_BASE_URLS = os.environ.get("OPENAI_API_BASE_URLS", "") +OPENAI_API_BASE_URLS = ( + OPENAI_API_BASE_URLS if OPENAI_API_BASE_URLS != "" else OPENAI_API_BASE_URL +) + +OPENAI_API_BASE_URLS = [ + url.strip() if url != "" else "https://api.openai.com/v1" + for url in OPENAI_API_BASE_URLS.split(";") +] +OPENAI_API_BASE_URLS = PersistentConfig( + "OPENAI_API_BASE_URLS", "openai.api_base_urls", OPENAI_API_BASE_URLS +) + +OPENAI_API_KEY = "" + +try: + OPENAI_API_KEY = OPENAI_API_KEYS.value[ + OPENAI_API_BASE_URLS.value.index("https://api.openai.com/v1") + ] +except: + pass + +OPENAI_API_BASE_URL = "https://api.openai.com/v1" + +#################################### +# WEBUI +#################################### + +ENABLE_SIGNUP = PersistentConfig( + "ENABLE_SIGNUP", + "ui.enable_signup", + ( + False + if not WEBUI_AUTH + else os.environ.get("ENABLE_SIGNUP", "True").lower() == "true" + ), +) + +ENABLE_LOGIN_FORM = PersistentConfig( + "ENABLE_LOGIN_FORM", + "ui.ENABLE_LOGIN_FORM", + os.environ.get("ENABLE_LOGIN_FORM", "True").lower() == "true", +) + +DEFAULT_LOCALE = PersistentConfig( + "DEFAULT_LOCALE", + "ui.default_locale", + os.environ.get("DEFAULT_LOCALE", ""), +) + +DEFAULT_MODELS = PersistentConfig( + "DEFAULT_MODELS", "ui.default_models", os.environ.get("DEFAULT_MODELS", None) +) + +DEFAULT_PROMPT_SUGGESTIONS = PersistentConfig( + "DEFAULT_PROMPT_SUGGESTIONS", + "ui.prompt_suggestions", + [ + { + "title": ["Help me study", "vocabulary for a college entrance exam"], + "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option.", + }, + { + "title": ["Give me ideas", "for what to do with my kids' art"], + "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter.", + }, + { + "title": ["Tell me a fun fact", "about the Roman Empire"], + "content": "Tell me a random fun fact about the Roman Empire", + }, + { + "title": ["Show me a code snippet", "of a website's sticky header"], + "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript.", + }, + { + "title": [ + "Explain options trading", + "if I'm familiar with buying and selling stocks", + ], + "content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks.", + }, + { + "title": ["Overcome procrastination", "give me tips"], + "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?", + }, + ], +) + +DEFAULT_USER_ROLE = PersistentConfig( + "DEFAULT_USER_ROLE", + "ui.default_user_role", + os.getenv("DEFAULT_USER_ROLE", "pending"), +) + +USER_PERMISSIONS_CHAT_DELETION = ( + os.environ.get("USER_PERMISSIONS_CHAT_DELETION", "True").lower() == "true" +) + +USER_PERMISSIONS = PersistentConfig( + "USER_PERMISSIONS", + "ui.user_permissions", + {"chat": {"deletion": USER_PERMISSIONS_CHAT_DELETION}}, +) + +ENABLE_MODEL_FILTER = PersistentConfig( + "ENABLE_MODEL_FILTER", + "model_filter.enable", + os.environ.get("ENABLE_MODEL_FILTER", "False").lower() == "true", +) +MODEL_FILTER_LIST = os.environ.get("MODEL_FILTER_LIST", "") +MODEL_FILTER_LIST = PersistentConfig( + "MODEL_FILTER_LIST", + "model_filter.list", + [model.strip() for model in MODEL_FILTER_LIST.split(";")], +) + +WEBHOOK_URL = PersistentConfig( + "WEBHOOK_URL", "webhook_url", os.environ.get("WEBHOOK_URL", "") +) + +ENABLE_ADMIN_EXPORT = os.environ.get("ENABLE_ADMIN_EXPORT", "True").lower() == "true" + +ENABLE_COMMUNITY_SHARING = PersistentConfig( + "ENABLE_COMMUNITY_SHARING", + "ui.enable_community_sharing", + os.environ.get("ENABLE_COMMUNITY_SHARING", "True").lower() == "true", +) + + +class BannerModel(BaseModel): + id: str + type: str + title: Optional[str] = None + content: str + dismissible: bool + timestamp: int + + +try: + banners = json.loads(os.environ.get("WEBUI_BANNERS", "[]")) + banners = [BannerModel(**banner) for banner in banners] +except Exception as e: + print(f"Error loading WEBUI_BANNERS: {e}") + banners = [] + +WEBUI_BANNERS = PersistentConfig("WEBUI_BANNERS", "ui.banners", banners) + + +SHOW_ADMIN_DETAILS = PersistentConfig( + "SHOW_ADMIN_DETAILS", + "auth.admin.show", + os.environ.get("SHOW_ADMIN_DETAILS", "true").lower() == "true", +) + +ADMIN_EMAIL = PersistentConfig( + "ADMIN_EMAIL", + "auth.admin.email", + os.environ.get("ADMIN_EMAIL", None), +) + + +#################################### +# TASKS +#################################### + + +TASK_MODEL = PersistentConfig( + "TASK_MODEL", + "task.model.default", + os.environ.get("TASK_MODEL", ""), +) + +TASK_MODEL_EXTERNAL = PersistentConfig( + "TASK_MODEL_EXTERNAL", + "task.model.external", + os.environ.get("TASK_MODEL_EXTERNAL", ""), +) + +TITLE_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + "TITLE_GENERATION_PROMPT_TEMPLATE", + "task.title.prompt_template", + os.environ.get( + "TITLE_GENERATION_PROMPT_TEMPLATE", + """Here is the query: +{{prompt:middletruncate:8000}} + +Create a concise, 3-5 word phrase with an emoji as a title for the previous query. Suitable Emojis for the summary can be used to enhance understanding but avoid quotation marks or special formatting. RESPOND ONLY WITH THE TITLE TEXT. + +Examples of titles: +📉 Stock Market Trends +🍪 Perfect Chocolate Chip Recipe +Evolution of Music Streaming +Remote Work Productivity Tips +Artificial Intelligence in Healthcare +🎮 Video Game Development Insights""", + ), +) + + +SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = PersistentConfig( + "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE", + "task.search.prompt_template", + os.environ.get( + "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE", + """You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is {{CURRENT_DATE}}. + +Question: +{{prompt:end:4000}}""", + ), +) + +SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = PersistentConfig( + "SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD", + "task.search.prompt_length_threshold", + int( + os.environ.get( + "SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD", + 100, + ) + ), +) + +TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = PersistentConfig( + "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", + "task.tools.prompt_template", + os.environ.get( + "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE", + """Tools: {{TOOLS}} +If a function tool doesn't match the query, return an empty string. Else, pick a function tool, fill in the parameters from the function tool's schema, and return it in the format { "name": \"functionName\", "parameters": { "key": "value" } }. Only pick a function if the user asks. Only return the object. Do not return any other text.""", + ), +) + + +#################################### +# WEBUI_SECRET_KEY +#################################### + +WEBUI_SECRET_KEY = os.environ.get( + "WEBUI_SECRET_KEY", + os.environ.get( + "WEBUI_JWT_SECRET_KEY", "t0p-s3cr3t" + ), # DEPRECATED: remove at next major version +) + +WEBUI_SESSION_COOKIE_SAME_SITE = os.environ.get( + "WEBUI_SESSION_COOKIE_SAME_SITE", + os.environ.get("WEBUI_SESSION_COOKIE_SAME_SITE", "lax"), +) + +WEBUI_SESSION_COOKIE_SECURE = os.environ.get( + "WEBUI_SESSION_COOKIE_SECURE", + os.environ.get("WEBUI_SESSION_COOKIE_SECURE", "false").lower() == "true", +) + +if WEBUI_AUTH and WEBUI_SECRET_KEY == "": + raise ValueError(ERROR_MESSAGES.ENV_VAR_NOT_FOUND) + +#################################### +# RAG document content extraction +#################################### + +CONTENT_EXTRACTION_ENGINE = PersistentConfig( + "CONTENT_EXTRACTION_ENGINE", + "rag.CONTENT_EXTRACTION_ENGINE", + os.environ.get("CONTENT_EXTRACTION_ENGINE", "").lower(), +) + +TIKA_SERVER_URL = PersistentConfig( + "TIKA_SERVER_URL", + "rag.tika_server_url", + os.getenv("TIKA_SERVER_URL", "http://tika:9998"), # Default for sidecar deployment +) + +#################################### +# RAG +#################################### + +CHROMA_DATA_PATH = f"{DATA_DIR}/vector_db" +CHROMA_TENANT = os.environ.get("CHROMA_TENANT", chromadb.DEFAULT_TENANT) +CHROMA_DATABASE = os.environ.get("CHROMA_DATABASE", chromadb.DEFAULT_DATABASE) +CHROMA_HTTP_HOST = os.environ.get("CHROMA_HTTP_HOST", "") +CHROMA_HTTP_PORT = int(os.environ.get("CHROMA_HTTP_PORT", "8000")) +# Comma-separated list of header=value pairs +CHROMA_HTTP_HEADERS = os.environ.get("CHROMA_HTTP_HEADERS", "") +if CHROMA_HTTP_HEADERS: + CHROMA_HTTP_HEADERS = dict( + [pair.split("=") for pair in CHROMA_HTTP_HEADERS.split(",")] + ) +else: + CHROMA_HTTP_HEADERS = None +CHROMA_HTTP_SSL = os.environ.get("CHROMA_HTTP_SSL", "false").lower() == "true" +# 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 (sentence-transformers/all-MiniLM-L6-v2) + +RAG_TOP_K = PersistentConfig( + "RAG_TOP_K", "rag.top_k", int(os.environ.get("RAG_TOP_K", "5")) +) +RAG_RELEVANCE_THRESHOLD = PersistentConfig( + "RAG_RELEVANCE_THRESHOLD", + "rag.relevance_threshold", + float(os.environ.get("RAG_RELEVANCE_THRESHOLD", "0.0")), +) + +ENABLE_RAG_HYBRID_SEARCH = PersistentConfig( + "ENABLE_RAG_HYBRID_SEARCH", + "rag.enable_hybrid_search", + os.environ.get("ENABLE_RAG_HYBRID_SEARCH", "").lower() == "true", +) + +ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION = PersistentConfig( + "ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", + "rag.enable_web_loader_ssl_verification", + os.environ.get("ENABLE_RAG_WEB_LOADER_SSL_VERIFICATION", "True").lower() == "true", +) + +RAG_EMBEDDING_ENGINE = PersistentConfig( + "RAG_EMBEDDING_ENGINE", + "rag.embedding_engine", + os.environ.get("RAG_EMBEDDING_ENGINE", ""), +) + +PDF_EXTRACT_IMAGES = PersistentConfig( + "PDF_EXTRACT_IMAGES", + "rag.pdf_extract_images", + os.environ.get("PDF_EXTRACT_IMAGES", "False").lower() == "true", +) + +RAG_EMBEDDING_MODEL = PersistentConfig( + "RAG_EMBEDDING_MODEL", + "rag.embedding_model", + os.environ.get("RAG_EMBEDDING_MODEL", "sentence-transformers/all-MiniLM-L6-v2"), +) +log.info(f"Embedding model set: {RAG_EMBEDDING_MODEL.value}"), + +RAG_EMBEDDING_MODEL_AUTO_UPDATE = ( + os.environ.get("RAG_EMBEDDING_MODEL_AUTO_UPDATE", "").lower() == "true" +) + +RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE = ( + os.environ.get("RAG_EMBEDDING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true" +) + +RAG_EMBEDDING_OPENAI_BATCH_SIZE = PersistentConfig( + "RAG_EMBEDDING_OPENAI_BATCH_SIZE", + "rag.embedding_openai_batch_size", + os.environ.get("RAG_EMBEDDING_OPENAI_BATCH_SIZE", 1), +) + +RAG_RERANKING_MODEL = PersistentConfig( + "RAG_RERANKING_MODEL", + "rag.reranking_model", + os.environ.get("RAG_RERANKING_MODEL", ""), +) +if RAG_RERANKING_MODEL.value != "": + log.info(f"Reranking model set: {RAG_RERANKING_MODEL.value}"), + +RAG_RERANKING_MODEL_AUTO_UPDATE = ( + os.environ.get("RAG_RERANKING_MODEL_AUTO_UPDATE", "").lower() == "true" +) + +RAG_RERANKING_MODEL_TRUST_REMOTE_CODE = ( + os.environ.get("RAG_RERANKING_MODEL_TRUST_REMOTE_CODE", "").lower() == "true" +) + + +if CHROMA_HTTP_HOST != "": + CHROMA_CLIENT = chromadb.HttpClient( + host=CHROMA_HTTP_HOST, + port=CHROMA_HTTP_PORT, + headers=CHROMA_HTTP_HEADERS, + ssl=CHROMA_HTTP_SSL, + tenant=CHROMA_TENANT, + database=CHROMA_DATABASE, + settings=Settings(allow_reset=True, anonymized_telemetry=False), + ) +else: + CHROMA_CLIENT = chromadb.PersistentClient( + path=CHROMA_DATA_PATH, + settings=Settings(allow_reset=True, anonymized_telemetry=False), + tenant=CHROMA_TENANT, + database=CHROMA_DATABASE, + ) + + +# device type embedding 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" + +CHUNK_SIZE = PersistentConfig( + "CHUNK_SIZE", "rag.chunk_size", int(os.environ.get("CHUNK_SIZE", "1500")) +) +CHUNK_OVERLAP = PersistentConfig( + "CHUNK_OVERLAP", + "rag.chunk_overlap", + int(os.environ.get("CHUNK_OVERLAP", "100")), +) + +DEFAULT_RAG_TEMPLATE = """Use the following context as your learned knowledge, inside <context></context> XML tags. +<context> + [context] +</context> + +When answer to user: +- If you don't know, just say that you don't know. +- If you don't know when you are not sure, ask for clarification. +Avoid mentioning that you obtained the information from the context. +And answer according to the language of the user's question. + +Given the context information, answer the query. +Query: [query]""" + +RAG_TEMPLATE = PersistentConfig( + "RAG_TEMPLATE", + "rag.template", + os.environ.get("RAG_TEMPLATE", DEFAULT_RAG_TEMPLATE), +) + +RAG_OPENAI_API_BASE_URL = PersistentConfig( + "RAG_OPENAI_API_BASE_URL", + "rag.openai_api_base_url", + os.getenv("RAG_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +) +RAG_OPENAI_API_KEY = PersistentConfig( + "RAG_OPENAI_API_KEY", + "rag.openai_api_key", + os.getenv("RAG_OPENAI_API_KEY", OPENAI_API_KEY), +) + +ENABLE_RAG_LOCAL_WEB_FETCH = ( + os.getenv("ENABLE_RAG_LOCAL_WEB_FETCH", "False").lower() == "true" +) + +YOUTUBE_LOADER_LANGUAGE = PersistentConfig( + "YOUTUBE_LOADER_LANGUAGE", + "rag.youtube_loader_language", + os.getenv("YOUTUBE_LOADER_LANGUAGE", "en").split(","), +) + + +ENABLE_RAG_WEB_SEARCH = PersistentConfig( + "ENABLE_RAG_WEB_SEARCH", + "rag.web.search.enable", + os.getenv("ENABLE_RAG_WEB_SEARCH", "False").lower() == "true", +) + +RAG_WEB_SEARCH_ENGINE = PersistentConfig( + "RAG_WEB_SEARCH_ENGINE", + "rag.web.search.engine", + os.getenv("RAG_WEB_SEARCH_ENGINE", ""), +) + +# You can provide a list of your own websites to filter after performing a web search. +# This ensures the highest level of safety and reliability of the information sources. +RAG_WEB_SEARCH_DOMAIN_FILTER_LIST = PersistentConfig( + "RAG_WEB_SEARCH_DOMAIN_FILTER_LIST", + "rag.rag.web.search.domain.filter_list", + [ + # "wikipedia.com", + # "wikimedia.org", + # "wikidata.org", + ], +) + +SEARXNG_QUERY_URL = PersistentConfig( + "SEARXNG_QUERY_URL", + "rag.web.search.searxng_query_url", + os.getenv("SEARXNG_QUERY_URL", ""), +) + +GOOGLE_PSE_API_KEY = PersistentConfig( + "GOOGLE_PSE_API_KEY", + "rag.web.search.google_pse_api_key", + os.getenv("GOOGLE_PSE_API_KEY", ""), +) + +GOOGLE_PSE_ENGINE_ID = PersistentConfig( + "GOOGLE_PSE_ENGINE_ID", + "rag.web.search.google_pse_engine_id", + os.getenv("GOOGLE_PSE_ENGINE_ID", ""), +) + +BRAVE_SEARCH_API_KEY = PersistentConfig( + "BRAVE_SEARCH_API_KEY", + "rag.web.search.brave_search_api_key", + os.getenv("BRAVE_SEARCH_API_KEY", ""), +) + +SERPSTACK_API_KEY = PersistentConfig( + "SERPSTACK_API_KEY", + "rag.web.search.serpstack_api_key", + os.getenv("SERPSTACK_API_KEY", ""), +) + +SERPSTACK_HTTPS = PersistentConfig( + "SERPSTACK_HTTPS", + "rag.web.search.serpstack_https", + os.getenv("SERPSTACK_HTTPS", "True").lower() == "true", +) + +SERPER_API_KEY = PersistentConfig( + "SERPER_API_KEY", + "rag.web.search.serper_api_key", + os.getenv("SERPER_API_KEY", ""), +) + +SERPLY_API_KEY = PersistentConfig( + "SERPLY_API_KEY", + "rag.web.search.serply_api_key", + os.getenv("SERPLY_API_KEY", ""), +) + +TAVILY_API_KEY = PersistentConfig( + "TAVILY_API_KEY", + "rag.web.search.tavily_api_key", + os.getenv("TAVILY_API_KEY", ""), +) + +RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig( + "RAG_WEB_SEARCH_RESULT_COUNT", + "rag.web.search.result_count", + int(os.getenv("RAG_WEB_SEARCH_RESULT_COUNT", "3")), +) + +RAG_WEB_SEARCH_CONCURRENT_REQUESTS = PersistentConfig( + "RAG_WEB_SEARCH_CONCURRENT_REQUESTS", + "rag.web.search.concurrent_requests", + int(os.getenv("RAG_WEB_SEARCH_CONCURRENT_REQUESTS", "10")), +) + + +#################################### +# Transcribe +#################################### + +WHISPER_MODEL = os.getenv("WHISPER_MODEL", "base") +WHISPER_MODEL_DIR = os.getenv("WHISPER_MODEL_DIR", f"{CACHE_DIR}/whisper/models") +WHISPER_MODEL_AUTO_UPDATE = ( + os.environ.get("WHISPER_MODEL_AUTO_UPDATE", "").lower() == "true" +) + + +#################################### +# Images +#################################### + +IMAGE_GENERATION_ENGINE = PersistentConfig( + "IMAGE_GENERATION_ENGINE", + "image_generation.engine", + os.getenv("IMAGE_GENERATION_ENGINE", ""), +) + +ENABLE_IMAGE_GENERATION = PersistentConfig( + "ENABLE_IMAGE_GENERATION", + "image_generation.enable", + os.environ.get("ENABLE_IMAGE_GENERATION", "").lower() == "true", +) +AUTOMATIC1111_BASE_URL = PersistentConfig( + "AUTOMATIC1111_BASE_URL", + "image_generation.automatic1111.base_url", + os.getenv("AUTOMATIC1111_BASE_URL", ""), +) +AUTOMATIC1111_API_AUTH = PersistentConfig( + "AUTOMATIC1111_API_AUTH", + "image_generation.automatic1111.api_auth", + os.getenv("AUTOMATIC1111_API_AUTH", ""), +) + +COMFYUI_BASE_URL = PersistentConfig( + "COMFYUI_BASE_URL", + "image_generation.comfyui.base_url", + os.getenv("COMFYUI_BASE_URL", ""), +) + +COMFYUI_CFG_SCALE = PersistentConfig( + "COMFYUI_CFG_SCALE", + "image_generation.comfyui.cfg_scale", + os.getenv("COMFYUI_CFG_SCALE", ""), +) + +COMFYUI_SAMPLER = PersistentConfig( + "COMFYUI_SAMPLER", + "image_generation.comfyui.sampler", + os.getenv("COMFYUI_SAMPLER", ""), +) + +COMFYUI_SCHEDULER = PersistentConfig( + "COMFYUI_SCHEDULER", + "image_generation.comfyui.scheduler", + os.getenv("COMFYUI_SCHEDULER", ""), +) + +COMFYUI_SD3 = PersistentConfig( + "COMFYUI_SD3", + "image_generation.comfyui.sd3", + os.environ.get("COMFYUI_SD3", "").lower() == "true", +) + +IMAGES_OPENAI_API_BASE_URL = PersistentConfig( + "IMAGES_OPENAI_API_BASE_URL", + "image_generation.openai.api_base_url", + os.getenv("IMAGES_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +) +IMAGES_OPENAI_API_KEY = PersistentConfig( + "IMAGES_OPENAI_API_KEY", + "image_generation.openai.api_key", + os.getenv("IMAGES_OPENAI_API_KEY", OPENAI_API_KEY), +) + +IMAGE_SIZE = PersistentConfig( + "IMAGE_SIZE", "image_generation.size", os.getenv("IMAGE_SIZE", "512x512") +) + +IMAGE_STEPS = PersistentConfig( + "IMAGE_STEPS", "image_generation.steps", int(os.getenv("IMAGE_STEPS", 50)) +) + +IMAGE_GENERATION_MODEL = PersistentConfig( + "IMAGE_GENERATION_MODEL", + "image_generation.model", + os.getenv("IMAGE_GENERATION_MODEL", ""), +) + +#################################### +# Audio +#################################### + +AUDIO_STT_OPENAI_API_BASE_URL = PersistentConfig( + "AUDIO_STT_OPENAI_API_BASE_URL", + "audio.stt.openai.api_base_url", + os.getenv("AUDIO_STT_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +) + +AUDIO_STT_OPENAI_API_KEY = PersistentConfig( + "AUDIO_STT_OPENAI_API_KEY", + "audio.stt.openai.api_key", + os.getenv("AUDIO_STT_OPENAI_API_KEY", OPENAI_API_KEY), +) + +AUDIO_STT_ENGINE = PersistentConfig( + "AUDIO_STT_ENGINE", + "audio.stt.engine", + os.getenv("AUDIO_STT_ENGINE", ""), +) + +AUDIO_STT_MODEL = PersistentConfig( + "AUDIO_STT_MODEL", + "audio.stt.model", + os.getenv("AUDIO_STT_MODEL", "whisper-1"), +) + +AUDIO_TTS_OPENAI_API_BASE_URL = PersistentConfig( + "AUDIO_TTS_OPENAI_API_BASE_URL", + "audio.tts.openai.api_base_url", + os.getenv("AUDIO_TTS_OPENAI_API_BASE_URL", OPENAI_API_BASE_URL), +) +AUDIO_TTS_OPENAI_API_KEY = PersistentConfig( + "AUDIO_TTS_OPENAI_API_KEY", + "audio.tts.openai.api_key", + os.getenv("AUDIO_TTS_OPENAI_API_KEY", OPENAI_API_KEY), +) + +AUDIO_TTS_API_KEY = PersistentConfig( + "AUDIO_TTS_API_KEY", + "audio.tts.api_key", + os.getenv("AUDIO_TTS_API_KEY", ""), +) + +AUDIO_TTS_ENGINE = PersistentConfig( + "AUDIO_TTS_ENGINE", + "audio.tts.engine", + os.getenv("AUDIO_TTS_ENGINE", ""), +) + + +AUDIO_TTS_MODEL = PersistentConfig( + "AUDIO_TTS_MODEL", + "audio.tts.model", + os.getenv("AUDIO_TTS_MODEL", "tts-1"), +) + +AUDIO_TTS_VOICE = PersistentConfig( + "AUDIO_TTS_VOICE", + "audio.tts.voice", + os.getenv("AUDIO_TTS_VOICE", "alloy"), +) + + +#################################### +# Database +#################################### + +DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite:///{DATA_DIR}/webui.db") + +# Replace the postgres:// with postgresql:// +if "postgres://" in DATABASE_URL: + DATABASE_URL = DATABASE_URL.replace("postgres://", "postgresql://") diff --git a/backend/constants.py b/backend/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..b9c7fc430d42b4fb842b6f6360b71be3a88dd892 --- /dev/null +++ b/backend/constants.py @@ -0,0 +1,102 @@ +from enum import Enum + + +class MESSAGES(str, Enum): + DEFAULT = lambda msg="": f"{msg if msg else ''}" + MODEL_ADDED = lambda model="": f"The model '{model}' has been added successfully." + MODEL_DELETED = ( + lambda model="": f"The model '{model}' has been deleted successfully." + ) + + +class WEBHOOK_MESSAGES(str, Enum): + DEFAULT = lambda msg="": f"{msg if msg else ''}" + USER_SIGNUP = lambda username="": ( + f"New user signed up: {username}" if username else "New user signed up" + ) + + +class ERROR_MESSAGES(str, Enum): + def __str__(self) -> str: + return super().__str__() + + DEFAULT = lambda err="": f"Something went wrong :/\n{err if err else ''}" + ENV_VAR_NOT_FOUND = "Required environment variable not found. Terminating now." + CREATE_USER_ERROR = "Oops! Something went wrong while creating your account. Please try again later. If the issue persists, contact support for assistance." + DELETE_USER_ERROR = "Oops! Something went wrong. We encountered an issue while trying to delete the user. Please give it another shot." + EMAIL_MISMATCH = "Uh-oh! This email does not match the email your provider is registered with. Please check your email and try again." + EMAIL_TAKEN = "Uh-oh! This email is already registered. Sign in with your existing account or choose another email to start anew." + USERNAME_TAKEN = ( + "Uh-oh! This username is already registered. Please choose another username." + ) + COMMAND_TAKEN = "Uh-oh! This command is already registered. Please choose another command string." + FILE_EXISTS = "Uh-oh! This file is already registered. Please choose another file." + + ID_TAKEN = "Uh-oh! This id is already registered. Please choose another id string." + MODEL_ID_TAKEN = "Uh-oh! This model id is already registered. Please choose another model id string." + + NAME_TAG_TAKEN = "Uh-oh! This name tag is already registered. Please choose another name tag string." + INVALID_TOKEN = ( + "Your session has expired or the token is invalid. Please sign in again." + ) + INVALID_CRED = "The email or password provided is incorrect. Please check for typos and try logging in again." + INVALID_EMAIL_FORMAT = "The email format you entered is invalid. Please double-check and make sure you're using a valid email address (e.g., yourname@example.com)." + INVALID_PASSWORD = ( + "The password provided is incorrect. Please check for typos and try again." + ) + INVALID_TRUSTED_HEADER = "Your provider has not provided a trusted header. Please contact your administrator for assistance." + + EXISTING_USERS = "You can't turn off authentication because there are existing users. If you want to disable WEBUI_AUTH, make sure your web interface doesn't have any existing users and is a fresh installation." + + UNAUTHORIZED = "401 Unauthorized" + ACCESS_PROHIBITED = "You do not have permission to access this resource. Please contact your administrator for assistance." + ACTION_PROHIBITED = ( + "The requested action has been restricted as a security measure." + ) + + FILE_NOT_SENT = "FILE_NOT_SENT" + FILE_NOT_SUPPORTED = "Oops! It seems like the file format you're trying to upload is not supported. Please upload a file with a supported format (e.g., JPG, PNG, PDF, TXT) and try again." + + NOT_FOUND = "We could not find what you're looking for :/" + USER_NOT_FOUND = "We could not find what you're looking for :/" + API_KEY_NOT_FOUND = "Oops! It looks like there's a hiccup. The API key is missing. Please make sure to provide a valid API key to access this feature." + + MALICIOUS = "Unusual activities detected, please try again in a few minutes." + + PANDOC_NOT_INSTALLED = "Pandoc is not installed on the server. Please contact your administrator for assistance." + INCORRECT_FORMAT = ( + lambda err="": f"Invalid format. Please use the correct format{err}" + ) + RATE_LIMIT_EXCEEDED = "API rate limit exceeded" + + MODEL_NOT_FOUND = lambda name="": f"Model '{name}' was not found" + OPENAI_NOT_FOUND = lambda name="": "OpenAI API was not found" + OLLAMA_NOT_FOUND = "WebUI could not connect to Ollama" + CREATE_API_KEY_ERROR = "Oops! Something went wrong while creating your API key. Please try again later. If the issue persists, contact support for assistance." + + EMPTY_CONTENT = "The content provided is empty. Please ensure that there is text or data present before proceeding." + + DB_NOT_SQLITE = "This feature is only available when running with SQLite databases." + + INVALID_URL = ( + "Oops! The URL you provided is invalid. Please double-check and try again." + ) + + WEB_SEARCH_ERROR = ( + lambda err="": f"{err if err else 'Oops! Something went wrong while searching the web.'}" + ) + + OLLAMA_API_DISABLED = ( + "The Ollama API is disabled. Please enable it to use this feature." + ) + + +class TASKS(str, Enum): + def __str__(self) -> str: + return super().__str__() + + DEFAULT = lambda task="": f"{task if task else 'generation'}" + TITLE_GENERATION = "title_generation" + EMOJI_GENERATION = "emoji_generation" + QUERY_GENERATION = "query_generation" + FUNCTION_CALLING = "function_calling" diff --git a/backend/data/config.json b/backend/data/config.json new file mode 100644 index 0000000000000000000000000000000000000000..7c7acde9176d836edc614a8e144098292a673e30 --- /dev/null +++ b/backend/data/config.json @@ -0,0 +1,36 @@ +{ + "version": 0, + "ui": { + "default_locale": "", + "prompt_suggestions": [ + { + "title": ["Help me study", "vocabulary for a college entrance exam"], + "content": "Help me study vocabulary: write a sentence for me to fill in the blank, and I'll try to pick the correct option." + }, + { + "title": ["Give me ideas", "for what to do with my kids' art"], + "content": "What are 5 creative things I could do with my kids' art? I don't want to throw them away, but it's also so much clutter." + }, + { + "title": ["Tell me a fun fact", "about the Roman Empire"], + "content": "Tell me a random fun fact about the Roman Empire" + }, + { + "title": ["Show me a code snippet", "of a website's sticky header"], + "content": "Show me a code snippet of a website's sticky header in CSS and JavaScript." + }, + { + "title": ["Explain options trading", "if I'm familiar with buying and selling stocks"], + "content": "Explain options trading in simple terms if I'm familiar with buying and selling stocks." + }, + { + "title": ["Overcome procrastination", "give me tips"], + "content": "Could you start by asking me about instances when I procrastinate the most and then give me some suggestions to overcome it?" + }, + { + "title": ["Grammar check", "rewrite it for better readability "], + "content": "Check the following sentence for grammar and clarity: \"[sentence]\". Rewrite it for better readability while maintaining its original meaning." + } + ] + } +} diff --git a/backend/data/litellm/config.yaml b/backend/data/litellm/config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7d9d2b72304cd82b7621891affb2d79a0303cd72 --- /dev/null +++ b/backend/data/litellm/config.yaml @@ -0,0 +1,4 @@ +general_settings: {} +litellm_settings: {} +model_list: [] +router_settings: {} diff --git a/backend/dev.sh b/backend/dev.sh new file mode 100755 index 0000000000000000000000000000000000000000..c66ae4ba95ebf57d53834ed3afbe8b26d77ddac5 --- /dev/null +++ b/backend/dev.sh @@ -0,0 +1,2 @@ +PORT="${PORT:-8080}" +uvicorn main:app --port $PORT --host 0.0.0.0 --forwarded-allow-ips '*' --reload \ No newline at end of file diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000000000000000000000000000000000000..d1d96b001216b4a525b638412b67f0518099dbb0 --- /dev/null +++ b/backend/main.py @@ -0,0 +1,2308 @@ +import base64 +import uuid +from contextlib import asynccontextmanager + +from authlib.integrations.starlette_client import OAuth +from authlib.oidc.core import UserInfo +import json +import time +import os +import sys +import logging +import aiohttp +import requests +import mimetypes +import shutil +import os +import uuid +import inspect + +from fastapi import FastAPI, Request, Depends, status, UploadFile, File, Form +from fastapi.staticfiles import StaticFiles +from fastapi.responses import JSONResponse +from fastapi import HTTPException +from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text +from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.middleware.sessions import SessionMiddleware +from starlette.responses import StreamingResponse, Response, RedirectResponse + + +from apps.socket.main import sio, app as socket_app, get_event_emitter, get_event_call +from apps.ollama.main import ( + app as ollama_app, + get_all_models as get_ollama_models, + generate_openai_chat_completion as generate_ollama_chat_completion, +) +from apps.openai.main import ( + app as openai_app, + get_all_models as get_openai_models, + generate_chat_completion as generate_openai_chat_completion, +) + +from apps.audio.main import app as audio_app +from apps.images.main import app as images_app +from apps.rag.main import app as rag_app +from apps.webui.main import ( + app as webui_app, + get_pipe_models, + generate_function_chat_completion, +) +from apps.webui.internal.db import Session + + +from pydantic import BaseModel +from typing import List, Optional + +from apps.webui.models.auths import Auths +from apps.webui.models.models import Models +from apps.webui.models.tools import Tools +from apps.webui.models.functions import Functions +from apps.webui.models.users import Users + +from apps.webui.utils import load_toolkit_module_by_id, load_function_module_by_id + +from utils.utils import ( + get_admin_user, + get_verified_user, + get_current_user, + get_http_authorization_cred, + get_password_hash, + create_token, +) +from utils.task import ( + title_generation_template, + search_query_generation_template, + tools_function_calling_generation_template, +) +from utils.misc import ( + get_last_user_message, + add_or_update_system_message, + prepend_to_first_user_message_content, + parse_duration, +) + +from apps.rag.utils import get_rag_context, rag_template + +from config import ( + WEBUI_NAME, + WEBUI_URL, + WEBUI_AUTH, + ENV, + VERSION, + CHANGELOG, + FRONTEND_BUILD_DIR, + CACHE_DIR, + STATIC_DIR, + DEFAULT_LOCALE, + ENABLE_OPENAI_API, + ENABLE_OLLAMA_API, + ENABLE_MODEL_FILTER, + MODEL_FILTER_LIST, + GLOBAL_LOG_LEVEL, + SRC_LOG_LEVELS, + WEBHOOK_URL, + ENABLE_ADMIN_EXPORT, + WEBUI_BUILD_HASH, + TASK_MODEL, + TASK_MODEL_EXTERNAL, + TITLE_GENERATION_PROMPT_TEMPLATE, + SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE, + SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + SAFE_MODE, + OAUTH_PROVIDERS, + ENABLE_OAUTH_SIGNUP, + OAUTH_MERGE_ACCOUNTS_BY_EMAIL, + WEBUI_SECRET_KEY, + WEBUI_SESSION_COOKIE_SAME_SITE, + WEBUI_SESSION_COOKIE_SECURE, + AppConfig, +) + +from constants import ERROR_MESSAGES, WEBHOOK_MESSAGES, TASKS +from utils.webhook import post_webhook + +if SAFE_MODE: + print("SAFE MODE ENABLED") + Functions.deactivate_all_functions() + + +logging.basicConfig(stream=sys.stdout, level=GLOBAL_LOG_LEVEL) +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["MAIN"]) + + +class SPAStaticFiles(StaticFiles): + async def get_response(self, path: str, scope): + try: + return await super().get_response(path, scope) + except (HTTPException, StarletteHTTPException) as ex: + if ex.status_code == 404: + return await super().get_response("index.html", scope) + else: + raise ex + + +print( + rf""" + ___ __ __ _ _ _ ___ + / _ \ _ __ ___ _ __ \ \ / /__| |__ | | | |_ _| +| | | | '_ \ / _ \ '_ \ \ \ /\ / / _ \ '_ \| | | || | +| |_| | |_) | __/ | | | \ V V / __/ |_) | |_| || | + \___/| .__/ \___|_| |_| \_/\_/ \___|_.__/ \___/|___| + |_| + + +v{VERSION} - building the best open-source AI user interface. +{f"Commit: {WEBUI_BUILD_HASH}" if WEBUI_BUILD_HASH != "dev-build" else ""} +https://github.com/open-webui/open-webui +""" +) + + +def run_migrations(): + try: + from alembic.config import Config + from alembic import command + + alembic_cfg = Config("alembic.ini") + command.upgrade(alembic_cfg, "head") + except Exception as e: + print(f"Error: {e}") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + run_migrations() + yield + + +app = FastAPI( + docs_url="/docs" if ENV == "dev" else None, redoc_url=None, lifespan=lifespan +) + +app.state.config = AppConfig() + +app.state.config.ENABLE_OPENAI_API = ENABLE_OPENAI_API +app.state.config.ENABLE_OLLAMA_API = ENABLE_OLLAMA_API + +app.state.config.ENABLE_MODEL_FILTER = ENABLE_MODEL_FILTER +app.state.config.MODEL_FILTER_LIST = MODEL_FILTER_LIST + +app.state.config.WEBHOOK_URL = WEBHOOK_URL + + +app.state.config.TASK_MODEL = TASK_MODEL +app.state.config.TASK_MODEL_EXTERNAL = TASK_MODEL_EXTERNAL +app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = TITLE_GENERATION_PROMPT_TEMPLATE +app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = ( + SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE +) +app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = ( + SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD +) +app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE +) + +app.state.MODELS = {} + +origins = ["*"] + + +################################## +# +# ChatCompletion Middleware +# +################################## + + +async def get_body_and_model_and_user(request): + # Read the original request body + body = await request.body() + body_str = body.decode("utf-8") + body = json.loads(body_str) if body_str else {} + + model_id = body["model"] + if model_id not in app.state.MODELS: + raise Exception("Model not found") + model = app.state.MODELS[model_id] + + user = get_current_user( + request, + get_http_authorization_cred(request.headers.get("Authorization")), + ) + + return body, model, user + + +def get_task_model_id(default_model_id): + # Set the task model + task_model_id = default_model_id + # Check if the user has a custom task model and use that model + if app.state.MODELS[task_model_id]["owned_by"] == "ollama": + if ( + app.state.config.TASK_MODEL + and app.state.config.TASK_MODEL in app.state.MODELS + ): + task_model_id = app.state.config.TASK_MODEL + else: + if ( + app.state.config.TASK_MODEL_EXTERNAL + and app.state.config.TASK_MODEL_EXTERNAL in app.state.MODELS + ): + task_model_id = app.state.config.TASK_MODEL_EXTERNAL + + return task_model_id + + +def get_filter_function_ids(model): + def get_priority(function_id): + function = Functions.get_function_by_id(function_id) + if function is not None and hasattr(function, "valves"): + return (function.valves if function.valves else {}).get("priority", 0) + return 0 + + filter_ids = [function.id for function in Functions.get_global_filter_functions()] + if "info" in model and "meta" in model["info"]: + filter_ids.extend(model["info"]["meta"].get("filterIds", [])) + filter_ids = list(set(filter_ids)) + + enabled_filter_ids = [ + function.id + for function in Functions.get_functions_by_type("filter", active_only=True) + ] + + filter_ids = [ + filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids + ] + + filter_ids.sort(key=get_priority) + return filter_ids + + +async def get_function_call_response( + messages, + files, + tool_id, + template, + task_model_id, + user, + __event_emitter__=None, + __event_call__=None, +): + tool = Tools.get_tool_by_id(tool_id) + tools_specs = json.dumps(tool.specs, indent=2) + content = tools_function_calling_generation_template(template, tools_specs) + + user_message = get_last_user_message(messages) + prompt = ( + "History:\n" + + "\n".join( + [ + f"{message['role'].upper()}: \"\"\"{message['content']}\"\"\"" + for message in messages[::-1][:4] + ] + ) + + f"\nQuery: {user_message}" + ) + + print(prompt) + + payload = { + "model": task_model_id, + "messages": [ + {"role": "system", "content": content}, + {"role": "user", "content": f"Query: {prompt}"}, + ], + "stream": False, + "task": str(TASKS.FUNCTION_CALLING), + } + + try: + payload = filter_pipeline(payload, user) + except Exception as e: + raise e + + model = app.state.MODELS[task_model_id] + + response = None + try: + response = await generate_chat_completions(form_data=payload, user=user) + content = None + + if hasattr(response, "body_iterator"): + async for chunk in response.body_iterator: + data = json.loads(chunk.decode("utf-8")) + content = data["choices"][0]["message"]["content"] + + # Cleanup any remaining background tasks if necessary + if response.background is not None: + await response.background() + else: + content = response["choices"][0]["message"]["content"] + + if content is None: + return None, None, False + + # Parse the function response + print(f"content: {content}") + result = json.loads(content) + print(result) + + citation = None + + if "name" not in result: + return None, None, False + + # Call the function + if tool_id in webui_app.state.TOOLS: + toolkit_module = webui_app.state.TOOLS[tool_id] + else: + toolkit_module, _ = load_toolkit_module_by_id(tool_id) + webui_app.state.TOOLS[tool_id] = toolkit_module + + file_handler = False + # check if toolkit_module has file_handler self variable + if hasattr(toolkit_module, "file_handler"): + file_handler = True + print("file_handler: ", file_handler) + + if hasattr(toolkit_module, "valves") and hasattr(toolkit_module, "Valves"): + valves = Tools.get_tool_valves_by_id(tool_id) + toolkit_module.valves = toolkit_module.Valves(**(valves if valves else {})) + + function = getattr(toolkit_module, result["name"]) + function_result = None + try: + # Get the signature of the function + sig = inspect.signature(function) + params = result["parameters"] + + # Extra parameters to be passed to the function + extra_params = { + "__model__": model, + "__id__": tool_id, + "__messages__": messages, + "__files__": files, + "__event_emitter__": __event_emitter__, + "__event_call__": __event_call__, + } + + # Add extra params in contained in function signature + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if "__user__" in sig.parameters: + # Call the function with the '__user__' parameter included + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(toolkit_module, "UserValves"): + __user__["valves"] = toolkit_module.UserValves( + **Tools.get_user_valves_by_id_and_user_id(tool_id, user.id) + ) + except Exception as e: + print(e) + + params = {**params, "__user__": __user__} + + if inspect.iscoroutinefunction(function): + function_result = await function(**params) + else: + function_result = function(**params) + + if hasattr(toolkit_module, "citation") and toolkit_module.citation: + citation = { + "source": {"name": f"TOOL:{tool.name}/{result['name']}"}, + "document": [function_result], + "metadata": [{"source": result["name"]}], + } + except Exception as e: + print(e) + + # Add the function result to the system prompt + if function_result is not None: + return function_result, citation, file_handler + except Exception as e: + print(f"Error: {e}") + + return None, None, False + + +async def chat_completion_functions_handler( + body, model, user, __event_emitter__, __event_call__ +): + skip_files = None + + filter_ids = get_filter_function_ids(model) + for filter_id in filter_ids: + filter = Functions.get_function_by_id(filter_id) + if not filter: + continue + + if filter_id in webui_app.state.FUNCTIONS: + function_module = webui_app.state.FUNCTIONS[filter_id] + else: + function_module, _, _ = load_function_module_by_id(filter_id) + webui_app.state.FUNCTIONS[filter_id] = function_module + + # Check if the function has a file_handler variable + if hasattr(function_module, "file_handler"): + skip_files = function_module.file_handler + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + if not hasattr(function_module, "inlet"): + continue + + try: + inlet = function_module.inlet + + # Get the signature of the function + sig = inspect.signature(inlet) + params = {"body": body} + + # Extra parameters to be passed to the function + extra_params = { + "__model__": model, + "__id__": filter_id, + "__event_emitter__": __event_emitter__, + "__event_call__": __event_call__, + } + + # Add extra params in contained in function signature + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if "__user__" in sig.parameters: + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(function_module, "UserValves"): + __user__["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + filter_id, user.id + ) + ) + except Exception as e: + print(e) + + params = {**params, "__user__": __user__} + + if inspect.iscoroutinefunction(inlet): + body = await inlet(**params) + else: + body = inlet(**params) + + except Exception as e: + print(f"Error: {e}") + raise e + + if skip_files: + if "files" in body: + del body["files"] + + return body, {} + + +async def chat_completion_tools_handler(body, user, __event_emitter__, __event_call__): + skip_files = None + + contexts = [] + citations = None + + task_model_id = get_task_model_id(body["model"]) + + # If tool_ids field is present, call the functions + if "tool_ids" in body: + print(body["tool_ids"]) + for tool_id in body["tool_ids"]: + print(tool_id) + try: + response, citation, file_handler = await get_function_call_response( + messages=body["messages"], + files=body.get("files", []), + tool_id=tool_id, + template=app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + task_model_id=task_model_id, + user=user, + __event_emitter__=__event_emitter__, + __event_call__=__event_call__, + ) + + print(file_handler) + if isinstance(response, str): + contexts.append(response) + + if citation: + if citations is None: + citations = [citation] + else: + citations.append(citation) + + if file_handler: + skip_files = True + + except Exception as e: + print(f"Error: {e}") + del body["tool_ids"] + print(f"tool_contexts: {contexts}") + + if skip_files: + if "files" in body: + del body["files"] + + return body, { + **({"contexts": contexts} if contexts is not None else {}), + **({"citations": citations} if citations is not None else {}), + } + + +async def chat_completion_files_handler(body): + contexts = [] + citations = None + + if "files" in body: + files = body["files"] + del body["files"] + + contexts, citations = get_rag_context( + files=files, + messages=body["messages"], + embedding_function=rag_app.state.EMBEDDING_FUNCTION, + k=rag_app.state.config.TOP_K, + reranking_function=rag_app.state.sentence_transformer_rf, + r=rag_app.state.config.RELEVANCE_THRESHOLD, + hybrid_search=rag_app.state.config.ENABLE_RAG_HYBRID_SEARCH, + ) + + log.debug(f"rag_contexts: {contexts}, citations: {citations}") + + return body, { + **({"contexts": contexts} if contexts is not None else {}), + **({"citations": citations} if citations is not None else {}), + } + + +class ChatCompletionMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.method == "POST" and any( + endpoint in request.url.path + for endpoint in ["/ollama/api/chat", "/chat/completions"] + ): + log.debug(f"request.url.path: {request.url.path}") + + try: + body, model, user = await get_body_and_model_and_user(request) + except Exception as e: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) + + # Extract valves from the request body + valves = None + if "valves" in body: + valves = body["valves"] + del body["valves"] + + # Extract session_id, chat_id and message_id from the request body + session_id = None + if "session_id" in body: + session_id = body["session_id"] + del body["session_id"] + chat_id = None + if "chat_id" in body: + chat_id = body["chat_id"] + del body["chat_id"] + message_id = None + if "id" in body: + message_id = body["id"] + del body["id"] + + __event_emitter__ = await get_event_emitter( + {"chat_id": chat_id, "message_id": message_id, "session_id": session_id} + ) + __event_call__ = await get_event_call( + {"chat_id": chat_id, "message_id": message_id, "session_id": session_id} + ) + + # Initialize data_items to store additional data to be sent to the client + data_items = [] + + # Initialize context, and citations + contexts = [] + citations = [] + + try: + body, flags = await chat_completion_functions_handler( + body, model, user, __event_emitter__, __event_call__ + ) + except Exception as e: + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) + + try: + body, flags = await chat_completion_tools_handler( + body, user, __event_emitter__, __event_call__ + ) + + contexts.extend(flags.get("contexts", [])) + citations.extend(flags.get("citations", [])) + except Exception as e: + print(e) + pass + + try: + body, flags = await chat_completion_files_handler(body) + + contexts.extend(flags.get("contexts", [])) + citations.extend(flags.get("citations", [])) + except Exception as e: + print(e) + pass + + # If context is not empty, insert it into the messages + if len(contexts) > 0: + context_string = "/n".join(contexts).strip() + prompt = get_last_user_message(body["messages"]) + + # Workaround for Ollama 2.0+ system prompt issue + # TODO: replace with add_or_update_system_message + if model["owned_by"] == "ollama": + body["messages"] = prepend_to_first_user_message_content( + rag_template( + rag_app.state.config.RAG_TEMPLATE, context_string, prompt + ), + body["messages"], + ) + else: + body["messages"] = add_or_update_system_message( + rag_template( + rag_app.state.config.RAG_TEMPLATE, context_string, prompt + ), + body["messages"], + ) + + # If there are citations, add them to the data_items + if len(citations) > 0: + data_items.append({"citations": citations}) + + body["metadata"] = { + "session_id": session_id, + "chat_id": chat_id, + "message_id": message_id, + "valves": valves, + } + + modified_body_bytes = json.dumps(body).encode("utf-8") + # Replace the request body with the modified one + request._body = modified_body_bytes + # Set custom header to ensure content-length matches new body length + request.headers.__dict__["_list"] = [ + (b"content-length", str(len(modified_body_bytes)).encode("utf-8")), + *[ + (k, v) + for k, v in request.headers.raw + if k.lower() != b"content-length" + ], + ] + + response = await call_next(request) + if isinstance(response, StreamingResponse): + # If it's a streaming response, inject it as SSE event or NDJSON line + content_type = response.headers.get("Content-Type") + if "text/event-stream" in content_type: + return StreamingResponse( + self.openai_stream_wrapper(response.body_iterator, data_items), + ) + if "application/x-ndjson" in content_type: + return StreamingResponse( + self.ollama_stream_wrapper(response.body_iterator, data_items), + ) + + return response + else: + return response + + # If it's not a chat completion request, just pass it through + response = await call_next(request) + return response + + async def _receive(self, body: bytes): + return {"type": "http.request", "body": body, "more_body": False} + + async def openai_stream_wrapper(self, original_generator, data_items): + for item in data_items: + yield f"data: {json.dumps(item)}\n\n" + + async for data in original_generator: + yield data + + async def ollama_stream_wrapper(self, original_generator, data_items): + for item in data_items: + yield f"{json.dumps(item)}\n" + + async for data in original_generator: + yield data + + +app.add_middleware(ChatCompletionMiddleware) + +################################## +# +# Pipeline Middleware +# +################################## + + +def get_sorted_filters(model_id): + filters = [ + model + for model in app.state.MODELS.values() + if "pipeline" in model + and "type" in model["pipeline"] + and model["pipeline"]["type"] == "filter" + and ( + model["pipeline"]["pipelines"] == ["*"] + or any( + model_id == target_model_id + for target_model_id in model["pipeline"]["pipelines"] + ) + ) + ] + sorted_filters = sorted(filters, key=lambda x: x["pipeline"]["priority"]) + return sorted_filters + + +def filter_pipeline(payload, user): + user = {"id": user.id, "email": user.email, "name": user.name, "role": user.role} + model_id = payload["model"] + sorted_filters = get_sorted_filters(model_id) + + model = app.state.MODELS[model_id] + + if "pipeline" in model: + sorted_filters.append(model) + + for filter in sorted_filters: + r = None + try: + urlIdx = filter["urlIdx"] + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + if key != "": + headers = {"Authorization": f"Bearer {key}"} + r = requests.post( + f"{url}/{filter['id']}/filter/inlet", + headers=headers, + json={ + "user": user, + "body": payload, + }, + ) + + r.raise_for_status() + payload = r.json() + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + if r is not None: + res = r.json() + if "detail" in res: + raise Exception(r.status_code, res["detail"]) + + return payload + + +class PipelineMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + if request.method == "POST" and ( + "/ollama/api/chat" in request.url.path + or "/chat/completions" in request.url.path + ): + log.debug(f"request.url.path: {request.url.path}") + + # Read the original request body + body = await request.body() + # Decode body to string + body_str = body.decode("utf-8") + # Parse string to JSON + data = json.loads(body_str) if body_str else {} + + user = get_current_user( + request, + get_http_authorization_cred(request.headers.get("Authorization")), + ) + + try: + data = filter_pipeline(data, user) + except Exception as e: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + + modified_body_bytes = json.dumps(data).encode("utf-8") + # Replace the request body with the modified one + request._body = modified_body_bytes + # Set custom header to ensure content-length matches new body length + request.headers.__dict__["_list"] = [ + (b"content-length", str(len(modified_body_bytes)).encode("utf-8")), + *[ + (k, v) + for k, v in request.headers.raw + if k.lower() != b"content-length" + ], + ] + + response = await call_next(request) + return response + + async def _receive(self, body: bytes): + return {"type": "http.request", "body": body, "more_body": False} + + +app.add_middleware(PipelineMiddleware) + + +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.middleware("http") +async def commit_session_after_request(request: Request, call_next): + response = await call_next(request) + log.debug("Commit session after request") + Session.commit() + return response + + +@app.middleware("http") +async def check_url(request: Request, call_next): + if len(app.state.MODELS) == 0: + await get_all_models() + else: + pass + + start_time = int(time.time()) + response = await call_next(request) + process_time = int(time.time()) - start_time + response.headers["X-Process-Time"] = str(process_time) + + return response + + +@app.middleware("http") +async def update_embedding_function(request: Request, call_next): + response = await call_next(request) + if "/embedding/update" in request.url.path: + webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION + return response + + +app.mount("/ws", socket_app) + +app.mount("/ollama", ollama_app) +app.mount("/openai", openai_app) + +app.mount("/images/api/v1", images_app) +app.mount("/audio/api/v1", audio_app) +app.mount("/rag/api/v1", rag_app) + +app.mount("/api/v1", webui_app) + +webui_app.state.EMBEDDING_FUNCTION = rag_app.state.EMBEDDING_FUNCTION + + +async def get_all_models(): + # TODO: Optimize this function + pipe_models = [] + openai_models = [] + ollama_models = [] + + pipe_models = await get_pipe_models() + + if app.state.config.ENABLE_OPENAI_API: + openai_models = await get_openai_models() + openai_models = openai_models["data"] + + if app.state.config.ENABLE_OLLAMA_API: + ollama_models = await get_ollama_models() + ollama_models = [ + { + "id": model["model"], + "name": model["name"], + "object": "model", + "created": int(time.time()), + "owned_by": "ollama", + "ollama": model, + } + for model in ollama_models["models"] + ] + + models = pipe_models + openai_models + ollama_models + + global_action_ids = [ + function.id for function in Functions.get_global_action_functions() + ] + enabled_action_ids = [ + function.id + for function in Functions.get_functions_by_type("action", active_only=True) + ] + + custom_models = Models.get_all_models() + for custom_model in custom_models: + if custom_model.base_model_id == None: + for model in models: + if ( + custom_model.id == model["id"] + or custom_model.id == model["id"].split(":")[0] + ): + model["name"] = custom_model.name + model["info"] = custom_model.model_dump() + + action_ids = [] + if "info" in model and "meta" in model["info"]: + action_ids.extend(model["info"]["meta"].get("actionIds", [])) + + model["action_ids"] = action_ids + else: + owned_by = "openai" + pipe = None + action_ids = [] + + for model in models: + if ( + custom_model.base_model_id == model["id"] + or custom_model.base_model_id == model["id"].split(":")[0] + ): + owned_by = model["owned_by"] + if "pipe" in model: + pipe = model["pipe"] + + if "info" in model and "meta" in model["info"]: + action_ids.extend(model["info"]["meta"].get("actionIds", [])) + break + + models.append( + { + "id": custom_model.id, + "name": custom_model.name, + "object": "model", + "created": custom_model.created_at, + "owned_by": owned_by, + "info": custom_model.model_dump(), + "preset": True, + **({"pipe": pipe} if pipe is not None else {}), + "action_ids": action_ids, + } + ) + + for model in models: + action_ids = [] + if "action_ids" in model: + action_ids = model["action_ids"] + del model["action_ids"] + + action_ids = action_ids + global_action_ids + action_ids = list(set(action_ids)) + action_ids = [ + action_id for action_id in action_ids if action_id in enabled_action_ids + ] + + model["actions"] = [] + for action_id in action_ids: + action = Functions.get_function_by_id(action_id) + model["actions"].append( + { + "id": action_id, + "name": action.name, + "description": action.meta.description, + "icon_url": action.meta.manifest.get("icon_url", None), + } + ) + + app.state.MODELS = {model["id"]: model for model in models} + webui_app.state.MODELS = app.state.MODELS + + return models + + +@app.get("/api/models") +async def get_models(user=Depends(get_verified_user)): + models = await get_all_models() + + # Filter out filter pipelines + models = [ + model + for model in models + if "pipeline" not in model or model["pipeline"].get("type", None) != "filter" + ] + + if app.state.config.ENABLE_MODEL_FILTER: + if user.role == "user": + models = list( + filter( + lambda model: model["id"] in app.state.config.MODEL_FILTER_LIST, + models, + ) + ) + return {"data": models} + + return {"data": models} + + +@app.post("/api/chat/completions") +async def generate_chat_completions(form_data: dict, user=Depends(get_verified_user)): + model_id = form_data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + model = app.state.MODELS[model_id] + + # `task` field is used to determine the type of the request, e.g. `title_generation`, `query_generation`, etc. + task = None + if "task" in form_data: + task = form_data["task"] + del form_data["task"] + + if task: + if "metadata" in form_data: + form_data["metadata"]["task"] = task + else: + form_data["metadata"] = {"task": task} + + if model.get("pipe"): + return await generate_function_chat_completion(form_data, user=user) + if model["owned_by"] == "ollama": + print("generate_ollama_chat_completion") + return await generate_ollama_chat_completion(form_data, user=user) + else: + return await generate_openai_chat_completion(form_data, user=user) + + +@app.post("/api/chat/completed") +async def chat_completed(form_data: dict, user=Depends(get_verified_user)): + data = form_data + model_id = data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + model = app.state.MODELS[model_id] + + sorted_filters = get_sorted_filters(model_id) + if "pipeline" in model: + sorted_filters = [model] + sorted_filters + + for filter in sorted_filters: + r = None + try: + urlIdx = filter["urlIdx"] + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + if key != "": + headers = {"Authorization": f"Bearer {key}"} + r = requests.post( + f"{url}/{filter['id']}/filter/outlet", + headers=headers, + json={ + "user": { + "id": user.id, + "name": user.name, + "email": user.email, + "role": user.role, + }, + "body": data, + }, + ) + + r.raise_for_status() + data = r.json() + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + if r is not None: + try: + res = r.json() + if "detail" in res: + return JSONResponse( + status_code=r.status_code, + content=res, + ) + except: + pass + + else: + pass + + __event_emitter__ = await get_event_emitter( + { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + } + ) + + __event_call__ = await get_event_call( + { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + } + ) + + def get_priority(function_id): + function = Functions.get_function_by_id(function_id) + if function is not None and hasattr(function, "valves"): + return (function.valves if function.valves else {}).get("priority", 0) + return 0 + + filter_ids = [function.id for function in Functions.get_global_filter_functions()] + if "info" in model and "meta" in model["info"]: + filter_ids.extend(model["info"]["meta"].get("filterIds", [])) + filter_ids = list(set(filter_ids)) + + enabled_filter_ids = [ + function.id + for function in Functions.get_functions_by_type("filter", active_only=True) + ] + filter_ids = [ + filter_id for filter_id in filter_ids if filter_id in enabled_filter_ids + ] + + # Sort filter_ids by priority, using the get_priority function + filter_ids.sort(key=get_priority) + + for filter_id in filter_ids: + filter = Functions.get_function_by_id(filter_id) + if not filter: + continue + + if filter_id in webui_app.state.FUNCTIONS: + function_module = webui_app.state.FUNCTIONS[filter_id] + else: + function_module, _, _ = load_function_module_by_id(filter_id) + webui_app.state.FUNCTIONS[filter_id] = function_module + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(filter_id) + function_module.valves = function_module.Valves( + **(valves if valves else {}) + ) + + if not hasattr(function_module, "outlet"): + continue + try: + outlet = function_module.outlet + + # Get the signature of the function + sig = inspect.signature(outlet) + params = {"body": data} + + # Extra parameters to be passed to the function + extra_params = { + "__model__": model, + "__id__": filter_id, + "__event_emitter__": __event_emitter__, + "__event_call__": __event_call__, + } + + # Add extra params in contained in function signature + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if "__user__" in sig.parameters: + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(function_module, "UserValves"): + __user__["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + filter_id, user.id + ) + ) + except Exception as e: + print(e) + + params = {**params, "__user__": __user__} + + if inspect.iscoroutinefunction(outlet): + data = await outlet(**params) + else: + data = outlet(**params) + + except Exception as e: + print(f"Error: {e}") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) + + return data + + +@app.post("/api/chat/actions/{action_id}") +async def chat_completed( + action_id: str, form_data: dict, user=Depends(get_verified_user) +): + action = Functions.get_function_by_id(action_id) + if not action: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Action not found", + ) + + data = form_data + model_id = data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + model = app.state.MODELS[model_id] + + __event_emitter__ = await get_event_emitter( + { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + } + ) + __event_call__ = await get_event_call( + { + "chat_id": data["chat_id"], + "message_id": data["id"], + "session_id": data["session_id"], + } + ) + + if action_id in webui_app.state.FUNCTIONS: + function_module = webui_app.state.FUNCTIONS[action_id] + else: + function_module, _, _ = load_function_module_by_id(action_id) + webui_app.state.FUNCTIONS[action_id] = function_module + + if hasattr(function_module, "valves") and hasattr(function_module, "Valves"): + valves = Functions.get_function_valves_by_id(action_id) + function_module.valves = function_module.Valves(**(valves if valves else {})) + + if hasattr(function_module, "action"): + try: + action = function_module.action + + # Get the signature of the function + sig = inspect.signature(action) + params = {"body": data} + + # Extra parameters to be passed to the function + extra_params = { + "__model__": model, + "__id__": action_id, + "__event_emitter__": __event_emitter__, + "__event_call__": __event_call__, + } + + # Add extra params in contained in function signature + for key, value in extra_params.items(): + if key in sig.parameters: + params[key] = value + + if "__user__" in sig.parameters: + __user__ = { + "id": user.id, + "email": user.email, + "name": user.name, + "role": user.role, + } + + try: + if hasattr(function_module, "UserValves"): + __user__["valves"] = function_module.UserValves( + **Functions.get_user_valves_by_id_and_user_id( + action_id, user.id + ) + ) + except Exception as e: + print(e) + + params = {**params, "__user__": __user__} + + if inspect.iscoroutinefunction(action): + data = await action(**params) + else: + data = action(**params) + + except Exception as e: + print(f"Error: {e}") + return JSONResponse( + status_code=status.HTTP_400_BAD_REQUEST, + content={"detail": str(e)}, + ) + + return data + + +################################## +# +# Task Endpoints +# +################################## + + +# TODO: Refactor task API endpoints below into a separate file + + +@app.get("/api/task/config") +async def get_task_config(user=Depends(get_verified_user)): + return { + "TASK_MODEL": app.state.config.TASK_MODEL, + "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL, + "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE, + "SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, + "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + } + + +class TaskConfigForm(BaseModel): + TASK_MODEL: Optional[str] + TASK_MODEL_EXTERNAL: Optional[str] + TITLE_GENERATION_PROMPT_TEMPLATE: str + SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: str + SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: int + TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE: str + + +@app.post("/api/task/config/update") +async def update_task_config(form_data: TaskConfigForm, user=Depends(get_admin_user)): + app.state.config.TASK_MODEL = form_data.TASK_MODEL + app.state.config.TASK_MODEL_EXTERNAL = form_data.TASK_MODEL_EXTERNAL + app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE = ( + form_data.TITLE_GENERATION_PROMPT_TEMPLATE + ) + app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE = ( + form_data.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE + ) + app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD = ( + form_data.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD + ) + app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE = ( + form_data.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE + ) + + return { + "TASK_MODEL": app.state.config.TASK_MODEL, + "TASK_MODEL_EXTERNAL": app.state.config.TASK_MODEL_EXTERNAL, + "TITLE_GENERATION_PROMPT_TEMPLATE": app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE, + "SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE": app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE, + "SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD": app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD, + "TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE": app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE, + } + + +@app.post("/api/task/title/completions") +async def generate_title(form_data: dict, user=Depends(get_verified_user)): + print("generate_title") + + model_id = form_data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + model_id = get_task_model_id(model_id) + + print(model_id) + + template = app.state.config.TITLE_GENERATION_PROMPT_TEMPLATE + + content = title_generation_template( + template, + form_data["prompt"], + { + "name": user.name, + "location": user.info.get("location") if user.info else None, + }, + ) + + payload = { + "model": model_id, + "messages": [{"role": "user", "content": content}], + "stream": False, + "max_tokens": 50, + "chat_id": form_data.get("chat_id", None), + "task": str(TASKS.TITLE_GENERATION), + } + + log.debug(payload) + + try: + payload = filter_pipeline(payload, user) + except Exception as e: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + + if "chat_id" in payload: + del payload["chat_id"] + + return await generate_chat_completions(form_data=payload, user=user) + + +@app.post("/api/task/query/completions") +async def generate_search_query(form_data: dict, user=Depends(get_verified_user)): + print("generate_search_query") + + if len(form_data["prompt"]) < app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Skip search query generation for short prompts (< {app.state.config.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} characters)", + ) + + model_id = form_data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + model_id = get_task_model_id(model_id) + + print(model_id) + + template = app.state.config.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE + + content = search_query_generation_template( + template, form_data["prompt"], {"name": user.name} + ) + + payload = { + "model": model_id, + "messages": [{"role": "user", "content": content}], + "stream": False, + "max_tokens": 30, + "task": str(TASKS.QUERY_GENERATION), + } + + print(payload) + + try: + payload = filter_pipeline(payload, user) + except Exception as e: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + + if "chat_id" in payload: + del payload["chat_id"] + + return await generate_chat_completions(form_data=payload, user=user) + + +@app.post("/api/task/emoji/completions") +async def generate_emoji(form_data: dict, user=Depends(get_verified_user)): + print("generate_emoji") + + model_id = form_data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + model_id = get_task_model_id(model_id) + + print(model_id) + + template = ''' +Your task is to reflect the speaker's likely facial expression through a fitting emoji. Interpret emotions from the message and reflect their facial expression using fitting, diverse emojis (e.g., 😊, 😢, 😡, 😱). + +Message: """{{prompt}}""" +''' + + content = title_generation_template( + template, + form_data["prompt"], + { + "name": user.name, + "location": user.info.get("location") if user.info else None, + }, + ) + + payload = { + "model": model_id, + "messages": [{"role": "user", "content": content}], + "stream": False, + "max_tokens": 4, + "chat_id": form_data.get("chat_id", None), + "task": str(TASKS.EMOJI_GENERATION), + } + + log.debug(payload) + + try: + payload = filter_pipeline(payload, user) + except Exception as e: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + + if "chat_id" in payload: + del payload["chat_id"] + + return await generate_chat_completions(form_data=payload, user=user) + + +@app.post("/api/task/tools/completions") +async def get_tools_function_calling(form_data: dict, user=Depends(get_verified_user)): + print("get_tools_function_calling") + + model_id = form_data["model"] + if model_id not in app.state.MODELS: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Model not found", + ) + + # Check if the user has a custom task model + # If the user has a custom task model, use that model + model_id = get_task_model_id(model_id) + + print(model_id) + template = app.state.config.TOOLS_FUNCTION_CALLING_PROMPT_TEMPLATE + + try: + context, _, _ = await get_function_call_response( + form_data["messages"], + form_data.get("files", []), + form_data["tool_id"], + template, + model_id, + user, + ) + return context + except Exception as e: + return JSONResponse( + status_code=e.args[0], + content={"detail": e.args[1]}, + ) + + +################################## +# +# Pipelines Endpoints +# +################################## + + +# TODO: Refactor pipelines API endpoints below into a separate file + + +@app.get("/api/pipelines/list") +async def get_pipelines_list(user=Depends(get_admin_user)): + responses = await get_openai_models(raw=True) + + print(responses) + urlIdxs = [ + idx + for idx, response in enumerate(responses) + if response != None and "pipelines" in response + ] + + return { + "data": [ + { + "url": openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx], + "idx": urlIdx, + } + for urlIdx in urlIdxs + ] + } + + +@app.post("/api/pipelines/upload") +async def upload_pipeline( + urlIdx: int = Form(...), file: UploadFile = File(...), user=Depends(get_admin_user) +): + print("upload_pipeline", urlIdx, file.filename) + # Check if the uploaded file is a python file + if not file.filename.endswith(".py"): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only Python (.py) files are allowed.", + ) + + upload_folder = f"{CACHE_DIR}/pipelines" + os.makedirs(upload_folder, exist_ok=True) + file_path = os.path.join(upload_folder, file.filename) + + r = None + try: + # Save the uploaded file + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + + with open(file_path, "rb") as f: + files = {"file": f} + r = requests.post(f"{url}/pipelines/upload", headers=headers, files=files) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + status_code = status.HTTP_404_NOT_FOUND + if r is not None: + status_code = r.status_code + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=status_code, + detail=detail, + ) + finally: + # Ensure the file is deleted after the upload is completed or on failure + if os.path.exists(file_path): + os.remove(file_path) + + +class AddPipelineForm(BaseModel): + url: str + urlIdx: int + + +@app.post("/api/pipelines/add") +async def add_pipeline(form_data: AddPipelineForm, user=Depends(get_admin_user)): + + r = None + try: + urlIdx = form_data.urlIdx + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.post( + f"{url}/pipelines/add", headers=headers, json={"url": form_data.url} + ) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +class DeletePipelineForm(BaseModel): + id: str + urlIdx: int + + +@app.delete("/api/pipelines/delete") +async def delete_pipeline(form_data: DeletePipelineForm, user=Depends(get_admin_user)): + + r = None + try: + urlIdx = form_data.urlIdx + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.delete( + f"{url}/pipelines/delete", headers=headers, json={"id": form_data.id} + ) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +@app.get("/api/pipelines") +async def get_pipelines(urlIdx: Optional[int] = None, user=Depends(get_admin_user)): + r = None + try: + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.get(f"{url}/pipelines", headers=headers) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +@app.get("/api/pipelines/{pipeline_id}/valves") +async def get_pipeline_valves( + urlIdx: Optional[int], + pipeline_id: str, + user=Depends(get_admin_user), +): + models = await get_all_models() + r = None + try: + + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.get(f"{url}/{pipeline_id}/valves", headers=headers) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +@app.get("/api/pipelines/{pipeline_id}/valves/spec") +async def get_pipeline_valves_spec( + urlIdx: Optional[int], + pipeline_id: str, + user=Depends(get_admin_user), +): + models = await get_all_models() + + r = None + try: + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.get(f"{url}/{pipeline_id}/valves/spec", headers=headers) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +@app.post("/api/pipelines/{pipeline_id}/valves/update") +async def update_pipeline_valves( + urlIdx: Optional[int], + pipeline_id: str, + form_data: dict, + user=Depends(get_admin_user), +): + models = await get_all_models() + + r = None + try: + url = openai_app.state.config.OPENAI_API_BASE_URLS[urlIdx] + key = openai_app.state.config.OPENAI_API_KEYS[urlIdx] + + headers = {"Authorization": f"Bearer {key}"} + r = requests.post( + f"{url}/{pipeline_id}/valves/update", + headers=headers, + json={**form_data}, + ) + + r.raise_for_status() + data = r.json() + + return {**data} + except Exception as e: + # Handle connection error here + print(f"Connection error: {e}") + + detail = "Pipeline not found" + + if r is not None: + try: + res = r.json() + if "detail" in res: + detail = res["detail"] + except: + pass + + raise HTTPException( + status_code=(r.status_code if r is not None else status.HTTP_404_NOT_FOUND), + detail=detail, + ) + + +################################## +# +# Config Endpoints +# +################################## + + +@app.get("/api/config") +async def get_app_config(): + return { + "status": True, + "name": WEBUI_NAME, + "version": VERSION, + "default_locale": str(DEFAULT_LOCALE), + "default_models": webui_app.state.config.DEFAULT_MODELS, + "default_prompt_suggestions": webui_app.state.config.DEFAULT_PROMPT_SUGGESTIONS, + "features": { + "auth": WEBUI_AUTH, + "auth_trusted_header": bool(webui_app.state.AUTH_TRUSTED_EMAIL_HEADER), + "enable_signup": webui_app.state.config.ENABLE_SIGNUP, + "enable_login_form": webui_app.state.config.ENABLE_LOGIN_FORM, + "enable_web_search": rag_app.state.config.ENABLE_RAG_WEB_SEARCH, + "enable_image_generation": images_app.state.config.ENABLED, + "enable_community_sharing": webui_app.state.config.ENABLE_COMMUNITY_SHARING, + "enable_admin_export": ENABLE_ADMIN_EXPORT, + }, + "audio": { + "tts": { + "engine": audio_app.state.config.TTS_ENGINE, + "voice": audio_app.state.config.TTS_VOICE, + }, + "stt": { + "engine": audio_app.state.config.STT_ENGINE, + }, + }, + "oauth": { + "providers": { + name: config.get("name", name) + for name, config in OAUTH_PROVIDERS.items() + } + }, + } + + +@app.get("/api/config/model/filter") +async def get_model_filter_config(user=Depends(get_admin_user)): + return { + "enabled": app.state.config.ENABLE_MODEL_FILTER, + "models": app.state.config.MODEL_FILTER_LIST, + } + + +class ModelFilterConfigForm(BaseModel): + enabled: bool + models: List[str] + + +@app.post("/api/config/model/filter") +async def update_model_filter_config( + form_data: ModelFilterConfigForm, user=Depends(get_admin_user) +): + app.state.config.ENABLE_MODEL_FILTER = form_data.enabled + app.state.config.MODEL_FILTER_LIST = form_data.models + + return { + "enabled": app.state.config.ENABLE_MODEL_FILTER, + "models": app.state.config.MODEL_FILTER_LIST, + } + + +# TODO: webhook endpoint should be under config endpoints + + +@app.get("/api/webhook") +async def get_webhook_url(user=Depends(get_admin_user)): + return { + "url": app.state.config.WEBHOOK_URL, + } + + +class UrlForm(BaseModel): + url: str + + +@app.post("/api/webhook") +async def update_webhook_url(form_data: UrlForm, user=Depends(get_admin_user)): + app.state.config.WEBHOOK_URL = form_data.url + webui_app.state.WEBHOOK_URL = app.state.config.WEBHOOK_URL + return {"url": app.state.config.WEBHOOK_URL} + + +@app.get("/api/version") +async def get_app_config(): + return { + "version": VERSION, + } + + +@app.get("/api/changelog") +async def get_app_changelog(): + return {key: CHANGELOG[key] for idx, key in enumerate(CHANGELOG) if idx < 5} + + +@app.get("/api/version/updates") +async def get_app_latest_release_version(): + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get( + "https://api.github.com/repos/open-webui/open-webui/releases/latest" + ) as response: + response.raise_for_status() + data = await response.json() + latest_version = data["tag_name"] + + return {"current": VERSION, "latest": latest_version[1:]} + except aiohttp.ClientError as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail=ERROR_MESSAGES.RATE_LIMIT_EXCEEDED, + ) + + +############################ +# OAuth Login & Callback +############################ + +oauth = OAuth() + +for provider_name, provider_config in OAUTH_PROVIDERS.items(): + oauth.register( + name=provider_name, + client_id=provider_config["client_id"], + client_secret=provider_config["client_secret"], + server_metadata_url=provider_config["server_metadata_url"], + client_kwargs={ + "scope": provider_config["scope"], + }, + ) + +# SessionMiddleware is used by authlib for oauth +if len(OAUTH_PROVIDERS) > 0: + app.add_middleware( + SessionMiddleware, + secret_key=WEBUI_SECRET_KEY, + session_cookie="oui-session", + same_site=WEBUI_SESSION_COOKIE_SAME_SITE, + https_only=WEBUI_SESSION_COOKIE_SECURE, + ) + + +@app.get("/oauth/{provider}/login") +async def oauth_login(provider: str, request: Request): + if provider not in OAUTH_PROVIDERS: + raise HTTPException(404) + redirect_uri = request.url_for("oauth_callback", provider=provider) + return await oauth.create_client(provider).authorize_redirect(request, redirect_uri) + + +# OAuth login logic is as follows: +# 1. Attempt to find a user with matching subject ID, tied to the provider +# 2. If OAUTH_MERGE_ACCOUNTS_BY_EMAIL is true, find a user with the email address provided via OAuth +# - This is considered insecure in general, as OAuth providers do not always verify email addresses +# 3. If there is no user, and ENABLE_OAUTH_SIGNUP is true, create a user +# - Email addresses are considered unique, so we fail registration if the email address is alreayd taken +@app.get("/oauth/{provider}/callback") +async def oauth_callback(provider: str, request: Request, response: Response): + if provider not in OAUTH_PROVIDERS: + raise HTTPException(404) + client = oauth.create_client(provider) + try: + token = await client.authorize_access_token(request) + except Exception as e: + log.warning(f"OAuth callback error: {e}") + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + user_data: UserInfo = token["userinfo"] + + sub = user_data.get("sub") + if not sub: + log.warning(f"OAuth callback failed, sub is missing: {user_data}") + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + provider_sub = f"{provider}@{sub}" + email = user_data.get("email", "").lower() + # We currently mandate that email addresses are provided + if not email: + log.warning(f"OAuth callback failed, email is missing: {user_data}") + raise HTTPException(400, detail=ERROR_MESSAGES.INVALID_CRED) + + # Check if the user exists + user = Users.get_user_by_oauth_sub(provider_sub) + + if not user: + # If the user does not exist, check if merging is enabled + if OAUTH_MERGE_ACCOUNTS_BY_EMAIL.value: + # Check if the user exists by email + user = Users.get_user_by_email(email) + if user: + # Update the user with the new oauth sub + Users.update_user_oauth_sub_by_id(user.id, provider_sub) + + if not user: + # If the user does not exist, check if signups are enabled + if ENABLE_OAUTH_SIGNUP.value: + # Check if an existing user with the same email already exists + existing_user = Users.get_user_by_email(user_data.get("email", "").lower()) + if existing_user: + raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN) + + picture_claim = webui_app.state.config.OAUTH_PICTURE_CLAIM + picture_url = user_data.get(picture_claim, "") + if picture_url: + # Download the profile image into a base64 string + try: + async with aiohttp.ClientSession() as session: + async with session.get(picture_url) as resp: + picture = await resp.read() + base64_encoded_picture = base64.b64encode(picture).decode( + "utf-8" + ) + guessed_mime_type = mimetypes.guess_type(picture_url)[0] + if guessed_mime_type is None: + # assume JPG, browsers are tolerant enough of image formats + guessed_mime_type = "image/jpeg" + picture_url = f"data:{guessed_mime_type};base64,{base64_encoded_picture}" + except Exception as e: + log.error(f"Error downloading profile image '{picture_url}': {e}") + picture_url = "" + if not picture_url: + picture_url = "/user.png" + username_claim = webui_app.state.config.OAUTH_USERNAME_CLAIM + role = ( + "admin" + if Users.get_num_users() == 0 + else webui_app.state.config.DEFAULT_USER_ROLE + ) + user = Auths.insert_new_auth( + email=email, + password=get_password_hash( + str(uuid.uuid4()) + ), # Random password, not used + name=user_data.get(username_claim, "User"), + profile_image_url=picture_url, + role=role, + oauth_sub=provider_sub, + ) + + if webui_app.state.config.WEBHOOK_URL: + post_webhook( + webui_app.state.config.WEBHOOK_URL, + WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + { + "action": "signup", + "message": WEBHOOK_MESSAGES.USER_SIGNUP(user.name), + "user": user.model_dump_json(exclude_none=True), + }, + ) + else: + raise HTTPException( + status.HTTP_403_FORBIDDEN, detail=ERROR_MESSAGES.ACCESS_PROHIBITED + ) + + jwt_token = create_token( + data={"id": user.id}, + expires_delta=parse_duration(webui_app.state.config.JWT_EXPIRES_IN), + ) + + # Set the cookie token + response.set_cookie( + key="token", + value=jwt_token, + httponly=True, # Ensures the cookie is not accessible via JavaScript + ) + + # Redirect back to the frontend with the JWT token + redirect_url = f"{request.base_url}auth#token={jwt_token}" + return RedirectResponse(url=redirect_url) + + +@app.get("/manifest.json") +async def get_manifest_json(): + return { + "name": WEBUI_NAME, + "short_name": WEBUI_NAME, + "start_url": "/", + "display": "standalone", + "background_color": "#343541", + "orientation": "portrait-primary", + "icons": [{"src": "/static/logo.png", "type": "image/png", "sizes": "500x500"}], + } + + +@app.get("/opensearch.xml") +async def get_opensearch_xml(): + xml_content = rf""" + <OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> + <ShortName>{WEBUI_NAME}</ShortName> + <Description>Search {WEBUI_NAME}</Description> + <InputEncoding>UTF-8</InputEncoding> + <Image width="16" height="16" type="image/x-icon">{WEBUI_URL}/static/favicon.png</Image> + <Url type="text/html" method="get" template="{WEBUI_URL}/?q={"{searchTerms}"}"/> + <moz:SearchForm>{WEBUI_URL}</moz:SearchForm> + </OpenSearchDescription> + """ + return Response(content=xml_content, media_type="application/xml") + + +@app.get("/health") +async def healthcheck(): + return {"status": True} + + +@app.get("/health/db") +async def healthcheck_with_db(): + Session.execute(text("SELECT 1;")).all() + return {"status": True} + + +app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") +app.mount("/cache", StaticFiles(directory=CACHE_DIR), name="cache") + +if os.path.exists(FRONTEND_BUILD_DIR): + mimetypes.add_type("text/javascript", ".js") + app.mount( + "/", + SPAStaticFiles(directory=FRONTEND_BUILD_DIR, html=True), + name="spa-static-files", + ) +else: + log.warning( + f"Frontend build directory not found at '{FRONTEND_BUILD_DIR}'. Serving API only." + ) diff --git a/backend/migrations/README b/backend/migrations/README new file mode 100644 index 0000000000000000000000000000000000000000..f1d93dff9dbd52e0a9fc8ce4d68e0784c07da697 --- /dev/null +++ b/backend/migrations/README @@ -0,0 +1,4 @@ +Generic single-database configuration. + +Create new migrations with +DATABASE_URL=<replace with actual url> alembic revision --autogenerate -m "a description" diff --git a/backend/migrations/env.py b/backend/migrations/env.py new file mode 100644 index 0000000000000000000000000000000000000000..8046abff3a597ca42604ab5a8557d29b1f8871e5 --- /dev/null +++ b/backend/migrations/env.py @@ -0,0 +1,96 @@ +import os +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from apps.webui.models.auths import Auth +from apps.webui.models.chats import Chat +from apps.webui.models.documents import Document +from apps.webui.models.memories import Memory +from apps.webui.models.models import Model +from apps.webui.models.prompts import Prompt +from apps.webui.models.tags import Tag, ChatIdTag +from apps.webui.models.tools import Tool +from apps.webui.models.users import User +from apps.webui.models.files import File +from apps.webui.models.functions import Function + +from config import DATABASE_URL + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name, disable_existing_loggers=False) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = Auth.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + +DB_URL = DATABASE_URL + +if DB_URL: + config.set_main_option("sqlalchemy.url", DB_URL.replace("%", "%%")) + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/migrations/script.py.mako b/backend/migrations/script.py.mako new file mode 100644 index 0000000000000000000000000000000000000000..5f667ccfe0b7732421efa90d85ec2790f3603b04 --- /dev/null +++ b/backend/migrations/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import apps.webui.internal.db +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/migrations/util.py b/backend/migrations/util.py new file mode 100644 index 0000000000000000000000000000000000000000..401bb94d03425aca522163c7c492405d953c9eea --- /dev/null +++ b/backend/migrations/util.py @@ -0,0 +1,9 @@ +from alembic import op +from sqlalchemy import Inspector + + +def get_existing_tables(): + con = op.get_bind() + inspector = Inspector.from_engine(con) + tables = set(inspector.get_table_names()) + return tables diff --git a/backend/migrations/versions/7e5b5dc7342b_init.py b/backend/migrations/versions/7e5b5dc7342b_init.py new file mode 100644 index 0000000000000000000000000000000000000000..b82627f5bc286da05d2f9f1fdf6005f00f91ed55 --- /dev/null +++ b/backend/migrations/versions/7e5b5dc7342b_init.py @@ -0,0 +1,202 @@ +"""init + +Revision ID: 7e5b5dc7342b +Revises: +Create Date: 2024-06-24 13:15:33.808998 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import apps.webui.internal.db +from migrations.util import get_existing_tables + +# revision identifiers, used by Alembic. +revision: str = "7e5b5dc7342b" +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + existing_tables = set(get_existing_tables()) + + # ### commands auto generated by Alembic - please adjust! ### + if "auth" not in existing_tables: + op.create_table( + "auth", + sa.Column("id", sa.String(), nullable=False), + sa.Column("email", sa.String(), nullable=True), + sa.Column("password", sa.Text(), nullable=True), + sa.Column("active", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "chat" not in existing_tables: + op.create_table( + "chat", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("chat", sa.Text(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("share_id", sa.Text(), nullable=True), + sa.Column("archived", sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("share_id"), + ) + + if "chatidtag" not in existing_tables: + op.create_table( + "chatidtag", + sa.Column("id", sa.String(), nullable=False), + sa.Column("tag_name", sa.String(), nullable=True), + sa.Column("chat_id", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("timestamp", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "document" not in existing_tables: + op.create_table( + "document", + sa.Column("collection_name", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("filename", sa.Text(), nullable=True), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("timestamp", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("collection_name"), + sa.UniqueConstraint("name"), + ) + + if "file" not in existing_tables: + op.create_table( + "file", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("filename", sa.Text(), nullable=True), + sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "function" not in existing_tables: + op.create_table( + "function", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("name", sa.Text(), nullable=True), + sa.Column("type", sa.Text(), nullable=True), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("is_global", sa.Boolean(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "memory" not in existing_tables: + op.create_table( + "memory", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "model" not in existing_tables: + op.create_table( + "model", + sa.Column("id", sa.Text(), nullable=False), + sa.Column("user_id", sa.Text(), nullable=True), + sa.Column("base_model_id", sa.Text(), nullable=True), + sa.Column("name", sa.Text(), nullable=True), + sa.Column("params", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "prompt" not in existing_tables: + op.create_table( + "prompt", + sa.Column("command", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("title", sa.Text(), nullable=True), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("timestamp", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("command"), + ) + + if "tag" not in existing_tables: + op.create_table( + "tag", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("data", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "tool" not in existing_tables: + op.create_table( + "tool", + sa.Column("id", sa.String(), nullable=False), + sa.Column("user_id", sa.String(), nullable=True), + sa.Column("name", sa.Text(), nullable=True), + sa.Column("content", sa.Text(), nullable=True), + sa.Column("specs", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("meta", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("valves", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + + if "user" not in existing_tables: + op.create_table( + "user", + sa.Column("id", sa.String(), nullable=False), + sa.Column("name", sa.String(), nullable=True), + sa.Column("email", sa.String(), nullable=True), + sa.Column("role", sa.String(), nullable=True), + sa.Column("profile_image_url", sa.Text(), nullable=True), + sa.Column("last_active_at", sa.BigInteger(), nullable=True), + sa.Column("updated_at", sa.BigInteger(), nullable=True), + sa.Column("created_at", sa.BigInteger(), nullable=True), + sa.Column("api_key", sa.String(), nullable=True), + sa.Column("settings", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("info", apps.webui.internal.db.JSONField(), nullable=True), + sa.Column("oauth_sub", sa.Text(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("api_key"), + sa.UniqueConstraint("oauth_sub"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("user") + op.drop_table("tool") + op.drop_table("tag") + op.drop_table("prompt") + op.drop_table("model") + op.drop_table("memory") + op.drop_table("function") + op.drop_table("file") + op.drop_table("document") + op.drop_table("chatidtag") + op.drop_table("chat") + op.drop_table("auth") + # ### end Alembic commands ### diff --git a/backend/open_webui/__init__.py b/backend/open_webui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1defac8247fe72890702f2f36122fd71c8b39f64 --- /dev/null +++ b/backend/open_webui/__init__.py @@ -0,0 +1,60 @@ +import base64 +import os +import random +from pathlib import Path + +import typer +import uvicorn + +app = typer.Typer() + +KEY_FILE = Path.cwd() / ".webui_secret_key" +if (frontend_build_dir := Path(__file__).parent / "frontend").exists(): + os.environ["FRONTEND_BUILD_DIR"] = str(frontend_build_dir) + + +@app.command() +def serve( + host: str = "0.0.0.0", + port: int = 8080, +): + if os.getenv("WEBUI_SECRET_KEY") is None: + typer.echo( + "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable." + ) + if not KEY_FILE.exists(): + typer.echo(f"Generating a new secret key and saving it to {KEY_FILE}") + KEY_FILE.write_bytes(base64.b64encode(random.randbytes(12))) + typer.echo(f"Loading WEBUI_SECRET_KEY from {KEY_FILE}") + os.environ["WEBUI_SECRET_KEY"] = KEY_FILE.read_text() + + if os.getenv("USE_CUDA_DOCKER", "false") == "true": + typer.echo( + "CUDA is enabled, appending LD_LIBRARY_PATH to include torch/cudnn & cublas libraries." + ) + LD_LIBRARY_PATH = os.getenv("LD_LIBRARY_PATH", "").split(":") + os.environ["LD_LIBRARY_PATH"] = ":".join( + LD_LIBRARY_PATH + + [ + "/usr/local/lib/python3.11/site-packages/torch/lib", + "/usr/local/lib/python3.11/site-packages/nvidia/cudnn/lib", + ] + ) + import main # we need set environment variables before importing main + + uvicorn.run(main.app, host=host, port=port, forwarded_allow_ips="*") + + +@app.command() +def dev( + host: str = "0.0.0.0", + port: int = 8080, + reload: bool = True, +): + uvicorn.run( + "main:app", host=host, port=port, reload=reload, forwarded_allow_ips="*" + ) + + +if __name__ == "__main__": + app() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8b12854a081d8c2795c70e87e30abf7075552b1a --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,79 @@ +fastapi==0.111.0 +uvicorn[standard]==0.22.0 +pydantic==2.8.2 +python-multipart==0.0.9 + +Flask==3.0.3 +Flask-Cors==4.0.1 + +python-socketio==5.11.3 +python-jose==3.3.0 +passlib[bcrypt]==1.7.4 + +requests==2.32.3 +aiohttp==3.9.5 +sqlalchemy==2.0.31 +alembic==1.13.2 +peewee==3.17.6 +peewee-migrate==1.12.2 +psycopg2-binary==2.9.9 +PyMySQL==1.1.1 +bcrypt==4.1.3 +SQLAlchemy +pymongo +redis +boto3==1.34.110 + +argon2-cffi==23.1.0 +APScheduler==3.10.4 + +# AI libraries +openai +anthropic +google-generativeai==0.7.2 +tiktoken + +langchain==0.2.11 +langchain-community==0.2.10 +langchain-chroma==0.1.2 + +fake-useragent==1.5.1 +chromadb==0.5.4 +sentence-transformers==3.0.1 +pypdf==4.2.0 +docx2txt==0.8 +python-pptx==0.6.23 +unstructured==0.15.0 +Markdown==3.6 +pypandoc==1.13 +pandas==2.2.2 +openpyxl==3.1.5 +pyxlsb==1.0.10 +xlrd==2.0.1 +validators==0.28.1 +psutil + +opencv-python-headless==4.10.0.84 +rapidocr-onnxruntime==1.3.24 + +fpdf2==2.7.9 +rank-bm25==0.2.2 + +faster-whisper==1.0.2 + +PyJWT[crypto]==2.8.0 +authlib==1.3.1 + +black==24.4.2 +langfuse==2.39.2 +youtube-transcript-api==0.6.2 +pytube==15.0.0 + +extract_msg +pydub +duckduckgo-search~=6.2.1 + +## Tests +docker~=7.1.0 +pytest~=8.2.2 +pytest-docker~=3.1.1 diff --git a/backend/start.sh b/backend/start.sh new file mode 100755 index 0000000000000000000000000000000000000000..aacc6daf10c5c544a146b6c3a2f5848c7c265a3d --- /dev/null +++ b/backend/start.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +cd "$SCRIPT_DIR" || exit + +KEY_FILE=.webui_secret_key + +PORT="${PORT:-8080}" +HOST="${HOST:-0.0.0.0}" +if test "$WEBUI_SECRET_KEY $WEBUI_JWT_SECRET_KEY" = " "; then + echo "Loading WEBUI_SECRET_KEY from file, not provided as an environment variable." + + if ! [ -e "$KEY_FILE" ]; then + 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" + fi + + 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 + + +# Check if SPACE_ID is set, if so, configure for space +if [ -n "$SPACE_ID" ]; then + echo "Configuring for HuggingFace Space deployment" + if [ -n "$ADMIN_USER_EMAIL" ] && [ -n "$ADMIN_USER_PASSWORD" ]; then + echo "Admin user configured, creating" + WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' & + webui_pid=$! + echo "Waiting for webui to start..." + while ! curl -s http://localhost:8080/health > /dev/null; do + sleep 1 + done + echo "Creating admin user..." + curl \ + -X POST "http://localhost:8080/api/v1/auths/signup" \ + -H "accept: application/json" \ + -H "Content-Type: application/json" \ + -d "{ \"email\": \"${ADMIN_USER_EMAIL}\", \"password\": \"${ADMIN_USER_PASSWORD}\", \"name\": \"Admin\" }" + echo "Shutting down webui..." + kill $webui_pid + fi + + if [ -n "$OAUTH_CLIENT_ID" ]; then + echo "OAuth client ID provided, configuring for OAuth" + export OPENID_PROVIDER_URL=${OPENID_PROVIDER_URL}/.well-known/openid-configuration + export OAUTH_PROVIDER_NAME="Hugging Face" + fi + + export WEBUI_URL=${SPACE_HOST} +fi + +WEBUI_SECRET_KEY="$WEBUI_SECRET_KEY" exec uvicorn main:app --host "$HOST" --port "$PORT" --forwarded-allow-ips '*' diff --git a/backend/start_windows.bat b/backend/start_windows.bat new file mode 100644 index 0000000000000000000000000000000000000000..b2498f9c2b0588c251d142b0b881440d6f191c5a --- /dev/null +++ b/backend/start_windows.bat @@ -0,0 +1,33 @@ +:: This method is not recommended, and we recommend you use the `start.sh` file with WSL instead. +@echo off +SETLOCAL ENABLEDELAYEDEXPANSION + +:: Get the directory of the current script +SET "SCRIPT_DIR=%~dp0" +cd /d "%SCRIPT_DIR%" || exit /b + +SET "KEY_FILE=.webui_secret_key" +IF "%PORT%"=="" SET PORT=8080 +IF "%HOST%"=="" SET HOST=0.0.0.0 +SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" +SET "WEBUI_JWT_SECRET_KEY=%WEBUI_JWT_SECRET_KEY%" + +:: Check if WEBUI_SECRET_KEY and WEBUI_JWT_SECRET_KEY are not set +IF "%WEBUI_SECRET_KEY%%WEBUI_JWT_SECRET_KEY%" == " " ( + echo Loading WEBUI_SECRET_KEY from file, not provided as an environment variable. + + IF NOT EXIST "%KEY_FILE%" ( + echo Generating WEBUI_SECRET_KEY + :: Generate a random value to use as a WEBUI_SECRET_KEY in case the user didn't provide one + SET /p WEBUI_SECRET_KEY=<nul + FOR /L %%i IN (1,1,12) DO SET /p WEBUI_SECRET_KEY=<!random!>>%KEY_FILE% + echo WEBUI_SECRET_KEY generated + ) + + echo Loading WEBUI_SECRET_KEY from %KEY_FILE% + SET /p WEBUI_SECRET_KEY=<%KEY_FILE% +) + +:: Execute uvicorn +SET "WEBUI_SECRET_KEY=%WEBUI_SECRET_KEY%" +uvicorn main:app --host "%HOST%" --port "%PORT%" --forwarded-allow-ips '*' diff --git a/backend/static/favicon.png b/backend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2074780847581edf9cf2ed0d2e9ebd8ff08c56 Binary files /dev/null and b/backend/static/favicon.png differ diff --git a/backend/static/fonts/NotoSans-Bold.ttf b/backend/static/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..56310ad1ad635e6ac20478daff20f14f0647e3ed --- /dev/null +++ b/backend/static/fonts/NotoSans-Bold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf382cad35e731fc4f13b1bf068c5085cd17bee2141014cc94919c140529488d +size 582604 diff --git a/backend/static/fonts/NotoSans-Italic.ttf b/backend/static/fonts/NotoSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..1ad4f126bc92b2188effdf3f0db8e4a260ecc5c4 --- /dev/null +++ b/backend/static/fonts/NotoSans-Italic.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:380a500e3dda76d955dadc77053227cc61149814737dc9f7d973d09415ad851f +size 597000 diff --git a/backend/static/fonts/NotoSans-Regular.ttf b/backend/static/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..defecf9b7931c29b64603ca1eed7eb4f513d42ad --- /dev/null +++ b/backend/static/fonts/NotoSans-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3be6b371cef19ed6add589bd106444ab74c9793bc812d3159298b73d00ee011c +size 582748 diff --git a/backend/static/fonts/NotoSansJP-Regular.ttf b/backend/static/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..34e480073b8a55f1e9476c669acde48e049bd0b5 --- /dev/null +++ b/backend/static/fonts/NotoSansJP-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb3df01b4182734d021d79ec5bac17903bb681e926a059c59ed81a373d612241 +size 5732824 diff --git a/backend/static/fonts/NotoSansKR-Regular.ttf b/backend/static/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c847342886ba5d1e326844ba2d679b9d0bdaa805 --- /dev/null +++ b/backend/static/fonts/NotoSansKR-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9db318b65ee9c575a43e7efd273dbdd1afef26e467eea3e1073a50e1a6595f6d +size 6192764 diff --git a/backend/static/logo.png b/backend/static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..519af1db620dbf4de3694660dae7abd7392f0b3c Binary files /dev/null and b/backend/static/logo.png differ diff --git a/backend/static/splash.png b/backend/static/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..389196ca6a364b9e4b7daa0fc13be463b914b251 Binary files /dev/null and b/backend/static/splash.png differ diff --git a/backend/static/user-import.csv b/backend/static/user-import.csv new file mode 100644 index 0000000000000000000000000000000000000000..918a92aad71d708ae13fedb8b91f79c29a5b3e9d --- /dev/null +++ b/backend/static/user-import.csv @@ -0,0 +1 @@ +Name,Email,Password,Role diff --git a/backend/test/__init__.py b/backend/test/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend/test/apps/webui/routers/test_auths.py b/backend/test/apps/webui/routers/test_auths.py new file mode 100644 index 0000000000000000000000000000000000000000..3a8695a6935f98e8c3c3ca4f59694fcf6ea3f147 --- /dev/null +++ b/backend/test/apps/webui/routers/test_auths.py @@ -0,0 +1,202 @@ +import pytest + +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestAuths(AbstractPostgresTest): + BASE_PATH = "/api/v1/auths" + + def setup_class(cls): + super().setup_class() + from apps.webui.models.users import Users + from apps.webui.models.auths import Auths + + cls.users = Users + cls.auths = Auths + + def test_get_session_user(self): + with mock_webui_user(): + response = self.fast_api_client.get(self.create_url("")) + assert response.status_code == 200 + assert response.json() == { + "id": "1", + "name": "John Doe", + "email": "john.doe@openwebui.com", + "role": "user", + "profile_image_url": "/user.png", + } + + def test_update_profile(self): + from utils.utils import get_password_hash + + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password=get_password_hash("old_password"), + name="John Doe", + profile_image_url="/user.png", + role="user", + ) + + with mock_webui_user(id=user.id): + response = self.fast_api_client.post( + self.create_url("/update/profile"), + json={"name": "John Doe 2", "profile_image_url": "/user2.png"}, + ) + assert response.status_code == 200 + db_user = self.users.get_user_by_id(user.id) + assert db_user.name == "John Doe 2" + assert db_user.profile_image_url == "/user2.png" + + def test_update_password(self): + from utils.utils import get_password_hash + + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password=get_password_hash("old_password"), + name="John Doe", + profile_image_url="/user.png", + role="user", + ) + + with mock_webui_user(id=user.id): + response = self.fast_api_client.post( + self.create_url("/update/password"), + json={"password": "old_password", "new_password": "new_password"}, + ) + assert response.status_code == 200 + + old_auth = self.auths.authenticate_user( + "john.doe@openwebui.com", "old_password" + ) + assert old_auth is None + new_auth = self.auths.authenticate_user( + "john.doe@openwebui.com", "new_password" + ) + assert new_auth is not None + + def test_signin(self): + from utils.utils import get_password_hash + + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password=get_password_hash("password"), + name="John Doe", + profile_image_url="/user.png", + role="user", + ) + response = self.fast_api_client.post( + self.create_url("/signin"), + json={"email": "john.doe@openwebui.com", "password": "password"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] == user.id + assert data["name"] == "John Doe" + assert data["email"] == "john.doe@openwebui.com" + assert data["role"] == "user" + assert data["profile_image_url"] == "/user.png" + assert data["token"] is not None and len(data["token"]) > 0 + assert data["token_type"] == "Bearer" + + def test_signup(self): + response = self.fast_api_client.post( + self.create_url("/signup"), + json={ + "name": "John Doe", + "email": "john.doe@openwebui.com", + "password": "password", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] is not None and len(data["id"]) > 0 + assert data["name"] == "John Doe" + assert data["email"] == "john.doe@openwebui.com" + assert data["role"] in ["admin", "user", "pending"] + assert data["profile_image_url"] == "/user.png" + assert data["token"] is not None and len(data["token"]) > 0 + assert data["token_type"] == "Bearer" + + def test_add_user(self): + with mock_webui_user(): + response = self.fast_api_client.post( + self.create_url("/add"), + json={ + "name": "John Doe 2", + "email": "john.doe2@openwebui.com", + "password": "password2", + "role": "admin", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] is not None and len(data["id"]) > 0 + assert data["name"] == "John Doe 2" + assert data["email"] == "john.doe2@openwebui.com" + assert data["role"] == "admin" + assert data["profile_image_url"] == "/user.png" + assert data["token"] is not None and len(data["token"]) > 0 + assert data["token_type"] == "Bearer" + + def test_get_admin_details(self): + self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password="password", + name="John Doe", + profile_image_url="/user.png", + role="admin", + ) + with mock_webui_user(): + response = self.fast_api_client.get(self.create_url("/admin/details")) + + assert response.status_code == 200 + assert response.json() == { + "name": "John Doe", + "email": "john.doe@openwebui.com", + } + + def test_create_api_key_(self): + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password="password", + name="John Doe", + profile_image_url="/user.png", + role="admin", + ) + with mock_webui_user(id=user.id): + response = self.fast_api_client.post(self.create_url("/api_key")) + assert response.status_code == 200 + data = response.json() + assert data["api_key"] is not None + assert len(data["api_key"]) > 0 + + def test_delete_api_key(self): + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password="password", + name="John Doe", + profile_image_url="/user.png", + role="admin", + ) + self.users.update_user_api_key_by_id(user.id, "abc") + with mock_webui_user(id=user.id): + response = self.fast_api_client.delete(self.create_url("/api_key")) + assert response.status_code == 200 + assert response.json() == True + db_user = self.users.get_user_by_id(user.id) + assert db_user.api_key is None + + def test_get_api_key(self): + user = self.auths.insert_new_auth( + email="john.doe@openwebui.com", + password="password", + name="John Doe", + profile_image_url="/user.png", + role="admin", + ) + self.users.update_user_api_key_by_id(user.id, "abc") + with mock_webui_user(id=user.id): + response = self.fast_api_client.get(self.create_url("/api_key")) + assert response.status_code == 200 + assert response.json() == {"api_key": "abc"} diff --git a/backend/test/apps/webui/routers/test_chats.py b/backend/test/apps/webui/routers/test_chats.py new file mode 100644 index 0000000000000000000000000000000000000000..f4661b62573a5891daa647b423b3bfdc540ac031 --- /dev/null +++ b/backend/test/apps/webui/routers/test_chats.py @@ -0,0 +1,238 @@ +import uuid + +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestChats(AbstractPostgresTest): + + BASE_PATH = "/api/v1/chats" + + def setup_class(cls): + super().setup_class() + + def setup_method(self): + super().setup_method() + from apps.webui.models.chats import ChatForm + from apps.webui.models.chats import Chats + + self.chats = Chats + self.chats.insert_new_chat( + "2", + ChatForm( + **{ + "chat": { + "name": "chat1", + "description": "chat1 description", + "tags": ["tag1", "tag2"], + "history": {"currentId": "1", "messages": []}, + } + } + ), + ) + + def test_get_session_user_chat_list(self): + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + first_chat = response.json()[0] + assert first_chat["id"] is not None + assert first_chat["title"] == "New Chat" + assert first_chat["created_at"] is not None + assert first_chat["updated_at"] is not None + + def test_delete_all_user_chats(self): + with mock_webui_user(id="2"): + response = self.fast_api_client.delete(self.create_url("/")) + assert response.status_code == 200 + assert len(self.chats.get_chats()) == 0 + + def test_get_user_chat_list_by_user_id(self): + with mock_webui_user(id="3"): + response = self.fast_api_client.get(self.create_url("/list/user/2")) + assert response.status_code == 200 + first_chat = response.json()[0] + assert first_chat["id"] is not None + assert first_chat["title"] == "New Chat" + assert first_chat["created_at"] is not None + assert first_chat["updated_at"] is not None + + def test_create_new_chat(self): + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/new"), + json={ + "chat": { + "name": "chat2", + "description": "chat2 description", + "tags": ["tag1", "tag2"], + } + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["archived"] is False + assert data["chat"] == { + "name": "chat2", + "description": "chat2 description", + "tags": ["tag1", "tag2"], + } + assert data["user_id"] == "2" + assert data["id"] is not None + assert data["share_id"] is None + assert data["title"] == "New Chat" + assert data["updated_at"] is not None + assert data["created_at"] is not None + assert len(self.chats.get_chats()) == 2 + + def test_get_user_chats(self): + self.test_get_session_user_chat_list() + + def test_get_user_archived_chats(self): + self.chats.archive_all_chats_by_user_id("2") + from apps.webui.internal.db import Session + + Session.commit() + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/all/archived")) + assert response.status_code == 200 + first_chat = response.json()[0] + assert first_chat["id"] is not None + assert first_chat["title"] == "New Chat" + assert first_chat["created_at"] is not None + assert first_chat["updated_at"] is not None + + def test_get_all_user_chats_in_db(self): + with mock_webui_user(id="4"): + response = self.fast_api_client.get(self.create_url("/all/db")) + assert response.status_code == 200 + assert len(response.json()) == 1 + + def test_get_archived_session_user_chat_list(self): + self.test_get_user_archived_chats() + + def test_archive_all_chats(self): + with mock_webui_user(id="2"): + response = self.fast_api_client.post(self.create_url("/archive/all")) + assert response.status_code == 200 + assert len(self.chats.get_archived_chats_by_user_id("2")) == 1 + + def test_get_shared_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + self.chats.update_chat_share_id_by_id(chat_id, chat_id) + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url(f"/share/{chat_id}")) + assert response.status_code == 200 + data = response.json() + assert data["id"] == chat_id + assert data["chat"] == { + "name": "chat1", + "description": "chat1 description", + "tags": ["tag1", "tag2"], + "history": {"currentId": "1", "messages": []}, + } + assert data["id"] == chat_id + assert data["share_id"] == chat_id + assert data["title"] == "New Chat" + + def test_get_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url(f"/{chat_id}")) + assert response.status_code == 200 + data = response.json() + assert data["id"] == chat_id + assert data["chat"] == { + "name": "chat1", + "description": "chat1 description", + "tags": ["tag1", "tag2"], + "history": {"currentId": "1", "messages": []}, + } + assert data["share_id"] is None + assert data["title"] == "New Chat" + assert data["user_id"] == "2" + + def test_update_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url(f"/{chat_id}"), + json={ + "chat": { + "name": "chat2", + "description": "chat2 description", + "tags": ["tag2", "tag4"], + "title": "Just another title", + } + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["id"] == chat_id + assert data["chat"] == { + "name": "chat2", + "title": "Just another title", + "description": "chat2 description", + "tags": ["tag2", "tag4"], + "history": {"currentId": "1", "messages": []}, + } + assert data["share_id"] is None + assert data["title"] == "Just another title" + assert data["user_id"] == "2" + + def test_delete_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.delete(self.create_url(f"/{chat_id}")) + assert response.status_code == 200 + assert response.json() is True + + def test_clone_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url(f"/{chat_id}/clone")) + + assert response.status_code == 200 + data = response.json() + assert data["id"] != chat_id + assert data["chat"] == { + "branchPointMessageId": "1", + "description": "chat1 description", + "history": {"currentId": "1", "messages": []}, + "name": "chat1", + "originalChatId": chat_id, + "tags": ["tag1", "tag2"], + "title": "Clone of New Chat", + } + assert data["share_id"] is None + assert data["title"] == "Clone of New Chat" + assert data["user_id"] == "2" + + def test_archive_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url(f"/{chat_id}/archive")) + assert response.status_code == 200 + + chat = self.chats.get_chat_by_id(chat_id) + assert chat.archived is True + + def test_share_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + with mock_webui_user(id="2"): + response = self.fast_api_client.post(self.create_url(f"/{chat_id}/share")) + assert response.status_code == 200 + + chat = self.chats.get_chat_by_id(chat_id) + assert chat.share_id is not None + + def test_delete_shared_chat_by_id(self): + chat_id = self.chats.get_chats()[0].id + share_id = str(uuid.uuid4()) + self.chats.update_chat_share_id_by_id(chat_id, share_id) + with mock_webui_user(id="2"): + response = self.fast_api_client.delete(self.create_url(f"/{chat_id}/share")) + assert response.status_code + + chat = self.chats.get_chat_by_id(chat_id) + assert chat.share_id is None diff --git a/backend/test/apps/webui/routers/test_documents.py b/backend/test/apps/webui/routers/test_documents.py new file mode 100644 index 0000000000000000000000000000000000000000..14ca339fd0b3332c9460865587cb2ad99e671cd6 --- /dev/null +++ b/backend/test/apps/webui/routers/test_documents.py @@ -0,0 +1,106 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestDocuments(AbstractPostgresTest): + + BASE_PATH = "/api/v1/documents" + + def setup_class(cls): + super().setup_class() + from apps.webui.models.documents import Documents + + cls.documents = Documents + + def test_documents(self): + # Empty database + assert len(self.documents.get_docs()) == 0 + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 0 + + # Create a new document + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/create"), + json={ + "name": "doc_name", + "title": "doc title", + "collection_name": "custom collection", + "filename": "doc_name.pdf", + "content": "", + }, + ) + assert response.status_code == 200 + assert response.json()["name"] == "doc_name" + assert len(self.documents.get_docs()) == 1 + + # Get the document + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/doc?name=doc_name")) + assert response.status_code == 200 + data = response.json() + assert data["collection_name"] == "custom collection" + assert data["name"] == "doc_name" + assert data["title"] == "doc title" + assert data["filename"] == "doc_name.pdf" + assert data["content"] == {} + + # Create another document + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/create"), + json={ + "name": "doc_name 2", + "title": "doc title 2", + "collection_name": "custom collection 2", + "filename": "doc_name2.pdf", + "content": "", + }, + ) + assert response.status_code == 200 + assert response.json()["name"] == "doc_name 2" + assert len(self.documents.get_docs()) == 2 + + # Get all documents + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 2 + + # Update the first document + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/doc/update?name=doc_name"), + json={"name": "doc_name rework", "title": "updated title"}, + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "doc_name rework" + assert data["title"] == "updated title" + + # Tag the first document + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/doc/tags"), + json={ + "name": "doc_name rework", + "tags": [{"name": "testing-tag"}, {"name": "another-tag"}], + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "doc_name rework" + assert data["content"] == { + "tags": [{"name": "testing-tag"}, {"name": "another-tag"}] + } + assert len(self.documents.get_docs()) == 2 + + # Delete the first document + with mock_webui_user(id="2"): + response = self.fast_api_client.delete( + self.create_url("/doc/delete?name=doc_name rework") + ) + assert response.status_code == 200 + assert len(self.documents.get_docs()) == 1 diff --git a/backend/test/apps/webui/routers/test_models.py b/backend/test/apps/webui/routers/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..410c4516a23f7af14d7425b504966edc873271ac --- /dev/null +++ b/backend/test/apps/webui/routers/test_models.py @@ -0,0 +1,62 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestModels(AbstractPostgresTest): + + BASE_PATH = "/api/v1/models" + + def setup_class(cls): + super().setup_class() + from apps.webui.models.models import Model + + cls.models = Model + + def test_models(self): + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 0 + + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/add"), + json={ + "id": "my-model", + "base_model_id": "base-model-id", + "name": "Hello World", + "meta": { + "profile_image_url": "/static/favicon.png", + "description": "description", + "capabilities": None, + "model_config": {}, + }, + "params": {}, + }, + ) + assert response.status_code == 200 + + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 1 + + with mock_webui_user(id="2"): + response = self.fast_api_client.get( + self.create_url(query_params={"id": "my-model"}) + ) + assert response.status_code == 200 + data = response.json()[0] + assert data["id"] == "my-model" + assert data["name"] == "Hello World" + + with mock_webui_user(id="2"): + response = self.fast_api_client.delete( + self.create_url("/delete?id=my-model") + ) + assert response.status_code == 200 + + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 0 diff --git a/backend/test/apps/webui/routers/test_prompts.py b/backend/test/apps/webui/routers/test_prompts.py new file mode 100644 index 0000000000000000000000000000000000000000..9f47be9923c11206612b473bb0334c9e80e2f2d3 --- /dev/null +++ b/backend/test/apps/webui/routers/test_prompts.py @@ -0,0 +1,92 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +class TestPrompts(AbstractPostgresTest): + + BASE_PATH = "/api/v1/prompts" + + def test_prompts(self): + # Get all prompts + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 0 + + # Create a two new prompts + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/create"), + json={ + "command": "/my-command", + "title": "Hello World", + "content": "description", + }, + ) + assert response.status_code == 200 + with mock_webui_user(id="3"): + response = self.fast_api_client.post( + self.create_url("/create"), + json={ + "command": "/my-command2", + "title": "Hello World 2", + "content": "description 2", + }, + ) + assert response.status_code == 200 + + # Get all prompts + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 2 + + # Get prompt by command + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/command/my-command")) + assert response.status_code == 200 + data = response.json() + assert data["command"] == "/my-command" + assert data["title"] == "Hello World" + assert data["content"] == "description" + assert data["user_id"] == "2" + + # Update prompt + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/command/my-command2/update"), + json={ + "command": "irrelevant for request", + "title": "Hello World Updated", + "content": "description Updated", + }, + ) + assert response.status_code == 200 + data = response.json() + assert data["command"] == "/my-command2" + assert data["title"] == "Hello World Updated" + assert data["content"] == "description Updated" + assert data["user_id"] == "3" + + # Get prompt by command + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/command/my-command2")) + assert response.status_code == 200 + data = response.json() + assert data["command"] == "/my-command2" + assert data["title"] == "Hello World Updated" + assert data["content"] == "description Updated" + assert data["user_id"] == "3" + + # Delete prompt + with mock_webui_user(id="2"): + response = self.fast_api_client.delete( + self.create_url("/command/my-command/delete") + ) + assert response.status_code == 200 + + # Get all prompts + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/")) + assert response.status_code == 200 + assert len(response.json()) == 1 diff --git a/backend/test/apps/webui/routers/test_users.py b/backend/test/apps/webui/routers/test_users.py new file mode 100644 index 0000000000000000000000000000000000000000..9736b4d32a6df5b8cf291c420d1e4b4c4fc61f96 --- /dev/null +++ b/backend/test/apps/webui/routers/test_users.py @@ -0,0 +1,168 @@ +from test.util.abstract_integration_test import AbstractPostgresTest +from test.util.mock_user import mock_webui_user + + +def _get_user_by_id(data, param): + return next((item for item in data if item["id"] == param), None) + + +def _assert_user(data, id, **kwargs): + user = _get_user_by_id(data, id) + assert user is not None + comparison_data = { + "name": f"user {id}", + "email": f"user{id}@openwebui.com", + "profile_image_url": f"/user{id}.png", + "role": "user", + **kwargs, + } + for key, value in comparison_data.items(): + assert user[key] == value + + +class TestUsers(AbstractPostgresTest): + + BASE_PATH = "/api/v1/users" + + def setup_class(cls): + super().setup_class() + from apps.webui.models.users import Users + + cls.users = Users + + def setup_method(self): + super().setup_method() + self.users.insert_new_user( + id="1", + name="user 1", + email="user1@openwebui.com", + profile_image_url="/user1.png", + role="user", + ) + self.users.insert_new_user( + id="2", + name="user 2", + email="user2@openwebui.com", + profile_image_url="/user2.png", + role="user", + ) + + def test_users(self): + # Get all users + with mock_webui_user(id="3"): + response = self.fast_api_client.get(self.create_url("")) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, "1") + _assert_user(data, "2") + + # update role + with mock_webui_user(id="3"): + response = self.fast_api_client.post( + self.create_url("/update/role"), json={"id": "2", "role": "admin"} + ) + assert response.status_code == 200 + _assert_user([response.json()], "2", role="admin") + + # Get all users + with mock_webui_user(id="3"): + response = self.fast_api_client.get(self.create_url("")) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, "1") + _assert_user(data, "2", role="admin") + + # Get (empty) user settings + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/user/settings")) + assert response.status_code == 200 + assert response.json() is None + + # Update user settings + with mock_webui_user(id="2"): + response = self.fast_api_client.post( + self.create_url("/user/settings/update"), + json={ + "ui": {"attr1": "value1", "attr2": "value2"}, + "model_config": {"attr3": "value3", "attr4": "value4"}, + }, + ) + assert response.status_code == 200 + + # Get user settings + with mock_webui_user(id="2"): + response = self.fast_api_client.get(self.create_url("/user/settings")) + assert response.status_code == 200 + assert response.json() == { + "ui": {"attr1": "value1", "attr2": "value2"}, + "model_config": {"attr3": "value3", "attr4": "value4"}, + } + + # Get (empty) user info + with mock_webui_user(id="1"): + response = self.fast_api_client.get(self.create_url("/user/info")) + assert response.status_code == 200 + assert response.json() is None + + # Update user info + with mock_webui_user(id="1"): + response = self.fast_api_client.post( + self.create_url("/user/info/update"), + json={"attr1": "value1", "attr2": "value2"}, + ) + assert response.status_code == 200 + + # Get user info + with mock_webui_user(id="1"): + response = self.fast_api_client.get(self.create_url("/user/info")) + assert response.status_code == 200 + assert response.json() == {"attr1": "value1", "attr2": "value2"} + + # Get user by id + with mock_webui_user(id="1"): + response = self.fast_api_client.get(self.create_url("/2")) + assert response.status_code == 200 + assert response.json() == {"name": "user 2", "profile_image_url": "/user2.png"} + + # Update user by id + with mock_webui_user(id="1"): + response = self.fast_api_client.post( + self.create_url("/2/update"), + json={ + "name": "user 2 updated", + "email": "user2-updated@openwebui.com", + "profile_image_url": "/user2-updated.png", + }, + ) + assert response.status_code == 200 + + # Get all users + with mock_webui_user(id="3"): + response = self.fast_api_client.get(self.create_url("")) + assert response.status_code == 200 + assert len(response.json()) == 2 + data = response.json() + _assert_user(data, "1") + _assert_user( + data, + "2", + role="admin", + name="user 2 updated", + email="user2-updated@openwebui.com", + profile_image_url="/user2-updated.png", + ) + + # Delete user by id + with mock_webui_user(id="1"): + response = self.fast_api_client.delete(self.create_url("/2")) + assert response.status_code == 200 + + # Get all users + with mock_webui_user(id="3"): + response = self.fast_api_client.get(self.create_url("")) + assert response.status_code == 200 + assert len(response.json()) == 1 + data = response.json() + _assert_user(data, "1") diff --git a/backend/test/util/abstract_integration_test.py b/backend/test/util/abstract_integration_test.py new file mode 100644 index 0000000000000000000000000000000000000000..8535221a8575ec887b73492cba6d54f7f1b46c08 --- /dev/null +++ b/backend/test/util/abstract_integration_test.py @@ -0,0 +1,161 @@ +import logging +import os +import time + +import docker +import pytest +from docker import DockerClient +from pytest_docker.plugin import get_docker_ip +from fastapi.testclient import TestClient +from sqlalchemy import text, create_engine + + +log = logging.getLogger(__name__) + + +def get_fast_api_client(): + from main import app + + with TestClient(app) as c: + return c + + +class AbstractIntegrationTest: + BASE_PATH = None + + def create_url(self, path="", query_params=None): + if self.BASE_PATH is None: + raise Exception("BASE_PATH is not set") + parts = self.BASE_PATH.split("/") + parts = [part.strip() for part in parts if part.strip() != ""] + path_parts = path.split("/") + path_parts = [part.strip() for part in path_parts if part.strip() != ""] + query_parts = "" + if query_params: + query_parts = "&".join( + [f"{key}={value}" for key, value in query_params.items()] + ) + query_parts = f"?{query_parts}" + return "/".join(parts + path_parts) + query_parts + + @classmethod + def setup_class(cls): + pass + + def setup_method(self): + pass + + @classmethod + def teardown_class(cls): + pass + + def teardown_method(self): + pass + + +class AbstractPostgresTest(AbstractIntegrationTest): + DOCKER_CONTAINER_NAME = "postgres-test-container-will-get-deleted" + docker_client: DockerClient + + @classmethod + def _create_db_url(cls, env_vars_postgres: dict) -> str: + host = get_docker_ip() + user = env_vars_postgres["POSTGRES_USER"] + pw = env_vars_postgres["POSTGRES_PASSWORD"] + port = 8081 + db = env_vars_postgres["POSTGRES_DB"] + return f"postgresql://{user}:{pw}@{host}:{port}/{db}" + + @classmethod + def setup_class(cls): + super().setup_class() + try: + env_vars_postgres = { + "POSTGRES_USER": "user", + "POSTGRES_PASSWORD": "example", + "POSTGRES_DB": "openwebui", + } + cls.docker_client = docker.from_env() + cls.docker_client.containers.run( + "postgres:16.2", + detach=True, + environment=env_vars_postgres, + name=cls.DOCKER_CONTAINER_NAME, + ports={5432: ("0.0.0.0", 8081)}, + command="postgres -c log_statement=all", + ) + time.sleep(0.5) + + database_url = cls._create_db_url(env_vars_postgres) + os.environ["DATABASE_URL"] = database_url + retries = 10 + db = None + while retries > 0: + try: + from config import BACKEND_DIR + + db = create_engine(database_url, pool_pre_ping=True) + db = db.connect() + log.info("postgres is ready!") + break + except Exception as e: + log.warning(e) + time.sleep(3) + retries -= 1 + + if db: + # import must be after setting env! + cls.fast_api_client = get_fast_api_client() + db.close() + else: + raise Exception("Could not connect to Postgres") + except Exception as ex: + log.error(ex) + cls.teardown_class() + pytest.fail(f"Could not setup test environment: {ex}") + + def _check_db_connection(self): + from apps.webui.internal.db import Session + + retries = 10 + while retries > 0: + try: + Session.execute(text("SELECT 1")) + Session.commit() + break + except Exception as e: + Session.rollback() + log.warning(e) + time.sleep(3) + retries -= 1 + + def setup_method(self): + super().setup_method() + self._check_db_connection() + + @classmethod + def teardown_class(cls) -> None: + super().teardown_class() + cls.docker_client.containers.get(cls.DOCKER_CONTAINER_NAME).remove(force=True) + + def teardown_method(self): + from apps.webui.internal.db import Session + + # rollback everything not yet committed + Session.commit() + + # truncate all tables + tables = [ + "auth", + "chat", + "chatidtag", + "document", + "memory", + "model", + "prompt", + "tag", + '"user"', + ] + for table in tables: + Session.execute(text(f"TRUNCATE TABLE {table}")) + Session.commit() diff --git a/backend/test/util/mock_user.py b/backend/test/util/mock_user.py new file mode 100644 index 0000000000000000000000000000000000000000..8d0300d3f9a8ec1f9daf501afb9a0dbb9a7cf561 --- /dev/null +++ b/backend/test/util/mock_user.py @@ -0,0 +1,45 @@ +from contextlib import contextmanager + +from fastapi import FastAPI + + +@contextmanager +def mock_webui_user(**kwargs): + from apps.webui.main import app + + with mock_user(app, **kwargs): + yield + + +@contextmanager +def mock_user(app: FastAPI, **kwargs): + from utils.utils import ( + get_current_user, + get_verified_user, + get_admin_user, + get_current_user_by_api_key, + ) + from apps.webui.models.users import User + + def create_user(): + user_parameters = { + "id": "1", + "name": "John Doe", + "email": "john.doe@openwebui.com", + "role": "user", + "profile_image_url": "/user.png", + "last_active_at": 1627351200, + "updated_at": 1627351200, + "created_at": 162735120, + **kwargs, + } + return User(**user_parameters) + + app.dependency_overrides = { + get_current_user: create_user, + get_verified_user: create_user, + get_admin_user: create_user, + get_current_user_by_api_key: create_user, + } + yield + app.dependency_overrides = {} diff --git a/backend/utils/logo.png b/backend/utils/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..519af1db620dbf4de3694660dae7abd7392f0b3c Binary files /dev/null and b/backend/utils/logo.png differ diff --git a/backend/utils/misc.py b/backend/utils/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..f44a7ce7ac51bdd8d3830ebbf6ded341faf426a7 --- /dev/null +++ b/backend/utils/misc.py @@ -0,0 +1,312 @@ +from pathlib import Path +import hashlib +import json +import re +from datetime import timedelta +from typing import Optional, List, Tuple +import uuid +import time + + +def get_last_user_message_item(messages: List[dict]) -> str: + for message in reversed(messages): + if message["role"] == "user": + return message + return None + + +def get_last_user_message(messages: List[dict]) -> str: + message = get_last_user_message_item(messages) + + if message is not None: + if isinstance(message["content"], list): + for item in message["content"]: + if item["type"] == "text": + return item["text"] + return message["content"] + return None + + +def get_last_assistant_message(messages: List[dict]) -> str: + for message in reversed(messages): + if message["role"] == "assistant": + if isinstance(message["content"], list): + for item in message["content"]: + if item["type"] == "text": + return item["text"] + return message["content"] + return None + + +def get_system_message(messages: List[dict]) -> dict: + for message in messages: + if message["role"] == "system": + return message + return None + + +def remove_system_message(messages: List[dict]) -> List[dict]: + return [message for message in messages if message["role"] != "system"] + + +def pop_system_message(messages: List[dict]) -> Tuple[dict, List[dict]]: + return get_system_message(messages), remove_system_message(messages) + + +def prepend_to_first_user_message_content( + content: str, messages: List[dict] +) -> List[dict]: + for message in messages: + if message["role"] == "user": + if isinstance(message["content"], list): + for item in message["content"]: + if item["type"] == "text": + item["text"] = f"{content}\n{item['text']}" + else: + message["content"] = f"{content}\n{message['content']}" + break + return messages + + +def add_or_update_system_message(content: str, messages: List[dict]): + """ + Adds a new system message at the beginning of the messages list + or updates the existing system message at the beginning. + + :param msg: The message to be added or appended. + :param messages: The list of message dictionaries. + :return: The updated list of message dictionaries. + """ + + if messages and messages[0].get("role") == "system": + messages[0]["content"] += f"{content}\n{messages[0]['content']}" + else: + # Insert at the beginning + messages.insert(0, {"role": "system", "content": content}) + + return messages + + +def stream_message_template(model: str, message: str): + return { + "id": f"{model}-{str(uuid.uuid4())}", + "object": "chat.completion.chunk", + "created": int(time.time()), + "model": model, + "choices": [ + { + "index": 0, + "delta": {"content": message}, + "logprobs": None, + "finish_reason": None, + } + ], + } + + +def get_gravatar_url(email): + # Trim leading and trailing whitespace from + # an email address and force all characters + # to lower case + address = str(email).strip().lower() + + # Create a SHA256 hash of the final string + hash_object = hashlib.sha256(address.encode()) + hash_hex = hash_object.hexdigest() + + # Grab the actual image URL + return f"https://www.gravatar.com/avatar/{hash_hex}?d=mp" + + +def calculate_sha256(file): + sha256 = hashlib.sha256() + # Read the file in chunks to efficiently handle large files + for chunk in iter(lambda: file.read(8192), b""): + sha256.update(chunk) + return sha256.hexdigest() + + +def calculate_sha256_string(string): + # Create a new SHA-256 hash object + sha256_hash = hashlib.sha256() + # Update the hash object with the bytes of the input string + sha256_hash.update(string.encode("utf-8")) + # Get the hexadecimal representation of the hash + hashed_string = sha256_hash.hexdigest() + return hashed_string + + +def validate_email_format(email: str) -> bool: + if email.endswith("@localhost"): + return True + + return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email)) + + +def sanitize_filename(file_name): + # Convert to lowercase + lower_case_file_name = file_name.lower() + + # Remove special characters using regular expression + sanitized_file_name = re.sub(r"[^\w\s]", "", lower_case_file_name) + + # Replace spaces with dashes + final_file_name = re.sub(r"\s+", "-", sanitized_file_name) + + return final_file_name + + +def extract_folders_after_data_docs(path): + # Convert the path to a Path object if it's not already + path = Path(path) + + # Extract parts of the path + parts = path.parts + + # Find the index of '/data/docs' in the path + try: + index_data_docs = parts.index("data") + 1 + index_docs = parts.index("docs", index_data_docs) + 1 + except ValueError: + return [] + + # Exclude the filename and accumulate folder names + tags = [] + + folders = parts[index_docs:-1] + for idx, part in enumerate(folders): + tags.append("/".join(folders[: idx + 1])) + + return tags + + +def parse_duration(duration: str) -> Optional[timedelta]: + if duration == "-1" or duration == "0": + return None + + # Regular expression to find number and unit pairs + pattern = r"(-?\d+(\.\d+)?)(ms|s|m|h|d|w)" + matches = re.findall(pattern, duration) + + if not matches: + raise ValueError("Invalid duration string") + + total_duration = timedelta() + + for number, _, unit in matches: + number = float(number) + if unit == "ms": + total_duration += timedelta(milliseconds=number) + elif unit == "s": + total_duration += timedelta(seconds=number) + elif unit == "m": + total_duration += timedelta(minutes=number) + elif unit == "h": + total_duration += timedelta(hours=number) + elif unit == "d": + total_duration += timedelta(days=number) + elif unit == "w": + total_duration += timedelta(weeks=number) + + return total_duration + + +def parse_ollama_modelfile(model_text): + parameters_meta = { + "mirostat": int, + "mirostat_eta": float, + "mirostat_tau": float, + "num_ctx": int, + "repeat_last_n": int, + "repeat_penalty": float, + "temperature": float, + "seed": int, + "tfs_z": float, + "num_predict": int, + "top_k": int, + "top_p": float, + "num_keep": int, + "typical_p": float, + "presence_penalty": float, + "frequency_penalty": float, + "penalize_newline": bool, + "numa": bool, + "num_batch": int, + "num_gpu": int, + "main_gpu": int, + "low_vram": bool, + "f16_kv": bool, + "vocab_only": bool, + "use_mmap": bool, + "use_mlock": bool, + "num_thread": int, + } + + data = {"base_model_id": None, "params": {}} + + # Parse base model + base_model_match = re.search( + r"^FROM\s+(\w+)", model_text, re.MULTILINE | re.IGNORECASE + ) + if base_model_match: + data["base_model_id"] = base_model_match.group(1) + + # Parse template + template_match = re.search( + r'TEMPLATE\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE + ) + if template_match: + data["params"] = {"template": template_match.group(1).strip()} + + # Parse stops + stops = re.findall(r'PARAMETER stop "(.*?)"', model_text, re.IGNORECASE) + if stops: + data["params"]["stop"] = stops + + # Parse other parameters from the provided list + for param, param_type in parameters_meta.items(): + param_match = re.search(rf"PARAMETER {param} (.+)", model_text, re.IGNORECASE) + if param_match: + value = param_match.group(1) + + try: + if param_type == int: + value = int(value) + elif param_type == float: + value = float(value) + elif param_type == bool: + value = value.lower() == "true" + except Exception as e: + print(e) + continue + + data["params"][param] = value + + # Parse adapter + adapter_match = re.search(r"ADAPTER (.+)", model_text, re.IGNORECASE) + if adapter_match: + data["params"]["adapter"] = adapter_match.group(1) + + # Parse system description + system_desc_match = re.search( + r'SYSTEM\s+"""(.+?)"""', model_text, re.DOTALL | re.IGNORECASE + ) + system_desc_match_single = re.search( + r"SYSTEM\s+([^\n]+)", model_text, re.IGNORECASE + ) + + if system_desc_match: + data["params"]["system"] = system_desc_match.group(1).strip() + elif system_desc_match_single: + data["params"]["system"] = system_desc_match_single.group(1).strip() + + # Parse messages + messages = [] + message_matches = re.findall(r"MESSAGE (\w+) (.+)", model_text, re.IGNORECASE) + for role, content in message_matches: + messages.append({"role": role, "content": content}) + + if messages: + data["params"]["messages"] = messages + + return data diff --git a/backend/utils/task.py b/backend/utils/task.py new file mode 100644 index 0000000000000000000000000000000000000000..053a526a801ccf8a07bafbdb5ad7e5f1a222e808 --- /dev/null +++ b/backend/utils/task.py @@ -0,0 +1,127 @@ +import re +import math + +from datetime import datetime +from typing import Optional + + +def prompt_template( + template: str, user_name: str = None, user_location: str = None +) -> str: + # Get the current date + current_date = datetime.now() + + # Format the date to YYYY-MM-DD + formatted_date = current_date.strftime("%Y-%m-%d") + formatted_time = current_date.strftime("%I:%M:%S %p") + + template = template.replace("{{CURRENT_DATE}}", formatted_date) + template = template.replace("{{CURRENT_TIME}}", formatted_time) + template = template.replace( + "{{CURRENT_DATETIME}}", f"{formatted_date} {formatted_time}" + ) + + if user_name: + # Replace {{USER_NAME}} in the template with the user's name + template = template.replace("{{USER_NAME}}", user_name) + else: + # Replace {{USER_NAME}} in the template with "Unknown" + template = template.replace("{{USER_NAME}}", "Unknown") + + if user_location: + # Replace {{USER_LOCATION}} in the template with the current location + template = template.replace("{{USER_LOCATION}}", user_location) + else: + # Replace {{USER_LOCATION}} in the template with "Unknown" + template = template.replace("{{USER_LOCATION}}", "Unknown") + + return template + + +def title_generation_template( + template: str, prompt: str, user: Optional[dict] = None +) -> str: + def replacement_function(match): + full_match = match.group(0) + start_length = match.group(1) + end_length = match.group(2) + middle_length = match.group(3) + + if full_match == "{{prompt}}": + return prompt + elif start_length is not None: + return prompt[: int(start_length)] + elif end_length is not None: + return prompt[-int(end_length) :] + elif middle_length is not None: + middle_length = int(middle_length) + if len(prompt) <= middle_length: + return prompt + start = prompt[: math.ceil(middle_length / 2)] + end = prompt[-math.floor(middle_length / 2) :] + return f"{start}...{end}" + return "" + + template = re.sub( + r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}", + replacement_function, + template, + ) + + template = prompt_template( + template, + **( + {"user_name": user.get("name"), "user_location": user.get("location")} + if user + else {} + ), + ) + + return template + + +def search_query_generation_template( + template: str, prompt: str, user: Optional[dict] = None +) -> str: + + def replacement_function(match): + full_match = match.group(0) + start_length = match.group(1) + end_length = match.group(2) + middle_length = match.group(3) + + if full_match == "{{prompt}}": + return prompt + elif start_length is not None: + return prompt[: int(start_length)] + elif end_length is not None: + return prompt[-int(end_length) :] + elif middle_length is not None: + middle_length = int(middle_length) + if len(prompt) <= middle_length: + return prompt + start = prompt[: math.ceil(middle_length / 2)] + end = prompt[-math.floor(middle_length / 2) :] + return f"{start}...{end}" + return "" + + template = re.sub( + r"{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}", + replacement_function, + template, + ) + + template = prompt_template( + template, + **( + {"user_name": user.get("name"), "user_location": user.get("location")} + if user + else {} + ), + ) + return template + + +def tools_function_calling_generation_template(template: str, tools_specs: str) -> str: + template = template.replace("{{TOOLS}}", tools_specs) + return template diff --git a/backend/utils/tools.py b/backend/utils/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..3e5d82fd6d15255e1f89faab2ec91b037eb88116 --- /dev/null +++ b/backend/utils/tools.py @@ -0,0 +1,79 @@ +import inspect +from typing import get_type_hints, List, Dict, Any + + +def doc_to_dict(docstring): + lines = docstring.split("\n") + description = lines[1].strip() + param_dict = {} + + for line in lines: + if ":param" in line: + line = line.replace(":param", "").strip() + param, desc = line.split(":", 1) + param_dict[param.strip()] = desc.strip() + ret_dict = {"description": description, "params": param_dict} + return ret_dict + + +def get_tools_specs(tools) -> List[dict]: + function_list = [ + {"name": func, "function": getattr(tools, func)} + for func in dir(tools) + if callable(getattr(tools, func)) + and not func.startswith("__") + and not inspect.isclass(getattr(tools, func)) + ] + + specs = [] + for function_item in function_list: + function_name = function_item["name"] + function = function_item["function"] + + function_doc = doc_to_dict(function.__doc__ or function_name) + specs.append( + { + "name": function_name, + # TODO: multi-line desc? + "description": function_doc.get("description", function_name), + "parameters": { + "type": "object", + "properties": { + param_name: { + "type": param_annotation.__name__.lower(), + **( + { + "enum": ( + str(param_annotation.__args__) + if hasattr(param_annotation, "__args__") + else None + ) + } + if hasattr(param_annotation, "__args__") + else {} + ), + "description": function_doc.get("params", {}).get( + param_name, param_name + ), + } + for param_name, param_annotation in get_type_hints( + function + ).items() + if param_name != "return" + and not ( + param_name.startswith("__") and param_name.endswith("__") + ) + }, + "required": [ + name + for name, param in inspect.signature( + function + ).parameters.items() + if param.default is param.empty + and not (name.startswith("__") and name.endswith("__")) + ], + }, + } + ) + + return specs diff --git a/backend/utils/utils.py b/backend/utils/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..fbc539af5c2de74edbb58c4273c211363b51ffdb --- /dev/null +++ b/backend/utils/utils.py @@ -0,0 +1,145 @@ +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from fastapi import HTTPException, status, Depends, Request +from sqlalchemy.orm import Session + +from apps.webui.models.users import Users + +from pydantic import BaseModel +from typing import Union, Optional +from constants import ERROR_MESSAGES +from passlib.context import CryptContext +from datetime import datetime, timedelta +import requests +import jwt +import uuid +import logging +import config + +logging.getLogger("passlib").setLevel(logging.ERROR) + + +SESSION_SECRET = config.WEBUI_SECRET_KEY +ALGORITHM = "HS256" + +############## +# Auth Utils +############## + +bearer_security = HTTPBearer(auto_error=False) +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def verify_password(plain_password, hashed_password): + return ( + pwd_context.verify(plain_password, hashed_password) if hashed_password else None + ) + + +def get_password_hash(password): + return pwd_context.hash(password) + + +def create_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: + payload = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + payload.update({"exp": expire}) + + encoded_jwt = jwt.encode(payload, SESSION_SECRET, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_token(token: str) -> Optional[dict]: + try: + decoded = jwt.decode(token, SESSION_SECRET, algorithms=[ALGORITHM]) + return decoded + except Exception as e: + return None + + +def extract_token_from_auth_header(auth_header: str): + return auth_header[len("Bearer ") :] + + +def create_api_key(): + key = str(uuid.uuid4()).replace("-", "") + return f"sk-{key}" + + +def get_http_authorization_cred(auth_header: str): + try: + scheme, credentials = auth_header.split(" ") + return HTTPAuthorizationCredentials(scheme=scheme, credentials=credentials) + except: + raise ValueError(ERROR_MESSAGES.INVALID_TOKEN) + + +def get_current_user( + request: Request, + auth_token: HTTPAuthorizationCredentials = Depends(bearer_security), +): + token = None + + if auth_token is not None: + token = auth_token.credentials + + if token is None and "token" in request.cookies: + token = request.cookies.get("token") + + if token is None: + raise HTTPException(status_code=403, detail="Not authenticated") + + # auth by api key + if token.startswith("sk-"): + return get_current_user_by_api_key(token) + + # auth by jwt token + data = decode_token(token) + if data != None and "id" in data: + user = Users.get_user_by_id(data["id"]) + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + else: + Users.update_user_last_active_by_id(user.id) + return user + else: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.UNAUTHORIZED, + ) + + +def get_current_user_by_api_key(api_key: str): + user = Users.get_user_by_api_key(api_key) + + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.INVALID_TOKEN, + ) + else: + Users.update_user_last_active_by_id(user.id) + + return user + + +def get_verified_user(user=Depends(get_current_user)): + if user.role not in {"user", "admin"}: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return user + + +def get_admin_user(user=Depends(get_current_user)): + if user.role != "admin": + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=ERROR_MESSAGES.ACCESS_PROHIBITED, + ) + return user diff --git a/backend/utils/webhook.py b/backend/utils/webhook.py new file mode 100644 index 0000000000000000000000000000000000000000..b6692e53a7bc3e93c69a1d5c8ac62565d636254f --- /dev/null +++ b/backend/utils/webhook.py @@ -0,0 +1,54 @@ +import json +import requests +import logging + +from config import SRC_LOG_LEVELS, VERSION, WEBUI_FAVICON_URL, WEBUI_NAME + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["WEBHOOK"]) + + +def post_webhook(url: str, message: str, event_data: dict) -> bool: + try: + payload = {} + + # Slack and Google Chat Webhooks + if "https://hooks.slack.com" in url or "https://chat.googleapis.com" in url: + payload["text"] = message + # Discord Webhooks + elif "https://discord.com/api/webhooks" in url: + payload["content"] = message + # Microsoft Teams Webhooks + elif "webhook.office.com" in url: + action = event_data.get("action", "undefined") + facts = [ + {"name": name, "value": value} + for name, value in json.loads(event_data.get("user", {})).items() + ] + payload = { + "@type": "MessageCard", + "@context": "http://schema.org/extensions", + "themeColor": "0076D7", + "summary": message, + "sections": [ + { + "activityTitle": message, + "activitySubtitle": f"{WEBUI_NAME} ({VERSION}) - {action}", + "activityImage": WEBUI_FAVICON_URL, + "facts": facts, + "markdown": True, + } + ], + } + # Default Payload + else: + payload = {**event_data} + + log.debug(f"payload: {payload}") + r = requests.post(url, json=payload) + r.raise_for_status() + log.debug(f"r.text: {r.text}") + return True + except Exception as e: + log.exception(e) + return False diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..e0a038da06fc8f3a52efc9e870fa86124ec5ad14 Binary files /dev/null and b/bun.lockb differ diff --git a/confirm_remove.sh b/confirm_remove.sh new file mode 100755 index 0000000000000000000000000000000000000000..051908e6de668bc47b3edd5b79b70d65094823a2 --- /dev/null +++ b/confirm_remove.sh @@ -0,0 +1,13 @@ +#!/bin/bash +echo "Warning: This will remove all containers and volumes, including persistent data. Do you want to continue? [Y/N]" +read ans +if [ "$ans" == "Y" ] || [ "$ans" == "y" ]; then + command docker-compose 2>/dev/null + if [ "$?" == "0" ]; then + docker-compose down -v + else + docker compose down -v + fi +else + echo "Operation cancelled." +fi diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbb538233809b1c26de4c0d33ae45e0c30405c2a --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + baseUrl: 'http://localhost:8080' + }, + video: true +}); diff --git a/cypress/data/example-doc.txt b/cypress/data/example-doc.txt new file mode 100644 index 0000000000000000000000000000000000000000..d4f6f455ed18711baf9ddb8ae814538ea7076e8d --- /dev/null +++ b/cypress/data/example-doc.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pellentesque elit eget gravida cum sociis natoque. Morbi tristique senectus et netus et malesuada. Sapien nec sagittis aliquam malesuada bibendum. Amet consectetur adipiscing elit duis tristique sollicitudin. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Arcu cursus vitae congue mauris rhoncus aenean vel elit scelerisque. Nec feugiat nisl pretium fusce id velit. Imperdiet proin fermentum leo vel. Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Pellentesque sit amet porttitor eget dolor morbi non arcu risus. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Et ultrices neque ornare aenean euismod. + +Enim nulla aliquet porttitor lacus luctus accumsan tortor posuere ac. Viverra nibh cras pulvinar mattis nunc. Lacinia at quis risus sed vulputate. Ac tortor vitae purus faucibus ornare suspendisse sed nisi lacus. Bibendum arcu vitae elementum curabitur vitae nunc. Consectetur adipiscing elit duis tristique sollicitudin nibh sit amet commodo. Velit egestas dui id ornare arcu odio ut. Et malesuada fames ac turpis egestas integer eget aliquet. Lacus suspendisse faucibus interdum posuere lorem ipsum dolor sit. Morbi tristique senectus et netus. Pretium viverra suspendisse potenti nullam ac tortor vitae. Parturient montes nascetur ridiculus mus mauris vitae. Quis viverra nibh cras pulvinar mattis nunc sed blandit libero. Euismod nisi porta lorem mollis aliquam ut porttitor leo. Mauris in aliquam sem fringilla ut morbi. Faucibus pulvinar elementum integer enim neque. Neque sodales ut etiam sit. Consectetur a erat nam at. + +Sed nisi lacus sed viverra tellus in hac habitasse. Proin sagittis nisl rhoncus mattis rhoncus. Risus commodo viverra maecenas accumsan lacus. Morbi quis commodo odio aenean sed adipiscing. Mollis nunc sed id semper risus in. Ultricies mi eget mauris pharetra et ultrices neque. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Eget magna fermentum iaculis eu non diam phasellus. Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Id donec ultrices tincidunt arcu non sodales. Sed cras ornare arcu dui vivamus arcu felis bibendum ut. Urna duis convallis convallis tellus id interdum velit. Rhoncus mattis rhoncus urna neque viverra justo nec. Purus semper eget duis at tellus at urna condimentum. Et odio pellentesque diam volutpat commodo sed egestas. Blandit volutpat maecenas volutpat blandit. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Est ullamcorper eget nulla facilisi etiam dignissim. + +Justo nec ultrices dui sapien eget mi proin sed. Purus gravida quis blandit turpis cursus in hac. Placerat orci nulla pellentesque dignissim enim sit. Morbi tristique senectus et netus et malesuada fames ac. Consequat mauris nunc congue nisi. Eu lobortis elementum nibh tellus molestie nunc non blandit. Viverra justo nec ultrices dui. Morbi non arcu risus quis. Elementum sagittis vitae et leo duis. Lectus mauris ultrices eros in cursus. Neque laoreet suspendisse interdum consectetur. + +Facilisis gravida neque convallis a cras. Nisl rhoncus mattis rhoncus urna neque viverra justo. Faucibus purus in massa tempor. Lacus laoreet non curabitur gravida arcu ac tortor. Tincidunt eget nullam non nisi est sit amet. Ornare lectus sit amet est placerat in egestas. Sollicitudin tempor id eu nisl nunc mi. Scelerisque viverra mauris in aliquam sem fringilla ut. Ullamcorper sit amet risus nullam. Mauris rhoncus aenean vel elit scelerisque mauris pellentesque pulvinar. Velit euismod in pellentesque massa placerat duis ultricies lacus. Pharetra magna ac placerat vestibulum lectus mauris ultrices eros in. Lorem ipsum dolor sit amet. Sit amet mauris commodo quis imperdiet. Quam pellentesque nec nam aliquam sem et tortor. Amet nisl purus in mollis nunc. Sed risus pretium quam vulputate dignissim suspendisse in est. Nisl condimentum id venenatis a condimentum. Velit euismod in pellentesque massa. Quam id leo in vitae turpis massa sed. diff --git a/cypress/e2e/chat.cy.ts b/cypress/e2e/chat.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..ddb33d6c06b54bcbe562241c73569c710b900948 --- /dev/null +++ b/cypress/e2e/chat.cy.ts @@ -0,0 +1,101 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// <reference path="../support/index.d.ts" /> + +// These tests run through the chat flow. +describe('Settings', () => { + // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames + after(() => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + }); + + beforeEach(() => { + // Login as the admin user + cy.loginAdmin(); + // Visit the home page + cy.visit('/'); + }); + + context('Ollama', () => { + it('user can select a model', () => { + // Click on the model selector + cy.get('button[aria-label="Select a model"]').click(); + // Select the first model + cy.get('button[aria-label="model-item"]').first().click(); + }); + + it('user can perform text chat', () => { + // Click on the model selector + cy.get('button[aria-label="Select a model"]').click(); + // Select the first model + cy.get('button[aria-label="model-item"]').first().click(); + // Type a message + cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', { + force: true + }); + // Send the message + cy.get('button[type="submit"]').click(); + // User's message should be visible + cy.get('.chat-user').should('exist'); + // Wait for the response + cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received + .find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received + .should('exist'); + }); + + it('user can share chat', () => { + // Click on the model selector + cy.get('button[aria-label="Select a model"]').click(); + // Select the first model + cy.get('button[aria-label="model-item"]').first().click(); + // Type a message + cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', { + force: true + }); + // Send the message + cy.get('button[type="submit"]').click(); + // User's message should be visible + cy.get('.chat-user').should('exist'); + // Wait for the response + cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received + .find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received + .should('exist'); + // spy on requests + const spy = cy.spy(); + cy.intercept('GET', '/api/v1/chats/*', spy); + // Open context menu + cy.get('#chat-context-menu-button').click(); + // Click share button + cy.get('#chat-share-button').click(); + // Check if the share dialog is visible + cy.get('#copy-and-share-chat-button').should('exist'); + cy.wrap({}, { timeout: 5000 }).should(() => { + // Check if the request was made twice (once for to replace chat object and once more due to change event) + expect(spy).to.be.callCount(2); + }); + }); + + it('user can generate image', () => { + // Click on the model selector + cy.get('button[aria-label="Select a model"]').click(); + // Select the first model + cy.get('button[aria-label="model-item"]').first().click(); + // Type a message + cy.get('#chat-textarea').type('Hi, what can you do? A single sentence only please.', { + force: true + }); + // Send the message + cy.get('button[type="submit"]').click(); + // User's message should be visible + cy.get('.chat-user').should('exist'); + // Wait for the response + cy.get('.chat-assistant', { timeout: 120_000 }) // .chat-assistant is created after the first token is received + .find('div[aria-label="Generation Info"]', { timeout: 120_000 }) // Generation Info is created after the stop token is received + .should('exist'); + // Click on the generate image button + cy.get('[aria-label="Generate Image"]').click(); + // Wait for image to be visible + cy.get('img[data-cy="image"]', { timeout: 60_000 }).should('be.visible'); + }); + }); +}); diff --git a/cypress/e2e/documents.cy.ts b/cypress/e2e/documents.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ca14980d2520f0c089a8f0306d04f5d7e5890e6 --- /dev/null +++ b/cypress/e2e/documents.cy.ts @@ -0,0 +1,46 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// <reference path="../support/index.d.ts" /> + +describe('Documents', () => { + const timestamp = Date.now(); + + before(() => { + cy.uploadTestDocument(timestamp); + }); + + after(() => { + cy.deleteTestDocument(timestamp); + }); + + context('Admin', () => { + beforeEach(() => { + // Login as the admin user + cy.loginAdmin(); + // Visit the home page + cy.visit('/workspace/documents'); + cy.get('button').contains('#cypress-test').click(); + }); + + it('can see documents', () => { + cy.get('div').contains(`document-test-initial-${timestamp}.txt`).should('have.length', 1); + }); + + it('can see edit button', () => { + cy.get('div') + .contains(`document-test-initial-${timestamp}.txt`) + .get("button[aria-label='Edit Doc']") + .should('exist'); + }); + + it('can see delete button', () => { + cy.get('div') + .contains(`document-test-initial-${timestamp}.txt`) + .get("button[aria-label='Delete Doc']") + .should('exist'); + }); + + it('can see upload button', () => { + cy.get("button[aria-label='Add Docs']").should('exist'); + }); + }); +}); diff --git a/cypress/e2e/registration.cy.ts b/cypress/e2e/registration.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..232d75e882a93870f318f96f98ffd56efbc4bd8f --- /dev/null +++ b/cypress/e2e/registration.cy.ts @@ -0,0 +1,52 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// <reference path="../support/index.d.ts" /> +import { adminUser } from '../support/e2e'; + +// These tests assume the following defaults: +// 1. No users exist in the database or that the test admin user is an admin +// 2. Language is set to English +// 3. The default role for new users is 'pending' +describe('Registration and Login', () => { + // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames + after(() => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + }); + + beforeEach(() => { + cy.visit('/'); + }); + + it('should register a new user as pending', () => { + const userName = `Test User - ${Date.now()}`; + const userEmail = `cypress-${Date.now()}@example.com`; + // Toggle from sign in to sign up + cy.contains('Sign up').click(); + // Fill out the form + cy.get('input[autocomplete="name"]').type(userName); + cy.get('input[autocomplete="email"]').type(userEmail); + cy.get('input[type="password"]').type('password'); + // Submit the form + cy.get('button[type="submit"]').click(); + // Wait until the user is redirected to the home page + cy.contains(userName); + // Expect the user to be pending + cy.contains('Check Again'); + }); + + it('can login with the admin user', () => { + // Fill out the form + cy.get('input[autocomplete="email"]').type(adminUser.email); + cy.get('input[type="password"]').type(adminUser.password); + // Submit the form + cy.get('button[type="submit"]').click(); + // Wait until the user is redirected to the home page + cy.contains(adminUser.name); + // Dismiss the changelog dialog if it is visible + cy.getAllLocalStorage().then((ls) => { + if (!ls['version']) { + cy.get('button').contains("Okay, Let's Go!").click(); + } + }); + }); +}); diff --git a/cypress/e2e/settings.cy.ts b/cypress/e2e/settings.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ea91698072fd25cb75a6961ac3d9f0652c303da --- /dev/null +++ b/cypress/e2e/settings.cy.ts @@ -0,0 +1,63 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// <reference path="../support/index.d.ts" /> +import { adminUser } from '../support/e2e'; + +// These tests run through the various settings pages, ensuring that the user can interact with them as expected +describe('Settings', () => { + // Wait for 2 seconds after all tests to fix an issue with Cypress's video recording missing the last few frames + after(() => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(2000); + }); + + beforeEach(() => { + // Login as the admin user + cy.loginAdmin(); + // Visit the home page + cy.visit('/'); + // Click on the user menu + cy.get('button[aria-label="User Menu"]').click(); + // Click on the settings link + cy.get('button').contains('Settings').click(); + }); + + context('General', () => { + it('user can open the General modal and hit save', () => { + cy.get('button').contains('General').click(); + cy.get('button').contains('Save').click(); + }); + }); + + context('Interface', () => { + it('user can open the Interface modal and hit save', () => { + cy.get('button').contains('Interface').click(); + cy.get('button').contains('Save').click(); + }); + }); + + context('Audio', () => { + it('user can open the Audio modal and hit save', () => { + cy.get('button').contains('Audio').click(); + cy.get('button').contains('Save').click(); + }); + }); + + context('Chats', () => { + it('user can open the Chats modal', () => { + cy.get('button').contains('Chats').click(); + }); + }); + + context('Account', () => { + it('user can open the Account modal and hit save', () => { + cy.get('button').contains('Account').click(); + cy.get('button').contains('Save').click(); + }); + }); + + context('About', () => { + it('user can open the About modal', () => { + cy.get('button').contains('About').click(); + }); + }); +}); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000000000000000000000000000000000000..9847887333348e70ca7ccaeb560f34fbb7486904 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,122 @@ +/// <reference types="cypress" /> +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// <reference path="../support/index.d.ts" /> + +export const adminUser = { + name: 'Admin User', + email: 'admin@example.com', + password: 'password' +}; + +const login = (email: string, password: string) => { + return cy.session( + email, + () => { + // Make sure to test against us english to have stable tests, + // regardless on local language preferences + localStorage.setItem('locale', 'en-US'); + // Visit auth page + cy.visit('/auth'); + // Fill out the form + cy.get('input[autocomplete="email"]').type(email); + cy.get('input[type="password"]').type(password); + // Submit the form + cy.get('button[type="submit"]').click(); + // Wait until the user is redirected to the home page + cy.get('#chat-search').should('exist'); + // Get the current version to skip the changelog dialog + if (localStorage.getItem('version') === null) { + cy.get('button').contains("Okay, Let's Go!").click(); + } + }, + { + validate: () => { + cy.request({ + method: 'GET', + url: '/api/v1/auths/', + headers: { + Authorization: 'Bearer ' + localStorage.getItem('token') + } + }); + } + } + ); +}; + +const register = (name: string, email: string, password: string) => { + return cy + .request({ + method: 'POST', + url: '/api/v1/auths/signup', + body: { + name: name, + email: email, + password: password + }, + failOnStatusCode: false + }) + .then((response) => { + expect(response.status).to.be.oneOf([200, 400]); + }); +}; + +const registerAdmin = () => { + return register(adminUser.name, adminUser.email, adminUser.password); +}; + +const loginAdmin = () => { + return login(adminUser.email, adminUser.password); +}; + +Cypress.Commands.add('login', (email, password) => login(email, password)); +Cypress.Commands.add('register', (name, email, password) => register(name, email, password)); +Cypress.Commands.add('registerAdmin', () => registerAdmin()); +Cypress.Commands.add('loginAdmin', () => loginAdmin()); + +Cypress.Commands.add('uploadTestDocument', (suffix: any) => { + // Login as admin + cy.loginAdmin(); + // upload example document + cy.visit('/workspace/documents'); + // Create a document + cy.get("button[aria-label='Add Docs']").click(); + cy.readFile('cypress/data/example-doc.txt').then((text) => { + // select file + cy.get('#upload-doc-input').selectFile( + { + contents: Cypress.Buffer.from(text + Date.now()), + fileName: `document-test-initial-${suffix}.txt`, + mimeType: 'text/plain', + lastModified: Date.now() + }, + { + force: true + } + ); + // open tag input + cy.get("button[aria-label='Add Tag']").click(); + cy.get("input[placeholder='Add a tag']").type('cypress-test'); + cy.get("button[aria-label='Save Tag']").click(); + + // submit to upload + cy.get("button[type='submit']").click(); + + // wait for upload to finish + cy.get('button').contains('#cypress-test').should('exist'); + cy.get('div').contains(`document-test-initial-${suffix}.txt`).should('exist'); + }); +}); + +Cypress.Commands.add('deleteTestDocument', (suffix: any) => { + cy.loginAdmin(); + cy.visit('/workspace/documents'); + // clean up uploaded documents + cy.get('div') + .contains(`document-test-initial-${suffix}.txt`) + .find("button[aria-label='Delete Doc']") + .click(); +}); + +before(() => { + cy.registerAdmin(); +}); diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..647db92115a3c6ee9e5b35b90b585a2a0a837306 --- /dev/null +++ b/cypress/support/index.d.ts @@ -0,0 +1,13 @@ +// load the global Cypress types +/// <reference types="cypress" /> + +declare namespace Cypress { + interface Chainable { + login(email: string, password: string): Chainable<Element>; + register(name: string, email: string, password: string): Chainable<Element>; + registerAdmin(): Chainable<Element>; + loginAdmin(): Chainable<Element>; + uploadTestDocument(suffix: any): Chainable<Element>; + deleteTestDocument(suffix: any): Chainable<Element>; + } +} diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..ff28d946496fa69c603c0ec9287317a7dcc83279 --- /dev/null +++ b/cypress/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "inlineSourceMap": true, + "sourceMap": false + } +} diff --git a/docker-compose.a1111-test.yaml b/docker-compose.a1111-test.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e6ab12c07f0995369107943d0b23a68ebac607b3 --- /dev/null +++ b/docker-compose.a1111-test.yaml @@ -0,0 +1,31 @@ +# This is an overlay that spins up stable-diffusion-webui for integration testing +# This is not designed to be used in production +services: + stable-diffusion-webui: + # Not built for ARM64 + platform: linux/amd64 + image: ghcr.io/neggles/sd-webui-docker:latest + restart: unless-stopped + environment: + CLI_ARGS: "--api --use-cpu all --precision full --no-half --skip-torch-cuda-test --ckpt /empty.pt --do-not-download-clip --disable-nan-check --disable-opt-split-attention" + PYTHONUNBUFFERED: "1" + TERM: "vt100" + SD_WEBUI_VARIANT: "default" + # Hack to get container working on Apple Silicon + # Rosetta creates a conflict ${HOME}/.cache folder + entrypoint: /bin/bash + command: + - -c + - | + export HOME=/root-home + rm -rf $${HOME}/.cache + /docker/entrypoint.sh python -u webui.py --listen --port $${WEBUI_PORT} --skip-version-check $${CLI_ARGS} + volumes: + - ./test/test_files/image_gen/sd-empty.pt:/empty.pt + + open-webui: + environment: + ENABLE_IMAGE_GENERATION: "true" + AUTOMATIC1111_BASE_URL: http://stable-diffusion-webui:7860 + IMAGE_SIZE: "64x64" + IMAGE_STEPS: "3" diff --git a/docker-compose.amdgpu.yaml b/docker-compose.amdgpu.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7a1295d94515c6f32a9c11d913ce603a989f504a --- /dev/null +++ b/docker-compose.amdgpu.yaml @@ -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}' \ No newline at end of file diff --git a/docker-compose.api.yaml b/docker-compose.api.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8f8fbe59ad03274ef4f6a414cc9333290588e0aa --- /dev/null +++ b/docker-compose.api.yaml @@ -0,0 +1,5 @@ +services: + ollama: + # Expose Ollama API outside the container stack + ports: + - ${OLLAMA_WEBAPI_PORT-11434}:11434 diff --git a/docker-compose.data.yaml b/docker-compose.data.yaml new file mode 100644 index 0000000000000000000000000000000000000000..4b70601f894a1ad3530cc25ea616a3a5ddb691a6 --- /dev/null +++ b/docker-compose.data.yaml @@ -0,0 +1,4 @@ +services: + ollama: + volumes: + - ${OLLAMA_DATA_DIR-./ollama-data}:/root/.ollama diff --git a/docker-compose.gpu.yaml b/docker-compose.gpu.yaml new file mode 100644 index 0000000000000000000000000000000000000000..de821235da42f5e116315f7407d2c6de0451c99c --- /dev/null +++ b/docker-compose.gpu.yaml @@ -0,0 +1,11 @@ +services: + ollama: + # GPU support + deploy: + resources: + reservations: + devices: + - driver: ${OLLAMA_GPU_DRIVER-nvidia} + count: ${OLLAMA_GPU_COUNT-1} + capabilities: + - gpu diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000000000000000000000000000000000000..74249febd9e37e2166fa6c07a73770812e745b7d --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,34 @@ +services: + ollama: + volumes: + - ollama:/root/.ollama + container_name: ollama + pull_policy: always + tty: true + restart: unless-stopped + image: ollama/ollama:${OLLAMA_DOCKER_TAG-latest} + + open-webui: + build: + context: . + args: + OLLAMA_BASE_URL: '/ollama' + dockerfile: Dockerfile + image: ghcr.io/open-webui/open-webui:${WEBUI_DOCKER_TAG-main} + container_name: open-webui + volumes: + - open-webui:/app/backend/data + depends_on: + - ollama + ports: + - ${OPEN_WEBUI_PORT-3000}:8080 + environment: + - 'OLLAMA_BASE_URL=http://ollama:11434' + - 'WEBUI_SECRET_KEY=' + extra_hosts: + - host.docker.internal:host-gateway + restart: unless-stopped + +volumes: + ollama: {} + open-webui: {} diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..325964b1a94a94b3a296a4da8e84525bc24d3fb8 --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to Open WebUI + +🚀 **Welcome, Contributors!** 🚀 + +Your interest in contributing to Open WebUI is greatly appreciated. This document is here to guide you through the process, ensuring your contributions enhance the project effectively. Let's make Open WebUI even better, together! + +## 📌 Key Points + +### 🦙 Ollama vs. Open WebUI + +It's crucial to distinguish between Ollama and Open WebUI: + +- **Open WebUI** focuses on providing an intuitive and responsive web interface for chat interactions. +- **Ollama** is the underlying technology that powers these interactions. + +If your issue or contribution pertains directly to the core Ollama technology, please direct it to the appropriate [Ollama project repository](https://ollama.com/). Open WebUI's repository is dedicated to the web interface aspect only. + +### 🚨 Reporting Issues + +Noticed something off? Have an idea? Check our [Issues tab](https://github.com/open-webui/open-webui/issues) to see if it's already been reported or suggested. If not, feel free to open a new issue. When reporting an issue, please follow our issue templates. These templates are designed to ensure that all necessary details are provided from the start, enabling us to address your concerns more efficiently. + +> [!IMPORTANT] +> +> - **Template Compliance:** Please be aware that failure to follow the provided issue template, or not providing the requested information at all, will likely result in your issue being closed without further consideration. This approach is critical for maintaining the manageability and integrity of issue tracking. +> +> - **Detail is Key:** To ensure your issue is understood and can be effectively addressed, it's imperative to include comprehensive details. Descriptions should be clear, including steps to reproduce, expected outcomes, and actual results. Lack of sufficient detail may hinder our ability to resolve your issue. + +### 🧭 Scope of Support + +We've noticed an uptick in issues not directly related to Open WebUI but rather to the environment it's run in, especially Docker setups. While we strive to support Docker deployment, understanding Docker fundamentals is crucial for a smooth experience. + +- **Docker Deployment Support**: Open WebUI supports Docker deployment. Familiarity with Docker is assumed. For Docker basics, please refer to the [official Docker documentation](https://docs.docker.com/get-started/overview/). + +- **Advanced Configurations**: Setting up reverse proxies for HTTPS and managing Docker deployments requires foundational knowledge. There are numerous online resources available to learn these skills. Ensuring you have this knowledge will greatly enhance your experience with Open WebUI and similar projects. + +## 💡 Contributing + +Looking to contribute? Great! Here's how you can help: + +### 🛠 Pull Requests + +We welcome pull requests. Before submitting one, please: + +1. Open a discussion regarding your ideas [here](https://github.com/open-webui/open-webui/discussions/new/choose). +2. Follow the project's coding standards and include tests for new features. +3. Update documentation as necessary. +4. Write clear, descriptive commit messages. +5. It's essential to complete your pull request in a timely manner. We move fast, and having PRs hang around too long is not feasible. If you can't get it done within a reasonable time frame, we may have to close it to keep the project moving forward. + +### 📚 Documentation & Tutorials + +Help us make Open WebUI more accessible by improving documentation, writing tutorials, or creating guides on setting up and optimizing the web UI. + +### 🌐 Translations and Internationalization + +Help us make Open WebUI available to a wider audience. In this section, we'll guide you through the process of adding new translations to the project. + +We use JSON files to store translations. You can find the existing translation files in the `src/lib/i18n/locales` directory. Each directory corresponds to a specific language, for example, `en-US` for English (US), `fr-FR` for French (France) and so on. You can refer to [ISO 639 Language Codes](http://www.lingoes.net/en/translator/langcode.htm) to find the appropriate code for a specific language. + +To add a new language: + +- Create a new directory in the `src/lib/i18n/locales` path with the appropriate language code as its name. For instance, if you're adding translations for Spanish (Spain), create a new directory named `es-ES`. +- Copy the American English translation file(s) (from `en-US` directory in `src/lib/i18n/locale`) to this new directory and update the string values in JSON format according to your language. Make sure to preserve the structure of the JSON object. +- Add the language code and its respective title to languages file at `src/lib/i18n/locales/languages.json`. + +### 🤔 Questions & Feedback + +Got questions or feedback? Join our [Discord community](https://discord.gg/5rJgQTnV4s) or open an issue. We're here to help! + +## 🙏 Thank You! + +Your contributions, big or small, make a significant impact on Open WebUI. We're excited to see what you bring to the project! + +Together, let's create an even more powerful tool for the community. 🌟 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4113c35a78f9d9c7d3096cf851f76f495421fc51 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,3 @@ +# Project workflow + +[](https://mermaid.live/edit#pako:eNq1k01rAjEQhv_KkFNLFe1N9iAUevFSRVl6Cci4Gd1ANtlmsmtF_O_N7iqtHxR76ClhMu87zwyZvcicIpEIpo-KbEavGjceC2lL9EFnukQbIGXygNye5y9TY7DAZTpZLsjXXVYXg3dapRM4hh9mu5A7-3hTfSXtAtJK21Tsj8dPl3USmJZkGVbebWNKD2rNOjAYl6HJHYdkNBwNpb3U9aNZvzFNYE6h8tFiSyZzBUGJG4K1dwVwTSYQrCptlLRvLt5dA5i2la5Ruk51Ux0VKQjuxPVbAwuyiuFlNgHfzJ5DoxtgqQf1813gnZRLZ5lAYcD7WT1lpGtiQKug9C4jZrrp-Fd-1-Y1bdzo4dvnZDLz7lPHyj8sOgfg4x84E7RTuEaZt8yRZqtDfgT_rwG2u3Dv_ERPFOQL1Cqu2F5aAClCTgVJkcSrojVWJkgh7SGmYhXcYmczkQRfUU9UZfQ4baRI1miYDl_QqlPg) diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000000000000000000000000000000000000..4a0c37e7c81cafd4c7793a666df34decd387c79a --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +Our primary goal is to ensure the protection and confidentiality of sensitive data stored by users on open-webui. + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| main | :white_check_mark: | +| others | :x: | + +## Reporting a Vulnerability + +If you discover a security issue within our system, please notify us immediately via a pull request or contact us on discord. + +## Product Security + +We regularly audit our internal processes and system's architecture for vulnerabilities using a combination of automated and manual testing techniques. + +We are planning on implementing SAST and SCA scans in our project soon. diff --git a/docs/apache.md b/docs/apache.md new file mode 100644 index 0000000000000000000000000000000000000000..be07f2345da88cabe39483587984b40f07658cb3 --- /dev/null +++ b/docs/apache.md @@ -0,0 +1,199 @@ +# Hosting UI and Models separately + +Sometimes, its beneficial to host Ollama, separate from the UI, but retain the RAG and RBAC support features shared across users: + +# Open WebUI Configuration + +## UI Configuration + +For the UI configuration, you can set up the Apache VirtualHost as follows: + +``` +# Assuming you have a website hosting this UI at "server.com" +<VirtualHost 192.168.1.100:80> + ServerName server.com + DocumentRoot /home/server/public_html + + ProxyPass / http://server.com:3000/ nocanon + ProxyPassReverse / http://server.com:3000/ + +</VirtualHost> +``` + +Enable the site first before you can request SSL: + +`a2ensite server.com.conf` # this will enable the site. a2ensite is short for "Apache 2 Enable Site" + +``` +# For SSL +<VirtualHost 192.168.1.100:443> + ServerName server.com + DocumentRoot /home/server/public_html + + ProxyPass / http://server.com:3000/ nocanon + ProxyPassReverse / http://server.com:3000/ + + SSLEngine on + SSLCertificateFile /etc/ssl/virtualmin/170514456861234/ssl.cert + SSLCertificateKeyFile /etc/ssl/virtualmin/170514456861234/ssl.key + SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1 + + SSLProxyEngine on + SSLCACertificateFile /etc/ssl/virtualmin/170514456865864/ssl.ca +</VirtualHost> + +``` + +I'm using virtualmin here for my SSL clusters, but you can also use certbot directly or your preferred SSL method. To use SSL: + +### Prerequisites. + +Run the following commands: + +`snap install certbot --classic` +`snap apt install python3-certbot-apache` (this will install the apache plugin). + +Navigate to the apache sites-available directory: + +`cd /etc/apache2/sites-available/` + +Create server.com.conf if it is not yet already created, containing the above `<virtualhost>` configuration (it should match your case. Modify as necessary). Use the one without the SSL: + +Once it's created, run `certbot --apache -d server.com`, this will request and add/create an SSL keys for you as well as create the server.com.le-ssl.conf + +# Configuring Ollama Server + +On your latest installation of Ollama, make sure that you have setup your api server from the official Ollama reference: + +[Ollama FAQ](https://github.com/jmorganca/ollama/blob/main/docs/faq.md) + +### TL;DR + +The guide doesn't seem to match the current updated service file on linux. So, we will address it here: + +Unless when you're compiling Ollama from source, installing with the standard install `curl https://ollama.com/install.sh | sh` creates a file called `ollama.service` in /etc/systemd/system. You can use nano to edit the file: + +``` +sudo nano /etc/systemd/system/ollama.service +``` + +Add the following lines: + +``` +Environment="OLLAMA_HOST=0.0.0.0:11434" # this line is mandatory. You can also specify +``` + +For instance: + +``` +[Unit] +Description=Ollama Service +After=network-online.target + +[Service] +ExecStart=/usr/local/bin/ollama serve +Environment="OLLAMA_HOST=0.0.0.0:11434" # this line is mandatory. You can also specify 192.168.254.109:DIFFERENT_PORT, format +Environment="OLLAMA_ORIGINS=http://192.168.254.106:11434,https://models.server.city" # this line is optional +User=ollama +Group=ollama +Restart=always +RestartSec=3 +Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/s> + +[Install] +WantedBy=default.target +``` + +Save the file by pressing CTRL+S, then press CTRL+X + +When your computer restarts, the Ollama server will now be listening on the IP:PORT you specified, in this case 0.0.0.0:11434, or 192.168.254.106:11434 (whatever your local IP address is). Make sure that your router is correctly configured to serve pages from that local IP by forwarding 11434 to your local IP server. + +# Ollama Model Configuration + +## For the Ollama model configuration, use the following Apache VirtualHost setup: + +Navigate to the apache sites-available directory: + +`cd /etc/apache2/sites-available/` + +`nano models.server.city.conf` # match this with your ollama server domain + +Add the folloing virtualhost containing this example (modify as needed): + +``` + +# Assuming you have a website hosting this UI at "models.server.city" +<IfModule mod_ssl.c> + <VirtualHost 192.168.254.109:443> + DocumentRoot "/var/www/html/" + ServerName models.server.city + <Directory "/var/www/html/"> + Options None + Require all granted + </Directory> + + ProxyRequests Off + ProxyPreserveHost On + ProxyAddHeaders On + SSLProxyEngine on + + ProxyPass / http://server.city:1000/ nocanon # or port 11434 + ProxyPassReverse / http://server.city:1000/ # or port 11434 + + SSLCertificateFile /etc/letsencrypt/live/models.server.city/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/models.server.city/privkey.pem + Include /etc/letsencrypt/options-ssl-apache.conf + </VirtualHost> +</IfModule> +``` + +You may need to enable the site first (if you haven't done so yet) before you can request SSL: + +`a2ensite models.server.city.conf` + +#### For the SSL part of Ollama server + +Run the following commands: + +Navigate to the apache sites-available directory: + +`cd /etc/apache2/sites-available/` +`certbot --apache -d server.com` + +``` +<VirtualHost 192.168.254.109:80> + DocumentRoot "/var/www/html/" + ServerName models.server.city + <Directory "/var/www/html/"> + Options None + Require all granted + </Directory> + + ProxyRequests Off + ProxyPreserveHost On + ProxyAddHeaders On + SSLProxyEngine on + + ProxyPass / http://server.city:1000/ nocanon # or port 11434 + ProxyPassReverse / http://server.city:1000/ # or port 11434 + + RewriteEngine on + RewriteCond %{SERVER_NAME} =models.server.city + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] +</VirtualHost> + +``` + +Don't forget to restart/reload Apache with `systemctl reload apache2` + +Open your site at https://server.com! + +**Congratulations**, your _**Open-AI-like Chat-GPT style UI**_ is now serving AI with RAG, RBAC and multimodal features! Download Ollama models if you haven't yet done so! + +If you encounter any misconfiguration or errors, please file an issue or engage with our discussion. There are a lot of friendly developers here to assist you. + +Let's make this UI much more user friendly for everyone! + +Thanks for making open-webui your UI Choice for AI! + +This doc is made by **Bob Reyes**, your **Open-WebUI** fan from the Philippines. diff --git a/hatch_build.py b/hatch_build.py new file mode 100644 index 0000000000000000000000000000000000000000..8ddaf0749bd6783b785fe63d6ce4b7f3e8a5b5d9 --- /dev/null +++ b/hatch_build.py @@ -0,0 +1,23 @@ +# noqa: INP001 +import os +import shutil +import subprocess +from sys import stderr + +from hatchling.builders.hooks.plugin.interface import BuildHookInterface + + +class CustomBuildHook(BuildHookInterface): + def initialize(self, version, build_data): + super().initialize(version, build_data) + stderr.write(">>> Building Open Webui frontend\n") + npm = shutil.which("npm") + if npm is None: + raise RuntimeError( + "NodeJS `npm` is required for building Open Webui but it was not found" + ) + stderr.write("### npm install\n") + subprocess.run([npm, "install"], check=True) # noqa: S603 + stderr.write("\n### npm run build\n") + os.environ["APP_BUILD_HASH"] = version + subprocess.run([npm, "run", "build"], check=True) # noqa: S603 diff --git a/i18next-parser.config.ts b/i18next-parser.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..37ce57ee1474e49787145f82cd6a64ad65773c4d --- /dev/null +++ b/i18next-parser.config.ts @@ -0,0 +1,38 @@ +// i18next-parser.config.ts +import { getLanguages } from './src/lib/i18n/index.ts'; + +const getLangCodes = async () => { + const languages = await getLanguages(); + return languages.map((l) => l.code); +}; + +export default { + contextSeparator: '_', + createOldCatalogs: false, + defaultNamespace: 'translation', + defaultValue: '', + indentation: 2, + keepRemoved: false, + keySeparator: false, + lexers: { + svelte: ['JavascriptLexer'], + js: ['JavascriptLexer'], + ts: ['JavascriptLexer'], + + default: ['JavascriptLexer'] + }, + lineEnding: 'auto', + locales: await getLangCodes(), + namespaceSeparator: false, + output: 'src/lib/i18n/locales/$LOCALE/$NAMESPACE.json', + pluralSeparator: '_', + input: 'src/**/*.{js,svelte}', + sort: true, + verbose: true, + failOnWarnings: false, + failOnUpdate: false, + customValueTemplate: null, + resetDefaultValueLocale: null, + i18nextOptions: null, + yamlOptions: null +}; diff --git a/kubernetes/helm/README.md b/kubernetes/helm/README.md new file mode 100644 index 0000000000000000000000000000000000000000..5737007d964de10301f85357e83a91a3bf635248 --- /dev/null +++ b/kubernetes/helm/README.md @@ -0,0 +1,4 @@ +# Helm Charts +Open WebUI Helm Charts are now hosted in a separate repo, which can be found here: https://github.com/open-webui/helm-charts + +The charts are released at https://helm.openwebui.com. \ No newline at end of file diff --git a/kubernetes/manifest/base/ollama-service.yaml b/kubernetes/manifest/base/ollama-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8bab65b59effa86ab8e80b6b3f8629ccdbf7ca39 --- /dev/null +++ b/kubernetes/manifest/base/ollama-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: ollama-service + namespace: open-webui +spec: + selector: + app: ollama + ports: + - protocol: TCP + port: 11434 + targetPort: 11434 \ No newline at end of file diff --git a/kubernetes/manifest/base/ollama-statefulset.yaml b/kubernetes/manifest/base/ollama-statefulset.yaml new file mode 100644 index 0000000000000000000000000000000000000000..cd1144caf9d506c7cf5922395c9a075621ad0072 --- /dev/null +++ b/kubernetes/manifest/base/ollama-statefulset.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ollama + namespace: open-webui +spec: + serviceName: "ollama" + replicas: 1 + selector: + matchLabels: + app: ollama + template: + metadata: + labels: + app: ollama + spec: + containers: + - name: ollama + image: ollama/ollama:latest + ports: + - containerPort: 11434 + resources: + requests: + cpu: "2000m" + memory: "2Gi" + limits: + cpu: "4000m" + memory: "4Gi" + nvidia.com/gpu: "0" + volumeMounts: + - name: ollama-volume + mountPath: /root/.ollama + tty: true + volumeClaimTemplates: + - metadata: + name: ollama-volume + spec: + accessModes: [ "ReadWriteOnce" ] + resources: + requests: + storage: 30Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/open-webui.yaml b/kubernetes/manifest/base/open-webui.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9c1a599f32690ddd8150be77b9bd8a7cd1b14245 --- /dev/null +++ b/kubernetes/manifest/base/open-webui.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: open-webui \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-deployment.yaml b/kubernetes/manifest/base/webui-deployment.yaml new file mode 100644 index 0000000000000000000000000000000000000000..79a0a9a23c9652bf755273e7801e3002499a5aa6 --- /dev/null +++ b/kubernetes/manifest/base/webui-deployment.yaml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: open-webui-deployment + namespace: open-webui +spec: + replicas: 1 + selector: + matchLabels: + app: open-webui + template: + metadata: + labels: + app: open-webui + spec: + containers: + - name: open-webui + image: ghcr.io/open-webui/open-webui:main + ports: + - containerPort: 8080 + resources: + requests: + cpu: "500m" + memory: "500Mi" + limits: + cpu: "1000m" + memory: "1Gi" + env: + - name: OLLAMA_BASE_URL + value: "http://ollama-service.open-webui.svc.cluster.local:11434" + tty: true + volumeMounts: + - name: webui-volume + mountPath: /app/backend/data + volumes: + - name: webui-volume + persistentVolumeClaim: + claimName: open-webui-pvc \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-ingress.yaml b/kubernetes/manifest/base/webui-ingress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..dc0b53ccd456e70910683d28bb117a0272992e1c --- /dev/null +++ b/kubernetes/manifest/base/webui-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: open-webui-ingress + namespace: open-webui + #annotations: + # Use appropriate annotations for your Ingress controller, e.g., for NGINX: + # nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: open-webui.minikube.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: open-webui-service + port: + number: 8080 diff --git a/kubernetes/manifest/base/webui-pvc.yaml b/kubernetes/manifest/base/webui-pvc.yaml new file mode 100644 index 0000000000000000000000000000000000000000..97fb761d422d510a2b725a08c3c52dca53c43ccd --- /dev/null +++ b/kubernetes/manifest/base/webui-pvc.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: open-webui + name: open-webui-pvc + namespace: open-webui +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 2Gi \ No newline at end of file diff --git a/kubernetes/manifest/base/webui-service.yaml b/kubernetes/manifest/base/webui-service.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d73845f00a8ec45bc39f629b819542e7d2ced84e --- /dev/null +++ b/kubernetes/manifest/base/webui-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: open-webui-service + namespace: open-webui +spec: + type: NodePort # Use LoadBalancer if you're on a cloud that supports it + selector: + app: open-webui + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + # If using NodePort, you can optionally specify the nodePort: + # nodePort: 30000 \ No newline at end of file diff --git a/kubernetes/manifest/kustomization.yaml b/kubernetes/manifest/kustomization.yaml new file mode 100644 index 0000000000000000000000000000000000000000..907bff3e1858858d74ad69f3f5a0290fbaed4108 --- /dev/null +++ b/kubernetes/manifest/kustomization.yaml @@ -0,0 +1,13 @@ +resources: +- base/open-webui.yaml +- base/ollama-service.yaml +- base/ollama-statefulset.yaml +- base/webui-deployment.yaml +- base/webui-service.yaml +- base/webui-ingress.yaml +- base/webui-pvc.yaml + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +patches: +- path: patches/ollama-statefulset-gpu.yaml diff --git a/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3e42443656d06c6df25075d094e5c8efb180fda1 --- /dev/null +++ b/kubernetes/manifest/patches/ollama-statefulset-gpu.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: ollama + namespace: open-webui +spec: + selector: + matchLabels: + app: ollama + serviceName: "ollama" + template: + spec: + containers: + - name: ollama + resources: + limits: + nvidia.com/gpu: "1" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..cf04da5c626b044ce4b7055c418928284803db27 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10459 @@ +{ + "name": "open-webui", + "version": "0.3.10", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "open-webui", + "version": "0.3.10", + "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/theme-one-dark": "^6.1.2", + "@pyscript/core": "^0.4.32", + "@sveltejs/adapter-node": "^1.3.1", + "async": "^3.2.5", + "bits-ui": "^0.19.7", + "codemirror": "^6.0.1", + "crc-32": "^1.2.2", + "dayjs": "^1.11.10", + "eventsource-parser": "^1.1.2", + "file-saver": "^2.0.5", + "highlight.js": "^11.9.0", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", + "idb": "^7.1.1", + "js-sha256": "^0.10.1", + "katex": "^0.16.9", + "marked": "^9.1.0", + "mermaid": "^10.9.1", + "pyodide": "^0.26.1", + "socket.io-client": "^4.2.0", + "sortablejs": "^1.15.2", + "svelte-sonner": "^0.3.19", + "tippy.js": "^6.3.7", + "turndown": "^7.2.0", + "uuid": "^9.0.1" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/adapter-static": "^2.0.3", + "@sveltejs/kit": "^1.30.0", + "@tailwindcss/typography": "^0.5.10", + "@types/bun": "latest", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", + "autoprefixer": "^10.4.16", + "cypress": "^13.8.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-cypress": "^3.0.2", + "eslint-plugin-svelte": "^2.30.0", + "i18next-parser": "^8.13.0", + "postcss": "^8.4.31", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.10.1", + "svelte": "^4.0.5", + "svelte-check": "^3.4.3", + "svelte-confetti": "^1.3.2", + "tailwindcss": "^3.3.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.4.2", + "vitest": "^1.6.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.16.2.tgz", + "integrity": "sha512-MjfDrHy0gHKlPWsvSsikhO1+BOh+eBHNgfH1OXs1+DAf30IonQldgMM3kxLDTG9ktE7kDLaA1j/l7KMPA4KNfw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", + "integrity": "sha512-qnY+b7j1UNcTS31Eenuc/5YJB6gQOzkUoNmJQc0rznwqSRpeaWWpjkWy2C/MPTcePpsKJEM26hXrOXl1+nceXg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", + "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.2.tgz", + "integrity": "sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.0.tgz", + "integrity": "sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.28.0.tgz", + "integrity": "sha512-fo7CelaUDKWIyemw4b+J57cWuRkOu4SWCCPfNDkPvfWkGjM9D5racHQXr4EQeYCD6zEBIBxGCeaKkQo+ysl0gA==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.1.tgz", + "integrity": "sha512-TWivJlJi8ZDx2wGOw1dbLuHJKUYX7bWySw377nlnGOW3hP9/MUKIsEdXT/YngWxVdgNCHRBmFlBipE+5/2ZZlQ==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.10.4", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", + "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", + "dependencies": { + "@floating-ui/utils": "^0.2.1" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", + "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@gulpjs/to-absolute-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz", + "integrity": "sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA==", + "dev": true, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", + "dev": true + }, + "node_modules/@internationalized/date": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.5.2.tgz", + "integrity": "sha512-vo1yOMUt2hzp63IutEaTUxROdvQg1qlMRsbCvbay2AK2Gai7wIgCyK5weEX3nHkiLgo4qCXHijFNC/ILhlRpOQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lezer/common": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", + "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", + "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.16", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.16.tgz", + "integrity": "sha512-84UXR3N7s11MPQHWgMnjb9571fr19MmXnr5zTv2XX0gHXXUvW3uPJ8GCjKrfTXmSdfktjRK0ayKklw+A13rk4g==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", + "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", + "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@melt-ui/svelte": { + "version": "0.76.0", + "resolved": "https://registry.npmjs.org/@melt-ui/svelte/-/svelte-0.76.0.tgz", + "integrity": "sha512-X1ktxKujjLjOBt8LBvfckHGDMrkHWceRt1jdsUTf0EH76ikNPP1ofSoiV0IhlduDoCBV+2YchJ8kXCDfDXfC9Q==", + "dependencies": { + "@floating-ui/core": "^1.3.1", + "@floating-ui/dom": "^1.4.5", + "@internationalized/date": "^3.5.0", + "dequal": "^2.0.3", + "focus-trap": "^7.5.2", + "nanoid": "^5.0.4" + }, + "peerDependencies": { + "svelte": ">=3 <5" + } + }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==" + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@pyscript/core": { + "version": "0.4.32", + "resolved": "https://registry.npmjs.org/@pyscript/core/-/core-0.4.32.tgz", + "integrity": "sha512-WQATzPp1ggf871+PukCmTypzScXkEB1EWD/vg5GNxpM96N6rDPqQ13msuA5XvwU01ZVhL8HHSFDLk4IfaXNGWg==", + "dependencies": { + "@ungap/with-resolvers": "^0.1.0", + "basic-devtools": "^0.1.6", + "polyscript": "^0.12.8", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1", + "type-checked-collections": "^0.1.7" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.7.tgz", + "integrity": "sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", + "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", + "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", + "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", + "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", + "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", + "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", + "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", + "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", + "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", + "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", + "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", + "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", + "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", + "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", + "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", + "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-2.1.1.tgz", + "integrity": "sha512-nzi6x/7/3Axh5VKQ8Eed3pYxastxoa06Y/bFhWb7h3Nu+nGRVxKAy3+hBJgmPCwWScy8n0TsstZjSVKfyrIHkg==", + "dev": true, + "dependencies": { + "import-meta-resolve": "^4.0.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^1.0.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-1.3.1.tgz", + "integrity": "sha512-A0VgRQDCDPzdLNoiAbcOxGw4zT1Mc+n1LwT1OmO350R7WxrEqdMUChPPOd1iMfIDWlP4ie6E2d/WQf5es2d4Zw==", + "dependencies": { + "@rollup/plugin-commonjs": "^25.0.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "rollup": "^3.7.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^1.0.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-2.0.3.tgz", + "integrity": "sha512-VUqTfXsxYGugCpMqQv1U0LIdbR3S5nBkMMDmpjGVJyM6Q2jHVMFtdWJCkeHMySc6mZxJ+0eZK3T7IgmUCDrcUQ==", + "dev": true, + "peerDependencies": { + "@sveltejs/kit": "^1.5.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "1.30.4", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.30.4.tgz", + "integrity": "sha512-JSQIQT6XvdchCRQEm7BABxPC56WP5RYVONAi+09S8tmzeP43fBsRlr95bFmsTQM2RHBldfgQk+jgdnsKI75daA==", + "hasInstallScript": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte": "^2.5.0", + "@types/cookie": "^0.5.1", + "cookie": "^0.5.0", + "devalue": "^4.3.1", + "esm-env": "^1.0.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.0", + "mrmime": "^1.0.1", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^2.0.2", + "tiny-glob": "^0.2.9", + "undici": "^5.28.3" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": "^16.14 || >=18" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0-next.0 || ^5.0.0-next.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.5.3.tgz", + "integrity": "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w==", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.3", + "svelte-hmr": "^0.15.3", + "vitefu": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0", + "vite": "^4.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", + "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^14.18.0 || >= 16" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^2.2.0", + "svelte": "^3.54.0 || ^4.0.0", + "vite": "^4.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.7.tgz", + "integrity": "sha512-BVvNZhx362+l2tSwSuyEUV4h7+jk9raNdoTSdLfwTshXJSaGmYKluGRJznziCI3KX02Z19DdsQrdfrpXAU3Hfg==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.10.tgz", + "integrity": "sha512-Pe8BuPJQJd3FfRnm6H0ulKIGoMEQS+Vq01R6M5aCrFB/ccR/shT+0kXLjouGC1gFLm9hopTFN+DMP0pfwRWzPw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@types/bun": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.0.10.tgz", + "integrity": "sha512-Jaz6YYAdm1u3NVlgSyEK+qGmrlLQ20sbWeEoXD64b9w6z/YKYNWlfaphu+xF2Kiy5Tpykm5Q9jIquLegwXx4ng==", + "dev": true, + "dependencies": { + "bun-types": "1.0.33" + } + }, + "node_modules/@types/cookie": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz", + "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", + "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, + "node_modules/@types/node": { + "version": "20.11.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", + "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "devOptional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/symlink-or-copy": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz", + "integrity": "sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", + "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + }, + "node_modules/@ungap/with-resolvers": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@ungap/with-resolvers/-/with-resolvers-0.1.0.tgz", + "integrity": "sha512-g7f0IkJdPW2xhY7H4iE72DAsIyfuwEFc6JWc2tYFwKDMWWAF699vGjrM348cwQuOXgHpe1gWFe+Eiyjx/ewvvw==" + }, + "node_modules/@vitest/expect": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", + "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", + "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.6.0", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", + "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", + "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", + "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@webreflection/fetch": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@webreflection/fetch/-/fetch-0.1.5.tgz", + "integrity": "sha512-zCcqCJoNLvdeF41asAK71XPlwSPieeRDsE09albBunJEksuYPYNillKNQjf8p5BqSoTKTuKrW3lUm3MNodUC4g==" + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "dev": true + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/basic-devtools": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/basic-devtools/-/basic-devtools-0.1.6.tgz", + "integrity": "sha512-g9zJ63GmdUesS3/Fwv0B5SYX6nR56TQXmGr+wE5PRTNCnGQMYWhUx/nZB/mMWnQJVLPPAp89oxDNlasdtNkW5Q==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bits-ui": { + "version": "0.19.7", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-0.19.7.tgz", + "integrity": "sha512-GHUpKvN7QyazhnZNkUy0lxg6W1M6KJHWSZ4a/UGCjPE6nQgk6vKbGysY67PkDtQMknZTZAzVoMj1Eic4IKeCRQ==", + "dependencies": { + "@internationalized/date": "^3.5.1", + "@melt-ui/svelte": "0.76.0", + "nanoid": "^5.0.5" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/broccoli-node-api": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz", + "integrity": "sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw==", + "dev": true + }, + "node_modules/broccoli-node-info": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz", + "integrity": "sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg==", + "dev": true, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/broccoli-output-wrapper": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz", + "integrity": "sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw==", + "dev": true, + "dependencies": { + "fs-extra": "^8.1.0", + "heimdalljs-logger": "^0.1.10", + "symlink-or-copy": "^1.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/broccoli-output-wrapper/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/broccoli-plugin": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz", + "integrity": "sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-output-wrapper": "^3.2.5", + "fs-merger": "^3.2.1", + "promise-map-series": "^0.3.0", + "quick-temp": "^0.1.8", + "rimraf": "^3.0.2", + "symlink-or-copy": "^1.3.1" + }, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bun-types": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.0.33.tgz", + "integrity": "sha512-L5tBIf9g6rBBkvshqysi5NoLQ9NnhSPU1pfJ9FzqoSfofYdyac3WLUnOIuQ+M5za/sooVUOP2ko+E6Tco0OLIA==", + "dev": true, + "dependencies": { + "@types/node": "~20.11.3", + "@types/ws": "~8.5.10" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001600", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", + "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-table3": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", + "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-table3/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", + "dev": true + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/code-red/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/codedent": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/codedent/-/codedent-0.1.2.tgz", + "integrity": "sha512-qEqzcy5viM3UoCN0jYHZeXZoyd4NZQzYFg0kOBj8O1CgoGG9WYYTF+VeQRsN0OSKFjF3G1u4WDUOtOsWEx6N2w==", + "dependencies": { + "plain-tag": "^0.1.3" + } + }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/coincident": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/coincident/-/coincident-1.2.3.tgz", + "integrity": "sha512-Uxz3BMTWIslzeWjuQnizGWVg0j6khbvHUQ8+5BdM7WuJEm4ALXwq3wluYoB+uF68uPBz/oUOeJnYURKyfjexlA==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "gc-hook": "^0.3.1", + "proxy-target": "^3.0.2" + }, + "optionalDependencies": { + "ws": "^8.16.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cypress": { + "version": "13.8.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.1.tgz", + "integrity": "sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.0", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cytoscape": { + "version": "3.29.2", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.29.2.tgz", + "integrity": "sha512-2G1ycU28Nh7OHT9rkXRLpCDP30MKH1dXJORZuBhtEhEW7pKwgPi77ImqlCWinouyE1PNepIOGZBOrE84DG7LyQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", + "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", + "dependencies": { + "d3": "^7.8.2", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.2.tgz", + "integrity": "sha512-KqFl6pOgOW+Y6wJgu80rHpo2/3H07vr8ntR9rkkFIRETewbf5GaYYcakYfiKz89K+sLsuPkQIZaXDMjUObZwWg==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz", + "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.715", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.715.tgz", + "integrity": "sha512-XzWNH4ZSa9BwVUQSDorPWAUQ5WGuYz7zJUNpNif40zFCiCl20t8zgylmreNmn26h5kiyw2lg7RfTmeMBsDklqg==", + "dev": true + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ensure-posix-path": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz", + "integrity": "sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/eol": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/eol/-/eol-0.9.1.tgz", + "integrity": "sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==", + "dev": true + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", + "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz", + "integrity": "sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.0.2.tgz", + "integrity": "sha512-5hIWc3SqXSuR+Sd7gmNMzx8yJ3LWQQS0e+qLvEVF4C1JfFtu1s9imtEm1KxlCBCcKb7+6CyR9KQYs0GiI02AlA==", + "dev": true, + "dependencies": { + "globals": "^13.20.0" + }, + "peerDependencies": { + "eslint": ">=7 <9" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz", + "integrity": "sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "debug": "^4.3.1", + "eslint-compat-utils": "^0.1.2", + "esutils": "^2.0.3", + "known-css-properties": "^0.29.0", + "postcss": "^8.4.5", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.0.11", + "semver": "^7.5.3", + "svelte-eslint-parser": ">=0.33.0 <1.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0", + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz", + "integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==" + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/eventsource-parser": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", + "integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==", + "engines": { + "node": ">=14.18" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-merger": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/fs-merger/-/fs-merger-3.2.1.tgz", + "integrity": "sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug==", + "dev": true, + "dependencies": { + "broccoli-node-api": "^1.7.0", + "broccoli-node-info": "^2.1.0", + "fs-extra": "^8.0.1", + "fs-tree-diff": "^2.0.1", + "walk-sync": "^2.2.0" + } + }, + "node_modules/fs-merger/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-merger/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/fs-merger/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs-mkdirp-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz", + "integrity": "sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/fs-tree-diff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz", + "integrity": "sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A==", + "dev": true, + "dependencies": { + "@types/symlink-or-copy": "^1.2.0", + "heimdalljs-logger": "^0.1.7", + "object-assign": "^4.1.0", + "path-posix": "^1.0.0", + "symlink-or-copy": "^1.1.8" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gc-hook": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gc-hook/-/gc-hook-0.3.1.tgz", + "integrity": "sha512-E5M+O/h2o7eZzGhzRZGex6hbB3k4NWqO0eA+OzLRLXxhdbYPajZnynPwAtphnh+cRHPwsj5Z80dqZlfI4eK55A==" + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-stream": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.0.tgz", + "integrity": "sha512-CdIUuwOkYNv9ZadR3jJvap8CMooKziQZ/QCSPhEb7zqfsEI5YnPmvca7IvbaVE3z58ZdUYD2JsU6AUWjL8WZJA==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/gulp-sort": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-sort/-/gulp-sort-2.0.0.tgz", + "integrity": "sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g==", + "dev": true, + "dependencies": { + "through2": "^2.0.1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/heimdalljs": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/heimdalljs/-/heimdalljs-0.2.6.tgz", + "integrity": "sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA==", + "dev": true, + "dependencies": { + "rsvp": "~3.2.1" + } + }, + "node_modules/heimdalljs-logger": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz", + "integrity": "sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "heimdalljs": "^0.2.6" + } + }, + "node_modules/heimdalljs-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/heimdalljs-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/heimdalljs/node_modules/rsvp": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.2.1.tgz", + "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==", + "dev": true + }, + "node_modules/highlight.js": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", + "integrity": "sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==" + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/i18next": { + "version": "23.10.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.10.1.tgz", + "integrity": "sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.2.0.tgz", + "integrity": "sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-parser": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/i18next-parser/-/i18next-parser-8.13.0.tgz", + "integrity": "sha512-XU7resoeNcpJazh29OncQQUH6HsgCxk06RqBBDAmLHldafxopfCHY1vElyG/o3EY0Sn7XjelAmPTV0SgddJEww==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "broccoli-plugin": "^4.0.7", + "cheerio": "^1.0.0-rc.2", + "colors": "1.4.0", + "commander": "~11.1.0", + "eol": "^0.9.1", + "esbuild": "^0.20.1", + "fs-extra": "^11.1.0", + "gulp-sort": "^2.0.0", + "i18next": "^23.5.1", + "js-yaml": "4.1.0", + "lilconfig": "^3.0.0", + "rsvp": "^4.8.2", + "sort-keys": "^5.0.0", + "typescript": "^5.0.4", + "vinyl": "~3.0.0", + "vinyl-fs": "^4.0.0", + "vue-template-compiler": "^2.6.11" + }, + "bin": { + "i18next": "bin/cli.js" + }, + "engines": { + "node": ">=16.0.0 || >=18.0.0 || >=20.0.0", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/i18next-resources-to-backend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.0.tgz", + "integrity": "sha512-8f1l03s+QxDmCfpSXCh9V+AFcxAwIp0UaroWuyOx+hmmv8484GcELHs+lnu54FrNij8cDBEXvEwhzZoXsKcVpg==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", + "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==" + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-sha256": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz", + "integrity": "sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==" + }, + "node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/katex": { + "version": "0.16.10", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.10.tgz", + "integrity": "sha512-ZiqaC04tp2O5utMsl2TEZTXxa6WSC4yo0fv5ML++D3QZv/vx2Mct0mTlRx3O+uUkjfuAgOkzsCmq5MiUEsDDdA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz", + "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==", + "dev": true + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/matcher-collection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/matcher-collection/-/matcher-collection-2.0.1.tgz", + "integrity": "sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "minimatch": "^3.0.2" + }, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/matcher-collection/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/matcher-collection/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz", + "integrity": "sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww==", + "dependencies": { + "@types/mdast": "^3.0.0", + "@types/unist": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "mdast-util-to-string": "^3.1.0", + "micromark": "^3.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-decode-string": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "unist-util-stringify-position": "^3.0.0", + "uvu": "^0.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz", + "integrity": "sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==", + "dependencies": { + "@types/mdast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.9.1.tgz", + "integrity": "sha512-Mx45Obds5W1UkW1nv/7dHRsbfMM1aOKA2+Pxs/IGHNonygDHwmng8xTHyS9z4KWVi0rbko8gjiBmuwwXQ7tiNA==", + "dependencies": { + "@braintree/sanitize-url": "^6.0.1", + "@types/d3-scale": "^4.0.3", + "@types/d3-scale-chromatic": "^3.0.0", + "cytoscape": "^3.28.1", + "cytoscape-cose-bilkent": "^4.1.0", + "d3": "^7.4.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.10", + "dayjs": "^1.11.7", + "dompurify": "^3.0.5", + "elkjs": "^0.9.0", + "katex": "^0.16.9", + "khroma": "^2.0.0", + "lodash-es": "^4.17.21", + "mdast-util-from-markdown": "^1.3.0", + "non-layered-tidy-tree-layout": "^2.0.2", + "stylis": "^4.1.3", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.0", + "web-worker": "^1.2.0" + } + }, + "node_modules/micromark": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-3.2.0.tgz", + "integrity": "sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "micromark-core-commonmark": "^1.0.1", + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-combine-extensions": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-sanitize-uri": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz", + "integrity": "sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-factory-destination": "^1.0.0", + "micromark-factory-label": "^1.0.0", + "micromark-factory-space": "^1.0.0", + "micromark-factory-title": "^1.0.0", + "micromark-factory-whitespace": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-chunked": "^1.0.0", + "micromark-util-classify-character": "^1.0.0", + "micromark-util-html-tag-name": "^1.0.0", + "micromark-util-normalize-identifier": "^1.0.0", + "micromark-util-resolve-all": "^1.0.0", + "micromark-util-subtokenize": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.1", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-destination": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz", + "integrity": "sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz", + "integrity": "sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", + "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz", + "integrity": "sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz", + "integrity": "sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", + "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz", + "integrity": "sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz", + "integrity": "sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz", + "integrity": "sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz", + "integrity": "sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz", + "integrity": "sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^1.0.0", + "micromark-util-decode-numeric-character-reference": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz", + "integrity": "sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz", + "integrity": "sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz", + "integrity": "sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz", + "integrity": "sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^1.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz", + "integrity": "sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^1.0.0", + "micromark-util-encode": "^1.0.0", + "micromark-util-symbol": "^1.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz", + "integrity": "sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^1.0.0", + "micromark-util-symbol": "^1.0.0", + "micromark-util-types": "^1.0.0", + "uvu": "^0.5.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", + "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", + "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mktemp": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/mktemp/-/mktemp-0.4.0.tgz", + "integrity": "sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A==", + "dev": true, + "engines": { + "node": ">0.9" + } + }, + "node_modules/mlly": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", + "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.0", + "ufo": "^1.5.3" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.6.tgz", + "integrity": "sha512-rRq0eMHoGZxlvaFOUdK1Ev83Bd1IgzzR+WJ3IbDJ7QOSdAxYjlurSPqFs9s4lJg29RT6nPwizFtJhQS6V5xgiA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-posix": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", + "integrity": "sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/periscopic/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/periscopic/node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", + "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", + "dev": true, + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.0", + "pathe": "^1.1.2" + } + }, + "node_modules/plain-tag": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/plain-tag/-/plain-tag-0.1.3.tgz", + "integrity": "sha512-yyVAOFKTAElc7KdLt2+UKGExNYwYb/Y/WE9i+1ezCQsJE8gbKSjewfpRqK2nQgZ4d4hhAAGgDCOcIZVilqE5UA==" + }, + "node_modules/polyscript": { + "version": "0.12.8", + "resolved": "https://registry.npmjs.org/polyscript/-/polyscript-0.12.8.tgz", + "integrity": "sha512-kcG3W9jU/s1sYjWOTAa2jAh5D2jm3zJRi+glSTsC+lA3D1b/Sd67pEIGpyL9bWNKYSimqAx4se6jAhQjJZ7+jQ==", + "dependencies": { + "@ungap/structured-clone": "^1.2.0", + "@ungap/with-resolvers": "^0.1.0", + "@webreflection/fetch": "^0.1.5", + "basic-devtools": "^0.1.6", + "codedent": "^0.1.2", + "coincident": "^1.2.3", + "gc-hook": "^0.3.1", + "html-escaper": "^3.0.3", + "proxy-target": "^3.0.2", + "sticky-module": "^0.1.1", + "to-json-callback": "^0.1.1" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.1.tgz", + "integrity": "sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==", + "dev": true, + "peerDependencies": { + "prettier": "^1.16.4 || ^2.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-map-series": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promise-map-series/-/promise-map-series-0.3.0.tgz", + "integrity": "sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA==", + "dev": true, + "engines": { + "node": "10.* || >= 12.*" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/proxy-target": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/proxy-target/-/proxy-target-3.0.2.tgz", + "integrity": "sha512-FFE1XNwXX/FNC3/P8HiKaJSy/Qk68RitG/QEcLy/bVnTAPlgTAWPZKh0pARLAnpfXQPKyalBhk009NRTgsk8vQ==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pyodide": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz", + "integrity": "sha512-P+Gm88nwZqY7uBgjbQH8CqqU6Ei/rDn7pS1t02sNZsbyLJMyE2OVXjgNuqVT3KqYWnyGREUN0DbBUCJqk8R0ew==", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/qs": { + "version": "6.10.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz", + "integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "node_modules/quick-temp": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/quick-temp/-/quick-temp-0.1.8.tgz", + "integrity": "sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA==", + "dev": true, + "dependencies": { + "mktemp": "~0.4.0", + "rimraf": "^2.5.4", + "underscore.string": "~3.3.4" + } + }, + "node_modules/quick-temp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/quick-temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/quick-temp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/quick-temp/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "dev": true + }, + "node_modules/replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.1.tgz", + "integrity": "sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, + "node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true, + "engines": { + "node": "6.* || >= 7.*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sander/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/sander/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sander/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sander/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sirv/node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sorcery": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", + "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^0.2.5", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/sort-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-5.0.0.tgz", + "integrity": "sha512-Pdz01AvCAottHTPQGzndktFNdbRA75BgOfeT1hH+AMnJFv8lynkPi42rfeEhpx1saTEI3YNMWxfqu0sFD1G8pw==", + "dev": true, + "dependencies": { + "is-plain-obj": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sortablejs": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==" + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/sticky-module": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sticky-module/-/sticky-module-0.1.1.tgz", + "integrity": "sha512-IuYgnyIMUx/m6rtu14l/LR2MaqOLtpXcWkxPmtPsiScRHEo+S4Tojk+DWFHOncSdFX/OsoLOM4+T92yOmI1AMw==" + }, + "node_modules/stream-composer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-composer/-/stream-composer-1.0.2.tgz", + "integrity": "sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w==", + "dev": true, + "dependencies": { + "streamx": "^2.13.2" + } + }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dev": true, + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, + "node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz", + "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-check": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.8.tgz", + "integrity": "sha512-rhXU7YCDtL+lq2gCqfJDXKTxJfSsCgcd08d7VWBFxTw6IWIbMWSaASbAOD3N0VV9TYSSLUqEBiratLd8WxAJJA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "chokidar": "^3.4.1", + "fast-glob": "^3.2.7", + "import-fresh": "^3.2.1", + "picocolors": "^1.0.0", + "sade": "^1.7.4", + "svelte-preprocess": "^5.1.3", + "typescript": "^5.0.3" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "peerDependencies": { + "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" + } + }, + "node_modules/svelte-confetti": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/svelte-confetti/-/svelte-confetti-1.3.2.tgz", + "integrity": "sha512-R+JwFTC7hIgWVA/OuXrkj384B7CMoceb0t9VacyW6dORTQg0pWojVBB8Bo3tM30cLEQE48Fekzqgx+XSzHESMA==", + "dev": true, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.33.1.tgz", + "integrity": "sha512-vo7xPGTlKBGdLH8T5L64FipvTrqv3OQRx9d2z5X05KKZDlF4rQk8KViZO4flKERY+5BiVdOh7zZ7JGJWo5P0uA==", + "dev": true, + "dependencies": { + "eslint-scope": "^7.0.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "postcss": "^8.4.29", + "postcss-scss": "^4.0.8" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-hmr": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", + "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.3.tgz", + "integrity": "sha512-xxAkmxGHT+J/GourS5mVJeOXZzne1FR5ljeOUAMXUkfEhkLEllRreXpbl3dIYJlcJRfL1LO1uIAPpBpBfiqGPw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0", + "pnpm": "^8.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte-sonner": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-0.3.19.tgz", + "integrity": "sha512-jpPOgLtHwRaB6Vqo2dUQMv15/yUV/BQWTjKpEqQ11uqRSHKjAYUKZyGrHB2cQsGmyjR0JUzBD58btpgNqINQ/Q==", + "peerDependencies": { + "svelte": ">=3 <5" + } + }, + "node_modules/svelte/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/symlink-or-copy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz", + "integrity": "sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA==", + "dev": true + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwindcss": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", + "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tailwindcss/node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tinybench": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-json-callback": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-json-callback/-/to-json-callback-0.1.1.tgz", + "integrity": "sha512-BzOeinTT3NjE+FJ2iCvWB8HvyuyBzoH3WlSnJ+AYVC4tlePyZWSYdkQIFOARWiq0t35/XhmI0uQsFiUsRksRqg==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/to-through": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-3.0.0.tgz", + "integrity": "sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw==", + "dev": true, + "dependencies": { + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turndown": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz", + "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-checked-collections": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/type-checked-collections/-/type-checked-collections-0.1.7.tgz", + "integrity": "sha512-fLIydlJy7IG9XL4wjRwEcKhxx/ekLXiWiMvcGo01cOMF+TN+5ZqajM1mRNRz2bNNi1bzou2yofhjZEQi7kgl9A==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", + "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", + "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", + "dev": true + }, + "node_modules/underscore.string": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.6.tgz", + "integrity": "sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ==", + "dev": true, + "dependencies": { + "sprintf-js": "^1.1.1", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "devOptional": true + }, + "node_modules/unist-util-stringify-position": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", + "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", + "dependencies": { + "@types/unist": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/uvu": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz", + "integrity": "sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==", + "dependencies": { + "dequal": "^2.0.0", + "diff": "^5.0.0", + "kleur": "^4.0.3", + "sade": "^1.7.3" + }, + "bin": { + "uvu": "bin.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "dependencies": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "dependencies": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "dependencies": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "dependencies": { + "convert-source-map": "^2.0.0", + "graceful-fs": "^4.2.10", + "now-and-later": "^3.0.0", + "streamx": "^2.12.5", + "vinyl": "^3.0.0", + "vinyl-contents": "^2.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/vite": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", + "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vite-node/node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", + "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.6.0", + "@vitest/runner": "1.6.0", + "@vitest/snapshot": "1.6.0", + "@vitest/spy": "1.6.0", + "@vitest/utils": "1.6.0", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.0", + "@vitest/ui": "1.6.0", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/vitest/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/vitest/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/rollup": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", + "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.17.2", + "@rollup/rollup-android-arm64": "4.17.2", + "@rollup/rollup-darwin-arm64": "4.17.2", + "@rollup/rollup-darwin-x64": "4.17.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", + "@rollup/rollup-linux-arm-musleabihf": "4.17.2", + "@rollup/rollup-linux-arm64-gnu": "4.17.2", + "@rollup/rollup-linux-arm64-musl": "4.17.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", + "@rollup/rollup-linux-riscv64-gnu": "4.17.2", + "@rollup/rollup-linux-s390x-gnu": "4.17.2", + "@rollup/rollup-linux-x64-gnu": "4.17.2", + "@rollup/rollup-linux-x64-musl": "4.17.2", + "@rollup/rollup-win32-arm64-msvc": "4.17.2", + "@rollup/rollup-win32-ia32-msvc": "4.17.2", + "@rollup/rollup-win32-x64-msvc": "4.17.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/vitest/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vitest/node_modules/vite": { + "version": "5.2.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", + "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.20.1", + "postcss": "^8.4.38", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/walk-sync": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/walk-sync/-/walk-sync-2.2.0.tgz", + "integrity": "sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg==", + "dev": true, + "dependencies": { + "@types/minimatch": "^3.0.3", + "ensure-posix-path": "^1.1.0", + "matcher-collection": "^2.0.0", + "minimatch": "^3.0.4" + }, + "engines": { + "node": "8.* || >= 10.*" + } + }, + "node_modules/walk-sync/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/walk-sync/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/web-worker": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.3.0.tgz", + "integrity": "sha512-BSR9wyRsy/KOValMgd5kMyr3JzpdeoR9KVId8u5GVlTTAtNChlsE4yTxeY7zMdNSyOmoKBv8NH2qeRY9Tg+IaA==" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..f7cc1259827490ef41a4cabcf8a8520948f041d7 --- /dev/null +++ b/package.json @@ -0,0 +1,80 @@ +{ + "name": "open-webui", + "version": "0.3.10", + "private": true, + "scripts": { + "dev": "npm run pyodide:fetch && vite dev --host", + "build": "npm run pyodide:fetch && vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "npm run lint:frontend ; npm run lint:types ; npm run lint:backend", + "lint:frontend": "eslint . --fix", + "lint:types": "npm run check", + "lint:backend": "pylint backend/", + "format": "prettier --plugin-search-dir --write \"**/*.{js,ts,svelte,css,md,html,json}\"", + "format:backend": "black . --exclude \".venv/|/venv/\"", + "i18n:parse": "i18next --config i18next-parser.config.ts && prettier --write \"src/lib/i18n/**/*.{js,json}\"", + "cy:open": "cypress open", + "test:frontend": "vitest --passWithNoTests", + "pyodide:fetch": "node scripts/prepare-pyodide.js" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^2.0.0", + "@sveltejs/adapter-static": "^2.0.3", + "@sveltejs/kit": "^1.30.0", + "@tailwindcss/typography": "^0.5.10", + "@types/bun": "latest", + "@typescript-eslint/eslint-plugin": "^6.17.0", + "@typescript-eslint/parser": "^6.17.0", + "autoprefixer": "^10.4.16", + "cypress": "^13.8.1", + "eslint": "^8.56.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-cypress": "^3.0.2", + "eslint-plugin-svelte": "^2.30.0", + "i18next-parser": "^8.13.0", + "postcss": "^8.4.31", + "prettier": "^2.8.0", + "prettier-plugin-svelte": "^2.10.1", + "svelte": "^4.0.5", + "svelte-check": "^3.4.3", + "svelte-confetti": "^1.3.2", + "tailwindcss": "^3.3.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^4.4.2", + "vitest": "^1.6.0" + }, + "type": "module", + "dependencies": { + "@codemirror/lang-javascript": "^6.2.2", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/theme-one-dark": "^6.1.2", + "@pyscript/core": "^0.4.32", + "@sveltejs/adapter-node": "^1.3.1", + "async": "^3.2.5", + "bits-ui": "^0.19.7", + "codemirror": "^6.0.1", + "crc-32": "^1.2.2", + "dayjs": "^1.11.10", + "eventsource-parser": "^1.1.2", + "file-saver": "^2.0.5", + "highlight.js": "^11.9.0", + "i18next": "^23.10.0", + "i18next-browser-languagedetector": "^7.2.0", + "i18next-resources-to-backend": "^1.2.0", + "idb": "^7.1.1", + "js-sha256": "^0.10.1", + "katex": "^0.16.9", + "marked": "^9.1.0", + "mermaid": "^10.9.1", + "pyodide": "^0.26.1", + "socket.io-client": "^4.2.0", + "sortablejs": "^1.15.2", + "svelte-sonner": "^0.3.19", + "tippy.js": "^6.3.7", + "turndown": "^7.2.0", + "uuid": "^9.0.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000000000000000000000000000000000000..0f7721681d725ddea512a5ed734891cf6545ca3c --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..efce1158fbb90868b0d7dfe589d2d3f7360ff70f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,118 @@ +[project] +name = "open-webui" +description = "Open WebUI (Formerly Ollama WebUI)" +authors = [ + { name = "Timothy Jaeryang Baek", email = "tim@openwebui.com" } +] +license = { file = "LICENSE" } +dependencies = [ + "fastapi==0.111.0", + "uvicorn[standard]==0.22.0", + "pydantic==2.7.1", + "python-multipart==0.0.9", + + "Flask==3.0.3", + "Flask-Cors==4.0.1", + + "python-socketio==5.11.2", + "python-jose==3.3.0", + "passlib[bcrypt]==1.7.4", + + "requests==2.32.2", + "aiohttp==3.9.5", + "peewee==3.17.5", + "peewee-migrate==1.12.2", + "psycopg2-binary==2.9.9", + "PyMySQL==1.1.1", + "bcrypt==4.1.3", + + "boto3==1.34.110", + + "argon2-cffi==23.1.0", + "APScheduler==3.10.4", + "google-generativeai==0.5.4", + + "langchain==0.2.0", + "langchain-community==0.2.9", + "langchain-chroma==0.1.1", + + "fake-useragent==1.5.1", + "chromadb==0.5.0", + "sentence-transformers==2.7.0", + "pypdf==4.2.0", + "docx2txt==0.8", + "unstructured==0.14.0", + "Markdown==3.6", + "pypandoc==1.13", + "pandas==2.2.2", + "openpyxl==3.1.2", + "pyxlsb==1.0.10", + "xlrd==2.0.1", + "validators==0.28.1", + + "opencv-python-headless==4.9.0.80", + "rapidocr-onnxruntime==1.3.22", + + "fpdf2==2.7.9", + "rank-bm25==0.2.2", + + "faster-whisper==1.0.2", + + "PyJWT[crypto]==2.8.0", + "authlib==1.3.1", + + "black==24.4.2", + "langfuse==2.33.0", + "youtube-transcript-api==0.6.2", + "pytube==15.0.0", + "extract_msg", + "pydub", + "duckduckgo-search~=6.1.5" + +] +readme = "README.md" +requires-python = ">= 3.11, < 3.12.0a1" +dynamic = ["version"] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Topic :: Communications :: Chat", + "Topic :: Multimedia", +] + +[project.scripts] +open-webui = "open_webui:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.version] +path = "package.json" +pattern = '"version":\s*"(?P<version>[^"]+)"' + +[tool.hatch.build.hooks.custom] # keep this for reading hooks from `hatch_build.py` + +[tool.hatch.build.targets.wheel] +sources = ["backend"] +exclude = [ + ".dockerignore", + ".gitignore", + ".webui_secret_key", + "dev.sh", + "requirements.txt", + "start.sh", + "start_windows.bat", + "webui.db", + "chroma.sqlite3", +] +force-include = { "CHANGELOG.md" = "open_webui/CHANGELOG.md", build = "open_webui/frontend" } diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000000000000000000000000000000000000..e56ad08f0decb5f59981be168623b2a64d9e31dc --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,684 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohttp==3.9.5 + # via langchain + # via langchain-community + # via open-webui +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +anyio==4.3.0 + # via httpx + # via starlette + # via watchfiles +apscheduler==3.10.4 + # via open-webui +argon2-cffi==23.1.0 + # via open-webui +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +asgiref==3.8.1 + # via opentelemetry-instrumentation-asgi +attrs==23.2.0 + # via aiohttp +authlib==1.3.1 + # via open-webui +av==11.0.0 + # via faster-whisper +backoff==2.2.1 + # via langfuse + # via posthog + # via unstructured +bcrypt==4.1.3 + # via chromadb + # via open-webui + # via passlib +beautifulsoup4==4.12.3 + # via extract-msg + # via unstructured +bidict==0.23.1 + # via python-socketio +black==24.4.2 + # via open-webui +blinker==1.8.2 + # via flask +boto3==1.34.110 + # via open-webui +botocore==1.34.110 + # via boto3 + # via s3transfer +build==1.2.1 + # via chromadb +cachetools==5.3.3 + # via google-auth +certifi==2024.2.2 + # via httpcore + # via httpx + # via kubernetes + # via requests + # via unstructured-client +cffi==1.16.0 + # via argon2-cffi-bindings + # via cryptography +chardet==5.2.0 + # via unstructured +charset-normalizer==3.3.2 + # via requests + # via unstructured-client +chroma-hnswlib==0.7.3 + # via chromadb +chromadb==0.5.0 + # via langchain-chroma + # via open-webui +click==8.1.7 + # via black + # via duckduckgo-search + # via flask + # via nltk + # via peewee-migrate + # via typer + # via uvicorn +colorclass==2.2.2 + # via oletools +coloredlogs==15.0.1 + # via onnxruntime +compressed-rtf==1.0.6 + # via extract-msg +cryptography==42.0.7 + # via authlib + # via msoffcrypto-tool + # via pyjwt +ctranslate2==4.2.1 + # via faster-whisper +dataclasses-json==0.6.6 + # via langchain + # via langchain-community + # via unstructured + # via unstructured-client +deepdiff==7.0.1 + # via unstructured-client +defusedxml==0.7.1 + # via fpdf2 +deprecated==1.2.14 + # via opentelemetry-api + # via opentelemetry-exporter-otlp-proto-grpc +dnspython==2.6.1 + # via email-validator +docx2txt==0.8 + # via open-webui +duckduckgo-search==6.1.5 + # via open-webui +easygui==0.98.3 + # via oletools +ebcdic==1.1.1 + # via extract-msg +ecdsa==0.19.0 + # via python-jose +email-validator==2.1.1 + # via fastapi +emoji==2.11.1 + # via unstructured +et-xmlfile==1.1.0 + # via openpyxl +extract-msg==0.48.5 + # via open-webui +fake-useragent==1.5.1 + # via open-webui +fastapi==0.111.0 + # via chromadb + # via langchain-chroma + # via open-webui +fastapi-cli==0.0.4 + # via fastapi +faster-whisper==1.0.2 + # via open-webui +filelock==3.14.0 + # via huggingface-hub + # via torch + # via transformers +filetype==1.2.0 + # via unstructured +flask==3.0.3 + # via flask-cors + # via open-webui +flask-cors==4.0.1 + # via open-webui +flatbuffers==24.3.25 + # via onnxruntime +fonttools==4.51.0 + # via fpdf2 +fpdf2==2.7.9 + # via open-webui +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +fsspec==2024.3.1 + # via huggingface-hub + # via torch +google-ai-generativelanguage==0.6.4 + # via google-generativeai +google-api-core==2.19.0 + # via google-ai-generativelanguage + # via google-api-python-client + # via google-generativeai +google-api-python-client==2.129.0 + # via google-generativeai +google-auth==2.29.0 + # via google-ai-generativelanguage + # via google-api-core + # via google-api-python-client + # via google-auth-httplib2 + # via google-generativeai + # via kubernetes +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-generativeai==0.5.4 + # via open-webui +googleapis-common-protos==1.63.0 + # via google-api-core + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc +grpcio==1.63.0 + # via chromadb + # via google-api-core + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc +grpcio-status==1.62.2 + # via google-api-core +h11==0.14.0 + # via httpcore + # via uvicorn + # via wsproto +httpcore==1.0.5 + # via httpx +httplib2==0.22.0 + # via google-api-python-client + # via google-auth-httplib2 +httptools==0.6.1 + # via uvicorn +httpx==0.27.0 + # via fastapi + # via langfuse +huggingface-hub==0.23.0 + # via faster-whisper + # via sentence-transformers + # via tokenizers + # via transformers +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via anyio + # via email-validator + # via httpx + # via langfuse + # via requests + # via unstructured-client + # via yarl +importlib-metadata==7.0.0 + # via opentelemetry-api +importlib-resources==6.4.0 + # via chromadb +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via fastapi + # via flask + # via torch +jmespath==1.0.1 + # via boto3 + # via botocore +joblib==1.4.2 + # via nltk + # via scikit-learn +jsonpatch==1.33 + # via langchain-core +jsonpath-python==1.0.6 + # via unstructured-client +jsonpointer==2.4 + # via jsonpatch +kubernetes==29.0.0 + # via chromadb +langchain==0.2.0 + # via langchain-community + # via open-webui +langchain-chroma==0.1.1 + # via open-webui +langchain-community==0.2.0 + # via open-webui +langchain-core==0.2.1 + # via langchain + # via langchain-chroma + # via langchain-community + # via langchain-text-splitters +langchain-text-splitters==0.2.0 + # via langchain +langdetect==1.0.9 + # via unstructured +langfuse==2.33.0 + # via open-webui +langsmith==0.1.57 + # via langchain + # via langchain-community + # via langchain-core +lark==1.1.8 + # via rtfde +lxml==5.2.2 + # via unstructured +markdown==3.6 + # via open-webui +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via werkzeug +marshmallow==3.21.2 + # via dataclasses-json + # via unstructured-client +mdurl==0.1.2 + # via markdown-it-py +mmh3==4.1.0 + # via chromadb +monotonic==1.6 + # via posthog +mpmath==1.3.0 + # via sympy +msoffcrypto-tool==5.4.1 + # via oletools +multidict==6.0.5 + # via aiohttp + # via yarl +mypy-extensions==1.0.0 + # via black + # via typing-inspect + # via unstructured-client +networkx==3.3 + # via torch +nltk==3.8.1 + # via unstructured +numpy==1.26.4 + # via chroma-hnswlib + # via chromadb + # via ctranslate2 + # via langchain + # via langchain-chroma + # via langchain-community + # via onnxruntime + # via opencv-python + # via opencv-python-headless + # via pandas + # via rank-bm25 + # via rapidocr-onnxruntime + # via scikit-learn + # via scipy + # via sentence-transformers + # via shapely + # via transformers + # via unstructured +oauthlib==3.2.2 + # via kubernetes + # via requests-oauthlib +olefile==0.47 + # via extract-msg + # via msoffcrypto-tool + # via oletools +oletools==0.60.1 + # via pcodedmp + # via rtfde +onnxruntime==1.17.3 + # via chromadb + # via faster-whisper + # via rapidocr-onnxruntime +opencv-python==4.9.0.80 + # via rapidocr-onnxruntime +opencv-python-headless==4.9.0.80 + # via open-webui +openpyxl==3.1.2 + # via open-webui +opentelemetry-api==1.24.0 + # via chromadb + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi + # via opentelemetry-sdk +opentelemetry-exporter-otlp-proto-common==1.24.0 + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-grpc==1.24.0 + # via chromadb +opentelemetry-instrumentation==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-asgi==0.45b0 + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-fastapi==0.45b0 + # via chromadb +opentelemetry-proto==1.24.0 + # via opentelemetry-exporter-otlp-proto-common + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-sdk==1.24.0 + # via chromadb + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-semantic-conventions==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi + # via opentelemetry-sdk +opentelemetry-util-http==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi +ordered-set==4.1.0 + # via deepdiff +orjson==3.10.3 + # via chromadb + # via duckduckgo-search + # via fastapi + # via langsmith +overrides==7.7.0 + # via chromadb +packaging==23.2 + # via black + # via build + # via huggingface-hub + # via langchain-core + # via langfuse + # via marshmallow + # via onnxruntime + # via transformers + # via unstructured-client +pandas==2.2.2 + # via open-webui +passlib==1.7.4 + # via open-webui +pathspec==0.12.1 + # via black +pcodedmp==1.2.6 + # via oletools +peewee==3.17.5 + # via open-webui + # via peewee-migrate +peewee-migrate==1.12.2 + # via open-webui +pillow==10.3.0 + # via fpdf2 + # via rapidocr-onnxruntime + # via sentence-transformers +platformdirs==4.2.1 + # via black +posthog==3.5.0 + # via chromadb +proto-plus==1.23.0 + # via google-ai-generativelanguage + # via google-api-core +protobuf==4.25.3 + # via google-ai-generativelanguage + # via google-api-core + # via google-generativeai + # via googleapis-common-protos + # via grpcio-status + # via onnxruntime + # via opentelemetry-proto + # via proto-plus +psycopg2-binary==2.9.9 + # via open-webui +pyasn1==0.6.0 + # via pyasn1-modules + # via python-jose + # via rsa +pyasn1-modules==0.4.0 + # via google-auth +pyclipper==1.3.0.post5 + # via rapidocr-onnxruntime +pycparser==2.22 + # via cffi +pydantic==2.7.1 + # via chromadb + # via fastapi + # via google-generativeai + # via langchain + # via langchain-core + # via langfuse + # via langsmith + # via open-webui +pydantic-core==2.18.2 + # via pydantic +pydub==0.25.1 + # via open-webui +pygments==2.18.0 + # via rich +pyjwt==2.8.0 + # via open-webui +pymysql==1.1.0 + # via open-webui +pypandoc==1.13 + # via open-webui +pyparsing==2.4.7 + # via httplib2 + # via oletools +pypdf==4.2.0 + # via open-webui + # via unstructured-client +pypika==0.48.9 + # via chromadb +pyproject-hooks==1.1.0 + # via build +pyreqwest-impersonate==0.4.7 + # via duckduckgo-search +python-dateutil==2.9.0.post0 + # via botocore + # via kubernetes + # via pandas + # via posthog + # via unstructured-client +python-dotenv==1.0.1 + # via uvicorn +python-engineio==4.9.0 + # via python-socketio +python-iso639==2024.4.27 + # via unstructured +python-jose==3.3.0 + # via open-webui +python-magic==0.4.27 + # via unstructured +python-multipart==0.0.9 + # via fastapi + # via open-webui +python-socketio==5.11.2 + # via open-webui +pytube==15.0.0 + # via open-webui +pytz==2024.1 + # via apscheduler + # via pandas +pyxlsb==1.0.10 + # via open-webui +pyyaml==6.0.1 + # via chromadb + # via ctranslate2 + # via huggingface-hub + # via kubernetes + # via langchain + # via langchain-community + # via langchain-core + # via rapidocr-onnxruntime + # via transformers + # via uvicorn +rank-bm25==0.2.2 + # via open-webui +rapidfuzz==3.9.0 + # via unstructured +rapidocr-onnxruntime==1.3.22 + # via open-webui +red-black-tree-mod==1.20 + # via extract-msg +regex==2024.5.10 + # via nltk + # via transformers +requests==2.32.2 + # via chromadb + # via google-api-core + # via huggingface-hub + # via kubernetes + # via langchain + # via langchain-community + # via langsmith + # via open-webui + # via posthog + # via requests-oauthlib + # via transformers + # via unstructured + # via unstructured-client + # via youtube-transcript-api +requests-oauthlib==2.0.0 + # via kubernetes +rich==13.7.1 + # via typer +rsa==4.9 + # via google-auth + # via python-jose +rtfde==0.1.1 + # via extract-msg +s3transfer==0.10.1 + # via boto3 +safetensors==0.4.3 + # via transformers +scikit-learn==1.4.2 + # via sentence-transformers +scipy==1.13.0 + # via scikit-learn + # via sentence-transformers +sentence-transformers==2.7.0 + # via open-webui +setuptools==69.5.1 + # via ctranslate2 + # via opentelemetry-instrumentation +shapely==2.0.4 + # via rapidocr-onnxruntime +shellingham==1.5.4 + # via typer +simple-websocket==1.0.0 + # via python-engineio +six==1.16.0 + # via apscheduler + # via ecdsa + # via kubernetes + # via langdetect + # via posthog + # via python-dateutil + # via rapidocr-onnxruntime + # via unstructured-client +sniffio==1.3.1 + # via anyio + # via httpx +soupsieve==2.5 + # via beautifulsoup4 +sqlalchemy==2.0.30 + # via langchain + # via langchain-community +starlette==0.37.2 + # via fastapi +sympy==1.12 + # via onnxruntime + # via torch +tabulate==0.9.0 + # via unstructured +tenacity==8.3.0 + # via chromadb + # via langchain + # via langchain-community + # via langchain-core +threadpoolctl==3.5.0 + # via scikit-learn +tokenizers==0.15.2 + # via chromadb + # via faster-whisper + # via transformers +torch==2.3.0 + # via sentence-transformers +tqdm==4.66.4 + # via chromadb + # via google-generativeai + # via huggingface-hub + # via nltk + # via sentence-transformers + # via transformers +transformers==4.39.3 + # via sentence-transformers +typer==0.12.3 + # via chromadb + # via fastapi-cli +typing-extensions==4.11.0 + # via chromadb + # via fastapi + # via google-generativeai + # via huggingface-hub + # via opentelemetry-sdk + # via pydantic + # via pydantic-core + # via sqlalchemy + # via torch + # via typer + # via typing-inspect + # via unstructured + # via unstructured-client +typing-inspect==0.9.0 + # via dataclasses-json + # via unstructured-client +tzdata==2024.1 + # via pandas +tzlocal==5.2 + # via apscheduler + # via extract-msg +ujson==5.10.0 + # via fastapi +unstructured==0.14.0 + # via open-webui +unstructured-client==0.22.0 + # via unstructured +uritemplate==4.1.1 + # via google-api-python-client +urllib3==2.2.1 + # via botocore + # via kubernetes + # via requests + # via unstructured-client +uvicorn==0.22.0 + # via chromadb + # via fastapi + # via open-webui +uvloop==0.19.0 + # via uvicorn +validators==0.28.1 + # via open-webui +watchfiles==0.21.0 + # via uvicorn +websocket-client==1.8.0 + # via kubernetes +websockets==12.0 + # via uvicorn +werkzeug==3.0.3 + # via flask +wrapt==1.16.0 + # via deprecated + # via langfuse + # via opentelemetry-instrumentation + # via unstructured +wsproto==1.2.0 + # via simple-websocket +xlrd==2.0.1 + # via open-webui +yarl==1.9.4 + # via aiohttp +youtube-transcript-api==0.6.2 + # via open-webui +zipp==3.18.1 + # via importlib-metadata diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000000000000000000000000000000000000..e56ad08f0decb5f59981be168623b2a64d9e31dc --- /dev/null +++ b/requirements.lock @@ -0,0 +1,684 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +aiohttp==3.9.5 + # via langchain + # via langchain-community + # via open-webui +aiosignal==1.3.1 + # via aiohttp +annotated-types==0.6.0 + # via pydantic +anyio==4.3.0 + # via httpx + # via starlette + # via watchfiles +apscheduler==3.10.4 + # via open-webui +argon2-cffi==23.1.0 + # via open-webui +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +asgiref==3.8.1 + # via opentelemetry-instrumentation-asgi +attrs==23.2.0 + # via aiohttp +authlib==1.3.1 + # via open-webui +av==11.0.0 + # via faster-whisper +backoff==2.2.1 + # via langfuse + # via posthog + # via unstructured +bcrypt==4.1.3 + # via chromadb + # via open-webui + # via passlib +beautifulsoup4==4.12.3 + # via extract-msg + # via unstructured +bidict==0.23.1 + # via python-socketio +black==24.4.2 + # via open-webui +blinker==1.8.2 + # via flask +boto3==1.34.110 + # via open-webui +botocore==1.34.110 + # via boto3 + # via s3transfer +build==1.2.1 + # via chromadb +cachetools==5.3.3 + # via google-auth +certifi==2024.2.2 + # via httpcore + # via httpx + # via kubernetes + # via requests + # via unstructured-client +cffi==1.16.0 + # via argon2-cffi-bindings + # via cryptography +chardet==5.2.0 + # via unstructured +charset-normalizer==3.3.2 + # via requests + # via unstructured-client +chroma-hnswlib==0.7.3 + # via chromadb +chromadb==0.5.0 + # via langchain-chroma + # via open-webui +click==8.1.7 + # via black + # via duckduckgo-search + # via flask + # via nltk + # via peewee-migrate + # via typer + # via uvicorn +colorclass==2.2.2 + # via oletools +coloredlogs==15.0.1 + # via onnxruntime +compressed-rtf==1.0.6 + # via extract-msg +cryptography==42.0.7 + # via authlib + # via msoffcrypto-tool + # via pyjwt +ctranslate2==4.2.1 + # via faster-whisper +dataclasses-json==0.6.6 + # via langchain + # via langchain-community + # via unstructured + # via unstructured-client +deepdiff==7.0.1 + # via unstructured-client +defusedxml==0.7.1 + # via fpdf2 +deprecated==1.2.14 + # via opentelemetry-api + # via opentelemetry-exporter-otlp-proto-grpc +dnspython==2.6.1 + # via email-validator +docx2txt==0.8 + # via open-webui +duckduckgo-search==6.1.5 + # via open-webui +easygui==0.98.3 + # via oletools +ebcdic==1.1.1 + # via extract-msg +ecdsa==0.19.0 + # via python-jose +email-validator==2.1.1 + # via fastapi +emoji==2.11.1 + # via unstructured +et-xmlfile==1.1.0 + # via openpyxl +extract-msg==0.48.5 + # via open-webui +fake-useragent==1.5.1 + # via open-webui +fastapi==0.111.0 + # via chromadb + # via langchain-chroma + # via open-webui +fastapi-cli==0.0.4 + # via fastapi +faster-whisper==1.0.2 + # via open-webui +filelock==3.14.0 + # via huggingface-hub + # via torch + # via transformers +filetype==1.2.0 + # via unstructured +flask==3.0.3 + # via flask-cors + # via open-webui +flask-cors==4.0.1 + # via open-webui +flatbuffers==24.3.25 + # via onnxruntime +fonttools==4.51.0 + # via fpdf2 +fpdf2==2.7.9 + # via open-webui +frozenlist==1.4.1 + # via aiohttp + # via aiosignal +fsspec==2024.3.1 + # via huggingface-hub + # via torch +google-ai-generativelanguage==0.6.4 + # via google-generativeai +google-api-core==2.19.0 + # via google-ai-generativelanguage + # via google-api-python-client + # via google-generativeai +google-api-python-client==2.129.0 + # via google-generativeai +google-auth==2.29.0 + # via google-ai-generativelanguage + # via google-api-core + # via google-api-python-client + # via google-auth-httplib2 + # via google-generativeai + # via kubernetes +google-auth-httplib2==0.2.0 + # via google-api-python-client +google-generativeai==0.5.4 + # via open-webui +googleapis-common-protos==1.63.0 + # via google-api-core + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc +grpcio==1.63.0 + # via chromadb + # via google-api-core + # via grpcio-status + # via opentelemetry-exporter-otlp-proto-grpc +grpcio-status==1.62.2 + # via google-api-core +h11==0.14.0 + # via httpcore + # via uvicorn + # via wsproto +httpcore==1.0.5 + # via httpx +httplib2==0.22.0 + # via google-api-python-client + # via google-auth-httplib2 +httptools==0.6.1 + # via uvicorn +httpx==0.27.0 + # via fastapi + # via langfuse +huggingface-hub==0.23.0 + # via faster-whisper + # via sentence-transformers + # via tokenizers + # via transformers +humanfriendly==10.0 + # via coloredlogs +idna==3.7 + # via anyio + # via email-validator + # via httpx + # via langfuse + # via requests + # via unstructured-client + # via yarl +importlib-metadata==7.0.0 + # via opentelemetry-api +importlib-resources==6.4.0 + # via chromadb +itsdangerous==2.2.0 + # via flask +jinja2==3.1.4 + # via fastapi + # via flask + # via torch +jmespath==1.0.1 + # via boto3 + # via botocore +joblib==1.4.2 + # via nltk + # via scikit-learn +jsonpatch==1.33 + # via langchain-core +jsonpath-python==1.0.6 + # via unstructured-client +jsonpointer==2.4 + # via jsonpatch +kubernetes==29.0.0 + # via chromadb +langchain==0.2.0 + # via langchain-community + # via open-webui +langchain-chroma==0.1.1 + # via open-webui +langchain-community==0.2.0 + # via open-webui +langchain-core==0.2.1 + # via langchain + # via langchain-chroma + # via langchain-community + # via langchain-text-splitters +langchain-text-splitters==0.2.0 + # via langchain +langdetect==1.0.9 + # via unstructured +langfuse==2.33.0 + # via open-webui +langsmith==0.1.57 + # via langchain + # via langchain-community + # via langchain-core +lark==1.1.8 + # via rtfde +lxml==5.2.2 + # via unstructured +markdown==3.6 + # via open-webui +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.5 + # via jinja2 + # via werkzeug +marshmallow==3.21.2 + # via dataclasses-json + # via unstructured-client +mdurl==0.1.2 + # via markdown-it-py +mmh3==4.1.0 + # via chromadb +monotonic==1.6 + # via posthog +mpmath==1.3.0 + # via sympy +msoffcrypto-tool==5.4.1 + # via oletools +multidict==6.0.5 + # via aiohttp + # via yarl +mypy-extensions==1.0.0 + # via black + # via typing-inspect + # via unstructured-client +networkx==3.3 + # via torch +nltk==3.8.1 + # via unstructured +numpy==1.26.4 + # via chroma-hnswlib + # via chromadb + # via ctranslate2 + # via langchain + # via langchain-chroma + # via langchain-community + # via onnxruntime + # via opencv-python + # via opencv-python-headless + # via pandas + # via rank-bm25 + # via rapidocr-onnxruntime + # via scikit-learn + # via scipy + # via sentence-transformers + # via shapely + # via transformers + # via unstructured +oauthlib==3.2.2 + # via kubernetes + # via requests-oauthlib +olefile==0.47 + # via extract-msg + # via msoffcrypto-tool + # via oletools +oletools==0.60.1 + # via pcodedmp + # via rtfde +onnxruntime==1.17.3 + # via chromadb + # via faster-whisper + # via rapidocr-onnxruntime +opencv-python==4.9.0.80 + # via rapidocr-onnxruntime +opencv-python-headless==4.9.0.80 + # via open-webui +openpyxl==3.1.2 + # via open-webui +opentelemetry-api==1.24.0 + # via chromadb + # via opentelemetry-exporter-otlp-proto-grpc + # via opentelemetry-instrumentation + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi + # via opentelemetry-sdk +opentelemetry-exporter-otlp-proto-common==1.24.0 + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-exporter-otlp-proto-grpc==1.24.0 + # via chromadb +opentelemetry-instrumentation==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-asgi==0.45b0 + # via opentelemetry-instrumentation-fastapi +opentelemetry-instrumentation-fastapi==0.45b0 + # via chromadb +opentelemetry-proto==1.24.0 + # via opentelemetry-exporter-otlp-proto-common + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-sdk==1.24.0 + # via chromadb + # via opentelemetry-exporter-otlp-proto-grpc +opentelemetry-semantic-conventions==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi + # via opentelemetry-sdk +opentelemetry-util-http==0.45b0 + # via opentelemetry-instrumentation-asgi + # via opentelemetry-instrumentation-fastapi +ordered-set==4.1.0 + # via deepdiff +orjson==3.10.3 + # via chromadb + # via duckduckgo-search + # via fastapi + # via langsmith +overrides==7.7.0 + # via chromadb +packaging==23.2 + # via black + # via build + # via huggingface-hub + # via langchain-core + # via langfuse + # via marshmallow + # via onnxruntime + # via transformers + # via unstructured-client +pandas==2.2.2 + # via open-webui +passlib==1.7.4 + # via open-webui +pathspec==0.12.1 + # via black +pcodedmp==1.2.6 + # via oletools +peewee==3.17.5 + # via open-webui + # via peewee-migrate +peewee-migrate==1.12.2 + # via open-webui +pillow==10.3.0 + # via fpdf2 + # via rapidocr-onnxruntime + # via sentence-transformers +platformdirs==4.2.1 + # via black +posthog==3.5.0 + # via chromadb +proto-plus==1.23.0 + # via google-ai-generativelanguage + # via google-api-core +protobuf==4.25.3 + # via google-ai-generativelanguage + # via google-api-core + # via google-generativeai + # via googleapis-common-protos + # via grpcio-status + # via onnxruntime + # via opentelemetry-proto + # via proto-plus +psycopg2-binary==2.9.9 + # via open-webui +pyasn1==0.6.0 + # via pyasn1-modules + # via python-jose + # via rsa +pyasn1-modules==0.4.0 + # via google-auth +pyclipper==1.3.0.post5 + # via rapidocr-onnxruntime +pycparser==2.22 + # via cffi +pydantic==2.7.1 + # via chromadb + # via fastapi + # via google-generativeai + # via langchain + # via langchain-core + # via langfuse + # via langsmith + # via open-webui +pydantic-core==2.18.2 + # via pydantic +pydub==0.25.1 + # via open-webui +pygments==2.18.0 + # via rich +pyjwt==2.8.0 + # via open-webui +pymysql==1.1.0 + # via open-webui +pypandoc==1.13 + # via open-webui +pyparsing==2.4.7 + # via httplib2 + # via oletools +pypdf==4.2.0 + # via open-webui + # via unstructured-client +pypika==0.48.9 + # via chromadb +pyproject-hooks==1.1.0 + # via build +pyreqwest-impersonate==0.4.7 + # via duckduckgo-search +python-dateutil==2.9.0.post0 + # via botocore + # via kubernetes + # via pandas + # via posthog + # via unstructured-client +python-dotenv==1.0.1 + # via uvicorn +python-engineio==4.9.0 + # via python-socketio +python-iso639==2024.4.27 + # via unstructured +python-jose==3.3.0 + # via open-webui +python-magic==0.4.27 + # via unstructured +python-multipart==0.0.9 + # via fastapi + # via open-webui +python-socketio==5.11.2 + # via open-webui +pytube==15.0.0 + # via open-webui +pytz==2024.1 + # via apscheduler + # via pandas +pyxlsb==1.0.10 + # via open-webui +pyyaml==6.0.1 + # via chromadb + # via ctranslate2 + # via huggingface-hub + # via kubernetes + # via langchain + # via langchain-community + # via langchain-core + # via rapidocr-onnxruntime + # via transformers + # via uvicorn +rank-bm25==0.2.2 + # via open-webui +rapidfuzz==3.9.0 + # via unstructured +rapidocr-onnxruntime==1.3.22 + # via open-webui +red-black-tree-mod==1.20 + # via extract-msg +regex==2024.5.10 + # via nltk + # via transformers +requests==2.32.2 + # via chromadb + # via google-api-core + # via huggingface-hub + # via kubernetes + # via langchain + # via langchain-community + # via langsmith + # via open-webui + # via posthog + # via requests-oauthlib + # via transformers + # via unstructured + # via unstructured-client + # via youtube-transcript-api +requests-oauthlib==2.0.0 + # via kubernetes +rich==13.7.1 + # via typer +rsa==4.9 + # via google-auth + # via python-jose +rtfde==0.1.1 + # via extract-msg +s3transfer==0.10.1 + # via boto3 +safetensors==0.4.3 + # via transformers +scikit-learn==1.4.2 + # via sentence-transformers +scipy==1.13.0 + # via scikit-learn + # via sentence-transformers +sentence-transformers==2.7.0 + # via open-webui +setuptools==69.5.1 + # via ctranslate2 + # via opentelemetry-instrumentation +shapely==2.0.4 + # via rapidocr-onnxruntime +shellingham==1.5.4 + # via typer +simple-websocket==1.0.0 + # via python-engineio +six==1.16.0 + # via apscheduler + # via ecdsa + # via kubernetes + # via langdetect + # via posthog + # via python-dateutil + # via rapidocr-onnxruntime + # via unstructured-client +sniffio==1.3.1 + # via anyio + # via httpx +soupsieve==2.5 + # via beautifulsoup4 +sqlalchemy==2.0.30 + # via langchain + # via langchain-community +starlette==0.37.2 + # via fastapi +sympy==1.12 + # via onnxruntime + # via torch +tabulate==0.9.0 + # via unstructured +tenacity==8.3.0 + # via chromadb + # via langchain + # via langchain-community + # via langchain-core +threadpoolctl==3.5.0 + # via scikit-learn +tokenizers==0.15.2 + # via chromadb + # via faster-whisper + # via transformers +torch==2.3.0 + # via sentence-transformers +tqdm==4.66.4 + # via chromadb + # via google-generativeai + # via huggingface-hub + # via nltk + # via sentence-transformers + # via transformers +transformers==4.39.3 + # via sentence-transformers +typer==0.12.3 + # via chromadb + # via fastapi-cli +typing-extensions==4.11.0 + # via chromadb + # via fastapi + # via google-generativeai + # via huggingface-hub + # via opentelemetry-sdk + # via pydantic + # via pydantic-core + # via sqlalchemy + # via torch + # via typer + # via typing-inspect + # via unstructured + # via unstructured-client +typing-inspect==0.9.0 + # via dataclasses-json + # via unstructured-client +tzdata==2024.1 + # via pandas +tzlocal==5.2 + # via apscheduler + # via extract-msg +ujson==5.10.0 + # via fastapi +unstructured==0.14.0 + # via open-webui +unstructured-client==0.22.0 + # via unstructured +uritemplate==4.1.1 + # via google-api-python-client +urllib3==2.2.1 + # via botocore + # via kubernetes + # via requests + # via unstructured-client +uvicorn==0.22.0 + # via chromadb + # via fastapi + # via open-webui +uvloop==0.19.0 + # via uvicorn +validators==0.28.1 + # via open-webui +watchfiles==0.21.0 + # via uvicorn +websocket-client==1.8.0 + # via kubernetes +websockets==12.0 + # via uvicorn +werkzeug==3.0.3 + # via flask +wrapt==1.16.0 + # via deprecated + # via langfuse + # via opentelemetry-instrumentation + # via unstructured +wsproto==1.2.0 + # via simple-websocket +xlrd==2.0.1 + # via open-webui +yarl==1.9.4 + # via aiohttp +youtube-transcript-api==0.6.2 + # via open-webui +zipp==3.18.1 + # via importlib-metadata diff --git a/run-compose.sh b/run-compose.sh new file mode 100755 index 0000000000000000000000000000000000000000..21574e95997fd512bc243adf0fa77456e570010d --- /dev/null +++ b/run-compose.sh @@ -0,0 +1,241 @@ +#!/bin/bash + +# Define color and formatting codes +BOLD='\033[1m' +GREEN='\033[1;32m' +WHITE='\033[1;37m' +RED='\033[0;31m' +NC='\033[0m' # No Color +# Unicode character for tick mark +TICK='\u2713' + +# Detect GPU driver +get_gpu_driver() { + # Detect NVIDIA GPUs using lspci or nvidia-smi + if lspci | grep -i nvidia >/dev/null || nvidia-smi >/dev/null 2>&1; then + echo "nvidia" + return + fi + + # Detect AMD GPUs (including GCN architecture check for amdgpu vs radeon) + if lspci | grep -i amd >/dev/null; then + # List of known GCN and later architecture cards + # This is a simplified list, and in a real-world scenario, you'd want a more comprehensive one + local gcn_and_later=("Radeon HD 7000" "Radeon HD 8000" "Radeon R5" "Radeon R7" "Radeon R9" "Radeon RX") + + # Get GPU information + local gpu_info=$(lspci | grep -i 'vga.*amd') + + for model in "${gcn_and_later[@]}"; do + if echo "$gpu_info" | grep -iq "$model"; then + echo "amdgpu" + return + fi + done + + # Default to radeon if no GCN or later architecture is detected + echo "radeon" + return + fi + + # Detect Intel GPUs + if lspci | grep -i intel >/dev/null; then + echo "i915" + return + fi + + # If no known GPU is detected + echo "Unknown or unsupported GPU driver" + exit 1 +} + +# Function for rolling animation +show_loading() { + local spin='-\|/' + local i=0 + + printf " " + + while kill -0 $1 2>/dev/null; do + i=$(( (i+1) %4 )) + printf "\b${spin:$i:1}" + sleep .1 + done + + # Replace the spinner with a tick + printf "\b${GREEN}${TICK}${NC}" +} + +# Usage information +usage() { + echo "Usage: $0 [OPTIONS]" + echo "Options:" + echo " --enable-gpu[count=COUNT] Enable GPU support with the specified count." + echo " --enable-api[port=PORT] Enable API and expose it on the specified port." + echo " --webui[port=PORT] Set the port for the web user interface." + echo " --data[folder=PATH] Bind mount for ollama data folder (by default will create the 'ollama' volume)." + echo " --build Build the docker image before running the compose project." + echo " --drop Drop the compose project." + echo " -q, --quiet Run script in headless mode." + echo " -h, --help Show this help message." + echo "" + echo "Examples:" + echo " $0 --drop" + echo " $0 --enable-gpu[count=1]" + echo " $0 --enable-gpu[count=all]" + echo " $0 --enable-api[port=11435]" + echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000]" + echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data]" + echo " $0 --enable-gpu[count=1] --enable-api[port=12345] --webui[port=3000] --data[folder=./ollama-data] --build" + echo "" + echo "This script configures and runs a docker-compose setup with optional GPU support, API exposure, and web UI configuration." + echo "About the gpu to use, the script automatically detects it using the "lspci" command." + echo "In this case the gpu detected is: $(get_gpu_driver)" +} + +# Default values +gpu_count=1 +api_port=11435 +webui_port=3000 +headless=false +build_image=false +kill_compose=false + +# Function to extract value from the parameter +extract_value() { + echo "$1" | sed -E 's/.*\[.*=(.*)\].*/\1/; t; s/.*//' +} + +# Parse arguments +while [[ $# -gt 0 ]]; do + key="$1" + + case $key in + --enable-gpu*) + enable_gpu=true + value=$(extract_value "$key") + gpu_count=${value:-1} + ;; + --enable-api*) + enable_api=true + value=$(extract_value "$key") + api_port=${value:-11435} + ;; + --webui*) + value=$(extract_value "$key") + webui_port=${value:-3000} + ;; + --data*) + value=$(extract_value "$key") + data_dir=${value:-"./ollama-data"} + ;; + --drop) + kill_compose=true + ;; + --build) + build_image=true + ;; + -q|--quiet) + headless=true + ;; + -h|--help) + usage + exit + ;; + *) + # Unknown option + echo "Unknown option: $key" + usage + exit 1 + ;; + esac + shift # past argument or value +done + +if [[ $kill_compose == true ]]; then + docker compose down --remove-orphans + echo -e "${GREEN}${BOLD}Compose project dropped successfully.${NC}" + exit +else + DEFAULT_COMPOSE_COMMAND="docker compose -f docker-compose.yaml" + if [[ $enable_gpu == true ]]; then + # Validate and process command-line arguments + if [[ -n $gpu_count ]]; then + if ! [[ $gpu_count =~ ^([0-9]+|all)$ ]]; then + echo "Invalid GPU count: $gpu_count" + exit 1 + fi + echo "Enabling GPU with $gpu_count GPUs" + # Add your GPU allocation logic here + export OLLAMA_GPU_DRIVER=$(get_gpu_driver) + export OLLAMA_GPU_COUNT=$gpu_count # Set OLLAMA_GPU_COUNT environment variable + fi + DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.gpu.yaml" + fi + if [[ $enable_api == true ]]; then + DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.api.yaml" + if [[ -n $api_port ]]; then + export OLLAMA_WEBAPI_PORT=$api_port # Set OLLAMA_WEBAPI_PORT environment variable + fi + fi + if [[ -n $data_dir ]]; then + DEFAULT_COMPOSE_COMMAND+=" -f docker-compose.data.yaml" + export OLLAMA_DATA_DIR=$data_dir # Set OLLAMA_DATA_DIR environment variable + fi + if [[ -n $webui_port ]]; then + export OPEN_WEBUI_PORT=$webui_port # Set OPEN_WEBUI_PORT environment variable + fi + DEFAULT_COMPOSE_COMMAND+=" up -d" + DEFAULT_COMPOSE_COMMAND+=" --remove-orphans" + DEFAULT_COMPOSE_COMMAND+=" --force-recreate" + if [[ $build_image == true ]]; then + DEFAULT_COMPOSE_COMMAND+=" --build" + fi +fi + +# Recap of environment variables +echo +echo -e "${WHITE}${BOLD}Current Setup:${NC}" +echo -e " ${GREEN}${BOLD}GPU Driver:${NC} ${OLLAMA_GPU_DRIVER:-Not Enabled}" +echo -e " ${GREEN}${BOLD}GPU Count:${NC} ${OLLAMA_GPU_COUNT:-Not Enabled}" +echo -e " ${GREEN}${BOLD}WebAPI Port:${NC} ${OLLAMA_WEBAPI_PORT:-Not Enabled}" +echo -e " ${GREEN}${BOLD}Data Folder:${NC} ${data_dir:-Using ollama volume}" +echo -e " ${GREEN}${BOLD}WebUI Port:${NC} $webui_port" +echo + +if [[ $headless == true ]]; then + echo -ne "${WHITE}${BOLD}Running in headless mode... ${NC}" + choice="y" +else + # Ask for user acceptance + echo -ne "${WHITE}${BOLD}Do you want to proceed with current setup? (Y/n): ${NC}" + read -n1 -s choice +fi + +echo + +if [[ $choice == "" || $choice == "y" ]]; then + # Execute the command with the current user + eval "$DEFAULT_COMPOSE_COMMAND" & + + # Capture the background process PID + PID=$! + + # Display the loading animation + #show_loading $PID + + # Wait for the command to finish + wait $PID + + echo + # Check exit status + if [ $? -eq 0 ]; then + echo -e "${GREEN}${BOLD}Compose project started successfully.${NC}" + else + echo -e "${RED}${BOLD}There was an error starting the compose project.${NC}" + fi +else + echo "Aborted." +fi + +echo diff --git a/run-ollama-docker.sh b/run-ollama-docker.sh new file mode 100644 index 0000000000000000000000000000000000000000..c2a025bea3fa88beab7c0e6640de682dc8169c02 --- /dev/null +++ b/run-ollama-docker.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +host_port=11434 +container_port=11434 + +read -r -p "Do you want ollama in Docker with GPU support? (y/n): " use_gpu + +docker rm -f ollama || true +docker pull ollama/ollama:latest + +docker_args="-d -v ollama:/root/.ollama -p $host_port:$container_port --name ollama ollama/ollama" + +if [ "$use_gpu" = "y" ]; then + docker_args="--gpus=all $docker_args" +fi + +docker run $docker_args + +docker image prune -f \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..6793fe16271c0b56b9ad1cc409d533bfe1e6ca83 --- /dev/null +++ b/run.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +image_name="open-webui" +container_name="open-webui" +host_port=3000 +container_port=8080 + +docker build -t "$image_name" . +docker stop "$container_name" &>/dev/null || true +docker rm "$container_name" &>/dev/null || true + +docker run -d -p "$host_port":"$container_port" \ + --add-host=host.docker.internal:host-gateway \ + -v "${image_name}:/app/backend/data" \ + --name "$container_name" \ + --restart always \ + "$image_name" + +docker image prune -f diff --git a/scripts/prepare-pyodide.js b/scripts/prepare-pyodide.js new file mode 100644 index 0000000000000000000000000000000000000000..5aaac5927e7dbf5674bd3dc4839b9ed1548bf7b4 --- /dev/null +++ b/scripts/prepare-pyodide.js @@ -0,0 +1,85 @@ +const packages = [ + 'micropip', + 'packaging', + 'requests', + 'beautifulsoup4', + 'numpy', + 'pandas', + 'matplotlib', + 'scikit-learn', + 'scipy', + 'regex', + 'seaborn' +]; + +import { loadPyodide } from 'pyodide'; +import { writeFile, readFile, copyFile, readdir, rmdir } from 'fs/promises'; + +async function downloadPackages() { + console.log('Setting up pyodide + micropip'); + + let pyodide; + try { + pyodide = await loadPyodide({ + packageCacheDir: 'static/pyodide' + }); + } catch (err) { + console.error('Failed to load Pyodide:', err); + return; + } + + const packageJson = JSON.parse(await readFile('package.json')); + const pyodideVersion = packageJson.dependencies.pyodide.replace('^', ''); + + try { + const pyodidePackageJson = JSON.parse(await readFile('static/pyodide/package.json')); + const pyodidePackageVersion = pyodidePackageJson.version.replace('^', ''); + + if (pyodideVersion !== pyodidePackageVersion) { + console.log('Pyodide version mismatch, removing static/pyodide directory'); + await rmdir('static/pyodide', { recursive: true }); + } + } catch (e) { + console.log('Pyodide package not found, proceeding with download.'); + } + + try { + console.log('Loading micropip package'); + await pyodide.loadPackage('micropip'); + + const micropip = pyodide.pyimport('micropip'); + console.log('Downloading Pyodide packages:', packages); + + try { + for (const pkg of packages) { + console.log(`Installing package: ${pkg}`); + await micropip.install(pkg); + } + } catch (err) { + console.error('Package installation failed:', err); + return; + } + + console.log('Pyodide packages downloaded, freezing into lock file'); + + try { + const lockFile = await micropip.freeze(); + await writeFile('static/pyodide/pyodide-lock.json', lockFile); + } catch (err) { + console.error('Failed to write lock file:', err); + } + } catch (err) { + console.error('Failed to load or install micropip:', err); + } +} + +async function copyPyodide() { + console.log('Copying Pyodide files into static directory'); + // Copy all files from node_modules/pyodide to static/pyodide + for await (const entry of await readdir('node_modules/pyodide')) { + await copyFile(`node_modules/pyodide/${entry}`, `static/pyodide/${entry}`); + } +} + +await downloadPackages(); +await copyPyodide(); diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000000000000000000000000000000000000..c3388f1d36d692f921c97f1b85d6b3a71c534567 --- /dev/null +++ b/src/app.css @@ -0,0 +1,156 @@ +@font-face { + font-family: 'Inter'; + src: url('/assets/fonts/Inter-Variable.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'Archivo'; + src: url('/assets/fonts/Archivo-Variable.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'Mona Sans'; + src: url('/assets/fonts/Mona-Sans.woff2'); + font-display: swap; +} + +html { + word-break: break-word; +} + +code { + /* white-space-collapse: preserve !important; */ + overflow-x: auto; + width: auto; +} + +math { + margin-top: 1rem; +} + +.hljs { + @apply rounded-lg; +} + +.markdown a { + @apply underline; +} + +.font-primary { + font-family: 'Archivo', sans-serif; +} + +iframe { + @apply rounded-lg; +} + +ol > li { + counter-increment: list-number; + display: block; + margin-bottom: 0; + margin-top: 0; + min-height: 28px; +} + +.prose ol > li::before { + content: counters(list-number, '.') '.'; + padding-right: 0.5rem; + color: var(--tw-prose-counters); + font-weight: 400; +} + +li p { + display: inline; +} + +::-webkit-scrollbar-thumb { + --tw-border-opacity: 1; + background-color: rgba(217, 217, 227, 0.8); + border-color: rgba(255, 255, 255, var(--tw-border-opacity)); + border-radius: 9999px; + border-width: 1px; +} + +::-webkit-scrollbar { + height: 0.4rem; + width: 0.4rem; +} + +::-webkit-scrollbar-track { + background-color: transparent; + border-radius: 9999px; +} + +select { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E"); + background-position: right 0.5rem center; + background-repeat: no-repeat; + background-size: 1.5em 1.5em; + padding-right: 2.5rem; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + /* for Firefox */ + -moz-appearance: none; + /* for Chrome */ + -webkit-appearance: none; +} + +.katex-mathml { + display: none; +} + +.scrollbar-hidden:active::-webkit-scrollbar-thumb, +.scrollbar-hidden:focus::-webkit-scrollbar-thumb, +.scrollbar-hidden:hover::-webkit-scrollbar-thumb { + visibility: visible; +} +.scrollbar-hidden::-webkit-scrollbar-thumb { + visibility: hidden; +} + +.scrollbar-hidden::-webkit-scrollbar-corner { + display: none; +} + +.scrollbar-none::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ +} + +.scrollbar-none::-webkit-scrollbar-corner { + display: none; +} + +.scrollbar-none { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ +} + +input[type='number'] { + -moz-appearance: textfield; /* Firefox */ +} + +.cm-editor { + height: 100%; + width: 100%; +} + +.cm-scroller { + @apply scrollbar-hidden; +} + +.cm-editor.cm-focused { + outline: none; +} + +.tippy-box[data-theme~='dark'] { + @apply rounded-lg bg-gray-950 text-xs border border-gray-900 shadow-xl; +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..f59b884c51ed3c31fc0738fd38d0d75b580df5e4 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000000000000000000000000000000000000..5d48e1d7e8532479cb3cd18f38d7871318b60571 --- /dev/null +++ b/src/app.html @@ -0,0 +1,219 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <link rel="icon" href="%sveltekit.assets%/favicon.png" /> + <link rel="manifest" href="%sveltekit.assets%/manifest.json" crossorigin="use-credentials" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <link + rel="search" + type="application/opensearchdescription+xml" + title="Open WebUI" + href="/opensearch.xml" + /> + + <script> + function resizeIframe(obj) { + obj.style.height = obj.contentWindow.document.documentElement.scrollHeight + 'px'; + } + </script> + + <script> + // On page load or when changing themes, best to add inline in `head` to avoid FOUC + (() => { + if (localStorage?.theme && localStorage?.theme.includes('oled')) { + document.documentElement.style.setProperty('--color-gray-800', '#101010'); + document.documentElement.style.setProperty('--color-gray-850', '#050505'); + document.documentElement.style.setProperty('--color-gray-900', '#000000'); + document.documentElement.style.setProperty('--color-gray-950', '#000000'); + document.documentElement.classList.add('dark'); + } else if ( + localStorage.theme === 'light' || + (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches) + ) { + document.documentElement.classList.add('light'); + } else if (localStorage.theme && localStorage.theme !== 'system') { + localStorage.theme.split(' ').forEach((e) => { + document.documentElement.classList.add(e); + }); + } else if (localStorage.theme && localStorage.theme === 'system') { + systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + document.documentElement.classList.add(systemTheme ? 'dark' : 'light'); + } else if (localStorage.theme && localStorage.theme === 'her') { + document.documentElement.classList.add('dark'); + document.documentElement.classList.add('her'); + } else { + document.documentElement.classList.add('dark'); + } + + window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => { + if (localStorage.theme === 'system') { + if (e.matches) { + document.documentElement.classList.add('dark'); + document.documentElement.classList.remove('light'); + } else { + document.documentElement.classList.add('light'); + document.documentElement.classList.remove('dark'); + } + } + }); + })(); + </script> + + <title>Open WebUI</title> + + %sveltekit.head% + </head> + + <body data-sveltekit-preload-data="hover"> + <div style="display: contents">%sveltekit.body%</div> + + <div + id="splash-screen" + style="position: fixed; z-index: 100; top: 0; left: 0; width: 100%; height: 100%" + > + <style type="text/css" nonce=""> + html { + overflow-y: scroll !important; + } + </style> + + <img + id="logo" + style=" + position: absolute; + width: auto; + height: 6rem; + top: 44%; + left: 50%; + transform: translateX(-50%); + " + src="/static/splash.png" + /> + + <div + style=" + position: absolute; + top: 33%; + left: 50%; + + width: 24rem; + transform: translateX(-50%); + + display: flex; + flex-direction: column; + align-items: center; + " + > + <img + id="logo-her" + style="width: auto; height: 13rem" + src="/static/splash.png" + class="animate-pulse-fast" + /> + + <div style="position: relative; width: 24rem; margin-top: 0.5rem"> + <div + id="progress-background" + style=" + position: absolute; + width: 100%; + height: 0.75rem; + + border-radius: 9999px; + background-color: #fafafa9a; + " + ></div> + + <div + id="progress-bar" + style=" + position: absolute; + width: 0%; + height: 0.75rem; + border-radius: 9999px; + background-color: #fff; + " + class="bg-white" + ></div> + </div> + </div> + + <!-- <span style="position: absolute; bottom: 32px; left: 50%; margin: -36px 0 0 -36px"> + Footer content + </span> --> + </div> + </body> +</html> + +<style type="text/css" nonce=""> + html { + overflow-y: hidden !important; + } + + #splash-screen { + background: #fff; + } + + html.dark #splash-screen { + background: #000; + } + + html.dark #splash-screen img { + filter: invert(1); + } + + html.her #splash-screen { + background: #983724; + } + + #logo-her { + display: none; + } + + #progress-background { + display: none; + } + + #progress-bar { + display: none; + } + + html.her #logo { + display: none; + } + + html.her #logo-her { + display: block; + filter: invert(1); + } + + html.her #progress-background { + display: block; + } + + html.her #progress-bar { + display: block; + } + + @media (max-width: 24rem) { + html.her #progress-background { + display: none; + } + + html.her #progress-bar { + display: none; + } + } + + @keyframes pulse { + 50% { + opacity: 0.65; + } + } + + .animate-pulse-fast { + animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; + } +</style> diff --git a/src/lib/apis/audio/index.ts b/src/lib/apis/audio/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9716c552a76a472185bb06f49097704ac9424e61 --- /dev/null +++ b/src/lib/apis/audio/index.ts @@ -0,0 +1,133 @@ +import { AUDIO_API_BASE_URL } from '$lib/constants'; + +export const getAudioConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OpenAIConfigForm = { + url: string; + key: string; + model: string; + speaker: string; +}; + +export const updateAudioConfig = async (token: string, payload: OpenAIConfigForm) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const transcribeAudio = async (token: string, file: File) => { + const data = new FormData(); + data.append('file', file); + + let error = null; + const res = await fetch(`${AUDIO_API_BASE_URL}/transcriptions`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + }, + body: data + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const synthesizeOpenAISpeech = async ( + token: string = '', + speaker: string = 'alloy', + text: string = '', + model?: string +) => { + let error = null; + + const res = await fetch(`${AUDIO_API_BASE_URL}/speech`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + input: text, + voice: speaker, + ...(model && { model }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res; + }) + .catch((err) => { + error = err.detail; + console.log(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/auths/index.ts b/src/lib/apis/auths/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1bdb74694447514c724257c2c7ac6860d79e05e7 --- /dev/null +++ b/src/lib/apis/auths/index.ts @@ -0,0 +1,525 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getAdminDetails = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/details`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAdminConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateAdminConfig = async (token: string, body: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/admin/config`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSessionUser = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + credentials: 'include' + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const userSignIn = async (email: string, password: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signin`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify({ + email: email, + password: password + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +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`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify({ + name: name, + email: email, + password: password, + profile_image_url: profile_image_url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addUser = async ( + token: string, + name: string, + email: string, + password: string, + role: string = 'pending' +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/add`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: name, + email: email, + password: password, + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserProfile = async (token: string, name: string, profileImageUrl: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/profile`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: name, + profile_image_url: profileImageUrl + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserPassword = async (token: string, password: string, newPassword: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/update/password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + password: password, + new_password: newPassword + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getSignUpEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/enabled`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDefaultUserRole = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateDefaultUserRole = async (token: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/user/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleSignUpEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/signup/enabled/toggle`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getJWTExpiresDuration = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateJWTExpiresDuration = async (token: string, duration: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/token/expires/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + duration: duration + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const createAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res.api_key; +}; + +export const getAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res.api_key; +}; + +export const deleteAPIKey = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/auths/api_key`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + if (error) { + throw error; + } + return res; +}; diff --git a/src/lib/apis/chats/index.ts b/src/lib/apis/chats/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b046f1b10d5300fdd2babf12afde528c9ac9f4ed --- /dev/null +++ b/src/lib/apis/chats/index.ts @@ -0,0 +1,757 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getTimeRange } from '$lib/utils'; + +export const createNewChat = async (token: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/new`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatListByUserId = async (token: string = '', userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/list/user/${userId}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getArchivedChatList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archived`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllArchivedChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/archived`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllUserChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/all/db`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAllChatTags = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags/all`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatListByTagName = async (token: string = '', tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/tags`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + name: tagName + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res.map((chat) => ({ + ...chat, + time_range: getTimeRange(chat.updated_at) + })); +}; + +export const getChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChatByShareId = async (token: string, share_id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/share/${share_id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const cloneChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/clone`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const shareChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const archiveChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/archive`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteSharedChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/share`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateChatById = async (token: string, id: string, chat: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + chat: chat + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteChatById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + tag_name: tagName, + chat_id: id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteTagById = async (token: string, id: string, tagName: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + tag_name: tagName, + chat_id: id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; +export const deleteTagsById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/${id}/tags/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const archiveAllChats = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/chats/archive/all`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/configs/index.ts b/src/lib/apis/configs/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..4f53c53c897ebd48cc117d234dbbb80fa5c99735 --- /dev/null +++ b/src/lib/apis/configs/index.ts @@ -0,0 +1,119 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import type { Banner } from '$lib/types'; + +export const setDefaultModels = async (token: string, models: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/models`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + models: models + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setDefaultPromptSuggestions = async (token: string, promptSuggestions: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/default/suggestions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + suggestions: promptSuggestions + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getBanners = async (token: string): Promise<Banner[]> => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const setBanners = async (token: string, banners: Banner[]) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/configs/banners`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + banners: banners + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/documents/index.ts b/src/lib/apis/documents/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d42feb19f6375985278e866ef6ed9f7138223ef --- /dev/null +++ b/src/lib/apis/documents/index.ts @@ -0,0 +1,232 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewDoc = async ( + token: string, + collection_name: string, + filename: string, + name: string, + title: string, + content: object | null = null +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_name: collection_name, + filename: filename, + name: name, + title: title, + ...(content ? { content: JSON.stringify(content) } : {}) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDocs = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDocByName = async (token: string, name: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('name', name); + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/docs?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type DocUpdateForm = { + name: string; + title: string; +}; + +export const updateDocByName = async (token: string, name: string, form: DocUpdateForm) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('name', name); + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/update?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: form.name, + title: form.title + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type TagDocForm = { + name: string; + tags: string[]; +}; + +export const tagDocByName = async (token: string, name: string, form: TagDocForm) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('name', name); + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/tags?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: form.name, + tags: form.tags + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteDocByName = async (token: string, name: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('name', name); + + const res = await fetch(`${WEBUI_API_BASE_URL}/documents/doc/delete?${searchParams.toString()}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..630a9e7c55675e5c27a979564719bd0fc3c3d5c7 --- /dev/null +++ b/src/lib/apis/files/index.ts @@ -0,0 +1,183 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const uploadFile = async (token: string, file: File) => { + const data = new FormData(); + data.append('file', file); + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + }, + body: data + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFiles = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFileById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFileContentById = async (id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}/content`, { + method: 'GET', + headers: { + Accept: 'application/json' + }, + credentials: 'include' + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return await res.blob(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFileById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteAllFiles = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/all`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/functions/index.ts b/src/lib/apis/functions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ed3306b3214967e98dee60ab0cf14042bc0bf141 --- /dev/null +++ b/src/lib/apis/functions/index.ts @@ -0,0 +1,455 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewFunction = async (token: string, func: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...func + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctions = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportFunctions = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/export`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFunctionById = async (token: string, id: string, func: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...func + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleFunctionById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/toggle`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleGlobalById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/toggle/global`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getFunctionValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateFunctionValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/functions/id/${id}/valves/user/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/images/index.ts b/src/lib/apis/images/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f624704eb4767683f3be50217ac54fa34e880fb --- /dev/null +++ b/src/lib/apis/images/index.ts @@ -0,0 +1,474 @@ +import { IMAGES_API_BASE_URL } from '$lib/constants'; + +export const getImageGenerationConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateImageGenerationConfig = async ( + token: string = '', + engine: string, + enabled: boolean +) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + engine, + enabled + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateOpenAIConfig = async (token: string = '', url: string, key: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/openai/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url, + key: key + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getImageGenerationEngineUrls = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateImageGenerationEngineUrls = async (token: string = '', urls: object = {}) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/url/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + ...urls + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getImageSize = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/size`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_SIZE; +}; + +export const updateImageSize = async (token: string = '', size: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/size/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + size: size + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_SIZE; +}; + +export const getImageSteps = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/steps`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_STEPS; +}; + +export const updateImageSteps = async (token: string = '', steps: number) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/steps/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ steps }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.IMAGE_STEPS; +}; + +export const getImageGenerationModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getDefaultImageGenerationModel = async (token: string = '') => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const updateDefaultImageGenerationModel = async (token: string = '', model: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/models/default/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + model: model + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.model; +}; + +export const imageGenerations = async (token: string = '', prompt: string) => { + let error = null; + + const res = await fetch(`${IMAGES_API_BASE_URL}/generations`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + prompt: prompt + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/index.ts b/src/lib/apis/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2e90855b677ab488cefa7d8e4812e20878ee6c1 --- /dev/null +++ b/src/lib/apis/index.ts @@ -0,0 +1,949 @@ +import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + +export const getModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + let models = res?.data ?? []; + + models = models + .filter((models) => models) + // Sort the models + .sort((a, b) => { + // Check if models have position property + const aHasPosition = a.info?.meta?.position !== undefined; + const bHasPosition = b.info?.meta?.position !== undefined; + + // If both a and b have the position property + if (aHasPosition && bHasPosition) { + return a.info.meta.position - b.info.meta.position; + } + + // If only a has the position property, it should come first + if (aHasPosition) return -1; + + // If only b has the position property, it should come first + if (bHasPosition) return 1; + + // Compare case-insensitively by name for models without position property + const lowerA = a.name.toLowerCase(); + const lowerB = b.name.toLowerCase(); + + if (lowerA < lowerB) return -1; + if (lowerA > lowerB) return 1; + + // If same case-insensitively, sort by original strings, + // lowercase will come before uppercase due to ASCII values + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + + return 0; // They are equal + }); + + console.log(models); + return models; +}; + +type ChatCompletedForm = { + model: string; + messages: string[]; + chat_id: string; +}; + +export const chatCompleted = async (token: string, body: ChatCompletedForm) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/chat/completed`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type ChatActionForm = { + model: string; + messages: string[]; + chat_id: string; +}; + +export const chatAction = async (token: string, action_id: string, body: ChatActionForm) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/chat/actions/${action_id}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(body) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTaskConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/task/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateTaskConfig = async (token: string, config: object) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/task/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(config) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTitle = async ( + token: string = '', + model: string, + prompt: string, + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/task/title/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat'; +}; + +export const generateEmoji = async ( + token: string = '', + model: string, + prompt: string, + chat_id?: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/task/emoji/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: prompt, + ...(chat_id && { chat_id: chat_id }) + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + const response = res?.choices[0]?.message?.content.replace(/["']/g, '') ?? null; + + if (response) { + if (/\p{Extended_Pictographic}/u.test(response)) { + return response.match(/\p{Extended_Pictographic}/gu)[0]; + } + } + + return null; +}; + +export const generateSearchQuery = async ( + token: string = '', + model: string, + messages: object[], + prompt: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/task/query/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: messages, + prompt: prompt + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? prompt; +}; + +export const getPipelinesList = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/list`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + let pipelines = res?.data ?? []; + return pipelines; +}; + +export const uploadPipeline = async (token: string, file: File, urlIdx: string) => { + let error = null; + + // Create a new FormData object to handle the file upload + const formData = new FormData(); + formData.append('file', file); + formData.append('urlIdx', urlIdx); + + const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/upload`, { + method: 'POST', + headers: { + ...(token && { authorization: `Bearer ${token}` }) + // 'Content-Type': 'multipart/form-data' is not needed as Fetch API will set it automatically + }, + body: formData + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const downloadPipeline = async (token: string, url: string, urlIdx: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + url: url, + urlIdx: urlIdx + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePipeline = async (token: string, id: string, urlIdx: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + id: id, + urlIdx: urlIdx + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPipelines = async (token: string, urlIdx?: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch(`${WEBUI_BASE_URL}/api/pipelines?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + let pipelines = res?.data ?? []; + return pipelines; +}; + +export const getPipelineValves = async (token: string, pipeline_id: string, urlIdx: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/pipelines/${pipeline_id}/valves?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPipelineValvesSpec = async (token: string, pipeline_id: string, urlIdx: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/pipelines/${pipeline_id}/valves/spec?${searchParams.toString()}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePipelineValves = async ( + token: string = '', + pipeline_id: string, + valves: object, + urlIdx: string +) => { + let error = null; + + const searchParams = new URLSearchParams(); + if (urlIdx !== undefined) { + searchParams.append('urlIdx', urlIdx); + } + + const res = await fetch( + `${WEBUI_BASE_URL}/api/pipelines/${pipeline_id}/valves/update?${searchParams.toString()}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify(valves) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + + if ('detail' in err) { + error = err.detail; + } else { + error = err; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getBackendConfig = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getChangelog = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/changelog`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getVersionUpdates = async () => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/version/updates`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelFilterConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelFilterConfig = async ( + token: string, + enabled: boolean, + models: string[] +) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/model/filter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + enabled: enabled, + models: models + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getWebhookUrl = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const updateWebhookUrl = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/webhook`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.url; +}; + +export const getCommunitySharingEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const toggleCommunitySharingEnabledStatus = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/community_sharing/toggle`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelConfig = async (token: string): Promise<GlobalModelConfig> => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res.models; +}; + +export interface ModelConfig { + id: string; + name: string; + meta: ModelMeta; + base_model_id?: string; + params: ModelParams; +} + +export interface ModelMeta { + description?: string; + capabilities?: object; +} + +export interface ModelParams {} + +export type GlobalModelConfig = ModelConfig[]; + +export const updateModelConfig = async (token: string, config: GlobalModelConfig) => { + let error = null; + + const res = await fetch(`${WEBUI_BASE_URL}/api/config/models`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + models: config + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/memories/index.ts b/src/lib/apis/memories/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c3c122adf8be662aa20d2914b067b5675dc1ecd9 --- /dev/null +++ b/src/lib/apis/memories/index.ts @@ -0,0 +1,186 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getMemories = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const addNewMemory = async (token: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateMemoryById = async (token: string, id: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryMemory = async (token: string, content: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/query`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteMemoryById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteMemoriesByUserId = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/memories/user`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/models/index.ts b/src/lib/apis/models/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9faa358d33100d3193b1a9200f361fd835d30098 --- /dev/null +++ b/src/lib/apis/models/index.ts @@ -0,0 +1,167 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const addNewModel = async (token: string, model: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/add`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(model) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelInfos = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/models`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getModelById = async (token: string, id: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('id', id); + + const res = await fetch(`${WEBUI_API_BASE_URL}/models?${searchParams.toString()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateModelById = async (token: string, id: string, model: object) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('id', id); + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/update?${searchParams.toString()}`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify(model) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModelById = async (token: string, id: string) => { + let error = null; + + const searchParams = new URLSearchParams(); + searchParams.append('id', id); + + const res = await fetch(`${WEBUI_API_BASE_URL}/models/delete?${searchParams.toString()}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/ollama/index.ts b/src/lib/apis/ollama/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..084d2d5f18a54dc11d73da5beba5c2dd93f700c5 --- /dev/null +++ b/src/lib/apis/ollama/index.ts @@ -0,0 +1,558 @@ +import { OLLAMA_API_BASE_URL } from '$lib/constants'; +import { titleGenerationTemplate } from '$lib/utils'; + +export const getOllamaConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateOllamaConfig = async (token: string = '', enable_ollama_api: boolean) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + enable_ollama_api: enable_ollama_api + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOllamaUrls = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OLLAMA_BASE_URLS; +}; + +export const updateOllamaUrls = async (token: string = '', urls: string[]) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/urls/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + urls: urls + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OLLAMA_BASE_URLS; +}; + +export const getOllamaVersion = async (token: string, urlIdx?: number) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/version${urlIdx ? `/${urlIdx}` : ''}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.version ?? false; +}; + +export const getOllamaModels = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/tags`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return (res?.models ?? []) + .map((model) => ({ id: model.model, name: model.name ?? model.model, ...model })) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }); +}; + +// TODO: migrate to backend +export const generateTitle = async ( + token: string = '', + template: string, + model: string, + prompt: string +) => { + let error = null; + + template = titleGenerationTemplate(template, prompt); + + console.log(template); + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: template, + stream: false, + options: { + // Restrict the number of tokens generated to 50 + num_predict: 50 + } + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.response.replace(/["']/g, '') ?? 'New Chat'; +}; + +export const generatePrompt = async (token: string = '', model: string, conversation: string) => { + let error = null; + + if (conversation === '') { + conversation = '[no existing conversation]'; + } + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: `Conversation: + ${conversation} + + As USER in the conversation above, your task is to continue the conversation. Remember, Your responses should be crafted as if you're a human conversing in a natural, realistic manner, keeping in mind the context and flow of the dialogue. Please generate a fitting response to the last message in the conversation, or if there is no existing conversation, initiate one as a normal person would. + + Response: + ` + }) + }).catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateEmbeddings = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/embeddings`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTextCompletion = async (token: string = '', model: string, text: string) => { + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/generate`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + prompt: text, + stream: true + }) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateChatCompletion = async (token: string = '', body: object) => { + let controller = new AbortController(); + let error = null; + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/chat`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(body) + }).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; + +export const createModel = async ( + token: string, + tagName: string, + content: string, + urlIdx: string | null = null +) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/api/create${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName, + modelfile: content + }) + } + ).catch((err) => { + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteModel = async (token: string, tagName: string, urlIdx: string | null = null) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/api/delete${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + console.log(json); + return true; + }) + .catch((err) => { + console.log(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const pullModel = async (token: string, tagName: string, urlIdx: string | null = null) => { + let error = null; + const controller = new AbortController(); + + const res = await fetch(`${OLLAMA_API_BASE_URL}/api/pull${urlIdx !== null ? `/${urlIdx}` : ''}`, { + signal: controller.signal, + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + name: tagName + }) + }).catch((err) => { + console.log(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return [res, controller]; +}; + +export const downloadModel = async ( + token: string, + download_url: string, + urlIdx: string | null = null +) => { + let error = null; + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/models/download${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: download_url + }) + } + ).catch((err) => { + console.log(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return res; +}; + +export const uploadModel = async (token: string, file: File, urlIdx: string | null = null) => { + let error = null; + + const formData = new FormData(); + formData.append('file', file); + + const res = await fetch( + `${OLLAMA_API_BASE_URL}/models/upload${urlIdx !== null ? `/${urlIdx}` : ''}`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + }, + body: formData + } + ).catch((err) => { + console.log(err); + error = err; + + if ('detail' in err) { + error = err.detail; + } + + return null; + }); + if (error) { + throw error; + } + return res; +}; + +// export const pullModel = async (token: string, tagName: string) => { +// return await fetch(`${OLLAMA_API_BASE_URL}/pull`, { +// method: 'POST', +// headers: { +// 'Content-Type': 'text/event-stream', +// Authorization: `Bearer ${token}` +// }, +// body: JSON.stringify({ +// name: tagName +// }) +// }); +// }; diff --git a/src/lib/apis/openai/index.ts b/src/lib/apis/openai/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2a52ebb3209ef80577732718646c4fe17759ef72 --- /dev/null +++ b/src/lib/apis/openai/index.ts @@ -0,0 +1,455 @@ +import { OPENAI_API_BASE_URL } from '$lib/constants'; +import { titleGenerationTemplate } from '$lib/utils'; +import { type Model, models, settings } from '$lib/stores'; + +export const getOpenAIConfig = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/config`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateOpenAIConfig = async (token: string = '', enable_openai_api: boolean) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + enable_openai_api: enable_openai_api + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIUrls = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/urls`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_BASE_URLS; +}; + +export const updateOpenAIUrls = async (token: string = '', urls: string[]) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/urls/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + urls: urls + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_BASE_URLS; +}; + +export const getOpenAIKeys = async (token: string = '') => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/keys`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_KEYS; +}; + +export const updateOpenAIKeys = async (token: string = '', keys: string[]) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/keys/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + }, + body: JSON.stringify({ + keys: keys + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } else { + error = 'Server connection failed'; + } + return null; + }); + + if (error) { + throw error; + } + + return res.OPENAI_API_KEYS; +}; + +export const getOpenAIModels = async (token: string, urlIdx?: number) => { + let error = null; + + const res = await fetch( + `${OPENAI_API_BASE_URL}/models${typeof urlIdx === 'number' ? `/${urlIdx}` : ''}`, + { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...(token && { authorization: `Bearer ${token}` }) + } + } + ) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return []; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getOpenAIModelsDirect = async ( + base_url: string = 'https://api.openai.com/v1', + api_key: string = '' +) => { + let error = null; + + const res = await fetch(`${base_url}/models`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${api_key}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = `OpenAI: ${err?.error?.message ?? 'Network Problem'}`; + return null; + }); + + if (error) { + throw error; + } + + const models = Array.isArray(res) ? res : res?.data ?? null; + + return models + .map((model) => ({ id: model.id, name: model.name ?? model.id, external: true })) + .filter((model) => (base_url.includes('openai') ? model.name.includes('gpt') : true)) + .sort((a, b) => { + return a.name.localeCompare(b.name); + }); +}; + +export const generateOpenAIChatCompletion = async ( + token: string = '', + body: object, + url: string = OPENAI_API_BASE_URL +): Promise<[Response | null, AbortController]> => { + const controller = new AbortController(); + let error = null; + + const res = await fetch(`${url}/chat/completions`, { + signal: controller.signal, + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }).catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return [res, controller]; +}; + +export const synthesizeOpenAISpeech = async ( + token: string = '', + speaker: string = 'alloy', + text: string = '', + model: string = 'tts-1' +) => { + let error = null; + + const res = await fetch(`${OPENAI_API_BASE_URL}/audio/speech`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: model, + input: text, + voice: speaker + }) + }).catch((err) => { + console.log(err); + error = err; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const generateTitle = async ( + token: string = '', + template: string, + model: string, + prompt: string, + chat_id?: string, + url: string = OPENAI_API_BASE_URL +) => { + let error = null; + + template = titleGenerationTemplate(template, prompt); + + console.log(template); + + const res = await fetch(`${url}/chat/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + messages: [ + { + role: 'user', + content: template + } + ], + stream: false, + // Restricting the max tokens to 50 to avoid long titles + max_tokens: 50, + ...(chat_id && { chat_id: chat_id }), + title: true + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? 'New Chat'; +}; + +export const generateSearchQuery = async ( + token: string = '', + model: string, + previousMessages: string[], + prompt: string, + url: string = OPENAI_API_BASE_URL +): Promise<string | undefined> => { + let error = null; + + // TODO: Allow users to specify the prompt + // Get the current date in the format "January 20, 2024" + const currentDate = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: '2-digit' + }).format(new Date()); + + const res = await fetch(`${url}/chat/completions`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + model: model, + // Few shot prompting + messages: [ + { + role: 'assistant', + content: `You are tasked with generating web search queries. Give me an appropriate query to answer my question for google search. Answer with only the query. Today is ${currentDate}.` + }, + { + role: 'user', + content: prompt + } + // { + // role: 'user', + // content: + // (previousMessages.length > 0 + // ? `Previous Questions:\n${previousMessages.join('\n')}\n\n` + // : '') + `Current Question: ${prompt}` + // } + ], + stream: false, + // Restricting the max tokens to 30 to avoid long search queries + max_tokens: 30 + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + if ('detail' in err) { + error = err.detail; + } + return undefined; + }); + + if (error) { + throw error; + } + + return res?.choices[0]?.message?.content.replace(/["']/g, '') ?? undefined; +}; diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ca9c7d543d7815340a667b74dc1b258f3ff496f8 --- /dev/null +++ b/src/lib/apis/prompts/index.ts @@ -0,0 +1,178 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewPrompt = async ( + token: string, + command: string, + title: string, + content: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + command: `/${command}`, + title: title, + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPrompts = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getPromptByCommand = async (token: string, command: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updatePromptByCommand = async ( + token: string, + command: string, + title: string, + content: string +) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + command: `/${command}`, + title: title, + content: content + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deletePromptByCommand = async (token: string, command: string) => { + let error = null; + + command = command.charAt(0) === '/' ? command.slice(1) : command; + + const res = await fetch(`${WEBUI_API_BASE_URL}/prompts/command/${command}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/rag/index.ts b/src/lib/apis/rag/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..5c0a47b35732da9d3378372b979ebcd0c289454d --- /dev/null +++ b/src/lib/apis/rag/index.ts @@ -0,0 +1,620 @@ +import { RAG_API_BASE_URL } from '$lib/constants'; + +export const getRAGConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type ChunkConfigForm = { + chunk_size: number; + chunk_overlap: number; +}; + +type ContentExtractConfigForm = { + engine: string; + tika_server_url: string | null; +}; + +type YoutubeConfigForm = { + language: string[]; + translation?: string | null; +}; + +type RAGConfigForm = { + pdf_extract_images?: boolean; + chunk?: ChunkConfigForm; + content_extraction?: ContentExtractConfigForm; + web_loader_ssl_verification?: boolean; + youtube?: YoutubeConfigForm; +}; + +export const updateRAGConfig = async (token: string, payload: RAGConfigForm) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/config/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getRAGTemplate = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/template`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res?.template ?? ''; +}; + +export const getQuerySettings = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type QuerySettings = { + k: number | null; + r: number | null; + template: string | null; +}; + +export const updateQuerySettings = async (token: string, settings: QuerySettings) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/settings/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...settings + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const processDocToVectorDB = async (token: string, file_id: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/process/doc`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + file_id: file_id + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const uploadDocToVectorDB = async (token: string, collection_name: string, file: File) => { + const data = new FormData(); + data.append('file', file); + data.append('collection_name', collection_name); + + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/doc`, { + method: 'POST', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + }, + body: data + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const uploadWebToVectorDB = async (token: string, collection_name: string, url: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/web`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url, + collection_name: collection_name + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const uploadYoutubeTranscriptionToVectorDB = async (token: string, url: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/youtube`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + url: url + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryDoc = async ( + token: string, + collection_name: string, + query: string, + k: number | null = null +) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/doc`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_name: collection_name, + query: query, + k: k + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const queryCollection = async ( + token: string, + collection_names: string, + query: string, + k: number | null = null +) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/query/collection`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + collection_names: collection_names, + query: query, + k: k + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const scanDocs = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/scan`, { + method: 'GET', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const resetUploadDir = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/reset/uploads`, { + method: 'GET', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const resetVectorDB = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/reset/db`, { + method: 'GET', + headers: { + Accept: 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getEmbeddingConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/embedding`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type OpenAIConfigForm = { + key: string; + url: string; + batch_size: number; +}; + +type EmbeddingModelUpdateForm = { + openai_config?: OpenAIConfigForm; + embedding_engine: string; + embedding_model: string; +}; + +export const updateEmbeddingConfig = async (token: string, payload: EmbeddingModelUpdateForm) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/embedding/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getRerankingConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/reranking`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type RerankingModelUpdateForm = { + reranking_model: string; +}; + +export const updateRerankingConfig = async (token: string, payload: RerankingModelUpdateForm) => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/reranking/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...payload + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const runWebSearch = async ( + token: string, + query: string, + collection_name?: string +): Promise<SearchDocument | null> => { + let error = null; + + const res = await fetch(`${RAG_API_BASE_URL}/web/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + query, + collection_name: collection_name ?? '' + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export interface SearchDocument { + status: boolean; + collection_name: string; + filenames: string[]; +} diff --git a/src/lib/apis/streaming/index.ts b/src/lib/apis/streaming/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f91edad83cf9afffd371290a2a32722b1a626c30 --- /dev/null +++ b/src/lib/apis/streaming/index.ts @@ -0,0 +1,116 @@ +import { EventSourceParserStream } from 'eventsource-parser/stream'; +import type { ParsedEvent } from 'eventsource-parser'; + +type TextStreamUpdate = { + done: boolean; + value: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + citations?: any; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error?: any; + usage?: ResponseUsage; +}; + +type ResponseUsage = { + /** Including images and tools if any */ + prompt_tokens: number; + /** The tokens generated */ + completion_tokens: number; + /** Sum of the above two fields */ + total_tokens: number; +}; + +// createOpenAITextStream takes a responseBody with a SSE response, +// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks +export async function createOpenAITextStream( + responseBody: ReadableStream<Uint8Array>, + splitLargeDeltas: boolean +): Promise<AsyncGenerator<TextStreamUpdate>> { + const eventStream = responseBody + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .getReader(); + let iterator = openAIStreamToIterator(eventStream); + if (splitLargeDeltas) { + iterator = streamLargeDeltasAsRandomChunks(iterator); + } + return iterator; +} + +async function* openAIStreamToIterator( + reader: ReadableStreamDefaultReader<ParsedEvent> +): AsyncGenerator<TextStreamUpdate> { + while (true) { + const { value, done } = await reader.read(); + if (done) { + yield { done: true, value: '' }; + break; + } + if (!value) { + continue; + } + const data = value.data; + if (data.startsWith('[DONE]')) { + yield { done: true, value: '' }; + break; + } + + try { + const parsedData = JSON.parse(data); + console.log(parsedData); + + if (parsedData.error) { + yield { done: true, value: '', error: parsedData.error }; + break; + } + + if (parsedData.citations) { + yield { done: false, value: '', citations: parsedData.citations }; + continue; + } + + yield { + done: false, + value: parsedData.choices?.[0]?.delta?.content ?? '', + usage: parsedData.usage + }; + } catch (e) { + console.error('Error extracting delta from SSE event:', e); + } + } +} + +// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters +// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once +async function* streamLargeDeltasAsRandomChunks( + iterator: AsyncGenerator<TextStreamUpdate> +): AsyncGenerator<TextStreamUpdate> { + for await (const textStreamUpdate of iterator) { + if (textStreamUpdate.done) { + yield textStreamUpdate; + return; + } + if (textStreamUpdate.citations) { + yield textStreamUpdate; + continue; + } + let content = textStreamUpdate.value; + if (content.length < 5) { + yield { done: false, value: content }; + continue; + } + while (content != '') { + const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length); + const chunk = content.slice(0, chunkSize); + yield { done: false, value: chunk }; + // Do not sleep if the tab is hidden + // Timers are throttled to 1s in hidden tabs + if (document?.visibilityState !== 'hidden') { + await sleep(5); + } + content = content.slice(chunkSize); + } + } +} + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/src/lib/apis/tools/index.ts b/src/lib/apis/tools/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..28e8dde86cff79fd0b1b496a4992f22d366c39f5 --- /dev/null +++ b/src/lib/apis/tools/index.ts @@ -0,0 +1,391 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const createNewTool = async (token: string, tool: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/create`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...tool + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getTools = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const exportTools = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/export`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateToolById = async (token: string, id: string, tool: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...tool + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const deleteToolById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/delete`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getToolValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateToolValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserValvesSpecById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user/spec`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserValvesById = async (token: string, id: string, valves: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/tools/id/${id}/valves/user/update`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...valves + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .then((json) => { + return json; + }) + .catch((err) => { + error = err.detail; + + console.log(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0b22b7171569eafff1b67a664efb6cfffe2c0968 --- /dev/null +++ b/src/lib/apis/users/index.ts @@ -0,0 +1,336 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; +import { getUserPosition } from '$lib/utils'; + +export const getUserPermissions = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserPermissions = async (token: string, permissions: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/permissions/user`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...permissions + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserRole = async (token: string, id: string, role: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/update/role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + id: id, + role: role + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUsers = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res ? res : []; +}; + +export const getUserSettings = async (token: string) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserSettings = async (token: string, settings: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/settings/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...settings + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getUserInfo = async (token: string) => { + let error = null; + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const updateUserInfo = async (token: string, info: object) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/user/info/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + ...info + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +export const getAndUpdateUserLocation = async (token: string) => { + const location = await getUserPosition().catch((err) => { + throw err; + }); + + if (location) { + await updateUserInfo(token, { location: location }); + return location; + } else { + throw new Error('Failed to get user location'); + } +}; + +export const deleteUserById = async (token: string, userId: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + +type UserUpdateForm = { + profile_image_url: string; + email: string; + name: string; + password: string; +}; + +export const updateUserById = async (token: string, userId: string, user: UserUpdateForm) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + profile_image_url: user.profile_image_url, + email: user.email, + name: user.name, + password: user.password !== '' ? user.password : undefined + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/apis/utils/index.ts b/src/lib/apis/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..f99ac564106ec83079097e1a6f4acc9a558f6080 --- /dev/null +++ b/src/lib/apis/utils/index.ts @@ -0,0 +1,179 @@ +import { WEBUI_API_BASE_URL } from '$lib/constants'; + +export const getGravatarUrl = async (email: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/gravatar?email=${email}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + return res; +}; + +export const formatPythonCode = async (code: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/code/format`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + code: code + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + + error = err; + if (err.detail) { + error = err.detail; + } + return null; + }); + + if (error) { + throw error; + } + + 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; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/markdown`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + md: md + }) + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + console.log(err); + error = err; + return null; + }); + + return res.html; +}; + +export const downloadDatabase = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/db/download`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (response) => { + if (!response.ok) { + throw await response.json(); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'webui.db'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } +}; + +export const downloadLiteLLMConfig = async (token: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/utils/litellm/config`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}` + } + }) + .then(async (response) => { + if (!response.ok) { + throw await response.json(); + } + return response.blob(); + }) + .then((blob) => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'config.yaml'; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + }) + .catch((err) => { + console.log(err); + error = err.detail; + return null; + }); + + if (error) { + throw error; + } +}; diff --git a/src/lib/components/AddFilesPlaceholder.svelte b/src/lib/components/AddFilesPlaceholder.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3bdbe9281a07d19b03752397e4be2bb200fb90ff --- /dev/null +++ b/src/lib/components/AddFilesPlaceholder.svelte @@ -0,0 +1,13 @@ +<script> + import { getContext } from 'svelte'; + const i18n = getContext('i18n'); +</script> + +<div class=" text-center text-6xl mb-3">📄</div> +<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div> + +<slot + ><div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> + {$i18n.t('Drop any files here to add to the conversation')} + </div> +</slot> diff --git a/src/lib/components/ChangelogModal.svelte b/src/lib/components/ChangelogModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48156f92473315ed60d1b814ef73dc21ee95491d --- /dev/null +++ b/src/lib/components/ChangelogModal.svelte @@ -0,0 +1,117 @@ +<script lang="ts"> + import { onMount, getContext } from 'svelte'; + import { Confetti } from 'svelte-confetti'; + + import { WEBUI_NAME, config } from '$lib/stores'; + + import { WEBUI_VERSION } from '$lib/constants'; + import { getChangelog } from '$lib/apis'; + + import Modal from './common/Modal.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + + let changelog = null; + + onMount(async () => { + const res = await getChangelog(); + changelog = res; + }); +</script> + +<Modal bind:show> + <div class="px-5 pt-4 dark:text-gray-300 text-gray-700"> + <div class="flex justify-between items-start"> + <div class="text-xl font-semibold"> + {$i18n.t('What’s New in')} + {$WEBUI_NAME} + <Confetti x={[-1, -0.25]} y={[0, 0.5]} /> + </div> + <button + class="self-center" + on:click={() => { + localStorage.version = $config.version; + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + <div class="flex items-center mt-1"> + <div class="text-sm dark:text-gray-200">{$i18n.t('Release Notes')}</div> + <div class="flex self-center w-[1px] h-6 mx-2.5 bg-gray-200 dark:bg-gray-700" /> + <div class="text-sm dark:text-gray-200"> + v{WEBUI_VERSION} + </div> + </div> + </div> + + <div class=" w-full p-4 px-5 text-gray-700 dark:text-gray-100"> + <div class=" overflow-y-scroll max-h-80 scrollbar-hidden"> + <div class="mb-3"> + {#if changelog} + {#each Object.keys(changelog) as version} + <div class=" mb-3 pr-2"> + <div class="font-semibold text-xl mb-1 dark:text-white"> + v{version} - {changelog[version].date} + </div> + + <hr class=" dark:border-gray-800 my-2" /> + + {#each Object.keys(changelog[version]).filter((section) => section !== 'date') as section} + <div class=""> + <div + class="font-semibold uppercase text-xs {section === 'added' + ? 'text-white bg-blue-600' + : section === 'fixed' + ? 'text-white bg-green-600' + : section === 'changed' + ? 'text-white bg-yellow-600' + : section === 'removed' + ? 'text-white bg-red-600' + : ''} w-fit px-3 rounded-full my-2.5" + > + {section} + </div> + + <div class="my-2.5 px-1.5"> + {#each Object.keys(changelog[version][section]) as item} + <div class="text-sm mb-2"> + <div class="font-semibold uppercase"> + {changelog[version][section][item].title} + </div> + <div class="mb-2 mt-1">{changelog[version][section][item].content}</div> + </div> + {/each} + </div> + </div> + {/each} + </div> + {/each} + {/if} + </div> + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + on:click={() => { + localStorage.version = $config.version; + show = false; + }} + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + > + <span class="relative">{$i18n.t("Okay, Let's Go!")}</span> + </button> + </div> + </div> +</Modal> diff --git a/src/lib/components/admin/AddUserModal.svelte b/src/lib/components/admin/AddUserModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8538ba04a8a0e52b9d822403f9b7e92029ff1009 --- /dev/null +++ b/src/lib/components/admin/AddUserModal.svelte @@ -0,0 +1,337 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { createEventDispatcher } from 'svelte'; + import { onMount, getContext } from 'svelte'; + import { addUser } from '$lib/apis/auths'; + + import Modal from '../common/Modal.svelte'; + import { WEBUI_BASE_URL } from '$lib/constants'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let show = false; + + let loading = false; + let tab = ''; + let inputFiles; + + let _user = { + name: '', + email: '', + password: '', + role: 'user' + }; + + $: if (show) { + _user = { + name: '', + email: '', + password: '', + role: 'user' + }; + } + + const submitHandler = async () => { + const stopLoading = () => { + dispatch('save'); + loading = false; + }; + + if (tab === '') { + loading = true; + + const res = await addUser( + localStorage.token, + _user.name, + _user.email, + _user.password, + _user.role + ).catch((error) => { + toast.error(error); + }); + + if (res) { + stopLoading(); + show = false; + } + } else { + if (inputFiles) { + loading = true; + + const file = inputFiles[0]; + const reader = new FileReader(); + + reader.onload = async (e) => { + const csv = e.target.result; + const rows = csv.split('\n'); + + let userCount = 0; + + for (const [idx, row] of rows.entries()) { + const columns = row.split(',').map((col) => col.trim()); + console.log(idx, columns); + + if (idx > 0) { + if ( + columns.length === 4 && + ['admin', 'user', 'pending'].includes(columns[3].toLowerCase()) + ) { + const res = await addUser( + localStorage.token, + columns[0], + columns[1], + columns[2], + columns[3].toLowerCase() + ).catch((error) => { + toast.error(`Row ${idx + 1}: ${error}`); + return null; + }); + + if (res) { + userCount = userCount + 1; + } + } else { + toast.error(`Row ${idx + 1}: invalid format.`); + } + } + } + + toast.success(`Successfully imported ${userCount} users.`); + inputFiles = null; + const uploadInputElement = document.getElementById('upload-user-csv-input'); + + if (uploadInputElement) { + uploadInputElement.value = null; + } + + stopLoading(); + }; + + reader.readAsText(file); + } else { + toast.error($i18n.t('File not found.')); + } + } + }; +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center">{$i18n.t('Add User')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="flex text-center text-sm font-medium rounded-xl bg-transparent/10 p-1 mb-2"> + <button + class="w-full rounded-lg p-1.5 {tab === '' ? 'bg-gray-50 dark:bg-gray-850' : ''}" + type="button" + on:click={() => { + tab = ''; + }}>{$i18n.t('Form')}</button + > + + <button + class="w-full rounded-lg p-1 {tab === 'import' ? 'bg-gray-50 dark:bg-gray-850' : ''}" + type="button" + on:click={() => { + tab = 'import'; + }}>{$i18n.t('CSV Import')}</button + > + </div> + <div class="px-1"> + {#if tab === ''} + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div> + + <div class="flex-1"> + <select + class="w-full capitalize rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + bind:value={_user.role} + placeholder={$i18n.t('Enter Your Role')} + required + > + <option value="pending"> {$i18n.t('pending')} </option> + <option value="user"> {$i18n.t('user')} </option> + <option value="admin"> {$i18n.t('admin')} </option> + </select> + </div> + </div> + + <div class="flex flex-col w-full mt-2"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div> + + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + type="text" + bind:value={_user.name} + placeholder={$i18n.t('Enter Your Full Name')} + autocomplete="off" + required + /> + </div> + </div> + + <hr class=" dark:border-gray-800 my-3 w-full" /> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div> + + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + type="email" + bind:value={_user.email} + placeholder={$i18n.t('Enter Your Email')} + autocomplete="off" + required + /> + </div> + </div> + + <div class="flex flex-col w-full mt-2"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Password')}</div> + + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + type="password" + bind:value={_user.password} + placeholder={$i18n.t('Enter Your Password')} + autocomplete="off" + /> + </div> + </div> + {:else if tab === 'import'} + <div> + <div class="mb-3 w-full"> + <input + id="upload-user-csv-input" + hidden + bind:files={inputFiles} + type="file" + accept=".csv" + /> + + <button + class="w-full text-sm font-medium py-3 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl" + type="button" + on:click={() => { + document.getElementById('upload-user-csv-input')?.click(); + }} + > + {#if inputFiles} + {inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. + {:else} + {$i18n.t('Click here to select a csv file.')} + {/if} + </button> + </div> + + <div class=" text-xs text-gray-500"> + ⓘ {$i18n.t( + 'Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.' + )} + <a + class="underline dark:text-gray-200" + href="{WEBUI_BASE_URL}/static/user-import.csv" + > + {$i18n.t('Click here to download user import template file.')} + </a> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading + ? ' cursor-not-allowed' + : ''}" + type="submit" + disabled={loading} + > + {$i18n.t('Submit')} + + {#if loading} + <div class="ml-2 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/admin/EditUserModal.svelte b/src/lib/components/admin/EditUserModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dcaff95b662a8c307515bb9ec274d4515a8d24e7 --- /dev/null +++ b/src/lib/components/admin/EditUserModal.svelte @@ -0,0 +1,174 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { createEventDispatcher } from 'svelte'; + import { onMount, getContext } from 'svelte'; + + import { updateUserById } from '$lib/apis/users'; + import Modal from '../common/Modal.svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let show = false; + export let selectedUser; + export let sessionUser; + + let _user = { + profile_image_url: '', + name: '', + email: '', + password: '' + }; + + const submitHandler = async () => { + const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => { + toast.error(error); + }); + + if (res) { + dispatch('save'); + show = false; + } + }; + + onMount(() => { + if (selectedUser) { + _user = selectedUser; + _user.password = ''; + } + }); +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 py-4"> + <div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + <hr class=" dark:border-gray-800" /> + + <div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class=" flex items-center rounded-md py-2 px-4 w-full"> + <div class=" self-center mr-5"> + <img + src={selectedUser.profile_image_url} + class=" max-w-[55px] object-cover rounded-full" + alt="User profile" + /> + </div> + + <div> + <div class=" self-center capitalize font-semibold">{selectedUser.name}</div> + + <div class="text-xs text-gray-500"> + {$i18n.t('Created at')} + {dayjs(selectedUser.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + </div> + </div> + </div> + + <hr class=" dark:border-gray-800 my-3 w-full" /> + + <div class=" flex flex-col space-y-1.5"> + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div> + + <div class="flex-1"> + <input + class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + type="email" + bind:value={_user.email} + autocomplete="off" + required + disabled={_user.id == sessionUser.id} + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$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" + type="text" + bind:value={_user.name} + autocomplete="off" + required + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</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" + type="password" + bind:value={_user.password} + autocomplete="new-password" + /> + </div> + </div> + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/admin/Settings.svelte b/src/lib/components/admin/Settings.svelte new file mode 100644 index 0000000000000000000000000000000000000000..afb8736ea13bd60af6f2bf881b7feb96b6a48fb1 --- /dev/null +++ b/src/lib/components/admin/Settings.svelte @@ -0,0 +1,404 @@ +<script> + import { getContext, tick, onMount } from 'svelte'; + import { toast } from 'svelte-sonner'; + + import Database from './Settings/Database.svelte'; + + import General from './Settings/General.svelte'; + import Users from './Settings/Users.svelte'; + + import Pipelines from './Settings/Pipelines.svelte'; + import Audio from './Settings/Audio.svelte'; + import Images from './Settings/Images.svelte'; + import Interface from './Settings/Interface.svelte'; + import Models from './Settings/Models.svelte'; + import Connections from './Settings/Connections.svelte'; + import Documents from './Settings/Documents.svelte'; + import WebSearch from './Settings/WebSearch.svelte'; + import { config } from '$lib/stores'; + import { getBackendConfig } from '$lib/apis'; + + const i18n = getContext('i18n'); + + let selectedTab = 'general'; + + onMount(() => { + const containerElement = document.getElementById('admin-settings-tabs-container'); + + if (containerElement) { + containerElement.addEventListener('wheel', function (event) { + if (event.deltaY !== 0) { + // Adjust horizontal scroll position based on vertical scroll + containerElement.scrollLeft += event.deltaY; + } + }); + } + }); +</script> + +<div class="flex flex-col lg:flex-row w-full h-full py-2 lg:space-x-4"> + <div + id="admin-settings-tabs-container" + class="tabs flex flex-row overflow-x-auto space-x-1 max-w-full lg:space-x-0 lg:space-y-1 lg:flex-col lg:flex-none lg:w-44 dark:text-gray-200 text-xs text-left scrollbar-none" + > + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab === + 'general' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'general'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('General')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'users' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'users'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM3.156 11.763c.16-.629.44-1.21.813-1.72a2.5 2.5 0 0 0-2.725 1.377c-.136.287.102.58.418.58h1.449c.01-.077.025-.156.045-.237ZM12.847 11.763c.02.08.036.16.046.237h1.446c.316 0 .554-.293.417-.579a2.5 2.5 0 0 0-2.722-1.378c.374.51.653 1.09.813 1.72ZM14 7.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0ZM3.5 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3ZM5 13c-.552 0-1.013-.455-.876-.99a4.002 4.002 0 0 1 7.753 0c.136.535-.324.99-.877.99H5Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Users')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'connections' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'connections'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Connections')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'models' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'models'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M10 1c3.866 0 7 1.79 7 4s-3.134 4-7 4-7-1.79-7-4 3.134-4 7-4zm5.694 8.13c.464-.264.91-.583 1.306-.952V10c0 2.21-3.134 4-7 4s-7-1.79-7-4V8.178c.396.37.842.688 1.306.953C5.838 10.006 7.854 10.5 10 10.5s4.162-.494 5.694-1.37zM3 13.179V15c0 2.21 3.134 4 7 4s7-1.79 7-4v-1.822c-.396.37-.842.688-1.306.953-1.532.875-3.548 1.369-5.694 1.369s-4.162-.494-5.694-1.37A7.009 7.009 0 013 13.179z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Models')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'documents' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'documents'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4 h-4" + > + <path d="M11.625 16.5a1.875 1.875 0 1 0 0-3.75 1.875 1.875 0 0 0 0 3.75Z" /> + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6 16.5c.66 0 1.277-.19 1.797-.518l1.048 1.048a.75.75 0 0 0 1.06-1.06l-1.047-1.048A3.375 3.375 0 1 0 11.625 18Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Documents')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'web' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'web'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Web Search')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'interface' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'interface'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Interface')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'audio' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'audio'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z" + /> + <path + d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Audio')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'images' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'images'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Images')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'pipelines' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'pipelines'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z" + /> + <path + d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z" + /> + <path + d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Pipelines')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'db' + ? 'bg-gray-100 dark:bg-gray-800' + : ' hover:bg-gray-50 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'db'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M8 7c3.314 0 6-1.343 6-3s-2.686-3-6-3-6 1.343-6 3 2.686 3 6 3Z" /> + <path + d="M8 8.5c1.84 0 3.579-.37 4.914-1.037A6.33 6.33 0 0 0 14 6.78V8c0 1.657-2.686 3-6 3S2 9.657 2 8V6.78c.346.273.72.5 1.087.683C4.42 8.131 6.16 8.5 8 8.5Z" + /> + <path + d="M8 12.5c1.84 0 3.579-.37 4.914-1.037.366-.183.74-.41 1.086-.684V12c0 1.657-2.686 3-6 3s-6-1.343-6-3v-1.22c.346.273.72.5 1.087.683C4.42 12.131 6.16 12.5 8 12.5Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Database')}</div> + </button> + </div> + + <div class="flex-1 mt-3 lg:mt-0 overflow-y-scroll"> + {#if selectedTab === 'general'} + <General + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'users'} + <Users + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'connections'} + <Connections + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'models'} + <Models /> + {:else if selectedTab === 'documents'} + <Documents + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'web'} + <WebSearch + saveHandler={async () => { + toast.success($i18n.t('Settings saved successfully!')); + + await tick(); + await config.set(await getBackendConfig()); + }} + /> + {:else if selectedTab === 'interface'} + <Interface + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'audio'} + <Audio + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'images'} + <Images + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'db'} + <Database + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'pipelines'} + <Pipelines + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {/if} + </div> +</div> diff --git a/src/lib/components/admin/Settings/Audio.svelte b/src/lib/components/admin/Settings/Audio.svelte new file mode 100644 index 0000000000000000000000000000000000000000..50ce7418e50d0750f0c5d9a9f219f517990abeff --- /dev/null +++ b/src/lib/components/admin/Settings/Audio.svelte @@ -0,0 +1,350 @@ +<script lang="ts"> + import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio'; + import { user, settings, config } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import Switch from '$lib/components/common/Switch.svelte'; + import { getBackendConfig } from '$lib/apis'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + // Audio + + let TTS_OPENAI_API_BASE_URL = ''; + let TTS_OPENAI_API_KEY = ''; + let TTS_API_KEY = ''; + let TTS_ENGINE = ''; + let TTS_MODEL = ''; + let TTS_VOICE = ''; + + let STT_OPENAI_API_BASE_URL = ''; + let STT_OPENAI_API_KEY = ''; + let STT_ENGINE = ''; + let STT_MODEL = ''; + + let voices = []; + let models = []; + let nonLocalVoices = false; + + const getOpenAIVoices = () => { + voices = [ + { name: 'alloy' }, + { name: 'echo' }, + { name: 'fable' }, + { name: 'onyx' }, + { name: 'nova' }, + { name: 'shimmer' } + ]; + }; + + const getOpenAIModels = () => { + models = [{ name: 'tts-1' }, { name: 'tts-1-hd' }]; + }; + + const getWebAPIVoices = () => { + const getVoicesLoop = setInterval(async () => { + voices = await speechSynthesis.getVoices(); + + // do your loop + if (voices.length > 0) { + clearInterval(getVoicesLoop); + } + }, 100); + }; + + const updateConfigHandler = async () => { + const res = await updateAudioConfig(localStorage.token, { + tts: { + OPENAI_API_BASE_URL: TTS_OPENAI_API_BASE_URL, + OPENAI_API_KEY: TTS_OPENAI_API_KEY, + API_KEY: TTS_API_KEY, + ENGINE: TTS_ENGINE, + MODEL: TTS_MODEL, + VOICE: TTS_VOICE + }, + stt: { + OPENAI_API_BASE_URL: STT_OPENAI_API_BASE_URL, + OPENAI_API_KEY: STT_OPENAI_API_KEY, + ENGINE: STT_ENGINE, + MODEL: STT_MODEL + } + }); + + if (res) { + toast.success($i18n.t('Audio settings updated successfully')); + + config.set(await getBackendConfig()); + } + }; + + onMount(async () => { + const res = await getAudioConfig(localStorage.token); + + if (res) { + console.log(res); + TTS_OPENAI_API_BASE_URL = res.tts.OPENAI_API_BASE_URL; + TTS_OPENAI_API_KEY = res.tts.OPENAI_API_KEY; + TTS_API_KEY = res.tts.API_KEY; + + TTS_ENGINE = res.tts.ENGINE; + TTS_MODEL = res.tts.MODEL; + TTS_VOICE = res.tts.VOICE; + + STT_OPENAI_API_BASE_URL = res.stt.OPENAI_API_BASE_URL; + STT_OPENAI_API_KEY = res.stt.OPENAI_API_KEY; + + STT_ENGINE = res.stt.ENGINE; + STT_MODEL = res.stt.MODEL; + } + + if (TTS_ENGINE === 'openai') { + getOpenAIVoices(); + getOpenAIModels(); + } else { + getWebAPIVoices(); + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + await updateConfigHandler(); + dispatch('save'); + }} +> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + <div class="flex flex-col gap-3"> + <div> + <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={STT_ENGINE} + placeholder="Select an engine" + > + <option value="">{$i18n.t('Whisper (Local)')}</option> + <option value="openai">OpenAI</option> + <option value="web">{$i18n.t('Web API')}</option> + </select> + </div> + </div> + + {#if STT_ENGINE === 'openai'} + <div> + <div class="mt-1 flex gap-2 mb-1"> + <input + class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Base URL')} + bind:value={STT_OPENAI_API_BASE_URL} + required + /> + + <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={STT_OPENAI_API_KEY} /> + </div> + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="model-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={STT_MODEL} + placeholder="Select a model" + /> + + <datalist id="model-list"> + <option value="whisper-1" /> + </datalist> + </div> + </div> + </div> + {/if} + </div> + + <hr class=" dark:border-gray-800" /> + + <div> + <div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div> + <div class="flex items-center relative"> + <select + class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={TTS_ENGINE} + placeholder="Select a mode" + on:change={(e) => { + if (e.target.value === 'openai') { + getOpenAIVoices(); + TTS_VOICE = 'alloy'; + TTS_MODEL = 'tts-1'; + } else { + getWebAPIVoices(); + TTS_VOICE = ''; + TTS_MODEL = ''; + } + }} + > + <option value="">{$i18n.t('Web API')}</option> + <option value="openai">{$i18n.t('OpenAI')}</option> + <option value="elevenlabs">{$i18n.t('ElevenLabs')}</option> + </select> + </div> + </div> + + {#if TTS_ENGINE === 'openai'} + <div> + <div class="mt-1 flex gap-2 mb-1"> + <input + class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Base URL')} + bind:value={TTS_OPENAI_API_BASE_URL} + required + /> + + <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={TTS_OPENAI_API_KEY} /> + </div> + </div> + {:else if TTS_ENGINE === 'elevenlabs'} + <div> + <div class="mt-1 flex gap-2 mb-1"> + <input + class="flex-1 w-full rounded-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Key')} + bind:value={TTS_API_KEY} + required + /> + </div> + </div> + {/if} + + <hr class=" dark:border-gray-850 my-2" /> + + {#if TTS_ENGINE === ''} + <div> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={TTS_VOICE} + > + <option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option> + {#each voices as voice} + <option + value={voice.voiceURI} + class="bg-gray-100 dark:bg-gray-700" + selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option + > + {/each} + </select> + </div> + </div> + </div> + {:else if TTS_ENGINE === 'openai'} + <div class=" flex gap-2"> + <div class="w-full"> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="voice-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={TTS_VOICE} + placeholder="Select a voice" + /> + + <datalist id="voice-list"> + {#each voices as voice} + <option value={voice.name} /> + {/each} + </datalist> + </div> + </div> + </div> + <div class="w-full"> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="model-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={TTS_MODEL} + placeholder="Select a model" + /> + + <datalist id="model-list"> + {#each models as model} + <option value={model.name} /> + {/each} + </datalist> + </div> + </div> + </div> + </div> + {:else if TTS_ENGINE === 'elevenlabs'} + <div class=" flex gap-2"> + <div class="w-full"> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="voice-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={TTS_VOICE} + placeholder="Select a voice" + /> + + <datalist id="voice-list"> + {#each voices as voice} + <option value={voice.name} /> + {/each} + </datalist> + </div> + </div> + </div> + <div class="w-full"> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="model-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={TTS_MODEL} + placeholder="Select a model" + /> + + <datalist id="model-list"> + {#each models as model} + <option value={model.name} /> + {/each} + </datalist> + </div> + </div> + </div> + </div> + {/if} + </div> + </div> + </div> + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Connections.svelte b/src/lib/components/admin/Settings/Connections.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fe71e4816dcf48b45dd9a01bb26b02af49735071 --- /dev/null +++ b/src/lib/components/admin/Settings/Connections.svelte @@ -0,0 +1,446 @@ +<script lang="ts"> + import { models, user } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import { + getOllamaConfig, + getOllamaUrls, + getOllamaVersion, + updateOllamaConfig, + updateOllamaUrls + } from '$lib/apis/ollama'; + import { + getOpenAIConfig, + getOpenAIKeys, + getOpenAIModels, + getOpenAIUrls, + updateOpenAIConfig, + updateOpenAIKeys, + updateOpenAIUrls + } from '$lib/apis/openai'; + import { toast } from 'svelte-sonner'; + import Switch from '$lib/components/common/Switch.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import { getModels as _getModels } from '$lib/apis'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + + const i18n = getContext('i18n'); + + const getModels = async () => { + const models = await _getModels(localStorage.token); + return models; + }; + + // External + let OLLAMA_BASE_URLS = ['']; + + let OPENAI_API_KEYS = ['']; + let OPENAI_API_BASE_URLS = ['']; + + let pipelineUrls = {}; + + let ENABLE_OPENAI_API = null; + let ENABLE_OLLAMA_API = null; + + const verifyOpenAIHandler = async (idx) => { + OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, '')); + + OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); + OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); + + const res = await getOpenAIModels(localStorage.token, idx).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Server connection verified')); + if (res.pipelines) { + pipelineUrls[OPENAI_API_BASE_URLS[idx]] = true; + } + } + + await models.set(await getModels()); + }; + + const verifyOllamaHandler = async (idx) => { + OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) => + url.replace(/\/$/, '') + ); + + OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS); + + const res = await getOllamaVersion(localStorage.token, idx).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Server connection verified')); + } + + await models.set(await getModels()); + }; + + const updateOpenAIHandler = async () => { + OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.map((url) => url.replace(/\/$/, '')); + + // Check if API KEYS length is same than API URLS length + if (OPENAI_API_KEYS.length !== OPENAI_API_BASE_URLS.length) { + // if there are more keys than urls, remove the extra keys + if (OPENAI_API_KEYS.length > OPENAI_API_BASE_URLS.length) { + OPENAI_API_KEYS = OPENAI_API_KEYS.slice(0, OPENAI_API_BASE_URLS.length); + } + + // if there are more urls than keys, add empty keys + if (OPENAI_API_KEYS.length < OPENAI_API_BASE_URLS.length) { + const diff = OPENAI_API_BASE_URLS.length - OPENAI_API_KEYS.length; + for (let i = 0; i < diff; i++) { + OPENAI_API_KEYS.push(''); + } + } + } + + OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS); + OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS); + await models.set(await getModels()); + }; + + const updateOllamaUrlsHandler = async () => { + OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url) => url !== '').map((url) => + url.replace(/\/$/, '') + ); + + console.log(OLLAMA_BASE_URLS); + + if (OLLAMA_BASE_URLS.length === 0) { + ENABLE_OLLAMA_API = false; + await updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API); + + toast.info($i18n.t('Ollama API disabled')); + } else { + OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS); + + const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (ollamaVersion) { + toast.success($i18n.t('Server connection verified')); + await models.set(await getModels()); + } + } + }; + + onMount(async () => { + if ($user.role === 'admin') { + await Promise.all([ + (async () => { + OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token); + })(), + (async () => { + OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token); + })(), + (async () => { + OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token); + })() + ]); + + OPENAI_API_BASE_URLS.forEach(async (url, idx) => { + const res = await getOpenAIModels(localStorage.token, idx); + if (res.pipelines) { + pipelineUrls[url] = true; + } + }); + + const ollamaConfig = await getOllamaConfig(localStorage.token); + const openaiConfig = await getOpenAIConfig(localStorage.token); + + ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API; + ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API; + } + }); +</script> + +<form + class="flex flex-col h-full justify-between text-sm" + on:submit|preventDefault={() => { + updateOpenAIHandler(); + updateOllamaUrlsHandler(); + + dispatch('save'); + }} +> + <div class="space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + {#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null} + <div class=" space-y-3"> + <div class="mt-2 space-y-2 pr-1.5"> + <div class="flex justify-between items-center text-sm"> + <div class=" font-medium">{$i18n.t('OpenAI API')}</div> + + <div class="mt-1"> + <Switch + bind:state={ENABLE_OPENAI_API} + on:change={async () => { + updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API); + }} + /> + </div> + </div> + + {#if ENABLE_OPENAI_API} + <div class="flex flex-col gap-1"> + {#each OPENAI_API_BASE_URLS as url, idx} + <div class="flex w-full gap-2"> + <div class="flex-1 relative"> + <input + class="w-full rounded-lg py-2 px-4 {pipelineUrls[url] + ? 'pr-8' + : ''} text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Base URL')} + bind:value={url} + autocomplete="off" + /> + + {#if pipelineUrls[url]} + <div class=" absolute top-2.5 right-2.5"> + <Tooltip content="Pipelines"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + d="M11.644 1.59a.75.75 0 0 1 .712 0l9.75 5.25a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.712 0l-9.75-5.25a.75.75 0 0 1 0-1.32l9.75-5.25Z" + /> + <path + d="m3.265 10.602 7.668 4.129a2.25 2.25 0 0 0 2.134 0l7.668-4.13 1.37.739a.75.75 0 0 1 0 1.32l-9.75 5.25a.75.75 0 0 1-.71 0l-9.75-5.25a.75.75 0 0 1 0-1.32l1.37-.738Z" + /> + <path + d="m10.933 19.231-7.668-4.13-1.37.739a.75.75 0 0 0 0 1.32l9.75 5.25c.221.12.489.12.71 0l9.75-5.25a.75.75 0 0 0 0-1.32l-1.37-.738-7.668 4.13a2.25 2.25 0 0 1-2.134-.001Z" + /> + </svg> + </Tooltip> + </div> + {/if} + </div> + + <SensitiveInput + placeholder={$i18n.t('API Key')} + bind:value={OPENAI_API_KEYS[idx]} + /> + <div class="self-center flex items-center"> + {#if idx === 0} + <button + class="px-1" + on:click={() => { + OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, '']; + OPENAI_API_KEYS = [...OPENAI_API_KEYS, '']; + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + {:else} + <button + class="px-1" + on:click={() => { + OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter( + (url, urlIdx) => idx !== urlIdx + ); + OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> + </svg> + </button> + {/if} + </div> + + <div class="flex"> + <Tooltip content="Verify connection" className="self-start mt-0.5"> + <button + class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition" + on:click={() => { + verifyOpenAIHandler(idx); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" + clip-rule="evenodd" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + <div class=" mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('WebUI will make requests to')} + <span class=" text-gray-200">'{url}/models'</span> + </div> + {/each} + </div> + {/if} + </div> + </div> + + <hr class=" dark:border-gray-850" /> + + <div class="pr-1.5 space-y-2"> + <div class="flex justify-between items-center text-sm"> + <div class=" font-medium">{$i18n.t('Ollama API')}</div> + + <div class="mt-1"> + <Switch + bind:state={ENABLE_OLLAMA_API} + on:change={async () => { + updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API); + + if (OLLAMA_BASE_URLS.length === 0) { + OLLAMA_BASE_URLS = ['']; + } + }} + /> + </div> + </div> + {#if ENABLE_OLLAMA_API} + <div class="flex w-full gap-1.5"> + <div class="flex-1 flex flex-col gap-2"> + {#each OLLAMA_BASE_URLS as url, idx} + <div class="flex gap-1.5"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')} + bind:value={url} + /> + + <div class="self-center flex items-center"> + {#if idx === 0} + <button + class="px-1" + on:click={() => { + OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, '']; + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + {:else} + <button + class="px-1" + on:click={() => { + OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter( + (url, urlIdx) => idx !== urlIdx + ); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> + </svg> + </button> + {/if} + </div> + + <div class="flex"> + <Tooltip content="Verify connection" className="self-start mt-0.5"> + <button + class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition" + on:click={() => { + verifyOllamaHandler(idx); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" + clip-rule="evenodd" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + {/each} + </div> + </div> + + <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Trouble accessing Ollama?')} + <a + class=" text-gray-300 font-medium underline" + href="https://github.com/open-webui/open-webui#troubleshooting" + target="_blank" + > + {$i18n.t('Click here for help.')} + </a> + </div> + {/if} + </div> + {:else} + <div class="flex h-full justify-center"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Database.svelte b/src/lib/components/admin/Settings/Database.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0ba45263e11a407708d936207be46fb80e49a5d1 --- /dev/null +++ b/src/lib/components/admin/Settings/Database.svelte @@ -0,0 +1,146 @@ +<script lang="ts"> + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { downloadDatabase, downloadLiteLLMConfig } from '$lib/apis/utils'; + import { onMount, getContext } from 'svelte'; + import { config, user } from '$lib/stores'; + import { toast } from 'svelte-sonner'; + import { getAllUserChats } from '$lib/apis/chats'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + const exportAllUserChats = async () => { + let blob = new Blob([JSON.stringify(await getAllUserChats(localStorage.token))], { + type: 'application/json' + }); + saveAs(blob, `all-chats-export-${Date.now()}.json`); + }; + + onMount(async () => { + // permissions = await getUserPermissions(localStorage.token); + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + saveHandler(); + }} +> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div> + + {#if $config?.features.enable_admin_export ?? true} + <div class=" flex w-full justify-between"> + <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> --> + + <button + class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + type="button" + on:click={() => { + // exportAllUserChats(); + + downloadDatabase(localStorage.token).catch((error) => { + toast.error(error); + }); + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> + <path + fill-rule="evenodd" + d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Download Database')}</div> + </button> + </div> + + <button + class=" flex rounded-md py-2 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + exportAllUserChats(); + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> + <path + fill-rule="evenodd" + d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM8.75 7.75a.75.75 0 0 0-1.5 0v2.69L6.03 9.22a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06l-1.22 1.22V7.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium"> + {$i18n.t('Export All Chats (All Users)')} + </div> + </button> + {/if} + + <hr class=" dark:border-gray-850 my-1" /> + + <div class=" flex w-full justify-between"> + <!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> --> + + <button + class=" flex rounded-md py-1.5 px-3 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + type="button" + on:click={() => { + downloadLiteLLMConfig(localStorage.token).catch((error) => { + toast.error(error); + }); + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm5.845 17.03a.75.75 0 0 0 1.06 0l3-3a.75.75 0 1 0-1.06-1.06l-1.72 1.72V12a.75.75 0 0 0-1.5 0v4.19l-1.72-1.72a.75.75 0 0 0-1.06 1.06l3 3Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium"> + {$i18n.t('Export LiteLLM config.yaml')} + </div> + </button> + </div> + </div> + </div> + + <!-- <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + + </div> --> +</form> diff --git a/src/lib/components/admin/Settings/Documents.svelte b/src/lib/components/admin/Settings/Documents.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1b0b2c3fa8803c9e5e7f7416f514eec6b01bb6c0 --- /dev/null +++ b/src/lib/components/admin/Settings/Documents.svelte @@ -0,0 +1,792 @@ +<script lang="ts"> + import { getDocs } from '$lib/apis/documents'; + import { deleteAllFiles, deleteFileById } from '$lib/apis/files'; + import { + getQuerySettings, + scanDocs, + updateQuerySettings, + resetVectorDB, + getEmbeddingConfig, + updateEmbeddingConfig, + getRerankingConfig, + updateRerankingConfig, + resetUploadDir, + getRAGConfig, + updateRAGConfig + } from '$lib/apis/rag'; + import ResetUploadDirConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + import ResetVectorDBConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + import { documents, models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let scanDirLoading = false; + let updateEmbeddingModelLoading = false; + let updateRerankingModelLoading = false; + + let showResetConfirm = false; + let showResetUploadDirConfirm = false; + + let embeddingEngine = ''; + let embeddingModel = ''; + let rerankingModel = ''; + + let contentExtractionEngine = 'default'; + let tikaServerUrl = ''; + let showTikaServerUrl = false; + + let chunkSize = 0; + let chunkOverlap = 0; + let pdfExtractImages = true; + + let OpenAIKey = ''; + let OpenAIUrl = ''; + let OpenAIBatchSize = 1; + + let querySettings = { + template: '', + r: 0.0, + k: 4, + hybrid: false + }; + + const scanHandler = async () => { + scanDirLoading = true; + const res = await scanDocs(localStorage.token); + scanDirLoading = false; + + if (res) { + await documents.set(await getDocs(localStorage.token)); + toast.success($i18n.t('Scan complete!')); + } + }; + + const embeddingModelUpdateHandler = async () => { + if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) { + toast.error( + $i18n.t( + 'Model filesystem path detected. Model shortname is required for update, cannot continue.' + ) + ); + return; + } + if (embeddingEngine === 'ollama' && embeddingModel === '') { + toast.error( + $i18n.t( + 'Model filesystem path detected. Model shortname is required for update, cannot continue.' + ) + ); + return; + } + + if (embeddingEngine === 'openai' && embeddingModel === '') { + toast.error( + $i18n.t( + 'Model filesystem path detected. Model shortname is required for update, cannot continue.' + ) + ); + return; + } + + if ((embeddingEngine === 'openai' && OpenAIKey === '') || OpenAIUrl === '') { + toast.error($i18n.t('OpenAI URL/Key required.')); + return; + } + + console.log('Update embedding model attempt:', embeddingModel); + + updateEmbeddingModelLoading = true; + const res = await updateEmbeddingConfig(localStorage.token, { + embedding_engine: embeddingEngine, + embedding_model: embeddingModel, + ...(embeddingEngine === 'openai' + ? { + openai_config: { + key: OpenAIKey, + url: OpenAIUrl, + batch_size: OpenAIBatchSize + } + } + : {}) + }).catch(async (error) => { + toast.error(error); + await setEmbeddingConfig(); + return null; + }); + updateEmbeddingModelLoading = false; + + if (res) { + console.log('embeddingModelUpdateHandler:', res); + if (res.status === true) { + toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), { + duration: 1000 * 10 + }); + } + } + }; + + const rerankingModelUpdateHandler = async () => { + console.log('Update reranking model attempt:', rerankingModel); + + updateRerankingModelLoading = true; + const res = await updateRerankingConfig(localStorage.token, { + reranking_model: rerankingModel + }).catch(async (error) => { + toast.error(error); + await setRerankingConfig(); + return null; + }); + updateRerankingModelLoading = false; + + if (res) { + console.log('rerankingModelUpdateHandler:', res); + if (res.status === true) { + if (rerankingModel === '') { + toast.success($i18n.t('Reranking model disabled', res), { + duration: 1000 * 10 + }); + } else { + toast.success($i18n.t('Reranking model set to "{{reranking_model}}"', res), { + duration: 1000 * 10 + }); + } + } + } + }; + + const submitHandler = async () => { + embeddingModelUpdateHandler(); + + if (querySettings.hybrid) { + rerankingModelUpdateHandler(); + } + + if (contentExtractionEngine === 'tika' && tikaServerUrl === '') { + toast.error($i18n.t('Tika Server URL required.')); + return; + } + + const res = await updateRAGConfig(localStorage.token, { + pdf_extract_images: pdfExtractImages, + chunk: { + chunk_overlap: chunkOverlap, + chunk_size: chunkSize + }, + content_extraction: { + engine: contentExtractionEngine, + tika_server_url: tikaServerUrl + } + }); + + await updateQuerySettings(localStorage.token, querySettings); + }; + + const setEmbeddingConfig = async () => { + const embeddingConfig = await getEmbeddingConfig(localStorage.token); + + if (embeddingConfig) { + embeddingEngine = embeddingConfig.embedding_engine; + embeddingModel = embeddingConfig.embedding_model; + + OpenAIKey = embeddingConfig.openai_config.key; + OpenAIUrl = embeddingConfig.openai_config.url; + OpenAIBatchSize = embeddingConfig.openai_config.batch_size ?? 1; + } + }; + + const setRerankingConfig = async () => { + const rerankingConfig = await getRerankingConfig(localStorage.token); + + if (rerankingConfig) { + rerankingModel = rerankingConfig.reranking_model; + } + }; + + const toggleHybridSearch = async () => { + querySettings.hybrid = !querySettings.hybrid; + querySettings = await updateQuerySettings(localStorage.token, querySettings); + }; + + onMount(async () => { + await setEmbeddingConfig(); + await setRerankingConfig(); + + querySettings = await getQuerySettings(localStorage.token); + + const res = await getRAGConfig(localStorage.token); + + if (res) { + pdfExtractImages = res.pdf_extract_images; + + chunkSize = res.chunk.chunk_size; + chunkOverlap = res.chunk.chunk_overlap; + + contentExtractionEngine = res.content_extraction.engine; + tikaServerUrl = res.content_extraction.tika_server_url; + showTikaServerUrl = contentExtractionEngine === 'tika'; + } + }); +</script> + +<ResetUploadDirConfirmDialog + bind:show={showResetUploadDirConfirm} + on:confirm={async () => { + const res = await deleteAllFiles(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Success')); + } + }} +/> + +<ResetVectorDBConfirmDialog + bind:show={showResetConfirm} + on:confirm={() => { + const res = resetVectorDB(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Success')); + } + }} +/> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + submitHandler(); + saveHandler(); + }} +> + <div class=" space-y-2.5 overflow-y-scroll scrollbar-hidden h-full pr-1.5"> + <div class="flex flex-col gap-0.5"> + <div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div> + + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Scan for documents from {{path}}', { path: 'DOCS_DIR (/data/docs)' })} + </div> + + <button + class=" self-center text-xs p-1 px-3 bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading + ? ' cursor-not-allowed' + : ''}" + on:click={() => { + scanHandler(); + console.log('check'); + }} + type="button" + disabled={scanDirLoading} + > + <div class="self-center font-medium">{$i18n.t('Scan')}</div> + + {#if scanDirLoading} + <div class="ml-3 self-center"> + <svg + class=" w-3 h-3" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {/if} + </button> + </div> + + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={embeddingEngine} + placeholder="Select an embedding model engine" + on:change={(e) => { + if (e.target.value === 'ollama') { + embeddingModel = ''; + } else if (e.target.value === 'openai') { + embeddingModel = 'text-embedding-3-small'; + } else if (e.target.value === '') { + embeddingModel = 'sentence-transformers/all-MiniLM-L6-v2'; + } + }} + > + <option value="">{$i18n.t('Default (SentenceTransformers)')}</option> + <option value="ollama">{$i18n.t('Ollama')}</option> + <option value="openai">{$i18n.t('OpenAI')}</option> + </select> + </div> + </div> + + {#if embeddingEngine === 'openai'} + <div class="my-0.5 flex gap-2"> + <input + class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Base URL')} + bind:value={OpenAIUrl} + required + /> + + <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} /> + </div> + <div class="flex mt-0.5 space-x-2"> + <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Batch Size')}</div> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="1" + max="2048" + step="1" + bind:value={OpenAIBatchSize} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={OpenAIBatchSize} + type="number" + class=" bg-transparent text-center w-14" + min="-2" + max="16000" + step="1" + /> + </div> + </div> + {/if} + + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleHybridSearch(); + }} + type="button" + > + {#if querySettings.hybrid === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <hr class="dark:border-gray-850" /> + + <div class="space-y-2" /> + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Embedding Model')}</div> + + {#if embeddingEngine === 'ollama'} + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={embeddingModel} + placeholder={$i18n.t('Select a model')} + required + > + {#if !embeddingModel} + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {/if} + {#each $models.filter((m) => m.id && m.ollama && !(m?.preset ?? false)) as model} + <option value={model.id} class="bg-gray-50 dark:bg-gray-700">{model.name}</option> + {/each} + </select> + </div> + </div> + {:else} + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Set embedding model (e.g. {{model}})', { + model: embeddingModel.slice(-40) + })} + bind:value={embeddingModel} + /> + </div> + + {#if embeddingEngine === ''} + <button + class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + embeddingModelUpdateHandler(); + }} + disabled={updateEmbeddingModelLoading} + > + {#if updateEmbeddingModelLoading} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + {/if} + </div> + {/if} + + <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t( + 'Warning: If you update or change your embedding model, you will need to re-import all documents.' + )} + </div> + + {#if querySettings.hybrid === true} + <div class=" "> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Reranking Model')}</div> + + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Set reranking model (e.g. {{model}})', { + model: 'BAAI/bge-reranker-v2-m3' + })} + bind:value={rerankingModel} + /> + </div> + <button + class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + rerankingModelUpdateHandler(); + }} + disabled={updateRerankingModelLoading} + > + {#if updateRerankingModelLoading} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + </div> + </div> + {/if} + </div> + + <hr class=" dark:border-gray-850" /> + + <div class=""> + <div class="text-sm font-medium">{$i18n.t('Content Extraction')}</div> + + <div class="flex w-full justify-between mt-2"> + <div class="self-center text-xs font-medium">{$i18n.t('Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={contentExtractionEngine} + on:change={(e) => { + showTikaServerUrl = e.target.value === 'tika'; + }} + > + <option value="">{$i18n.t('Default')} </option> + <option value="tika">{$i18n.t('Tika')}</option> + </select> + </div> + </div> + + {#if showTikaServerUrl} + <div class="flex w-full mt-2"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter Tika Server URL')} + bind:value={tikaServerUrl} + /> + </div> + </div> + {/if} + </div> + <hr class=" dark:border-gray-850" /> + + <div class=" "> + <div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> + + <div class=" flex gap-1"> + <div class=" flex w-full justify-between"> + <div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div> + + <div class="self-center p-3"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Top K')} + bind:value={querySettings.k} + autocomplete="off" + min="0" + /> + </div> + </div> + + {#if querySettings.hybrid === true} + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium min-w-fit"> + {$i18n.t('Minimum Score')} + </div> + + <div class="self-center p-3"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + step="0.01" + placeholder={$i18n.t('Enter Score')} + bind:value={querySettings.r} + autocomplete="off" + min="0.0" + title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')} + /> + </div> + </div> + {/if} + </div> + + {#if querySettings.hybrid === true} + <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t( + 'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.' + )} + </div> + + <hr class=" dark:border-gray-850 my-3" /> + {/if} + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div> + <textarea + bind:value={querySettings.template} + class="w-full rounded-lg px-4 py-3 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + rows="4" + /> + </div> + </div> + + <hr class=" dark:border-gray-850" /> + + <div class=" "> + <div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> + + <div class=" my-2 flex gap-1.5"> + <div class=" w-full justify-between"> + <div class="self-center text-xs font-medium min-w-fit mb-1">{$i18n.t('Chunk Size')}</div> + <div class="self-center"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Chunk Size')} + bind:value={chunkSize} + autocomplete="off" + min="0" + /> + </div> + </div> + + <div class="w-full"> + <div class=" self-center text-xs font-medium min-w-fit mb-1"> + {$i18n.t('Chunk Overlap')} + </div> + + <div class="self-center"> + <input + class="w-full rounded-lg py-1.5 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Chunk Overlap')} + bind:value={chunkOverlap} + autocomplete="off" + min="0" + /> + </div> + </div> + </div> + + <div class="my-3"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div> + + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + pdfExtractImages = !pdfExtractImages; + }}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button + > + </div> + </div> + </div> + + <hr class=" dark:border-gray-850" /> + + <div> + <button + class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + showResetUploadDirConfirm = true; + }} + type="button" + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM9.75 14.25a.75.75 0 0 0 0 1.5H15a.75.75 0 0 0 0-1.5H9.75Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Reset Upload Directory')}</div> + </button> + + <button + class=" flex rounded-xl py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + showResetConfirm = true; + }} + type="button" + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M3.5 2A1.5 1.5 0 0 0 2 3.5v9A1.5 1.5 0 0 0 3.5 14h9a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 12.5 4H9.621a1.5 1.5 0 0 1-1.06-.44L7.439 2.44A1.5 1.5 0 0 0 6.38 2H3.5Zm6.75 7.75a.75.75 0 0 0 0-1.5h-4.5a.75.75 0 0 0 0 1.5h4.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div> + </button> + </div> + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/General.svelte b/src/lib/components/admin/Settings/General.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bc66c2e01fa07589c5031c9c8cddb5cd0e5121a4 --- /dev/null +++ b/src/lib/components/admin/Settings/General.svelte @@ -0,0 +1,153 @@ +<script lang="ts"> + import { + getCommunitySharingEnabledStatus, + getWebhookUrl, + toggleCommunitySharingEnabledStatus, + updateWebhookUrl + } from '$lib/apis'; + import { + getAdminConfig, + getDefaultUserRole, + getJWTExpiresDuration, + getSignUpEnabledStatus, + toggleSignUpEnabledStatus, + updateAdminConfig, + updateDefaultUserRole, + updateJWTExpiresDuration + } from '$lib/apis/auths'; + import Switch from '$lib/components/common/Switch.svelte'; + import { onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let adminConfig = null; + let webhookUrl = ''; + + const updateHandler = async () => { + webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl); + const res = await updateAdminConfig(localStorage.token, adminConfig); + + if (res) { + toast.success(i18n.t('Settings updated successfully')); + } else { + toast.error(i18n.t('Failed to update settings')); + } + }; + + onMount(async () => { + await Promise.all([ + (async () => { + adminConfig = await getAdminConfig(localStorage.token); + })(), + + (async () => { + webhookUrl = await getWebhookUrl(localStorage.token); + })() + ]); + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + updateHandler(); + saveHandler(); + }} +> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + {#if adminConfig !== null} + <div> + <div class=" mb-3 text-sm font-medium">{$i18n.t('General Settings')}</div> + + <div class=" flex w-full justify-between pr-2"> + <div class=" self-center text-xs font-medium">{$i18n.t('Enable New Sign Ups')}</div> + + <Switch bind:state={adminConfig.ENABLE_SIGNUP} /> + </div> + + <div class=" my-3 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Default User Role')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 text-xs bg-transparent outline-none text-right" + bind:value={adminConfig.DEFAULT_USER_ROLE} + placeholder="Select a role" + > + <option value="pending">{$i18n.t('pending')}</option> + <option value="user">{$i18n.t('user')}</option> + <option value="admin">{$i18n.t('admin')}</option> + </select> + </div> + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div class="my-3 flex w-full items-center justify-between pr-2"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Show Admin Details in Account Pending Overlay')} + </div> + + <Switch bind:state={adminConfig.SHOW_ADMIN_DETAILS} /> + </div> + + <div class="my-3 flex w-full items-center justify-between pr-2"> + <div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div> + + <Switch bind:state={adminConfig.ENABLE_COMMUNITY_SHARING} /> + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div class=" w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('JWT Expiration')}</div> + </div> + + <div class="flex mt-2 space-x-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={`e.g.) "30m","1h", "10d". `} + bind:value={adminConfig.JWT_EXPIRES_IN} + /> + </div> + + <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Valid time units:')} + <span class=" text-gray-300 font-medium" + >{$i18n.t("'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.")}</span + > + </div> + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div class=" w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Webhook URL')}</div> + </div> + + <div class="flex mt-2 space-x-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={`https://example.com/webhook`} + bind:value={webhookUrl} + /> + </div> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Images.svelte b/src/lib/components/admin/Settings/Images.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9838792f28599e1ad2735d5965178b8e432cfba3 --- /dev/null +++ b/src/lib/components/admin/Settings/Images.svelte @@ -0,0 +1,457 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + + import { createEventDispatcher, onMount, getContext } from 'svelte'; + import { config, user } from '$lib/stores'; + import { + getImageGenerationModels, + getDefaultImageGenerationModel, + updateDefaultImageGenerationModel, + getImageSize, + getImageGenerationConfig, + updateImageGenerationConfig, + getImageGenerationEngineUrls, + updateImageGenerationEngineUrls, + updateImageSize, + getImageSteps, + updateImageSteps, + getOpenAIConfig, + updateOpenAIConfig + } from '$lib/apis/images'; + import { getBackendConfig } from '$lib/apis'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + let loading = false; + + let imageGenerationEngine = ''; + let enableImageGeneration = false; + + let AUTOMATIC1111_BASE_URL = ''; + let AUTOMATIC1111_API_AUTH = ''; + let COMFYUI_BASE_URL = ''; + + let OPENAI_API_BASE_URL = ''; + let OPENAI_API_KEY = ''; + + let selectedModel = ''; + let models = null; + + let imageSize = ''; + let steps = 50; + + const getModels = async () => { + models = await getImageGenerationModels(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => { + return ''; + }); + }; + + const updateUrlHandler = async () => { + if (imageGenerationEngine === 'comfyui') { + const res = await updateImageGenerationEngineUrls(localStorage.token, { + COMFYUI_BASE_URL: COMFYUI_BASE_URL + }).catch((error) => { + toast.error(error); + + console.log(error); + return null; + }); + + if (res) { + COMFYUI_BASE_URL = res.COMFYUI_BASE_URL; + + await getModels(); + + if (models) { + toast.success($i18n.t('Server connection verified')); + } + } else { + ({ COMFYUI_BASE_URL } = await getImageGenerationEngineUrls(localStorage.token)); + } + } else { + const res = await updateImageGenerationEngineUrls(localStorage.token, { + AUTOMATIC1111_BASE_URL: AUTOMATIC1111_BASE_URL, + AUTOMATIC1111_API_AUTH: AUTOMATIC1111_API_AUTH + }).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + AUTOMATIC1111_BASE_URL = res.AUTOMATIC1111_BASE_URL; + AUTOMATIC1111_API_AUTH = res.AUTOMATIC1111_API_AUTH; + + await getModels(); + + if (models) { + toast.success($i18n.t('Server connection verified')); + } + } else { + ({ AUTOMATIC1111_BASE_URL, AUTOMATIC1111_API_AUTH } = await getImageGenerationEngineUrls( + localStorage.token + )); + } + } + }; + const updateImageGeneration = async () => { + const res = await updateImageGenerationConfig( + localStorage.token, + imageGenerationEngine, + enableImageGeneration + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + imageGenerationEngine = res.engine; + enableImageGeneration = res.enabled; + } + + if (enableImageGeneration) { + config.set(await getBackendConfig(localStorage.token)); + getModels(); + } + }; + + onMount(async () => { + if ($user.role === 'admin') { + const res = await getImageGenerationConfig(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + imageGenerationEngine = res.engine; + enableImageGeneration = res.enabled; + } + const URLS = await getImageGenerationEngineUrls(localStorage.token); + + AUTOMATIC1111_BASE_URL = URLS.AUTOMATIC1111_BASE_URL; + AUTOMATIC1111_API_AUTH = URLS.AUTOMATIC1111_API_AUTH; + COMFYUI_BASE_URL = URLS.COMFYUI_BASE_URL; + + const config = await getOpenAIConfig(localStorage.token); + + OPENAI_API_KEY = config.OPENAI_API_KEY; + OPENAI_API_BASE_URL = config.OPENAI_API_BASE_URL; + + imageSize = await getImageSize(localStorage.token); + steps = await getImageSteps(localStorage.token); + + if (enableImageGeneration) { + getModels(); + } + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + loading = true; + + if (imageGenerationEngine === 'openai') { + await updateOpenAIConfig(localStorage.token, OPENAI_API_BASE_URL, OPENAI_API_KEY); + } + + await updateDefaultImageGenerationModel(localStorage.token, selectedModel); + + await updateImageSize(localStorage.token, imageSize).catch((error) => { + toast.error(error); + return null; + }); + await updateImageSteps(localStorage.token, steps).catch((error) => { + toast.error(error); + return null; + }); + + dispatch('save'); + loading = false; + }} +> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden"> + <div> + <div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div> + <div class="flex items-center relative"> + <select + class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={imageGenerationEngine} + placeholder={$i18n.t('Select a mode')} + on:change={async () => { + await updateImageGeneration(); + }} + > + <option value="">{$i18n.t('Default (Automatic1111)')}</option> + <option value="comfyui">{$i18n.t('ComfyUI')}</option> + <option value="openai">{$i18n.t('Open AI (Dall-E)')}</option> + </select> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Image Generation (Experimental)')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') { + toast.error($i18n.t('AUTOMATIC1111 Base URL is required.')); + enableImageGeneration = false; + } else if (imageGenerationEngine === 'comfyui' && COMFYUI_BASE_URL === '') { + toast.error($i18n.t('ComfyUI Base URL is required.')); + enableImageGeneration = false; + } else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') { + toast.error($i18n.t('OpenAI API Key is required.')); + enableImageGeneration = false; + } else { + enableImageGeneration = !enableImageGeneration; + } + + updateImageGeneration(); + }} + type="button" + > + {#if enableImageGeneration === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + </div> + <hr class=" dark:border-gray-850" /> + + {#if imageGenerationEngine === ''} + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} + bind:value={AUTOMATIC1111_BASE_URL} + /> + </div> + <button + class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + type="button" + on:click={() => { + updateUrlHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + + <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Include `--api` flag when running stable-diffusion-webui')} + <a + class=" text-gray-300 font-medium" + href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734" + target="_blank" + > + {$i18n.t('(e.g. `sh webui.sh --api`)')} + </a> + </div> + + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Api Auth String')}</div> + <SensitiveInput + placeholder={$i18n.t('Enter api auth string (e.g. username:password)')} + bind:value={AUTOMATIC1111_API_AUTH} + required={false} + /> + + <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Include `--api-auth` flag when running stable-diffusion-webui')} + <a + class=" text-gray-300 font-medium" + href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/13993" + target="_blank" + > + {$i18n.t('(e.g. `sh webui.sh --api --api-auth username_password`)').replace('_', ':')} + </a> + </div> + {:else if imageGenerationEngine === 'comfyui'} + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('ComfyUI Base URL')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')} + bind:value={COMFYUI_BASE_URL} + /> + </div> + <button + class="px-2.5 bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + type="button" + on:click={() => { + updateUrlHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + {:else if imageGenerationEngine === 'openai'} + <div> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('OpenAI API Config')}</div> + + <div class="flex gap-2 mb-1"> + <input + class="flex-1 w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('API Base URL')} + bind:value={OPENAI_API_BASE_URL} + required + /> + + <SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OPENAI_API_KEY} /> + </div> + </div> + {/if} + + {#if enableImageGeneration} + <hr class=" dark:border-gray-850" /> + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default Model')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + {#if imageGenerationEngine === 'openai' && !OPENAI_API_BASE_URL.includes('https://api.openai.com')} + <div class="flex w-full"> + <div class="flex-1"> + <input + list="model-list" + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedModel} + placeholder="Select a model" + /> + + <datalist id="model-list"> + {#each models ?? [] as model} + <option value={model.id}>{model.name}</option> + {/each} + </datalist> + </div> + </div> + {:else} + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedModel} + placeholder={$i18n.t('Select a model')} + required + > + {#if !selectedModel} + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {/if} + {#each models ?? [] as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option + > + {/each} + </select> + {/if} + </div> + </div> + </div> + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter Image Size (e.g. 512x512)')} + bind:value={imageSize} + /> + </div> + </div> + </div> + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter Number of Steps (e.g. 50)')} + bind:value={steps} + /> + </div> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading + ? ' cursor-not-allowed' + : ''}" + type="submit" + disabled={loading} + > + {$i18n.t('Save')} + + {#if loading} + <div class="ml-2 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a5147a37576b623f4e00d666412f4309ed5b21ca --- /dev/null +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -0,0 +1,341 @@ +<script lang="ts"> + import { v4 as uuidv4 } from 'uuid'; + import { toast } from 'svelte-sonner'; + + import { getBackendConfig, getTaskConfig, updateTaskConfig } from '$lib/apis'; + import { setDefaultPromptSuggestions } from '$lib/apis/configs'; + import { config, models, settings, user } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext } from 'svelte'; + + import { banners as _banners } from '$lib/stores'; + import type { Banner } from '$lib/types'; + + import { getBanners, setBanners } from '$lib/apis/configs'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + let taskConfig = { + TASK_MODEL: '', + TASK_MODEL_EXTERNAL: '', + TITLE_GENERATION_PROMPT_TEMPLATE: '', + SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE: '', + SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD: 0 + }; + + let promptSuggestions = []; + let banners: Banner[] = []; + + const updateInterfaceHandler = async () => { + taskConfig = await updateTaskConfig(localStorage.token, taskConfig); + + promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions); + await updateBanners(); + + await config.set(await getBackendConfig()); + }; + + onMount(async () => { + taskConfig = await getTaskConfig(localStorage.token); + + promptSuggestions = $config?.default_prompt_suggestions; + + banners = await getBanners(localStorage.token); + }); + + const updateBanners = async () => { + _banners.set(await setBanners(localStorage.token, banners)); + }; +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + updateInterfaceHandler(); + dispatch('save'); + }} +> + <div class=" overflow-y-scroll scrollbar-hidden h-full pr-1.5"> + <div> + <div class=" mb-2.5 text-sm font-medium flex"> + <div class=" mr-1">{$i18n.t('Set Task Model')}</div> + <Tooltip + content={$i18n.t( + 'A task model is used when performing tasks such as generating titles for chats and web search queries' + )} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" + /> + </svg> + </Tooltip> + </div> + <div class="flex w-full gap-2"> + <div class="flex-1"> + <div class=" text-xs mb-1">{$i18n.t('Local Models')}</div> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={taskConfig.TASK_MODEL} + placeholder={$i18n.t('Select a model')} + > + <option value="" selected>{$i18n.t('Current Model')}</option> + {#each $models.filter((m) => m.owned_by === 'ollama') as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700"> + {model.name} + </option> + {/each} + </select> + </div> + + <div class="flex-1"> + <div class=" text-xs mb-1">{$i18n.t('External Models')}</div> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={taskConfig.TASK_MODEL_EXTERNAL} + placeholder={$i18n.t('Select a model')} + > + <option value="" selected>{$i18n.t('Current Model')}</option> + {#each $models as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700"> + {model.name} + </option> + {/each} + </select> + </div> + </div> + + <div class="mt-3"> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div> + <textarea + bind:value={taskConfig.TITLE_GENERATION_PROMPT_TEMPLATE} + class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + rows="6" + /> + </div> + + <div class="mt-3"> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Search Query Generation Prompt')}</div> + <textarea + bind:value={taskConfig.SEARCH_QUERY_GENERATION_PROMPT_TEMPLATE} + class="w-full rounded-lg py-3 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + rows="6" + /> + </div> + + <div class="mt-3"> + <div class=" mb-2.5 text-sm font-medium"> + {$i18n.t('Search Query Generation Prompt Length Threshold')} + </div> + <input + bind:value={taskConfig.SEARCH_QUERY_PROMPT_LENGTH_THRESHOLD} + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + type="number" + /> + </div> + </div> + + <hr class=" dark:border-gray-850 my-3" /> + + <div class=" space-y-3 {banners.length > 0 ? ' mb-3' : ''}"> + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-semibold"> + {$i18n.t('Banners')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + if (banners.length === 0 || banners.at(-1).content !== '') { + banners = [ + ...banners, + { + id: uuidv4(), + type: '', + title: '', + content: '', + dismissible: true, + timestamp: Math.floor(Date.now() / 1000) + } + ]; + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" + /> + </svg> + </button> + </div> + <div class="flex flex-col space-y-1"> + {#each banners as banner, bannerIdx} + <div class=" flex justify-between"> + <div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800"> + <select + class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none" + bind:value={banner.type} + required + > + {#if banner.type == ''} + <option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option + > + {/if} + <option value="info" class="text-gray-900">{$i18n.t('Info')}</option> + <option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option> + <option value="error" class="text-gray-900">{$i18n.t('Error')}</option> + <option value="success" class="text-gray-900">{$i18n.t('Success')}</option> + </select> + + <input + class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none" + placeholder={$i18n.t('Content')} + bind:value={banner.content} + /> + + <div class="relative top-1.5 -left-2"> + <Tooltip content={$i18n.t('Dismissible')} className="flex h-fit items-center"> + <Switch bind:state={banner.dismissible} /> + </Tooltip> + </div> + </div> + + <button + class="px-2" + type="button" + on:click={() => { + banners.splice(bannerIdx, 1); + banners = banners; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + {/each} + </div> + </div> + + {#if $user.role === 'admin'} + <div class=" space-y-3"> + <div class="flex w-full justify-between mb-2"> + <div class=" self-center text-sm font-semibold"> + {$i18n.t('Default Prompt Suggestions')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + if (promptSuggestions.length === 0 || promptSuggestions.at(-1).content !== '') { + promptSuggestions = [...promptSuggestions, { content: '', title: ['', ''] }]; + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" + /> + </svg> + </button> + </div> + <div class="grid lg:grid-cols-2 flex-col gap-1.5"> + {#each promptSuggestions as prompt, promptIdx} + <div + class=" flex border border-gray-100 dark:border-none dark:bg-gray-850 rounded-xl py-1.5" + > + <div class="flex flex-col flex-1 pl-1"> + <div class="flex border-b border-gray-100 dark:border-gray-800 w-full"> + <input + class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800" + placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')} + bind:value={prompt.title[0]} + /> + + <input + class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800" + placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')} + bind:value={prompt.title[1]} + /> + </div> + + <input + class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r border-gray-100 dark:border-gray-800" + placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')} + bind:value={prompt.content} + /> + </div> + + <button + class="px-3" + type="button" + on:click={() => { + promptSuggestions.splice(promptIdx, 1); + promptSuggestions = promptSuggestions; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + {/each} + </div> + + {#if promptSuggestions.length > 0} + <div class="text-xs text-left w-full mt-2"> + {$i18n.t('Adjusting these settings will apply changes universally to all users.')} + </div> + {/if} + </div> + {/if} + </div> + + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Models.svelte b/src/lib/components/admin/Settings/Models.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5e975ec0a2fc4a9e856efb917afcc7e747d79267 --- /dev/null +++ b/src/lib/components/admin/Settings/Models.svelte @@ -0,0 +1,1090 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { onMount, getContext } from 'svelte'; + + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores'; + import { splitStream } from '$lib/utils'; + + import { + createModel, + deleteModel, + downloadModel, + getOllamaUrls, + getOllamaVersion, + pullModel, + uploadModel, + getOllamaConfig + } from '$lib/apis/ollama'; + import { getModels as _getModels } from '$lib/apis'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import ModelDeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + const getModels = async () => { + return await _getModels(localStorage.token); + }; + + let modelUploadInputElement: HTMLInputElement; + + let showModelDeleteConfirm = false; + + // Models + + let ollamaEnabled = null; + + let OLLAMA_URLS = []; + let selectedOllamaUrlIdx: string | null = null; + + let updateModelId = null; + let updateProgress = null; + + let showExperimentalOllama = false; + + let ollamaVersion = null; + const MAX_PARALLEL_DOWNLOADS = 3; + + let modelTransferring = false; + let modelTag = ''; + + let createModelLoading = false; + let createModelTag = ''; + let createModelContent = ''; + let createModelDigest = ''; + let createModelPullProgress = null; + + let digest = ''; + let pullProgress = null; + + let modelUploadMode = 'file'; + let modelInputFile: File[] | null = null; + let modelFileUrl = ''; + let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`; + let modelFileDigest = ''; + + let uploadProgress = null; + let uploadMessage = ''; + + let deleteModelTag = ''; + + const updateModelsHandler = async () => { + for (const model of $models.filter( + (m) => + !(m?.preset ?? false) && + m.owned_by === 'ollama' && + (selectedOllamaUrlIdx === null + ? true + : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx)) + )) { + console.log(model); + + updateModelId = model.id; + const [res, controller] = await pullModel( + localStorage.token, + model.id, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + try { + const { value, done } = await reader.read(); + if (done) break; + + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line); + + console.log(data); + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + if (data.status) { + if (data.digest) { + updateProgress = 0; + if (data.completed) { + updateProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + updateProgress = 100; + } + } else { + toast.success(data.status); + } + } + } + } + } catch (error) { + console.log(error); + } + } + } + } + + updateModelId = null; + updateProgress = null; + }; + + const pullModelHandler = async () => { + const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, ''); + console.log($MODEL_DOWNLOAD_POOL); + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) { + toast.error( + $i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, { + modelTag: sanitizedModelTag + }) + ); + return; + } + if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) { + toast.error( + $i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.') + ); + return; + } + + const [res, controller] = await pullModel( + localStorage.token, + sanitizedModelTag, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + abortController: controller, + reader, + done: false + } + }); + + while (true) { + try { + const { value, done } = await reader.read(); + if (done) break; + + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line); + console.log(data); + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if (data.digest) { + let downloadProgress = 0; + if (data.completed) { + downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + downloadProgress = 100; + } + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + pullProgress: downloadProgress, + digest: data.digest + } + }); + } else { + toast.success(data.status); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + done: data.status === 'success' + } + }); + } + } + } + } + } catch (error) { + console.log(error); + if (typeof error !== 'string') { + error = error.message; + } + + toast.error(error); + // opts.callback({ success: false, error, modelName: opts.modelName }); + } + } + + console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]); + + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) { + toast.success( + $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { + modelName: sanitizedModelTag + }) + ); + + models.set(await getModels()); + } else { + toast.error($i18n.t('Download canceled')); + } + + delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag]; + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + } + + modelTag = ''; + modelTransferring = false; + }; + + const uploadModelHandler = async () => { + modelTransferring = true; + + let uploaded = false; + let fileResponse = null; + let name = ''; + + if (modelUploadMode === 'file') { + const file = modelInputFile ? modelInputFile[0] : null; + + if (file) { + uploadMessage = 'Uploading...'; + + fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch( + (error) => { + toast.error(error); + return null; + } + ); + } + } else { + uploadProgress = 0; + fileResponse = await downloadModel( + localStorage.token, + modelFileUrl, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + } + + if (fileResponse && fileResponse.ok) { + const reader = fileResponse.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line.replace(/^data: /, '')); + + if (data.progress) { + if (uploadMessage) { + uploadMessage = ''; + } + uploadProgress = data.progress; + } + + if (data.error) { + throw data.error; + } + + if (data.done) { + modelFileDigest = data.blob; + name = data.name; + uploaded = true; + } + } + } + } catch (error) { + console.log(error); + } + } + } else { + const error = await fileResponse?.json(); + toast.error(error?.detail ?? error); + } + + if (uploaded) { + const res = await createModel( + localStorage.token, + `${name}:latest`, + `FROM @${modelFileDigest}\n${modelFileContent}` + ); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + console.log(data); + + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if ( + !data.digest && + !data.status.includes('writing') && + !data.status.includes('sha256') + ) { + toast.success(data.status); + } else { + if (data.digest) { + digest = data.digest; + + if (data.completed) { + pullProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + pullProgress = 100; + } + } + } + } + } + } + } catch (error) { + console.log(error); + toast.error(error); + } + } + } + } + + modelFileUrl = ''; + + if (modelUploadInputElement) { + modelUploadInputElement.value = ''; + } + modelInputFile = null; + modelTransferring = false; + uploadProgress = null; + + models.set(await getModels()); + }; + + const deleteModelHandler = async () => { + const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch( + (error) => { + toast.error(error); + } + ); + + if (res) { + toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag })); + } + + deleteModelTag = ''; + models.set(await getModels()); + }; + + const cancelModelPullHandler = async (model: string) => { + const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model]; + if (abortController) { + abortController.abort(); + } + if (reader) { + await reader.cancel(); + delete $MODEL_DOWNLOAD_POOL[model]; + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + await deleteModel(localStorage.token, model); + toast.success(`${model} download has been canceled`); + } + }; + + const createModelHandler = async () => { + createModelLoading = true; + const res = await createModel( + localStorage.token, + createModelTag, + createModelContent, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + console.log(data); + + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if ( + !data.digest && + !data.status.includes('writing') && + !data.status.includes('sha256') + ) { + toast.success(data.status); + } else { + if (data.digest) { + createModelDigest = data.digest; + + if (data.completed) { + createModelPullProgress = + Math.round((data.completed / data.total) * 1000) / 10; + } else { + createModelPullProgress = 100; + } + } + } + } + } + } + } catch (error) { + console.log(error); + toast.error(error); + } + } + } + + models.set(await getModels()); + + createModelLoading = false; + + createModelTag = ''; + createModelContent = ''; + createModelDigest = ''; + createModelPullProgress = null; + }; + + onMount(async () => { + const ollamaConfig = await getOllamaConfig(localStorage.token); + + if (ollamaConfig.ENABLE_OLLAMA_API) { + ollamaEnabled = true; + + await Promise.all([ + (async () => { + OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => { + toast.error(error); + return []; + }); + + if (OLLAMA_URLS.length > 0) { + selectedOllamaUrlIdx = 0; + } + })(), + (async () => { + ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); + })() + ]); + } else { + ollamaEnabled = false; + toast.error($i18n.t('Ollama API is disabled')); + } + }); +</script> + +<ModelDeleteConfirmDialog + bind:show={showModelDeleteConfirm} + on:confirm={() => { + deleteModelHandler(); + }} +/> + +<div class="flex flex-col h-full justify-between text-sm"> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + {#if ollamaEnabled} + {#if ollamaVersion !== null} + <div class="space-y-2 pr-1.5"> + <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> + + {#if OLLAMA_URLS.length > 0} + <div class="flex gap-2"> + <div class="flex-1 pb-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedOllamaUrlIdx} + placeholder={$i18n.t('Select an Ollama instance')} + > + {#each OLLAMA_URLS as url, idx} + <option value={idx} class="bg-gray-50 dark:bg-gray-700">{url}</option> + {/each} + </select> + </div> + + <div> + <div class="flex w-full justify-end"> + <Tooltip content="Update All Models" placement="top"> + <button + class="p-2.5 flex gap-2 items-center bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + updateModelsHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z" + /> + <path + d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + </div> + + {#if updateModelId} + Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''} + {/if} + {/if} + + <div class="space-y-2"> + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { + modelTag: 'mistral:7b' + })} + bind:value={modelTag} + /> + </div> + <button + class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + pullModelHandler(); + }} + disabled={modelTransferring} + > + {#if modelTransferring} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + </div> + + <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('To access the available model names for downloading,')} + <a + class=" text-gray-500 dark:text-gray-300 font-medium underline" + href="https://ollama.com/library" + target="_blank">{$i18n.t('click here.')}</a + > + </div> + + {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0} + {#each Object.keys($MODEL_DOWNLOAD_POOL) as model} + {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]} + <div class="flex flex-col"> + <div class="font-medium mb-1">{model}</div> + <div class=""> + <div class="flex flex-row justify-between space-x-4 pr-2"> + <div class=" flex-1"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max( + 15, + $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0 + )}%" + > + {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}% + </div> + </div> + + <Tooltip content={$i18n.t('Cancel')}> + <button + class="text-gray-800 dark:text-gray-100" + on:click={() => { + cancelModelPullHandler(model); + }} + > + <svg + class="w-4 h-4 text-gray-800 dark:text-white" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="currentColor" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M6 18 17.94 6M18 18 6.06 6" + /> + </svg> + </button> + </Tooltip> + </div> + {#if 'digest' in $MODEL_DOWNLOAD_POOL[model]} + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {$MODEL_DOWNLOAD_POOL[model].digest} + </div> + {/if} + </div> + </div> + {/if} + {/each} + {/if} + </div> + + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={deleteModelTag} + placeholder={$i18n.t('Select a model')} + > + {#if !deleteModelTag} + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {/if} + {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} + <option value={model.name} class="bg-gray-50 dark:bg-gray-700" + >{model.name + + ' (' + + (model.ollama.size / 1024 ** 3).toFixed(1) + + ' GB)'}</option + > + {/each} + </select> + </div> + <button + class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + showModelDeleteConfirm = true; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + </div> + + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2 flex flex-col gap-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { + modelTag: 'my-modelfile' + })} + bind:value={createModelTag} + disabled={createModelLoading} + /> + + <textarea + bind:value={createModelContent} + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden" + rows="6" + placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`} + disabled={createModelLoading} + /> + </div> + + <div class="flex self-start"> + <button + class="px-2.5 py-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed" + on:click={() => { + createModelHandler(); + }} + disabled={createModelLoading} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + </button> + </div> + </div> + + {#if createModelDigest !== ''} + <div class="flex flex-col mt-1"> + <div class="font-medium mb-1">{createModelTag}</div> + <div class=""> + <div class="flex flex-row justify-between space-x-4 pr-2"> + <div class=" flex-1"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max(15, createModelPullProgress ?? 0)}%" + > + {createModelPullProgress ?? 0}% + </div> + </div> + </div> + {#if createModelDigest} + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {createModelDigest} + </div> + {/if} + </div> + </div> + {/if} + </div> + + <div class="pt-1"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div> + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + showExperimentalOllama = !showExperimentalOllama; + }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button + > + </div> + </div> + + {#if showExperimentalOllama} + <form + on:submit|preventDefault={() => { + uploadModelHandler(); + }} + > + <div class=" mb-2 flex w-full justify-between"> + <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + if (modelUploadMode === 'file') { + modelUploadMode = 'url'; + } else { + modelUploadMode = 'file'; + } + }} + type="button" + > + {#if modelUploadMode === 'file'} + <span class="ml-2 self-center">{$i18n.t('File Mode')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span> + {/if} + </button> + </div> + + <div class="flex w-full mb-1.5"> + <div class="flex flex-col w-full"> + {#if modelUploadMode === 'file'} + <div + class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}" + > + <input + id="model-upload-input" + bind:this={modelUploadInputElement} + type="file" + bind:files={modelInputFile} + on:change={() => { + console.log(modelInputFile); + }} + accept=".gguf,.safetensors" + required + hidden + /> + + <button + type="button" + class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850" + on:click={() => { + modelUploadInputElement.click(); + }} + > + {#if modelInputFile && modelInputFile.length > 0} + {modelInputFile[0].name} + {:else} + {$i18n.t('Click here to select')} + {/if} + </button> + </div> + {:else} + <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> + <input + class="w-full rounded-lg text-left py-2 px-4 bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== + '' + ? 'mr-2' + : ''}" + type="url" + required + bind:value={modelFileUrl} + placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')} + /> + </div> + {/if} + </div> + + {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} + <button + class="px-2.5 bg-gray-50 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition" + type="submit" + disabled={modelTransferring} + > + {#if modelTransferring} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + {/if} + </div> + + {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} + <div> + <div> + <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div> + <textarea + bind:value={modelFileContent} + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" + rows="6" + /> + </div> + </div> + {/if} + <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('To access the GGUF models available for downloading,')} + <a + class=" text-gray-500 dark:text-gray-300 font-medium underline" + href="https://huggingface.co/models?search=gguf" + target="_blank">{$i18n.t('click here.')}</a + > + </div> + + {#if uploadMessage} + <div class="mt-2"> + <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> + + <div class="w-full rounded-full dark:bg-gray-800"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: 100%" + > + {uploadMessage} + </div> + </div> + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {modelFileDigest} + </div> + </div> + {:else if uploadProgress !== null} + <div class="mt-2"> + <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> + + <div class="w-full rounded-full dark:bg-gray-800"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max(15, uploadProgress ?? 0)}%" + > + {uploadProgress ?? 0}% + </div> + </div> + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {modelFileDigest} + </div> + </div> + {/if} + </form> + {/if} + </div> + </div> + {:else if ollamaVersion === false} + <div>Ollama Not Detected</div> + {:else} + <div class="flex h-full justify-center"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + {:else if ollamaEnabled === false} + <div>{$i18n.t('Ollama API is disabled')}</div> + {:else} + <div class="flex h-full justify-center"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/admin/Settings/Pipelines.svelte b/src/lib/components/admin/Settings/Pipelines.svelte new file mode 100644 index 0000000000000000000000000000000000000000..15a46f34a9cd05c1d7a155670cb9dc02ece7b034 --- /dev/null +++ b/src/lib/components/admin/Settings/Pipelines.svelte @@ -0,0 +1,557 @@ +<script lang="ts"> + import { v4 as uuidv4 } from 'uuid'; + + import { toast } from 'svelte-sonner'; + import { models } from '$lib/stores'; + import { getContext, onMount, tick } from 'svelte'; + import type { Writable } from 'svelte/store'; + import type { i18n as i18nType } from 'i18next'; + import { + getPipelineValves, + getPipelineValvesSpec, + updatePipelineValves, + getPipelines, + getModels, + getPipelinesList, + downloadPipeline, + deletePipeline, + uploadPipeline + } from '$lib/apis'; + + import Spinner from '$lib/components/common/Spinner.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + + const i18n: Writable<i18nType> = getContext('i18n'); + + export let saveHandler: Function; + + let downloading = false; + let uploading = false; + + let pipelineFiles; + + let PIPELINES_LIST = null; + let selectedPipelinesUrlIdx = ''; + + let pipelines = null; + + let valves = null; + let valves_spec = null; + let selectedPipelineIdx = null; + + let pipelineDownloadUrl = ''; + + const updateHandler = async () => { + const pipeline = pipelines[selectedPipelineIdx]; + + if (pipeline && (pipeline?.valves ?? false)) { + for (const property in valves_spec.properties) { + if (valves_spec.properties[property]?.type === 'array') { + valves[property] = valves[property].split(',').map((v) => v.trim()); + } + } + + const res = await updatePipelineValves( + localStorage.token, + pipeline.id, + valves, + selectedPipelinesUrlIdx + ).catch((error) => { + toast.error(error); + }); + + if (res) { + toast.success($i18n.t('Valves updated successfully')); + setPipelines(); + models.set(await getModels(localStorage.token)); + saveHandler(); + } + } else { + toast.error($i18n.t('No valves to update')); + } + }; + + const getValves = async (idx) => { + valves = null; + valves_spec = null; + + valves_spec = await getPipelineValvesSpec( + localStorage.token, + pipelines[idx].id, + selectedPipelinesUrlIdx + ); + valves = await getPipelineValves( + localStorage.token, + pipelines[idx].id, + selectedPipelinesUrlIdx + ); + + for (const property in valves_spec.properties) { + if (valves_spec.properties[property]?.type === 'array') { + valves[property] = valves[property].join(','); + } + } + }; + + const setPipelines = async () => { + pipelines = null; + valves = null; + valves_spec = null; + + if (PIPELINES_LIST.length > 0) { + console.log(selectedPipelinesUrlIdx); + pipelines = await getPipelines(localStorage.token, selectedPipelinesUrlIdx); + + if (pipelines.length > 0) { + selectedPipelineIdx = 0; + await getValves(selectedPipelineIdx); + } + } else { + pipelines = []; + } + }; + + const addPipelineHandler = async () => { + downloading = true; + const res = await downloadPipeline( + localStorage.token, + pipelineDownloadUrl, + selectedPipelinesUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Pipeline downloaded successfully')); + setPipelines(); + models.set(await getModels(localStorage.token)); + } + + downloading = false; + }; + + const uploadPipelineHandler = async () => { + uploading = true; + + if (pipelineFiles && pipelineFiles.length !== 0) { + const file = pipelineFiles[0]; + + console.log(file); + + const res = await uploadPipeline(localStorage.token, file, selectedPipelinesUrlIdx).catch( + (error) => { + console.log(error); + toast.error('Something went wrong :/'); + return null; + } + ); + + if (res) { + toast.success($i18n.t('Pipeline downloaded successfully')); + setPipelines(); + models.set(await getModels(localStorage.token)); + } + } else { + toast.error($i18n.t('No file selected')); + } + + pipelineFiles = null; + const pipelineUploadInputElement = document.getElementById('pipeline-upload-input'); + + if (pipelineUploadInputElement) { + pipelineUploadInputElement.value = null; + } + + uploading = false; + }; + + const deletePipelineHandler = async () => { + const res = await deletePipeline( + localStorage.token, + pipelines[selectedPipelineIdx].id, + selectedPipelinesUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Pipeline deleted successfully')); + setPipelines(); + models.set(await getModels(localStorage.token)); + } + }; + + onMount(async () => { + PIPELINES_LIST = await getPipelinesList(localStorage.token); + console.log(PIPELINES_LIST); + + if (PIPELINES_LIST.length > 0) { + selectedPipelinesUrlIdx = PIPELINES_LIST[0]['idx'].toString(); + } + + await setPipelines(); + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + updateHandler(); + }} +> + <div class="overflow-y-scroll scrollbar-hidden h-full"> + {#if PIPELINES_LIST !== null} + <div class="flex w-full justify-between mb-2"> + <div class=" self-center text-sm font-semibold"> + {$i18n.t('Manage Pipelines')} + </div> + </div> + + {#if PIPELINES_LIST.length > 0} + <div class="space-y-1"> + <div class="flex gap-2"> + <div class="flex-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedPipelinesUrlIdx} + placeholder={$i18n.t('Select a pipeline url')} + on:change={async () => { + await tick(); + await setPipelines(); + }} + > + <option value="" selected disabled class="bg-gray-100 dark:bg-gray-700" + >{$i18n.t('Select a pipeline url')}</option + > + + {#each PIPELINES_LIST as pipelines, idx} + <option value={pipelines.idx.toString()} class="bg-gray-100 dark:bg-gray-700" + >{pipelines.url}</option + > + {/each} + </select> + </div> + </div> + </div> + + <div class=" my-2"> + <div class=" mb-2 text-sm font-medium"> + {$i18n.t('Upload Pipeline')} + </div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + id="pipelines-upload-input" + bind:files={pipelineFiles} + type="file" + accept=".py" + hidden + /> + + <button + class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-800 dark:hover:bg-gray-850 text-center rounded-xl" + type="button" + on:click={() => { + document.getElementById('pipelines-upload-input')?.click(); + }} + > + {#if pipelineFiles} + {pipelineFiles.length > 0 ? `${pipelineFiles.length}` : ''} pipeline(s) selected. + {:else} + {$i18n.t('Click here to select a py file.')} + {/if} + </button> + </div> + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + uploadPipelineHandler(); + }} + disabled={uploading} + type="button" + > + {#if uploading} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + </div> + </div> + + <div class=" my-2"> + <div class=" mb-2 text-sm font-medium"> + {$i18n.t('Install from Github URL')} + </div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter Github Raw URL')} + bind:value={pipelineDownloadUrl} + /> + </div> + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + addPipelineHandler(); + }} + disabled={downloading} + type="button" + > + {#if downloading} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + </div> + + <div class="mt-2 text-xs text-gray-500"> + <span class=" font-semibold dark:text-gray-200">Warning:</span> Pipelines are a plugin + system with arbitrary code execution — + <span class=" font-medium dark:text-gray-400" + >don't fetch random pipelines from sources you don't trust.</span + > + </div> + </div> + + <hr class=" dark:border-gray-800 my-3 w-full" /> + + {#if pipelines !== null} + {#if pipelines.length > 0} + <div class="flex w-full justify-between mb-2"> + <div class=" self-center text-sm font-semibold"> + {$i18n.t('Pipelines Valves')} + </div> + </div> + <div class="space-y-1"> + {#if pipelines.length > 0} + <div class="flex gap-2"> + <div class="flex-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedPipelineIdx} + placeholder={$i18n.t('Select a pipeline')} + on:change={async () => { + await tick(); + await getValves(selectedPipelineIdx); + }} + > + {#each pipelines as pipeline, idx} + <option value={idx} class="bg-gray-100 dark:bg-gray-700" + >{pipeline.name} ({pipeline.type ?? 'pipe'})</option + > + {/each} + </select> + </div> + + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + deletePipelineHandler(); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + {/if} + + <div class="space-y-1"> + {#if pipelines[selectedPipelineIdx].valves} + {#if valves} + {#each Object.keys(valves_spec.properties) as property, idx} + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {valves_spec.properties[property].title} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + valves[property] = (valves[property] ?? null) === null ? '' : null; + }} + > + {#if (valves[property] ?? null) === null} + <span class="ml-2 self-center"> {$i18n.t('None')} </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if (valves[property] ?? null) !== null} + <!-- {valves[property]} --> + <div class="flex mt-0.5 mb-1.5 space-x-2"> + <div class=" flex-1"> + {#if valves_spec.properties[property]?.enum ?? null} + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={valves[property]} + > + {#each valves_spec.properties[property].enum as option} + <option value={option} selected={option === valves[property]}> + {option} + </option> + {/each} + </select> + {:else if (valves_spec.properties[property]?.type ?? null) === 'boolean'} + <div class="flex justify-between items-center"> + <div class="text-xs text-gray-500"> + {valves[property] ? 'Enabled' : 'Disabled'} + </div> + + <div class=" pr-2"> + <Switch bind:state={valves[property]} /> + </div> + </div> + {:else} + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={valves_spec.properties[property].title} + bind:value={valves[property]} + autocomplete="off" + required + /> + {/if} + </div> + </div> + {/if} + </div> + {/each} + {:else} + <Spinner className="size-5" /> + {/if} + {:else} + <div>No valves</div> + {/if} + </div> + </div> + {:else if pipelines.length === 0} + <div>Pipelines Not Detected</div> + {/if} + {:else} + <div class="flex justify-center"> + <div class="my-auto"> + <Spinner className="size-4" /> + </div> + </div> + {/if} + {:else} + <div>{$i18n.t('Pipelines Not Detected')}</div> + {/if} + {:else} + <div class="flex justify-center h-full"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/Users.svelte b/src/lib/components/admin/Settings/Users.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d9d537c542e03e7aebe5a9cda3fff970db54bc22 --- /dev/null +++ b/src/lib/components/admin/Settings/Users.svelte @@ -0,0 +1,221 @@ +<script lang="ts"> + import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis'; + import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; + import { getUserPermissions, updateUserPermissions } from '$lib/apis/users'; + + import { onMount, getContext } from 'svelte'; + import { models, config } from '$lib/stores'; + import Switch from '$lib/components/common/Switch.svelte'; + import { setDefaultModels } from '$lib/apis/configs'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let defaultModelId = ''; + + let whitelistEnabled = false; + let whitelistModels = ['']; + let permissions = { + chat: { + deletion: true + } + }; + + onMount(async () => { + permissions = await getUserPermissions(localStorage.token); + + const res = await getModelFilterConfig(localStorage.token); + if (res) { + whitelistEnabled = res.enabled; + whitelistModels = res.models.length > 0 ? res.models : ['']; + } + + defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : ''; + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + // console.log('submit'); + + await setDefaultModels(localStorage.token, defaultModelId); + await updateUserPermissions(localStorage.token, permissions); + await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels); + saveHandler(); + + await config.set(await getBackendConfig()); + }} +> + <div class=" space-y-3 overflow-y-scroll max-h-full"> + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('User Permissions')}</div> + + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + permissions.chat.deletion = !(permissions?.chat?.deletion ?? true); + }} + type="button" + > + {#if permissions?.chat?.deletion ?? true} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z" + /> + </svg> + <span class="ml-2 self-center">{$i18n.t('Allow')}</span> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" + clip-rule="evenodd" + /> + </svg> + + <span class="ml-2 self-center">{$i18n.t("Don't Allow")}</span> + {/if} + </button> + </div> + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div class="mt-2 space-y-3"> + <div> + <div class="mb-2"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div> + </div> + </div> + <div class=" space-y-1 mb-3"> + <div class="mb-2"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-xs font-medium">{$i18n.t('Default Model')}</div> + </div> + </div> + + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={defaultModelId} + placeholder="Select a model" + > + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {#each $models.filter((model) => model.id) as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> + {/each} + </select> + </div> + </div> + + <div class=" space-y-1"> + <div class="mb-2"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div> + + <Switch bind:state={whitelistEnabled} /> + </div> + </div> + + {#if whitelistEnabled} + <div> + <div class=" space-y-1.5"> + {#each whitelistModels as modelId, modelIdx} + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={modelId} + placeholder="Select a model" + > + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {#each $models.filter((model) => model.id) as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700" + >{model.name}</option + > + {/each} + </select> + </div> + + {#if modelIdx === 0} + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition" + type="button" + on:click={() => { + if (whitelistModels.at(-1) !== '') { + whitelistModels = [...whitelistModels, '']; + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + {:else} + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-900 dark:text-white rounded-lg transition" + type="button" + on:click={() => { + whitelistModels.splice(modelIdx, 1); + whitelistModels = whitelistModels; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" /> + </svg> + </button> + {/if} + </div> + {/each} + </div> + + <div class="flex justify-end items-center text-xs mt-1.5 text-right"> + <div class=" text-xs font-medium"> + {whitelistModels.length} + {$i18n.t('Model(s) Whitelisted')} + </div> + </div> + </div> + {/if} + </div> + </div> + </div> + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1e5531dd8288c434cfcb292be04c99da3f3b0dc2 --- /dev/null +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -0,0 +1,290 @@ +<script lang="ts"> + import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag'; + import Switch from '$lib/components/common/Switch.svelte'; + + import { documents, models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let webConfig = null; + let webSearchEngines = [ + 'searxng', + 'google_pse', + 'brave', + 'serpstack', + 'serper', + 'serply', + 'duckduckgo', + 'tavily', + 'jina' + ]; + + let youtubeLanguage = 'en'; + let youtubeTranslation = null; + + const submitHandler = async () => { + const res = await updateRAGConfig(localStorage.token, { + web: webConfig, + youtube: { + language: youtubeLanguage.split(',').map((lang) => lang.trim()), + translation: youtubeTranslation + } + }); + }; + + onMount(async () => { + const res = await getRAGConfig(localStorage.token); + + if (res) { + webConfig = res.web; + + youtubeLanguage = res.youtube.language.join(','); + youtubeTranslation = res.youtube.translation; + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + await submitHandler(); + saveHandler(); + }} +> + <div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full"> + {#if webConfig} + <div> + <div class=" mb-1 text-sm font-medium"> + {$i18n.t('Web Search')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Enable Web Search')} + </div> + + <Switch bind:state={webConfig.search.enabled} /> + </div> + </div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={webConfig.search.engine} + placeholder={$i18n.t('Select a engine')} + required + > + <option disabled selected value="">{$i18n.t('Select a engine')}</option> + {#each webSearchEngines as engine} + <option value={engine}>{engine}</option> + {/each} + </select> + </div> + </div> + + {#if webConfig.search.engine !== ''} + <div class="mt-1.5"> + {#if webConfig.search.engine === 'searxng'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Searxng Query URL')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Searxng Query URL')} + bind:value={webConfig.search.searxng_query_url} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'google_pse'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Google PSE API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Google PSE API Key')} + bind:value={webConfig.search.google_pse_api_key} + /> + </div> + <div class="mt-1.5"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Google PSE Engine Id')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Google PSE Engine Id')} + bind:value={webConfig.search.google_pse_engine_id} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'brave'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Brave Search API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Brave Search API Key')} + bind:value={webConfig.search.brave_search_api_key} + /> + </div> + {:else if webConfig.search.engine === 'serpstack'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Serpstack API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Serpstack API Key')} + bind:value={webConfig.search.serpstack_api_key} + /> + </div> + {:else if webConfig.search.engine === 'serper'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Serper API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Serper API Key')} + bind:value={webConfig.search.serper_api_key} + /> + </div> + {:else if webConfig.search.engine === 'serply'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Serply API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Serply API Key')} + bind:value={webConfig.search.serply_api_key} + /> + </div> + {:else if webConfig.search.engine === 'tavily'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Tavily API Key')} + </div> + + <SensitiveInput + placeholder={$i18n.t('Enter Tavily API Key')} + bind:value={webConfig.search.tavily_api_key} + /> + </div> + {/if} + </div> + {/if} + + {#if webConfig.search.enabled} + <div class="mt-2 flex gap-2 mb-1"> + <div class="w-full"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Search Result Count')} + </div> + + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Search Result Count')} + bind:value={webConfig.search.result_count} + required + /> + </div> + + <div class="w-full"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Concurrent Requests')} + </div> + + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Concurrent Requests')} + bind:value={webConfig.search.concurrent_requests} + required + /> + </div> + </div> + {/if} + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div> + <div class=" mb-1 text-sm font-medium"> + {$i18n.t('Web Loader Settings')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Bypass SSL verification for Websites')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + webConfig.ssl_verification = !webConfig.ssl_verification; + submitHandler(); + }} + type="button" + > + {#if webConfig.ssl_verification === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div class=" mt-2 mb-1 text-sm font-medium"> + {$i18n.t('Youtube Loader Settings')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div> + <div class=" flex-1 self-center"> + <input + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter language codes')} + bind:value={youtubeLanguage} + autocomplete="off" + /> + </div> + </div> + </div> + </div> + {/if} + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/admin/UserChatsModal.svelte b/src/lib/components/admin/UserChatsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..535dee0740334b5e805c7c013b2e46e8f02b7231 --- /dev/null +++ b/src/lib/components/admin/UserChatsModal.svelte @@ -0,0 +1,198 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { getContext, createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import Modal from '$lib/components/common/Modal.svelte'; + import { getChatListByUserId, deleteChatById, getArchivedChatList } from '$lib/apis/chats'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + export let user; + + let chats = []; + + const deleteChatHandler = async (chatId) => { + const res = await deleteChatById(localStorage.token, chatId).catch((error) => { + toast.error(error); + }); + + chats = await getChatListByUserId(localStorage.token, user.id); + }; + + $: if (show) { + (async () => { + if (user.id) { + chats = await getChatListByUserId(localStorage.token, user.id); + } + })(); + } + + let sortKey = 'updated_at'; // default sort key + let sortOrder = 'desc'; // default sort order + function setSortKey(key) { + if (sortKey === key) { + sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; + } else { + sortKey = key; + sortOrder = 'asc'; + } + } +</script> + +<Modal size="lg" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 py-4"> + <div class=" text-lg font-medium self-center capitalize"> + {$i18n.t("{{user}}'s Chats", { user: user.name })} + </div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + <hr class=" dark:border-gray-850" /> + + <div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + {#if chats.length > 0} + <div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll"> + <div class="relative overflow-x-auto"> + <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> + <thead + class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800" + > + <tr> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('title')} + > + {$i18n.t('Title')} + {#if sortKey === 'title'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('created_at')} + > + {$i18n.t('Created at')} + {#if sortKey === 'created_at'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 hidden md:flex cursor-pointer select-none" + on:click={() => setSortKey('updated_at')} + > + {$i18n.t('Updated at')} + {#if sortKey === 'updated_at'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th scope="col" class="px-3 py-2 text-right" /> + </tr> + </thead> + <tbody> + {#each chats.sort((a, b) => { + if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1; + if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1; + return 0; + }) as chat, idx} + <tr + class="bg-transparent {idx !== chats.length - 1 && + 'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs" + > + <td class="px-3 py-1"> + <a href="/s/{chat.id}" target="_blank"> + <div class=" underline line-clamp-1"> + {chat.title} + </div> + </a> + </td> + + <td class=" px-3 py-1 h-[2.5rem]"> + <div class="my-auto"> + {dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} + </div> + </td> + <td class=" px-3 py-1 hidden md:flex h-[2.5rem]"> + <div class="my-auto"> + {dayjs(chat.updated_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} + </div> + </td> + + <td class="px-3 py-1 text-right"> + <div class="flex justify-end w-full"> + <Tooltip content={$i18n.t('Delete Chat')}> + <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 () => { + deleteChatHandler(chat.id); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </Tooltip> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + </div> + <!-- {#each chats as chat} + <div> + {JSON.stringify(chat)} + </div> + {/each} --> + </div> + {:else} + <div class="text-left text-sm w-full mb-8"> + {user.name} + {$i18n.t('has no conversations.')} + </div> + {/if} + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte new file mode 100644 index 0000000000000000000000000000000000000000..24e74e69557c0b4db117a1eb8e7aa1c16f2d2742 --- /dev/null +++ b/src/lib/components/chat/Chat.svelte @@ -0,0 +1,1645 @@ +<script lang="ts"> + import { v4 as uuidv4 } from 'uuid'; + import { toast } from 'svelte-sonner'; + import mermaid from 'mermaid'; + + import { getContext, onMount, tick } from 'svelte'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + + import type { Writable } from 'svelte/store'; + import type { i18n as i18nType } from 'i18next'; + import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + + import { + chatId, + chats, + config, + type Model, + models, + settings, + showSidebar, + tags as _tags, + WEBUI_NAME, + banners, + user, + socket, + showCallOverlay, + tools + } from '$lib/stores'; + import { + convertMessagesToHistory, + copyToClipboard, + extractSentencesForAudio, + getUserPosition, + promptTemplate, + splitStream + } from '$lib/utils'; + + import { generateChatCompletion } from '$lib/apis/ollama'; + import { + addTagById, + createNewChat, + deleteTagById, + getAllChatTags, + getChatById, + getChatList, + getTagsById, + updateChatById + } from '$lib/apis/chats'; + import { generateOpenAIChatCompletion } from '$lib/apis/openai'; + import { runWebSearch } from '$lib/apis/rag'; + import { createOpenAITextStream } from '$lib/apis/streaming'; + import { queryMemory } from '$lib/apis/memories'; + import { getAndUpdateUserLocation, getUserSettings } from '$lib/apis/users'; + import { chatCompleted, generateTitle, generateSearchQuery, chatAction } from '$lib/apis'; + + import Banner from '../common/Banner.svelte'; + import MessageInput from '$lib/components/chat/MessageInput.svelte'; + import Messages from '$lib/components/chat/Messages.svelte'; + import Navbar from '$lib/components/layout/Navbar.svelte'; + import CallOverlay from './MessageInput/CallOverlay.svelte'; + import { error } from '@sveltejs/kit'; + import ChatControls from './ChatControls.svelte'; + import EventConfirmDialog from '../common/ConfirmDialog.svelte'; + + const i18n: Writable<i18nType> = getContext('i18n'); + + export let chatIdProp = ''; + let loaded = false; + const eventTarget = new EventTarget(); + + let showControls = false; + let stopResponseFlag = false; + let autoScroll = true; + let processing = ''; + let messagesContainerElement: HTMLDivElement; + + let showEventConfirmation = false; + let eventConfirmationTitle = ''; + let eventConfirmationMessage = ''; + let eventConfirmationInput = false; + let eventConfirmationInputPlaceholder = ''; + let eventConfirmationInputValue = ''; + let eventCallback = null; + + let showModelSelector = true; + + let selectedModels = ['']; + let atSelectedModel: Model | undefined; + + let selectedModelIds = []; + $: selectedModelIds = atSelectedModel !== undefined ? [atSelectedModel.id] : selectedModels; + + let selectedToolIds = []; + let webSearchEnabled = false; + + let chat = null; + let tags = []; + + let title = ''; + let prompt = ''; + + let chatFiles = []; + let files = []; + let messages = []; + let history = { + messages: {}, + currentId: null + }; + + let params = {}; + let valves = {}; + + $: if (history.currentId !== null) { + let _messages = []; + + let currentMessage = history.messages[history.currentId]; + while (currentMessage !== null) { + _messages.unshift({ ...currentMessage }); + currentMessage = + currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null; + } + messages = _messages; + } else { + messages = []; + } + + $: if (chatIdProp) { + (async () => { + console.log(chatIdProp); + if (chatIdProp && (await loadChat())) { + await tick(); + loaded = true; + + window.setTimeout(() => scrollToBottom(), 0); + const chatInput = document.getElementById('chat-textarea'); + chatInput?.focus(); + } else { + await goto('/'); + } + })(); + } + + const chatEventHandler = async (event, cb) => { + if (event.chat_id === $chatId) { + await tick(); + console.log(event); + let message = history.messages[event.message_id]; + + const type = event?.data?.type ?? null; + const data = event?.data?.data ?? null; + + if (type === 'status') { + if (message?.statusHistory) { + message.statusHistory.push(data); + } else { + message.statusHistory = [data]; + } + } else if (type === 'citation') { + if (message?.citations) { + message.citations.push(data); + } else { + message.citations = [data]; + } + } else if (type === 'message') { + message.content += data.content; + } else if (type === 'replace') { + message.content = data.content; + } else if (type === 'confirmation') { + eventCallback = cb; + + eventConfirmationInput = false; + showEventConfirmation = true; + + eventConfirmationTitle = data.title; + eventConfirmationMessage = data.message; + } else if (type === 'input') { + eventCallback = cb; + + eventConfirmationInput = true; + showEventConfirmation = true; + + eventConfirmationTitle = data.title; + eventConfirmationMessage = data.message; + eventConfirmationInputPlaceholder = data.placeholder; + eventConfirmationInputValue = data?.value ?? ''; + } else { + console.log('Unknown message type', data); + } + + messages = messages; + } + }; + + onMount(async () => { + const onMessageHandler = async (event) => { + if (event.origin === window.origin) { + // Replace with your iframe's origin + console.log('Message received from iframe:', event.data); + if (event.data.type === 'input:prompt') { + console.log(event.data.text); + + const inputElement = document.getElementById('chat-textarea'); + + if (inputElement) { + prompt = event.data.text; + inputElement.focus(); + } + } + + if (event.data.type === 'action:submit') { + console.log(event.data.text); + + if (prompt !== '') { + await tick(); + submitPrompt(prompt); + } + } + + if (event.data.type === 'input:prompt:submit') { + console.log(event.data.text); + + if (prompt !== '') { + await tick(); + submitPrompt(event.data.text); + } + } + } + }; + window.addEventListener('message', onMessageHandler); + + $socket.on('chat-events', chatEventHandler); + + if (!$chatId) { + chatId.subscribe(async (value) => { + if (!value) { + await initNewChat(); + } + }); + } else { + if (!($settings.saveChatHistory ?? true)) { + await goto('/'); + } + } + + return () => { + window.removeEventListener('message', onMessageHandler); + + $socket.off('chat-events'); + }; + }); + + ////////////////////////// + // Web functions + ////////////////////////// + + const initNewChat = async () => { + window.history.replaceState(history.state, '', `/`); + await chatId.set(''); + + autoScroll = true; + + title = ''; + messages = []; + history = { + messages: {}, + currentId: null + }; + + chatFiles = []; + params = {}; + + if ($page.url.searchParams.get('models')) { + selectedModels = $page.url.searchParams.get('models')?.split(','); + } else if ($settings?.models) { + selectedModels = $settings?.models; + } else if ($config?.default_models) { + console.log($config?.default_models.split(',') ?? ''); + selectedModels = $config?.default_models.split(','); + } else { + selectedModels = ['']; + } + + if ($page.url.searchParams.get('q')) { + prompt = $page.url.searchParams.get('q') ?? ''; + + if (prompt) { + await tick(); + submitPrompt(prompt); + } + } + + selectedModels = selectedModels.map((modelId) => + $models.map((m) => m.id).includes(modelId) ? modelId : '' + ); + + const userSettings = await getUserSettings(localStorage.token); + + if (userSettings) { + settings.set(userSettings.ui); + } else { + settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); + } + + const chatInput = document.getElementById('chat-textarea'); + setTimeout(() => chatInput?.focus(), 0); + }; + + const loadChat = async () => { + chatId.set(chatIdProp); + chat = await getChatById(localStorage.token, $chatId).catch(async (error) => { + await goto('/'); + return null; + }); + + if (chat) { + tags = await getTags(); + const chatContent = chat.chat; + + if (chatContent) { + console.log(chatContent); + + selectedModels = + (chatContent?.models ?? undefined) !== undefined + ? chatContent.models + : [chatContent.models ?? '']; + history = + (chatContent?.history ?? undefined) !== undefined + ? chatContent.history + : convertMessagesToHistory(chatContent.messages); + title = chatContent.title; + + const userSettings = await getUserSettings(localStorage.token); + + if (userSettings) { + await settings.set(userSettings.ui); + } else { + await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); + } + + params = chatContent?.params ?? {}; + chatFiles = chatContent?.files ?? []; + + autoScroll = true; + await tick(); + + if (messages.length > 0) { + history.messages[messages.at(-1).id].done = true; + } + await tick(); + + return true; + } else { + return null; + } + } + }; + + const scrollToBottom = async () => { + await tick(); + if (messagesContainerElement) { + messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight; + } + }; + + const createMessagesList = (responseMessageId) => { + const message = history.messages[responseMessageId]; + if (message.parentId) { + return [...createMessagesList(message.parentId), message]; + } else { + return [message]; + } + }; + + const chatCompletedHandler = async (chatId, modelId, responseMessageId, messages) => { + await mermaid.run({ + querySelector: '.mermaid' + }); + + const res = await chatCompleted(localStorage.token, { + model: modelId, + messages: messages.map((m) => ({ + id: m.id, + role: m.role, + content: m.content, + info: m.info ? m.info : undefined, + timestamp: m.timestamp + })), + chat_id: chatId, + session_id: $socket?.id, + id: responseMessageId + }).catch((error) => { + toast.error(error); + messages.at(-1).error = { content: error }; + + return null; + }); + + if (res !== null) { + // Update chat history with the new messages + for (const message of res.messages) { + history.messages[message.id] = { + ...history.messages[message.id], + ...(history.messages[message.id].content !== message.content + ? { originalContent: history.messages[message.id].content } + : {}), + ...message + }; + } + } + + if ($chatId == chatId) { + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, chatId, { + models: selectedModels, + messages: messages, + history: history, + params: params, + files: chatFiles + }); + await chats.set(await getChatList(localStorage.token)); + } + } + }; + + const chatActionHandler = async (chatId, actionId, modelId, responseMessageId) => { + const res = await chatAction(localStorage.token, actionId, { + model: modelId, + messages: messages.map((m) => ({ + id: m.id, + role: m.role, + content: m.content, + info: m.info ? m.info : undefined, + timestamp: m.timestamp + })), + chat_id: chatId, + session_id: $socket?.id, + id: responseMessageId + }).catch((error) => { + toast.error(error); + messages.at(-1).error = { content: error }; + return null; + }); + + if (res !== null) { + // Update chat history with the new messages + for (const message of res.messages) { + history.messages[message.id] = { + ...history.messages[message.id], + ...(history.messages[message.id].content !== message.content + ? { originalContent: history.messages[message.id].content } + : {}), + ...message + }; + } + } + + if ($chatId == chatId) { + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, chatId, { + models: selectedModels, + messages: messages, + history: history, + params: params, + files: chatFiles + }); + await chats.set(await getChatList(localStorage.token)); + } + } + }; + + const getChatEventEmitter = async (modelId: string, chatId: string = '') => { + return setInterval(() => { + $socket?.emit('usage', { + action: 'chat', + model: modelId, + chat_id: chatId + }); + }, 1000); + }; + + ////////////////////////// + // Chat functions + ////////////////////////// + + const submitPrompt = async (userPrompt, { _raw = false } = {}) => { + let _responses = []; + console.log('submitPrompt', $chatId); + + selectedModels = selectedModels.map((modelId) => + $models.map((m) => m.id).includes(modelId) ? modelId : '' + ); + + if (selectedModels.includes('')) { + toast.error($i18n.t('Model not selected')); + } else if (messages.length != 0 && messages.at(-1).done != true) { + // Response not done + console.log('wait'); + } else if (messages.length != 0 && messages.at(-1).error) { + // Error in response + toast.error( + $i18n.t( + `Oops! There was an error in the previous response. Please try again or contact admin.` + ) + ); + } else if ( + files.length > 0 && + files.filter((file) => file.type !== 'image' && file.status !== 'processed').length > 0 + ) { + // Upload not done + toast.error( + $i18n.t( + `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.` + ) + ); + } else { + // Reset chat input textarea + const chatTextAreaElement = document.getElementById('chat-textarea'); + + if (chatTextAreaElement) { + chatTextAreaElement.value = ''; + chatTextAreaElement.style.height = ''; + } + + const _files = JSON.parse(JSON.stringify(files)); + chatFiles.push(..._files.filter((item) => ['doc', 'file', 'collection'].includes(item.type))); + chatFiles = chatFiles.filter( + // Remove duplicates + (item, index, array) => + array.findIndex((i) => JSON.stringify(i) === JSON.stringify(item)) === index + ); + + files = []; + + prompt = ''; + + // Create user message + let userMessageId = uuidv4(); + let userMessage = { + id: userMessageId, + parentId: messages.length !== 0 ? messages.at(-1).id : null, + childrenIds: [], + role: 'user', + content: userPrompt, + files: _files.length > 0 ? _files : undefined, + timestamp: Math.floor(Date.now() / 1000), // Unix epoch + models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx) + }; + + // Add message to history and Set currentId to messageId + history.messages[userMessageId] = userMessage; + history.currentId = userMessageId; + + // Append messageId to childrenIds of parent message + if (messages.length !== 0) { + history.messages[messages.at(-1).id].childrenIds.push(userMessageId); + } + + // Wait until history/message have been updated + await tick(); + _responses = await sendPrompt(userPrompt, userMessageId, { newChat: true }); + } + + return _responses; + }; + + const sendPrompt = async (prompt, parentId, { modelId = null, newChat = false } = {}) => { + let _responses = []; + + // If modelId is provided, use it, else use selected model + let selectedModelIds = modelId + ? [modelId] + : atSelectedModel !== undefined + ? [atSelectedModel.id] + : selectedModels; + + // Create response messages for each selected model + const responseMessageIds = {}; + for (const modelId of selectedModelIds) { + const model = $models.filter((m) => m.id === modelId).at(0); + + if (model) { + let responseMessageId = uuidv4(); + let responseMessage = { + parentId: parentId, + id: responseMessageId, + childrenIds: [], + role: 'assistant', + content: '', + model: model.id, + modelName: model.name ?? model.id, + userContext: null, + timestamp: Math.floor(Date.now() / 1000) // Unix epoch + }; + + // Add message to history and Set currentId to messageId + history.messages[responseMessageId] = responseMessage; + history.currentId = responseMessageId; + + // Append messageId to childrenIds of parent message + if (parentId !== null) { + history.messages[parentId].childrenIds = [ + ...history.messages[parentId].childrenIds, + responseMessageId + ]; + } + + responseMessageIds[modelId] = responseMessageId; + } + } + await tick(); + + // Create new chat if only one message in messages + if (newChat && messages.length == 2) { + if ($settings.saveChatHistory ?? true) { + chat = await createNewChat(localStorage.token, { + id: $chatId, + title: $i18n.t('New Chat'), + models: selectedModels, + system: $settings.system ?? undefined, + params: params, + messages: messages, + history: history, + tags: [], + timestamp: Date.now() + }); + await chats.set(await getChatList(localStorage.token)); + await chatId.set(chat.id); + } else { + await chatId.set('local'); + } + await tick(); + } + + const _chatId = JSON.parse(JSON.stringify($chatId)); + + await Promise.all( + selectedModelIds.map(async (modelId) => { + console.log('modelId', modelId); + const model = $models.filter((m) => m.id === modelId).at(0); + + if (model) { + // If there are image files, check if model is vision capable + const hasImages = messages.some((message) => + message.files?.some((file) => file.type === 'image') + ); + + if (hasImages && !(model.info?.meta?.capabilities?.vision ?? true)) { + toast.error( + $i18n.t('Model {{modelName}} is not vision capable', { + modelName: model.name ?? model.id + }) + ); + } + + let responseMessageId = responseMessageIds[modelId]; + let responseMessage = history.messages[responseMessageId]; + + let userContext = null; + if ($settings?.memory ?? false) { + if (userContext === null) { + const res = await queryMemory(localStorage.token, prompt).catch((error) => { + toast.error(error); + return null; + }); + if (res) { + if (res.documents[0].length > 0) { + userContext = res.documents[0].reduce((acc, doc, index) => { + const createdAtTimestamp = res.metadatas[0][index].created_at; + const createdAtDate = new Date(createdAtTimestamp * 1000) + .toISOString() + .split('T')[0]; + return `${acc}${index + 1}. [${createdAtDate}]. ${doc}\n`; + }, ''); + } + + console.log(userContext); + } + } + } + responseMessage.userContext = userContext; + + const chatEventEmitter = await getChatEventEmitter(model.id, _chatId); + if (webSearchEnabled) { + await getWebSearchResults(model.id, parentId, responseMessageId); + } + + let _response = null; + if (model?.owned_by === 'openai') { + _response = await sendPromptOpenAI(model, prompt, responseMessageId, _chatId); + } else if (model) { + _response = await sendPromptOllama(model, prompt, responseMessageId, _chatId); + } + _responses.push(_response); + + if (chatEventEmitter) clearInterval(chatEventEmitter); + } else { + toast.error($i18n.t(`Model {{modelId}} not found`, { modelId })); + } + }) + ); + + await chats.set(await getChatList(localStorage.token)); + return _responses; + }; + + const sendPromptOllama = async (model, userPrompt, responseMessageId, _chatId) => { + let _response = null; + + const responseMessage = history.messages[responseMessageId]; + const userMessage = history.messages[responseMessage.parentId]; + + // Wait until history/message have been updated + await tick(); + + // Scroll down + scrollToBottom(); + + const messagesBody = [ + params?.system || $settings.system || (responseMessage?.userContext ?? null) + ? { + role: 'system', + content: `${promptTemplate( + params?.system ?? $settings?.system ?? '', + $user.name, + $settings?.userLocation + ? await getAndUpdateUserLocation(localStorage.token) + : undefined + )}${ + responseMessage?.userContext ?? null + ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}` + : '' + }` + } + : undefined, + ...messages + ] + .filter((message) => message?.content?.trim()) + .map((message, idx, arr) => { + // Prepare the base message object + const baseMessage = { + role: message.role, + content: message.content + }; + + // Extract and format image URLs if any exist + const imageUrls = message.files + ?.filter((file) => file.type === 'image') + .map((file) => file.url.slice(file.url.indexOf(',') + 1)); + + // Add images array only if it contains elements + if (imageUrls && imageUrls.length > 0 && message.role === 'user') { + baseMessage.images = imageUrls; + } + return baseMessage; + }); + + let lastImageIndex = -1; + + // Find the index of the last object with images + messagesBody.forEach((item, index) => { + if (item.images) { + lastImageIndex = index; + } + }); + + // Remove images from all but the last one + messagesBody.forEach((item, index) => { + if (index !== lastImageIndex) { + delete item.images; + } + }); + + let files = JSON.parse(JSON.stringify(chatFiles)); + if (model?.info?.meta?.knowledge ?? false) { + files.push(...model.info.meta.knowledge); + } + files.push( + ...(userMessage?.files ?? []).filter((item) => + ['doc', 'file', 'collection'].includes(item.type) + ), + ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type)) + ); + + eventTarget.dispatchEvent( + new CustomEvent('chat:start', { + detail: { + id: responseMessageId + } + }) + ); + + await tick(); + + const [res, controller] = await generateChatCompletion(localStorage.token, { + stream: true, + model: model.id, + messages: messagesBody, + options: { + ...(params ?? $settings.params ?? {}), + stop: + params?.stop ?? $settings?.params?.stop ?? undefined + ? (params?.stop ?? $settings.params.stop).map((str) => + decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) + ) + : undefined, + num_predict: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, + repeat_penalty: + params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined + }, + format: $settings.requestFormat ?? undefined, + keep_alive: $settings.keepAlive ?? undefined, + tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, + files: files.length > 0 ? files : undefined, + ...(Object.keys(valves).length ? { valves } : {}), + session_id: $socket?.id, + chat_id: $chatId, + id: responseMessageId + }); + + if (res && res.ok) { + console.log('controller', controller); + + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done || stopResponseFlag || _chatId !== $chatId) { + responseMessage.done = true; + messages = messages; + + if (stopResponseFlag) { + controller.abort('User: Stop Response'); + } else { + const messages = createMessagesList(responseMessageId); + await chatCompletedHandler(_chatId, model.id, responseMessageId, messages); + } + + _response = responseMessage.content; + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + + if ('citations' in data) { + responseMessage.citations = data.citations; + continue; + } + + if ('detail' in data) { + throw data; + } + + if (data.done == false) { + if (responseMessage.content == '' && data.message.content == '\n') { + continue; + } else { + responseMessage.content += data.message.content; + + const sentences = extractSentencesForAudio(responseMessage.content); + sentences.pop(); + + // dispatch only last sentence and make sure it hasn't been dispatched before + if ( + sentences.length > 0 && + sentences[sentences.length - 1] !== responseMessage.lastSentence + ) { + responseMessage.lastSentence = sentences[sentences.length - 1]; + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { id: responseMessageId, content: sentences[sentences.length - 1] } + }) + ); + } + + messages = messages; + } + } else { + responseMessage.done = true; + + if (responseMessage.content == '') { + responseMessage.error = { + code: 400, + content: `Oops! No text generated from Ollama, Please try again.` + }; + } + + responseMessage.context = data.context ?? null; + responseMessage.info = { + total_duration: data.total_duration, + load_duration: data.load_duration, + sample_count: data.sample_count, + sample_duration: data.sample_duration, + prompt_eval_count: data.prompt_eval_count, + prompt_eval_duration: data.prompt_eval_duration, + eval_count: data.eval_count, + eval_duration: data.eval_duration + }; + messages = messages; + + if ($settings.notificationEnabled && !document.hasFocus()) { + const notification = new Notification(`${model.id}`, { + body: responseMessage.content, + icon: `${WEBUI_BASE_URL}/static/favicon.png` + }); + } + + if ($settings?.responseAutoCopy ?? false) { + copyToClipboard(responseMessage.content); + } + + if ($settings.responseAutoPlayback && !$showCallOverlay) { + await tick(); + document.getElementById(`speak-button-${responseMessage.id}`)?.click(); + } + } + } + } + } catch (error) { + console.log(error); + if ('detail' in error) { + toast.error(error.detail); + } + break; + } + + if (autoScroll) { + scrollToBottom(); + } + } + + if ($chatId == _chatId) { + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { + messages: messages, + history: history, + models: selectedModels, + params: params, + files: chatFiles + }); + await chats.set(await getChatList(localStorage.token)); + } + } + } else { + if (res !== null) { + const error = await res.json(); + console.log(error); + if ('detail' in error) { + toast.error(error.detail); + responseMessage.error = { content: error.detail }; + } else { + toast.error(error.error); + responseMessage.error = { content: error.error }; + } + } else { + toast.error( + $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { provider: 'Ollama' }) + ); + responseMessage.error = { + content: $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { + provider: 'Ollama' + }) + }; + } + responseMessage.done = true; + messages = messages; + } + + stopResponseFlag = false; + await tick(); + + let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? ''; + if (lastSentence) { + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { id: responseMessageId, content: lastSentence } + }) + ); + } + eventTarget.dispatchEvent( + new CustomEvent('chat:finish', { + detail: { + id: responseMessageId, + content: responseMessage.content + } + }) + ); + + if (autoScroll) { + scrollToBottom(); + } + + if (messages.length == 2 && messages.at(1).content !== '' && selectedModels[0] === model.id) { + window.history.replaceState(history.state, '', `/c/${_chatId}`); + const _title = await generateChatTitle(userPrompt); + await setChatTitle(_chatId, _title); + } + + return _response; + }; + + const sendPromptOpenAI = async (model, userPrompt, responseMessageId, _chatId) => { + let _response = null; + + const responseMessage = history.messages[responseMessageId]; + const userMessage = history.messages[responseMessage.parentId]; + + let files = JSON.parse(JSON.stringify(chatFiles)); + if (model?.info?.meta?.knowledge ?? false) { + files.push(...model.info.meta.knowledge); + } + files.push( + ...(userMessage?.files ?? []).filter((item) => + ['doc', 'file', 'collection'].includes(item.type) + ), + ...(responseMessage?.files ?? []).filter((item) => ['web_search_results'].includes(item.type)) + ); + + scrollToBottom(); + + eventTarget.dispatchEvent( + new CustomEvent('chat:start', { + detail: { + id: responseMessageId + } + }) + ); + await tick(); + + try { + const [res, controller] = await generateOpenAIChatCompletion( + localStorage.token, + { + stream: true, + model: model.id, + stream_options: + model.info?.meta?.capabilities?.usage ?? false + ? { + include_usage: true + } + : undefined, + messages: [ + params?.system || $settings.system || (responseMessage?.userContext ?? null) + ? { + role: 'system', + content: `${promptTemplate( + params?.system ?? $settings?.system ?? '', + $user.name, + $settings?.userLocation + ? await getAndUpdateUserLocation(localStorage.token) + : undefined + )}${ + responseMessage?.userContext ?? null + ? `\n\nUser Context:\n${responseMessage?.userContext ?? ''}` + : '' + }` + } + : undefined, + ...messages + ] + .filter((message) => message?.content?.trim()) + .map((message, idx, arr) => ({ + role: message.role, + ...((message.files?.filter((file) => file.type === 'image').length > 0 ?? false) && + message.role === 'user' + ? { + content: [ + { + type: 'text', + text: + arr.length - 1 !== idx + ? message.content + : message?.raContent ?? message.content + }, + ...message.files + .filter((file) => file.type === 'image') + .map((file) => ({ + type: 'image_url', + image_url: { + url: file.url + } + })) + ] + } + : { + content: + arr.length - 1 !== idx + ? message.content + : message?.raContent ?? message.content + }) + })), + seed: params?.seed ?? $settings?.params?.seed ?? undefined, + stop: + params?.stop ?? $settings?.params?.stop ?? undefined + ? (params?.stop ?? $settings.params.stop).map((str) => + decodeURIComponent(JSON.parse('"' + str.replace(/\"/g, '\\"') + '"')) + ) + : undefined, + temperature: params?.temperature ?? $settings?.params?.temperature ?? undefined, + top_p: params?.top_p ?? $settings?.params?.top_p ?? undefined, + frequency_penalty: + params?.frequency_penalty ?? $settings?.params?.frequency_penalty ?? undefined, + max_tokens: params?.max_tokens ?? $settings?.params?.max_tokens ?? undefined, + tool_ids: selectedToolIds.length > 0 ? selectedToolIds : undefined, + files: files.length > 0 ? files : undefined, + ...(Object.keys(valves).length ? { valves } : {}), + session_id: $socket?.id, + chat_id: $chatId, + id: responseMessageId + }, + `${WEBUI_BASE_URL}/api` + ); + + // Wait until history/message have been updated + await tick(); + + scrollToBottom(); + + if (res && res.ok && res.body) { + const textStream = await createOpenAITextStream(res.body, $settings.splitLargeChunks); + let lastUsage = null; + + for await (const update of textStream) { + const { value, done, citations, error, usage } = update; + if (error) { + await handleOpenAIError(error, null, model, responseMessage); + break; + } + if (done || stopResponseFlag || _chatId !== $chatId) { + responseMessage.done = true; + messages = messages; + + if (stopResponseFlag) { + controller.abort('User: Stop Response'); + } else { + const messages = createMessagesList(responseMessageId); + + await chatCompletedHandler(_chatId, model.id, responseMessageId, messages); + } + + _response = responseMessage.content; + + break; + } + + if (usage) { + lastUsage = usage; + } + + if (citations) { + responseMessage.citations = citations; + continue; + } + + if (responseMessage.content == '' && value == '\n') { + continue; + } else { + responseMessage.content += value; + + const sentences = extractSentencesForAudio(responseMessage.content); + sentences.pop(); + + // dispatch only last sentence and make sure it hasn't been dispatched before + if ( + sentences.length > 0 && + sentences[sentences.length - 1] !== responseMessage.lastSentence + ) { + responseMessage.lastSentence = sentences[sentences.length - 1]; + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { id: responseMessageId, content: sentences[sentences.length - 1] } + }) + ); + } + + messages = messages; + } + + if (autoScroll) { + scrollToBottom(); + } + } + + if ($settings.notificationEnabled && !document.hasFocus()) { + const notification = new Notification(`${model.id}`, { + body: responseMessage.content, + icon: `${WEBUI_BASE_URL}/static/favicon.png` + }); + } + + if ($settings.responseAutoCopy) { + copyToClipboard(responseMessage.content); + } + + if ($settings.responseAutoPlayback && !$showCallOverlay) { + await tick(); + + document.getElementById(`speak-button-${responseMessage.id}`)?.click(); + } + + if (lastUsage) { + responseMessage.info = { ...lastUsage, openai: true }; + } + + if ($chatId == _chatId) { + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { + models: selectedModels, + messages: messages, + history: history, + params: params, + files: chatFiles + }); + await chats.set(await getChatList(localStorage.token)); + } + } + } else { + await handleOpenAIError(null, res, model, responseMessage); + } + } catch (error) { + await handleOpenAIError(error, null, model, responseMessage); + } + messages = messages; + + stopResponseFlag = false; + await tick(); + + let lastSentence = extractSentencesForAudio(responseMessage.content)?.at(-1) ?? ''; + if (lastSentence) { + eventTarget.dispatchEvent( + new CustomEvent('chat', { + detail: { id: responseMessageId, content: lastSentence } + }) + ); + } + + eventTarget.dispatchEvent( + new CustomEvent('chat:finish', { + detail: { + id: responseMessageId, + content: responseMessage.content + } + }) + ); + + if (autoScroll) { + scrollToBottom(); + } + + if (messages.length == 2 && selectedModels[0] === model.id) { + window.history.replaceState(history.state, '', `/c/${_chatId}`); + + const _title = await generateChatTitle(userPrompt); + await setChatTitle(_chatId, _title); + } + + return _response; + }; + + const handleOpenAIError = async (error, res: Response | null, model, responseMessage) => { + let errorMessage = ''; + let innerError; + + if (error) { + innerError = error; + } else if (res !== null) { + innerError = await res.json(); + } + console.error(innerError); + if ('detail' in innerError) { + toast.error(innerError.detail); + errorMessage = innerError.detail; + } else if ('error' in innerError) { + if ('message' in innerError.error) { + toast.error(innerError.error.message); + errorMessage = innerError.error.message; + } else { + toast.error(innerError.error); + errorMessage = innerError.error; + } + } else if ('message' in innerError) { + toast.error(innerError.message); + errorMessage = innerError.message; + } + + responseMessage.error = { + content: + $i18n.t(`Uh-oh! There was an issue connecting to {{provider}}.`, { + provider: model.name ?? model.id + }) + + '\n' + + errorMessage + }; + responseMessage.done = true; + + messages = messages; + }; + + const stopResponse = () => { + stopResponseFlag = true; + console.log('stopResponse'); + }; + + const regenerateResponse = async (message) => { + console.log('regenerateResponse'); + + if (messages.length != 0) { + let userMessage = history.messages[message.parentId]; + let userPrompt = userMessage.content; + + if ((userMessage?.models ?? [...selectedModels]).length == 1) { + // If user message has only one model selected, sendPrompt automatically selects it for regeneration + await sendPrompt(userPrompt, userMessage.id); + } else { + // If there are multiple models selected, use the model of the response message for regeneration + // e.g. many model chat + await sendPrompt(userPrompt, userMessage.id, { modelId: message.model }); + } + } + }; + + const continueGeneration = async () => { + console.log('continueGeneration'); + const _chatId = JSON.parse(JSON.stringify($chatId)); + + if (messages.length != 0 && messages.at(-1).done == true) { + const responseMessage = history.messages[history.currentId]; + responseMessage.done = false; + await tick(); + + const model = $models.filter((m) => m.id === responseMessage.model).at(0); + + if (model) { + if (model?.owned_by === 'openai') { + await sendPromptOpenAI( + model, + history.messages[responseMessage.parentId].content, + responseMessage.id, + _chatId + ); + } else + await sendPromptOllama( + model, + history.messages[responseMessage.parentId].content, + responseMessage.id, + _chatId + ); + } + } else { + toast.error($i18n.t(`Model {{modelId}} not found`, { modelId })); + } + }; + + const generateChatTitle = async (userPrompt) => { + if ($settings?.title?.auto ?? true) { + const title = await generateTitle( + localStorage.token, + selectedModels[0], + userPrompt, + $chatId + ).catch((error) => { + console.error(error); + return 'New Chat'; + }); + + return title; + } else { + return `${userPrompt}`; + } + }; + + const setChatTitle = async (_chatId, _title) => { + if (_chatId === $chatId) { + title = _title; + } + + if ($settings.saveChatHistory ?? true) { + chat = await updateChatById(localStorage.token, _chatId, { title: _title }); + await chats.set(await getChatList(localStorage.token)); + } + }; + + const getWebSearchResults = async (model: string, parentId: string, responseId: string) => { + const responseMessage = history.messages[responseId]; + const userMessage = history.messages[parentId]; + + responseMessage.statusHistory = [ + { + done: false, + action: 'web_search', + description: $i18n.t('Generating search query') + } + ]; + messages = messages; + + const prompt = userMessage.content; + let searchQuery = await generateSearchQuery(localStorage.token, model, messages, prompt).catch( + (error) => { + console.log(error); + return prompt; + } + ); + + if (!searchQuery) { + toast.warning($i18n.t('No search query generated')); + responseMessage.statusHistory.push({ + done: true, + error: true, + action: 'web_search', + description: 'No search query generated' + }); + + messages = messages; + } + + responseMessage.statusHistory.push({ + done: false, + action: 'web_search', + description: $i18n.t(`Searching "{{searchQuery}}"`, { searchQuery }) + }); + messages = messages; + + const results = await runWebSearch(localStorage.token, searchQuery).catch((error) => { + console.log(error); + toast.error(error); + + return null; + }); + + if (results) { + responseMessage.statusHistory.push({ + done: true, + action: 'web_search', + description: $i18n.t('Searched {{count}} sites', { count: results.filenames.length }), + query: searchQuery, + urls: results.filenames + }); + + if (responseMessage?.files ?? undefined === undefined) { + responseMessage.files = []; + } + + responseMessage.files.push({ + collection_name: results.collection_name, + name: searchQuery, + type: 'web_search_results', + urls: results.filenames + }); + + messages = messages; + } else { + responseMessage.statusHistory.push({ + done: true, + error: true, + action: 'web_search', + description: 'No search results found' + }); + messages = messages; + } + }; + + const getTags = async () => { + return await getTagsById(localStorage.token, $chatId).catch(async (error) => { + return []; + }); + }; +</script> + +<svelte:head> + <title> + {title + ? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}` + : `${$WEBUI_NAME}`} + </title> +</svelte:head> + +<audio id="audioElement" src="" style="display: none;" /> + +<EventConfirmDialog + bind:show={showEventConfirmation} + title={eventConfirmationTitle} + message={eventConfirmationMessage} + input={eventConfirmationInput} + inputPlaceholder={eventConfirmationInputPlaceholder} + inputValue={eventConfirmationInputValue} + on:confirm={(e) => { + if (e.detail) { + eventCallback(e.detail); + } else { + eventCallback(true); + } + }} + on:cancel={() => { + eventCallback(false); + }} +/> + +{#if $showCallOverlay} + <CallOverlay + {submitPrompt} + {stopResponse} + bind:files + modelId={selectedModelIds?.at(0) ?? null} + chatId={$chatId} + {eventTarget} + /> +{/if} + +{#if !chatIdProp || (loaded && chatIdProp)} + <div + class="h-screen max-h-[100dvh] {$showSidebar + ? 'md:max-w-[calc(100%-260px)]' + : ''} w-full max-w-full flex flex-col" + > + {#if $settings?.backgroundImageUrl ?? null} + <div + class="absolute {$showSidebar + ? 'md:max-w-[calc(100%-260px)] md:translate-x-[260px]' + : ''} top-0 left-0 w-full h-full bg-cover bg-center bg-no-repeat" + style="background-image: url({$settings.backgroundImageUrl}) " + /> + + <div + class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-white to-white/85 dark:from-gray-900 dark:to-[#171717]/90 z-0" + /> + {/if} + + <Navbar + {title} + bind:selectedModels + bind:showModelSelector + bind:showControls + shareEnabled={messages.length > 0} + {chat} + {initNewChat} + /> + + {#if $banners.length > 0 && messages.length === 0 && !$chatId && selectedModels.length <= 1} + <div + class="absolute top-[4.25rem] w-full {$showSidebar + ? 'md:max-w-[calc(100%-260px)]' + : ''} {showControls ? 'lg:pr-[24rem]' : ''} z-20" + > + <div class=" flex flex-col gap-1 w-full"> + {#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner} + <Banner + {banner} + on:dismiss={(e) => { + const bannerId = e.detail; + + localStorage.setItem( + 'dismissedBannerIds', + JSON.stringify( + [ + bannerId, + ...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]') + ].filter((id) => $banners.find((b) => b.id === id)) + ) + ); + }} + /> + {/each} + </div> + </div> + {/if} + + <div class="flex flex-col flex-auto z-10"> + <div + class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full z-10 scrollbar-hidden {showControls + ? 'lg:pr-[24rem]' + : ''}" + id="messages-container" + bind:this={messagesContainerElement} + on:scroll={(e) => { + autoScroll = + messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <= + messagesContainerElement.clientHeight + 5; + }} + > + <div class=" h-full w-full flex flex-col {chatIdProp ? 'py-4' : 'pt-2 pb-4'}"> + <Messages + chatId={$chatId} + {selectedModels} + {processing} + bind:history + bind:messages + bind:autoScroll + bind:prompt + bottomPadding={files.length > 0} + {sendPrompt} + {continueGeneration} + {regenerateResponse} + {chatActionHandler} + /> + </div> + </div> + + <div class={showControls ? 'lg:pr-[24rem]' : ''}> + <MessageInput + bind:files + bind:prompt + bind:autoScroll + bind:selectedToolIds + bind:webSearchEnabled + bind:atSelectedModel + availableToolIds={selectedModelIds.reduce((a, e, i, arr) => { + const model = $models.find((m) => m.id === e); + if (model?.info?.meta?.toolIds ?? false) { + return [...new Set([...a, ...model.info.meta.toolIds])]; + } + return a; + }, [])} + transparentBackground={$settings?.backgroundImageUrl ?? false} + {selectedModels} + {messages} + {submitPrompt} + {stopResponse} + /> + </div> + </div> + + <ChatControls + models={selectedModelIds.reduce((a, e, i, arr) => { + const model = $models.find((m) => m.id === e); + if (model) { + return [...a, model]; + } + return a; + }, [])} + bind:show={showControls} + bind:chatFiles + bind:params + bind:valves + /> + </div> +{/if} diff --git a/src/lib/components/chat/ChatControls.svelte b/src/lib/components/chat/ChatControls.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f67e6d6efda32e350354ac24b195a07155f83f86 --- /dev/null +++ b/src/lib/components/chat/ChatControls.svelte @@ -0,0 +1,74 @@ +<script lang="ts"> + import { slide } from 'svelte/transition'; + import Modal from '../common/Modal.svelte'; + import Controls from './Controls/Controls.svelte'; + import { onMount } from 'svelte'; + + export let show = false; + + export let models = []; + + export let chatId = null; + + export let chatFiles = []; + export let valves = {}; + export let params = {}; + + let largeScreen = false; + onMount(() => { + // listen to resize 1024px + const mediaQuery = window.matchMedia('(min-width: 1024px)'); + + const handleMediaQuery = (e) => { + if (e.matches) { + largeScreen = true; + } else { + largeScreen = false; + } + }; + + mediaQuery.addEventListener('change', handleMediaQuery); + + handleMediaQuery(mediaQuery); + + return () => { + mediaQuery.removeEventListener('change', handleMediaQuery); + }; + }); +</script> + +{#if largeScreen} + {#if show} + <div class=" absolute bottom-0 right-0 z-20 h-full pointer-events-none"> + <div class="pr-4 pt-14 pb-8 w-[24rem] h-full" in:slide={{ duration: 200, axis: 'x' }}> + <div + class="w-full h-full px-5 py-4 bg-white dark:shadow-lg dark:bg-gray-850 border border-gray-50 dark:border-gray-800 rounded-xl z-50 pointer-events-auto overflow-y-auto scrollbar-hidden" + > + <Controls + on:close={() => { + show = false; + }} + {models} + bind:chatFiles + bind:valves + bind:params + /> + </div> + </div> + </div> + {/if} +{:else} + <Modal bind:show> + <div class=" px-6 py-4 h-full"> + <Controls + on:close={() => { + show = false; + }} + {models} + bind:chatFiles + bind:valves + bind:params + /> + </div> + </Modal> +{/if} diff --git a/src/lib/components/chat/Controls/Controls.svelte b/src/lib/components/chat/Controls/Controls.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ee8fcfff33fe95be0592c9eaabc69ab98a7f6c02 --- /dev/null +++ b/src/lib/components/chat/Controls/Controls.svelte @@ -0,0 +1,93 @@ +<script> + import { createEventDispatcher, getContext } from 'svelte'; + const dispatch = createEventDispatcher(); + const i18n = getContext('i18n'); + + import XMark from '$lib/components/icons/XMark.svelte'; + import AdvancedParams from '../Settings/Advanced/AdvancedParams.svelte'; + import Valves from '$lib/components/common/Valves.svelte'; + import FileItem from '$lib/components/common/FileItem.svelte'; + + export let models = []; + + export let chatFiles = []; + export let valves = {}; + export let params = {}; +</script> + +<div class=" dark:text-white"> + <div class=" flex justify-between dark:text-gray-100 mb-2"> + <div class=" text-lg font-medium self-center font-primary">{$i18n.t('Chat Controls')}</div> + <button + class="self-center" + on:click={() => { + dispatch('close'); + }} + > + <XMark className="size-4" /> + </button> + </div> + + <div class=" dark:text-gray-200 text-sm font-primary"> + {#if chatFiles.length > 0} + <div> + <div class="mb-1.5 font-medium">{$i18n.t('Files')}</div> + + <div class="flex flex-col gap-1"> + {#each chatFiles as file, fileIdx} + <FileItem + className="w-full" + url={`${file?.url}`} + name={file.name} + type={file.type} + dismissible={true} + on:dismiss={() => { + // Remove the file from the chatFiles array + + chatFiles.splice(fileIdx, 1); + chatFiles = chatFiles; + }} + /> + {/each} + </div> + </div> + + <hr class="my-2 border-gray-100 dark:border-gray-800" /> + {/if} + + {#if models.length === 1 && models[0]?.pipe?.valves_spec} + <div> + <div class=" font-medium">{$i18n.t('Valves')}</div> + + <div> + <Valves valvesSpec={models[0]?.pipe?.valves_spec} bind:valves /> + </div> + </div> + + <hr class="my-2 border-gray-100 dark:border-gray-800" /> + {/if} + + <div> + <div class="mb-1.5 font-medium">{$i18n.t('System Prompt')}</div> + + <div> + <textarea + bind:value={params.system} + class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 border border-gray-100 dark:border-gray-800 outline-none resize-none" + rows="3" + placeholder={$i18n.t('Enter system prompt')} + /> + </div> + </div> + + <hr class="my-2 border-gray-100 dark:border-gray-800" /> + + <div> + <div class="mb-1.5 font-medium">{$i18n.t('Advanced Params')}</div> + + <div> + <AdvancedParams bind:params /> + </div> + </div> + </div> +</div> diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5ea541c7fcb34698f19185fa24066651518dc4cc --- /dev/null +++ b/src/lib/components/chat/MessageInput.svelte @@ -0,0 +1,927 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { onMount, tick, getContext } from 'svelte'; + import { + type Model, + mobile, + settings, + showSidebar, + models, + config, + showCallOverlay, + tools, + user as _user + } from '$lib/stores'; + import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; + + import { + processDocToVectorDB, + uploadDocToVectorDB, + uploadWebToVectorDB, + uploadYoutubeTranscriptionToVectorDB + } from '$lib/apis/rag'; + + import { uploadFile } from '$lib/apis/files'; + import { + SUPPORTED_FILE_TYPE, + SUPPORTED_FILE_EXTENSIONS, + WEBUI_BASE_URL, + WEBUI_API_BASE_URL + } from '$lib/constants'; + + import Prompts from './MessageInput/PromptCommands.svelte'; + import Suggestions from './MessageInput/Suggestions.svelte'; + import AddFilesPlaceholder from '../AddFilesPlaceholder.svelte'; + import Documents from './MessageInput/Documents.svelte'; + import Models from './MessageInput/Models.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + import XMark from '$lib/components/icons/XMark.svelte'; + import InputMenu from './MessageInput/InputMenu.svelte'; + import Headphone from '../icons/Headphone.svelte'; + import VoiceRecording from './MessageInput/VoiceRecording.svelte'; + import { transcribeAudio } from '$lib/apis/audio'; + import FileItem from '../common/FileItem.svelte'; + import FilesOverlay from './MessageInput/FilesOverlay.svelte'; + + const i18n = getContext('i18n'); + + export let transparentBackground = false; + + export let submitPrompt: Function; + export let stopResponse: Function; + + export let autoScroll = true; + + export let atSelectedModel: Model | undefined; + export let selectedModels: ['']; + + let recording = false; + + let chatTextAreaElement: HTMLTextAreaElement; + let filesInputElement; + + let promptsElement; + let documentsElement; + let modelsElement; + + let inputFiles; + let dragged = false; + + let user = null; + let chatInputPlaceholder = ''; + + export let files = []; + + export let availableToolIds = []; + export let selectedToolIds = []; + export let webSearchEnabled = false; + + export let prompt = ''; + export let messages = []; + + let visionCapableModels = []; + $: visionCapableModels = [...(atSelectedModel ? [atSelectedModel] : selectedModels)].filter( + (model) => $models.find((m) => m.id === model)?.info?.meta?.capabilities?.vision ?? true + ); + + $: if (prompt) { + if (chatTextAreaElement) { + chatTextAreaElement.style.height = ''; + chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px'; + } + } + + const scrollToBottom = () => { + const element = document.getElementById('messages-container'); + element.scrollTop = element.scrollHeight; + }; + + const uploadFileHandler = async (file) => { + console.log(file); + // Check if the file is an audio file and transcribe/convert it to text file + if (['audio/mpeg', 'audio/wav'].includes(file['type'])) { + const res = await transcribeAudio(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + console.log(res); + const blob = new Blob([res.text], { type: 'text/plain' }); + file = blobToFile(blob, `${file.name}.txt`); + } + } + + // Upload the file to the server + const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + if (uploadedFile) { + const fileItem = { + type: 'file', + file: uploadedFile, + id: uploadedFile.id, + url: `${WEBUI_API_BASE_URL}/files/${uploadedFile.id}`, + name: file.name, + collection_name: '', + status: 'uploaded', + error: '' + }; + files = [...files, fileItem]; + + // TODO: Check if tools & functions have files support to skip this step to delegate file processing + // Default Upload to VectorDB + if ( + SUPPORTED_FILE_TYPE.includes(file['type']) || + SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) + ) { + processFileItem(fileItem); + } else { + toast.error( + $i18n.t(`Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.`, { + file_type: file['type'] + }) + ); + processFileItem(fileItem); + } + } + }; + + const processFileItem = async (fileItem) => { + try { + const res = await processDocToVectorDB(localStorage.token, fileItem.id); + + if (res) { + fileItem.status = 'processed'; + fileItem.collection_name = res.collection_name; + files = files; + } + } catch (e) { + // Remove the failed doc from the files array + // files = files.filter((f) => f.id !== fileItem.id); + toast.error(e); + + fileItem.status = 'processed'; + files = files; + } + }; + + const uploadWeb = async (url) => { + console.log(url); + + const doc = { + type: 'doc', + name: url, + collection_name: '', + status: false, + url: url, + error: '' + }; + + try { + files = [...files, doc]; + const res = await uploadWebToVectorDB(localStorage.token, '', url); + + if (res) { + doc.status = 'processed'; + doc.collection_name = res.collection_name; + files = files; + } + } catch (e) { + // Remove the failed doc from the files array + files = files.filter((f) => f.name !== url); + toast.error(e); + } + }; + + const uploadYoutubeTranscription = async (url) => { + console.log(url); + + const doc = { + type: 'doc', + name: url, + collection_name: '', + status: false, + url: url, + error: '' + }; + + try { + files = [...files, doc]; + const res = await uploadYoutubeTranscriptionToVectorDB(localStorage.token, url); + + if (res) { + doc.status = 'processed'; + doc.collection_name = res.collection_name; + files = files; + } + } catch (e) { + // Remove the failed doc from the files array + files = files.filter((f) => f.name !== url); + toast.error(e); + } + }; + + onMount(() => { + window.setTimeout(() => chatTextAreaElement?.focus(), 0); + + 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; + }; + + const onDragLeave = () => { + dragged = false; + }; + + const onDrop = async (e) => { + e.preventDefault(); + console.log(e); + + if (e.dataTransfer?.files) { + const inputFiles = Array.from(e.dataTransfer?.files); + + if (inputFiles && inputFiles.length > 0) { + inputFiles.forEach((file) => { + console.log(file, file.name.split('.').at(-1)); + if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { + if (visionCapableModels.length === 0) { + toast.error($i18n.t('Selected model(s) do not support image inputs')); + return; + } + let reader = new FileReader(); + reader.onload = (event) => { + files = [ + ...files, + { + type: 'image', + url: `${event.target.result}` + } + ]; + }; + reader.readAsDataURL(file); + } else { + uploadFileHandler(file); + } + }); + } else { + toast.error($i18n.t(`File not found.`)); + } + } + + 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); + }; + }); +</script> + +<FilesOverlay show={dragged} /> + +<div class="w-full font-primary"> + <div class=" -mb-0.5 mx-auto inset-x-0 bg-transparent flex justify-center"> + <div class="flex flex-col max-w-6xl px-2.5 md:px-6 w-full"> + <div class="relative"> + {#if autoScroll === false && messages.length > 0} + <div + class=" absolute -top-12 left-0 right-0 flex justify-center z-30 pointer-events-none" + > + <button + class=" bg-white border border-gray-100 dark:border-none dark:bg-white/20 p-1.5 rounded-full pointer-events-auto" + on:click={() => { + autoScroll = true; + scrollToBottom(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + fill-rule="evenodd" + d="M10 3a.75.75 0 01.75.75v10.638l3.96-4.158a.75.75 0 111.08 1.04l-5.25 5.5a.75.75 0 01-1.08 0l-5.25-5.5a.75.75 0 111.08-1.04l3.96 4.158V3.75A.75.75 0 0110 3z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + {/if} + </div> + + <div class="w-full relative"> + {#if prompt.charAt(0) === '/'} + <Prompts bind:this={promptsElement} bind:prompt bind:files /> + {:else if prompt.charAt(0) === '#'} + <Documents + bind:this={documentsElement} + bind:prompt + on:youtube={(e) => { + console.log(e); + uploadYoutubeTranscription(e.detail); + }} + on:url={(e) => { + console.log(e); + uploadWeb(e.detail); + }} + on:select={(e) => { + console.log(e); + files = [ + ...files, + { + type: e?.detail?.type ?? 'file', + ...e.detail, + status: 'processed' + } + ]; + }} + /> + {/if} + + <Models + bind:this={modelsElement} + bind:prompt + bind:chatInputPlaceholder + {messages} + on:select={(e) => { + atSelectedModel = e.detail; + chatTextAreaElement?.focus(); + }} + /> + + {#if atSelectedModel !== undefined} + <div + class="px-3 py-2.5 text-left w-full flex justify-between items-center absolute bottom-0 left-0 right-0 bg-gradient-to-t from-50% from-white dark:from-gray-900" + > + <div class="flex items-center gap-2 text-sm dark:text-gray-500"> + <img + crossorigin="anonymous" + alt="model profile" + class="size-5 max-w-[28px] object-cover rounded-full" + src={$models.find((model) => model.id === atSelectedModel.id)?.info?.meta + ?.profile_image_url ?? + ($i18n.language === 'dg-DG' + ? `/doge.png` + : `${WEBUI_BASE_URL}/static/favicon.png`)} + /> + <div> + Talking to <span class=" font-medium">{atSelectedModel.name}</span> + </div> + </div> + <div> + <button + class="flex items-center" + on:click={() => { + atSelectedModel = undefined; + }} + > + <XMark /> + </button> + </div> + </div> + {/if} + </div> + </div> + </div> + + <div class="{transparentBackground ? 'bg-transparent' : 'bg-white dark:bg-gray-900'} "> + <div class="max-w-6xl px-2.5 md:px-6 mx-auto inset-x-0"> + <div class=" pb-2"> + <input + bind:this={filesInputElement} + bind:files={inputFiles} + type="file" + hidden + multiple + on:change={async () => { + if (inputFiles && inputFiles.length > 0) { + const _inputFiles = Array.from(inputFiles); + _inputFiles.forEach((file) => { + if (['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(file['type'])) { + if (visionCapableModels.length === 0) { + toast.error($i18n.t('Selected model(s) do not support image inputs')); + return; + } + let reader = new FileReader(); + reader.onload = (event) => { + files = [ + ...files, + { + type: 'image', + url: `${event.target.result}` + } + ]; + }; + reader.readAsDataURL(file); + } else { + uploadFileHandler(file); + } + }); + } else { + toast.error($i18n.t(`File not found.`)); + } + + filesInputElement.value = ''; + }} + /> + + {#if recording} + <VoiceRecording + bind:recording + on:cancel={async () => { + recording = false; + + await tick(); + document.getElementById('chat-textarea')?.focus(); + }} + on:confirm={async (e) => { + const response = e.detail; + prompt = `${prompt}${response} `; + + recording = false; + + await tick(); + document.getElementById('chat-textarea')?.focus(); + + if ($settings?.speechAutoSend ?? false) { + submitPrompt(prompt); + } + }} + /> + {:else} + <form + class="w-full flex gap-1.5" + on:submit|preventDefault={() => { + // check if selectedModels support image input + submitPrompt(prompt); + }} + > + <div + class="flex-1 flex flex-col relative w-full rounded-3xl px-1.5 bg-gray-50 dark:bg-gray-850 dark:text-gray-100" + dir={$settings?.chatDirection ?? 'LTR'} + > + {#if files.length > 0} + <div class="mx-1 mt-2.5 mb-1 flex flex-wrap gap-2"> + {#each files as file, fileIdx} + {#if file.type === 'image'} + <div class=" relative group"> + <div class="relative"> + <img + src={file.url} + alt="input" + class=" h-16 w-16 rounded-xl object-cover" + /> + {#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length} + <Tooltip + className=" absolute top-1 left-1" + content={$i18n.t('{{ models }}', { + models: [...(atSelectedModel ? [atSelectedModel] : selectedModels)] + .filter((id) => !visionCapableModels.includes(id)) + .join(', ') + })} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4 fill-yellow-300" + > + <path + fill-rule="evenodd" + d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" + clip-rule="evenodd" + /> + </svg> + </Tooltip> + {/if} + </div> + <div class=" absolute -top-1 -right-1"> + <button + class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" + type="button" + on:click={() => { + files.splice(fileIdx, 1); + files = files; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + </div> + {:else} + <FileItem + name={file.name} + type={file.type} + status={file.status} + dismissible={true} + on:dismiss={() => { + files.splice(fileIdx, 1); + files = files; + }} + /> + {/if} + {/each} + </div> + {/if} + + <div class=" flex"> + <div class=" ml-0.5 self-end mb-1.5 flex space-x-1"> + <InputMenu + bind:webSearchEnabled + bind:selectedToolIds + tools={$tools.reduce((a, e, i, arr) => { + if (availableToolIds.includes(e.id) || ($_user?.role ?? 'user') === 'admin') { + a[e.id] = { + name: e.name, + description: e.meta.description, + enabled: false + }; + } + return a; + }, {})} + uploadFilesHandler={() => { + filesInputElement.click(); + }} + onClose={async () => { + await tick(); + chatTextAreaElement?.focus(); + }} + > + <button + class="bg-gray-50 hover:bg-gray-100 text-gray-800 dark:bg-gray-850 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-2 outline-none focus:outline-none" + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-5" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + </InputMenu> + </div> + + <textarea + id="chat-textarea" + bind:this={chatTextAreaElement} + class="scrollbar-hidden bg-gray-50 dark:bg-gray-850 dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]" + placeholder={chatInputPlaceholder !== '' + ? chatInputPlaceholder + : $i18n.t('Send a Message')} + bind:value={prompt} + on:keypress={(e) => { + if ( + !$mobile || + !( + 'ontouchstart' in window || + navigator.maxTouchPoints > 0 || + navigator.msMaxTouchPoints > 0 + ) + ) { + // Prevent Enter key from creating a new line + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + } + + // Submit the prompt when Enter key is pressed + if (prompt !== '' && e.key === 'Enter' && !e.shiftKey) { + submitPrompt(prompt); + } + } + }} + on:keydown={async (e) => { + const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac + + // Check if Ctrl + R is pressed + if (prompt === '' && isCtrlPressed && e.key.toLowerCase() === 'r') { + e.preventDefault(); + console.log('regenerate'); + + const regenerateButton = [ + ...document.getElementsByClassName('regenerate-response-button') + ]?.at(-1); + + regenerateButton?.click(); + } + + if (prompt === '' && e.key == 'ArrowUp') { + e.preventDefault(); + + const userMessageElement = [ + ...document.getElementsByClassName('user-message') + ]?.at(-1); + + const editButton = [ + ...document.getElementsByClassName('edit-user-message-button') + ]?.at(-1); + + console.log(userMessageElement); + + userMessageElement.scrollIntoView({ block: 'center' }); + editButton?.click(); + } + + if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowUp') { + e.preventDefault(); + + (promptsElement || documentsElement || modelsElement).selectUp(); + + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + commandOptionButton.scrollIntoView({ block: 'center' }); + } + + if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'ArrowDown') { + e.preventDefault(); + + (promptsElement || documentsElement || modelsElement).selectDown(); + + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + commandOptionButton.scrollIntoView({ block: 'center' }); + } + + if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Enter') { + e.preventDefault(); + + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + + if (e.shiftKey) { + prompt = `${prompt}\n`; + } else if (commandOptionButton) { + commandOptionButton?.click(); + } else { + document.getElementById('send-message-button')?.click(); + } + } + + if (['/', '#', '@'].includes(prompt.charAt(0)) && e.key === 'Tab') { + e.preventDefault(); + + const commandOptionButton = [ + ...document.getElementsByClassName('selected-command-option-button') + ]?.at(-1); + + commandOptionButton?.click(); + } else if (e.key === 'Tab') { + const words = findWordIndices(prompt); + + if (words.length > 0) { + const word = words.at(0); + const fullPrompt = prompt; + + prompt = prompt.substring(0, word?.endIndex + 1); + await tick(); + + e.target.scrollTop = e.target.scrollHeight; + prompt = fullPrompt; + await tick(); + + e.preventDefault(); + e.target.setSelectionRange(word?.startIndex, word.endIndex + 1); + } + + e.target.style.height = ''; + e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; + } + + if (e.key === 'Escape') { + console.log('Escape'); + atSelectedModel = undefined; + } + }} + rows="1" + on:input={(e) => { + e.target.style.height = ''; + e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; + user = null; + }} + on:focus={(e) => { + e.target.style.height = ''; + e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; + }} + on:paste={(e) => { + const clipboardData = e.clipboardData || window.clipboardData; + + if (clipboardData && clipboardData.items) { + for (const item of clipboardData.items) { + if (item.type.indexOf('image') !== -1) { + const blob = item.getAsFile(); + const reader = new FileReader(); + + reader.onload = function (e) { + files = [ + ...files, + { + type: 'image', + url: `${e.target.result}` + } + ]; + }; + + reader.readAsDataURL(blob); + } + } + } + }} + /> + + <div class="self-end mb-2 flex space-x-1 mr-1"> + {#if messages.length == 0 || messages.at(-1).done == true} + <Tooltip content={$i18n.t('Record voice')}> + <button + id="voice-input-button" + class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-1.5 mr-0.5 self-center" + type="button" + on:click={async () => { + try { + const res = await navigator.mediaDevices + .getUserMedia({ audio: true }) + .catch(function (err) { + toast.error( + $i18n.t( + `Permission denied when accessing microphone: {{error}}`, + { + error: err + } + ) + ); + return null; + }); + + if (res) { + recording = true; + } + } catch { + toast.error($i18n.t('Permission denied when accessing microphone')); + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5 translate-y-[0.5px]" + > + <path d="M7 4a3 3 0 016 0v6a3 3 0 11-6 0V4z" /> + <path + d="M5.5 9.643a.75.75 0 00-1.5 0V10c0 3.06 2.29 5.585 5.25 5.954V17.5h-1.5a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-1.5v-1.546A6.001 6.001 0 0016 10v-.357a.75.75 0 00-1.5 0V10a4.5 4.5 0 01-9 0v-.357z" + /> + </svg> + </button> + </Tooltip> + {/if} + </div> + </div> + </div> + <div class="flex items-end w-10"> + {#if messages.length == 0 || messages.at(-1).done == true} + {#if prompt === ''} + <div class=" flex items-center mb-1"> + <Tooltip content={$i18n.t('Call')}> + <button + class=" text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-full p-2 self-center" + type="button" + on:click={async () => { + if (selectedModels.length > 1) { + toast.error($i18n.t('Select only one model to call')); + + return; + } + + if ($config.audio.stt.engine === 'web') { + toast.error( + $i18n.t('Call feature is not supported when using Web STT engine') + ); + + return; + } + // check if user has access to getUserMedia + try { + await navigator.mediaDevices.getUserMedia({ audio: true }); + // If the user grants the permission, proceed to show the call overlay + + showCallOverlay.set(true); + } catch (err) { + // If the user denies the permission or an error occurs, show an error message + toast.error($i18n.t('Permission denied when accessing media devices')); + } + }} + > + <Headphone className="size-6" /> + </button> + </Tooltip> + </div> + {:else} + <div class=" flex items-center mb-1"> + <Tooltip content={$i18n.t('Send message')}> + <button + id="send-message-button" + class="{prompt !== '' + ? 'bg-black text-white hover:bg-gray-900 dark:bg-white dark:text-black dark:hover:bg-gray-100 ' + : 'text-white bg-gray-200 dark:text-gray-900 dark:bg-gray-700 disabled'} transition rounded-full p-1.5 m-0.5 self-center" + type="submit" + disabled={prompt === ''} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-6" + > + <path + fill-rule="evenodd" + d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z" + clip-rule="evenodd" + /> + </svg> + </button> + </Tooltip> + </div> + {/if} + {:else} + <div class=" flex items-center mb-1.5"> + <button + class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5" + on:click={() => { + stopResponse(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-6" + > + <path + fill-rule="evenodd" + d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm6-2.438c0-.724.588-1.312 1.313-1.312h4.874c.725 0 1.313.588 1.313 1.313v4.874c0 .725-.588 1.313-1.313 1.313H9.564a1.312 1.312 0 01-1.313-1.313V9.564z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + {/if} + </div> + </form> + {/if} + + <div class="mt-1.5 text-xs text-gray-500 text-center line-clamp-1"> + {$i18n.t('LLMs can make mistakes. Verify important information.')} + </div> + </div> + </div> + </div> +</div> + +<style> + .scrollbar-hidden:active::-webkit-scrollbar-thumb, + .scrollbar-hidden:focus::-webkit-scrollbar-thumb, + .scrollbar-hidden:hover::-webkit-scrollbar-thumb { + visibility: visible; + } + .scrollbar-hidden::-webkit-scrollbar-thumb { + visibility: hidden; + } +</style> diff --git a/src/lib/components/chat/MessageInput/CallOverlay.svelte b/src/lib/components/chat/MessageInput/CallOverlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e28b4b5bd0619cab7ba1b2c6a5ed80fc650fc08f --- /dev/null +++ b/src/lib/components/chat/MessageInput/CallOverlay.svelte @@ -0,0 +1,900 @@ +<script lang="ts"> + import { config, models, settings, showCallOverlay } from '$lib/stores'; + import { onMount, tick, getContext } from 'svelte'; + + import { + blobToFile, + calculateSHA256, + extractSentencesForAudio, + findWordIndices + } from '$lib/utils'; + import { generateEmoji } from '$lib/apis'; + import { synthesizeOpenAISpeech, transcribeAudio } from '$lib/apis/audio'; + + import { toast } from 'svelte-sonner'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import VideoInputMenu from './CallOverlay/VideoInputMenu.svelte'; + + const i18n = getContext('i18n'); + + export let eventTarget: EventTarget; + + export let submitPrompt: Function; + export let stopResponse: Function; + + export let files; + + export let chatId; + export let modelId; + + let model = null; + + let loading = false; + let confirmed = false; + let interrupted = false; + let assistantSpeaking = false; + + let emoji = null; + + let camera = false; + let cameraStream = null; + + let chatStreaming = false; + + let rmsLevel = 0; + let hasStartedSpeaking = false; + let mediaRecorder; + let audioChunks = []; + + let videoInputDevices = []; + let selectedVideoInputDeviceId = null; + + const getVideoInputDevices = async () => { + const devices = await navigator.mediaDevices.enumerateDevices(); + videoInputDevices = devices.filter((device) => device.kind === 'videoinput'); + + if (!!navigator.mediaDevices.getDisplayMedia) { + videoInputDevices = [ + ...videoInputDevices, + { + deviceId: 'screen', + label: 'Screen Share' + } + ]; + } + + console.log(videoInputDevices); + if (selectedVideoInputDeviceId === null && videoInputDevices.length > 0) { + selectedVideoInputDeviceId = videoInputDevices[0].deviceId; + } + }; + + const startCamera = async () => { + await getVideoInputDevices(); + + if (cameraStream === null) { + camera = true; + await tick(); + try { + await startVideoStream(); + } catch (err) { + console.error('Error accessing webcam: ', err); + } + } + }; + + const startVideoStream = async () => { + const video = document.getElementById('camera-feed'); + if (video) { + if (selectedVideoInputDeviceId === 'screen') { + cameraStream = await navigator.mediaDevices.getDisplayMedia({ + video: { + cursor: 'always' + }, + audio: false + }); + } else { + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { + deviceId: selectedVideoInputDeviceId ? { exact: selectedVideoInputDeviceId } : undefined + } + }); + } + + if (cameraStream) { + await getVideoInputDevices(); + video.srcObject = cameraStream; + await video.play(); + } + } + }; + + const stopVideoStream = async () => { + if (cameraStream) { + const tracks = cameraStream.getTracks(); + tracks.forEach((track) => track.stop()); + } + + cameraStream = null; + }; + + const takeScreenshot = () => { + const video = document.getElementById('camera-feed'); + const canvas = document.getElementById('camera-canvas'); + + if (!canvas) { + return; + } + + const context = canvas.getContext('2d'); + + // Make the canvas match the video dimensions + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + + // Draw the image from the video onto the canvas + context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); + + // Convert the canvas to a data base64 URL and console log it + const dataURL = canvas.toDataURL('image/png'); + console.log(dataURL); + + return dataURL; + }; + + const stopCamera = async () => { + await stopVideoStream(); + camera = false; + }; + + const MIN_DECIBELS = -55; + const VISUALIZER_BUFFER_LENGTH = 300; + + const transcribeHandler = async (audioBlob) => { + // Create a blob from the audio chunks + + await tick(); + const file = blobToFile(audioBlob, 'recording.wav'); + + const res = await transcribeAudio(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + console.log(res.text); + + if (res.text !== '') { + const _responses = await submitPrompt(res.text, { _raw: true }); + console.log(_responses); + } + } + }; + + const stopRecordingCallback = async (_continue = true) => { + if ($showCallOverlay) { + console.log('%c%s', 'color: red; font-size: 20px;', '🚨 stopRecordingCallback 🚨'); + + // deep copy the audioChunks array + const _audioChunks = audioChunks.slice(0); + + audioChunks = []; + mediaRecorder = false; + + if (_continue) { + startRecording(); + } + + if (confirmed) { + loading = true; + emoji = null; + + if (cameraStream) { + const imageUrl = takeScreenshot(); + + files = [ + { + type: 'image', + url: imageUrl + } + ]; + } + + const audioBlob = new Blob(_audioChunks, { type: 'audio/wav' }); + await transcribeHandler(audioBlob); + + confirmed = false; + loading = false; + } + } else { + audioChunks = []; + mediaRecorder = false; + } + }; + + const startRecording = async () => { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + + mediaRecorder.onstart = () => { + console.log('Recording started'); + audioChunks = []; + analyseAudio(stream); + }; + + mediaRecorder.ondataavailable = (event) => { + if (hasStartedSpeaking) { + audioChunks.push(event.data); + } + }; + + mediaRecorder.onstop = (e) => { + console.log('Recording stopped', e); + stopRecordingCallback(); + }; + + mediaRecorder.start(); + }; + + // Function to calculate the RMS level from time domain data + const calculateRMS = (data: Uint8Array) => { + let sumSquares = 0; + for (let i = 0; i < data.length; i++) { + const normalizedValue = (data[i] - 128) / 128; // Normalize the data + sumSquares += normalizedValue * normalizedValue; + } + return Math.sqrt(sumSquares / data.length); + }; + + const analyseAudio = (stream) => { + const audioContext = new AudioContext(); + const audioStreamSource = audioContext.createMediaStreamSource(stream); + + const analyser = audioContext.createAnalyser(); + analyser.minDecibels = MIN_DECIBELS; + audioStreamSource.connect(analyser); + + const bufferLength = analyser.frequencyBinCount; + + const domainData = new Uint8Array(bufferLength); + const timeDomainData = new Uint8Array(analyser.fftSize); + + let lastSoundTime = Date.now(); + hasStartedSpeaking = false; + + console.log('🔊 Sound detection started', lastSoundTime, hasStartedSpeaking); + + const detectSound = () => { + const processFrame = () => { + if (!mediaRecorder || !$showCallOverlay) { + return; + } + + if (assistantSpeaking && !($settings?.voiceInterruption ?? false)) { + // Mute the audio if the assistant is speaking + analyser.maxDecibels = 0; + analyser.minDecibels = -1; + } else { + analyser.minDecibels = MIN_DECIBELS; + analyser.maxDecibels = -30; + } + + analyser.getByteTimeDomainData(timeDomainData); + analyser.getByteFrequencyData(domainData); + + // Calculate RMS level from time domain data + rmsLevel = calculateRMS(timeDomainData); + + // Check if initial speech/noise has started + const hasSound = domainData.some((value) => value > 0); + if (hasSound) { + // BIG RED TEXT + console.log('%c%s', 'color: red; font-size: 20px;', '🔊 Sound detected'); + + if (!hasStartedSpeaking) { + hasStartedSpeaking = true; + stopAllAudio(); + } + + lastSoundTime = Date.now(); + } + + // Start silence detection only after initial speech/noise has been detected + if (hasStartedSpeaking) { + if (Date.now() - lastSoundTime > 2000) { + confirmed = true; + + if (mediaRecorder) { + console.log('%c%s', 'color: red; font-size: 20px;', '🔇 Silence detected'); + mediaRecorder.stop(); + return; + } + } + } + + window.requestAnimationFrame(processFrame); + }; + + window.requestAnimationFrame(processFrame); + }; + + detectSound(); + }; + + let finishedMessages = {}; + let currentMessageId = null; + let currentUtterance = null; + + const speakSpeechSynthesisHandler = (content) => { + if ($showCallOverlay) { + return new Promise((resolve) => { + let voices = []; + const getVoicesLoop = setInterval(async () => { + voices = await speechSynthesis.getVoices(); + if (voices.length > 0) { + clearInterval(getVoicesLoop); + + const voice = + voices + ?.filter( + (v) => v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) + ) + ?.at(0) ?? undefined; + + currentUtterance = new SpeechSynthesisUtterance(content); + + if (voice) { + currentUtterance.voice = voice; + } + + speechSynthesis.speak(currentUtterance); + currentUtterance.onend = async (e) => { + await new Promise((r) => setTimeout(r, 200)); + resolve(e); + }; + } + }, 100); + }); + } else { + return Promise.resolve(); + } + }; + + const playAudio = (audio) => { + if ($showCallOverlay) { + return new Promise((resolve) => { + const audioElement = document.getElementById('audioElement'); + + if (audioElement) { + audioElement.src = audio.src; + audioElement.muted = true; + + audioElement + .play() + .then(() => { + audioElement.muted = false; + }) + .catch((error) => { + console.error(error); + }); + + audioElement.onended = async (e) => { + await new Promise((r) => setTimeout(r, 100)); + resolve(e); + }; + } + }); + } else { + return Promise.resolve(); + } + }; + + const stopAllAudio = async () => { + assistantSpeaking = false; + interrupted = true; + + if (chatStreaming) { + stopResponse(); + } + + if (currentUtterance) { + speechSynthesis.cancel(); + currentUtterance = null; + } + + const audioElement = document.getElementById('audioElement'); + if (audioElement) { + audioElement.muted = true; + audioElement.pause(); + audioElement.currentTime = 0; + } + }; + + let audioAbortController = new AbortController(); + + // Audio cache map where key is the content and value is the Audio object. + const audioCache = new Map(); + const emojiCache = new Map(); + + const fetchAudio = async (content) => { + if (!audioCache.has(content)) { + try { + // Set the emoji for the content if needed + if ($settings?.showEmojiInCall ?? false) { + const emoji = await generateEmoji(localStorage.token, modelId, content, chatId); + if (emoji) { + emojiCache.set(content, emoji); + } + } + + if ($config.audio.tts.engine !== '') { + const res = await synthesizeOpenAISpeech( + localStorage.token, + $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, + content + ).catch((error) => { + console.error(error); + return null; + }); + + if (res) { + const blob = await res.blob(); + const blobUrl = URL.createObjectURL(blob); + audioCache.set(content, new Audio(blobUrl)); + } + } else { + audioCache.set(content, true); + } + } catch (error) { + console.error('Error synthesizing speech:', error); + } + } + + return audioCache.get(content); + }; + + let messages = {}; + + const monitorAndPlayAudio = async (id, signal) => { + while (!signal.aborted) { + if (messages[id] && messages[id].length > 0) { + // Retrieve the next content string from the queue + const content = messages[id].shift(); // Dequeues the content for playing + + if (audioCache.has(content)) { + // If content is available in the cache, play it + + // Set the emoji for the content if available + if (($settings?.showEmojiInCall ?? false) && emojiCache.has(content)) { + emoji = emojiCache.get(content); + } else { + emoji = null; + } + + if ($config.audio.tts.engine !== '') { + try { + console.log( + '%c%s', + 'color: red; font-size: 20px;', + `Playing audio for content: ${content}` + ); + + const audio = audioCache.get(content); + await playAudio(audio); // Here ensure that playAudio is indeed correct method to execute + console.log(`Played audio for content: ${content}`); + await new Promise((resolve) => setTimeout(resolve, 200)); // Wait before retrying to reduce tight loop + } catch (error) { + console.error('Error playing audio:', error); + } + } else { + await speakSpeechSynthesisHandler(content); + } + } else { + // If not available in the cache, push it back to the queue and delay + messages[id].unshift(content); // Re-queue the content at the start + console.log(`Audio for "${content}" not yet available in the cache, re-queued...`); + await new Promise((resolve) => setTimeout(resolve, 200)); // Wait before retrying to reduce tight loop + } + } else if (finishedMessages[id] && messages[id] && messages[id].length === 0) { + // If the message is finished and there are no more messages to process, break the loop + assistantSpeaking = false; + break; + } else { + // No messages to process, sleep for a bit + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + console.log(`Audio monitoring and playing stopped for message ID ${id}`); + }; + + onMount(async () => { + model = $models.find((m) => m.id === modelId); + + startRecording(); + + const chatStartHandler = async (e) => { + const { id } = e.detail; + + chatStreaming = true; + + if (currentMessageId !== id) { + console.log(`Received chat start event for message ID ${id}`); + + currentMessageId = id; + if (audioAbortController) { + audioAbortController.abort(); + } + audioAbortController = new AbortController(); + + assistantSpeaking = true; + // Start monitoring and playing audio for the message ID + monitorAndPlayAudio(id, audioAbortController.signal); + } + }; + + const chatEventHandler = async (e) => { + const { id, content } = e.detail; + // "id" here is message id + // if "id" is not the same as "currentMessageId" then do not process + // "content" here is a sentence from the assistant, + // there will be many sentences for the same "id" + + if (currentMessageId === id) { + console.log(`Received chat event for message ID ${id}: ${content}`); + + try { + if (messages[id] === undefined) { + messages[id] = [content]; + } else { + messages[id].push(content); + } + + console.log(content); + + fetchAudio(content); + } catch (error) { + console.error('Failed to fetch or play audio:', error); + } + } + }; + + const chatFinishHandler = async (e) => { + const { id, content } = e.detail; + // "content" here is the entire message from the assistant + finishedMessages[id] = true; + + chatStreaming = false; + }; + + eventTarget.addEventListener('chat:start', chatStartHandler); + eventTarget.addEventListener('chat', chatEventHandler); + eventTarget.addEventListener('chat:finish', chatFinishHandler); + + return async () => { + eventTarget.removeEventListener('chat:start', chatStartHandler); + eventTarget.removeEventListener('chat', chatEventHandler); + eventTarget.removeEventListener('chat:finish', chatFinishHandler); + + audioAbortController.abort(); + await tick(); + + await stopAllAudio(); + + await stopRecordingCallback(false); + await stopCamera(); + }; + }); +</script> + +{#if $showCallOverlay} + <div class=" absolute w-full h-screen max-h-[100dvh] flex z-[999] overflow-hidden"> + <div + class="absolute w-full h-screen max-h-[100dvh] bg-white text-gray-700 dark:bg-black dark:text-gray-300 flex justify-center" + > + <div class="max-w-lg w-full h-screen max-h-[100dvh] flex flex-col justify-between p-3 md:p-6"> + {#if camera} + <button + type="button" + class="flex justify-center items-center w-full h-20 min-h-20" + on:click={() => { + if (assistantSpeaking) { + stopAllAudio(); + } + }} + > + {#if emoji} + <div + class=" transition-all rounded-full" + style="font-size:{rmsLevel * 100 > 4 + ? '4.5' + : rmsLevel * 100 > 2 + ? '4.25' + : rmsLevel * 100 > 1 + ? '3.75' + : '3.5'}rem;width: 100%; text-align:center;" + > + {emoji} + </div> + {:else if loading || assistantSpeaking} + <svg + class="size-12 text-gray-900 dark:text-gray-400" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_qM83 { + animation: spinner_8HQG 1.05s infinite; + } + .spinner_oXPr { + animation-delay: 0.1s; + } + .spinner_ZTLf { + animation-delay: 0.2s; + } + @keyframes spinner_8HQG { + 0%, + 57.14% { + animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); + transform: translate(0); + } + 28.57% { + animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); + transform: translateY(-6px); + } + 100% { + transform: translate(0); + } + } + </style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle + class="spinner_qM83 spinner_oXPr" + cx="12" + cy="12" + r="3" + /><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg + > + {:else} + <div + class=" {rmsLevel * 100 > 4 + ? ' size-[4.5rem]' + : rmsLevel * 100 > 2 + ? ' size-16' + : rmsLevel * 100 > 1 + ? 'size-14' + : 'size-12'} transition-all rounded-full {(model?.info?.meta + ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' + ? ' bg-cover bg-center bg-no-repeat' + : 'bg-black dark:bg-white'} bg-black dark:bg-white" + style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !== + '/static/favicon.png' + ? `background-image: url('${model?.info?.meta?.profile_image_url}');` + : ''} + /> + {/if} + <!-- navbar --> + </button> + {/if} + + <div class="flex justify-center items-center flex-1 h-full w-full max-h-full"> + {#if !camera} + <button + type="button" + on:click={() => { + if (assistantSpeaking) { + stopAllAudio(); + } + }} + > + {#if emoji} + <div + class=" transition-all rounded-full" + style="font-size:{rmsLevel * 100 > 4 + ? '13' + : rmsLevel * 100 > 2 + ? '12' + : rmsLevel * 100 > 1 + ? '11.5' + : '11'}rem;width:100%;text-align:center;" + > + {emoji} + </div> + {:else if loading || assistantSpeaking} + <svg + class="size-44 text-gray-900 dark:text-gray-400" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_qM83 { + animation: spinner_8HQG 1.05s infinite; + } + .spinner_oXPr { + animation-delay: 0.1s; + } + .spinner_ZTLf { + animation-delay: 0.2s; + } + @keyframes spinner_8HQG { + 0%, + 57.14% { + animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); + transform: translate(0); + } + 28.57% { + animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); + transform: translateY(-6px); + } + 100% { + transform: translate(0); + } + } + </style><circle class="spinner_qM83" cx="4" cy="12" r="3" /><circle + class="spinner_qM83 spinner_oXPr" + cx="12" + cy="12" + r="3" + /><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="3" /></svg + > + {:else} + <div + class=" {rmsLevel * 100 > 4 + ? ' size-52' + : rmsLevel * 100 > 2 + ? 'size-48' + : rmsLevel * 100 > 1 + ? 'size-[11.5rem]' + : 'size-44'} transition-all rounded-full {(model?.info?.meta + ?.profile_image_url ?? '/static/favicon.png') !== '/static/favicon.png' + ? ' bg-cover bg-center bg-no-repeat' + : 'bg-black dark:bg-white'} " + style={(model?.info?.meta?.profile_image_url ?? '/static/favicon.png') !== + '/static/favicon.png' + ? `background-image: url('${model?.info?.meta?.profile_image_url}');` + : ''} + /> + {/if} + </button> + {:else} + <div + class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full" + > + <video + id="camera-feed" + autoplay + class="rounded-2xl h-full min-w-full object-cover object-center" + playsinline + /> + + <canvas id="camera-canvas" style="display:none;" /> + + <div class=" absolute top-4 md:top-8 left-4"> + <button + type="button" + class="p-1.5 text-white cursor-pointer backdrop-blur-xl bg-black/10 rounded-full" + on:click={() => { + stopCamera(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-6" + > + <path + d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" + /> + </svg> + </button> + </div> + </div> + {/if} + </div> + + <div class="flex justify-between items-center pb-2 w-full"> + <div> + {#if camera} + <VideoInputMenu + devices={videoInputDevices} + on:change={async (e) => { + console.log(e.detail); + selectedVideoInputDeviceId = e.detail; + await stopVideoStream(); + await startVideoStream(); + }} + > + <button class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" type="button"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="size-5" + > + <path + fill-rule="evenodd" + d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" + clip-rule="evenodd" + /> + </svg> + </button> + </VideoInputMenu> + {:else} + <Tooltip content={$i18n.t('Camera')}> + <button + class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" + type="button" + on:click={async () => { + await navigator.mediaDevices.getUserMedia({ video: true }); + startCamera(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.827 6.175A2.31 2.31 0 0 1 5.186 7.23c-.38.054-.757.112-1.134.175C2.999 7.58 2.25 8.507 2.25 9.574V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18V9.574c0-1.067-.75-1.994-1.802-2.169a47.865 47.865 0 0 0-1.134-.175 2.31 2.31 0 0 1-1.64-1.055l-.822-1.316a2.192 2.192 0 0 0-1.736-1.039 48.774 48.774 0 0 0-5.232 0 2.192 2.192 0 0 0-1.736 1.039l-.821 1.316Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.5 12.75a4.5 4.5 0 1 1-9 0 4.5 4.5 0 0 1 9 0ZM18.75 10.5h.008v.008h-.008V10.5Z" + /> + </svg> + </button> + </Tooltip> + {/if} + </div> + + <div> + <button + type="button" + on:click={() => { + if (assistantSpeaking) { + stopAllAudio(); + } + }} + > + <div class=" line-clamp-1 text-sm font-medium"> + {#if loading} + {$i18n.t('Thinking...')} + {:else if assistantSpeaking} + {$i18n.t('Tap to interrupt')} + {:else} + {$i18n.t('Listening...')} + {/if} + </div> + </button> + </div> + + <div> + <button + class=" p-3 rounded-full bg-gray-50 dark:bg-gray-900" + on:click={async () => { + showCallOverlay.set(false); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="size-5" + > + <path + d="M6.28 5.22a.75.75 0 0 0-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 1 0 1.06 1.06L10 11.06l3.72 3.72a.75.75 0 1 0 1.06-1.06L11.06 10l3.72-3.72a.75.75 0 0 0-1.06-1.06L10 8.94 6.28 5.22Z" + /> + </svg> + </button> + </div> + </div> + </div> + </div> + </div> +{/if} diff --git a/src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte b/src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3b0cd0559272bcc2da2485eee0dbf68ae38408d8 --- /dev/null +++ b/src/lib/components/chat/MessageInput/CallOverlay/VideoInputMenu.svelte @@ -0,0 +1,51 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext, createEventDispatcher } from 'svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + + export let onClose: Function = () => {}; + export let devices: any; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <slot /> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-[9999] bg-white dark:bg-gray-900 dark:text-white shadow-sm" + sideOffset={6} + side="top" + align="start" + transition={flyAndScale} + > + {#each devices as device} + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + dispatch('change', device.deviceId); + }} + > + <div class="flex items-center"> + <div class=" line-clamp-1"> + {device?.label ?? 'Camera'} + </div> + </div> + </DropdownMenu.Item> + {/each} + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/chat/MessageInput/Documents.svelte b/src/lib/components/chat/MessageInput/Documents.svelte new file mode 100644 index 0000000000000000000000000000000000000000..64c4bc458b4ad22c70567f3a697de0e3873ea601 --- /dev/null +++ b/src/lib/components/chat/MessageInput/Documents.svelte @@ -0,0 +1,214 @@ +<script lang="ts"> + import { createEventDispatcher } from 'svelte'; + + import { documents } from '$lib/stores'; + import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils'; + import { tick, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + export let prompt = ''; + + const dispatch = createEventDispatcher(); + let selectedIdx = 0; + + let filteredItems = []; + let filteredDocs = []; + + let collections = []; + + $: collections = [ + ...($documents.length > 0 + ? [ + { + name: 'All Documents', + type: 'collection', + title: $i18n.t('All Documents'), + collection_names: $documents.map((doc) => doc.collection_name) + } + ] + : []), + ...$documents + .reduce((a, e, i, arr) => { + return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])]; + }, []) + .map((tag) => ({ + name: tag, + type: 'collection', + collection_names: $documents + .filter((doc) => (doc?.content?.tags ?? []).map((tag) => tag.name).includes(tag)) + .map((doc) => doc.collection_name) + })) + ]; + + $: filteredCollections = collections + .filter((collection) => findByName(collection, prompt)) + .sort((a, b) => a.name.localeCompare(b.name)); + + $: filteredDocs = $documents + .filter((doc) => findByName(doc, prompt)) + .sort((a, b) => a.title.localeCompare(b.title)); + + $: filteredItems = [...filteredCollections, ...filteredDocs]; + + $: if (prompt) { + selectedIdx = 0; + + console.log(filteredCollections); + } + + type ObjectWithName = { + name: string; + }; + + const findByName = (obj: ObjectWithName, prompt: string) => { + const name = obj.name.toLowerCase(); + return name.includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? ''); + }; + + export const selectUp = () => { + selectedIdx = Math.max(0, selectedIdx - 1); + }; + + export const selectDown = () => { + selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1); + }; + + const confirmSelect = async (doc) => { + dispatch('select', doc); + + prompt = removeFirstHashWord(prompt); + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + chatInputElement?.focus(); + await tick(); + }; + + const confirmSelectWeb = async (url) => { + dispatch('url', url); + + prompt = removeFirstHashWord(prompt); + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + chatInputElement?.focus(); + await tick(); + }; + + const confirmSelectYoutube = async (url) => { + dispatch('youtube', url); + + prompt = removeFirstHashWord(prompt); + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + chatInputElement?.focus(); + await tick(); + }; +</script> + +{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} + <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"> + <div class="flex w-full dark:border dark:border-gray-850 rounded-lg"> + <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center"> + <div class=" text-lg font-semibold mt-2">#</div> + </div> + + <div + class="max-h-60 flex flex-col w-full rounded-r-xl bg-white dark:bg-gray-900 dark:text-gray-100" + > + <div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden"> + {#each filteredItems as doc, docIdx} + <button + class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx + ? ' bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button' + : ''}" + type="button" + on:click={() => { + console.log(doc); + + confirmSelect(doc); + }} + on:mousemove={() => { + selectedIdx = docIdx; + }} + on:focus={() => {}} + > + {#if doc.type === 'collection'} + <div class=" font-medium text-black dark:text-gray-100 line-clamp-1"> + {doc?.title ?? `#${doc.name}`} + </div> + + <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1"> + {$i18n.t('Collection')} + </div> + {:else} + <div class=" font-medium text-black dark:text-gray-100 line-clamp-1"> + #{doc.name} ({doc.filename}) + </div> + + <div class=" text-xs text-gray-600 dark:text-gray-100 line-clamp-1"> + {doc.title} + </div> + {/if} + </button> + {/each} + + {#if prompt + .split(' ') + .some((s) => s.substring(1).startsWith('https://www.youtube.com') || s + .substring(1) + .startsWith('https://youtu.be'))} + <button + class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button" + type="button" + on:click={() => { + const url = prompt.split(' ')?.at(0)?.substring(1); + if (isValidHttpUrl(url)) { + confirmSelectYoutube(url); + } else { + toast.error( + $i18n.t( + 'Oops! Looks like the URL is invalid. Please double-check and try again.' + ) + ); + } + }} + > + <div class=" font-medium text-black dark:text-gray-100 line-clamp-1"> + {prompt.split(' ')?.at(0)?.substring(1)} + </div> + + <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Youtube')}</div> + </button> + {:else if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')} + <button + class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-50 dark:bg-gray-850 dark:text-gray-100 selected-command-option-button" + type="button" + on:click={() => { + const url = prompt.split(' ')?.at(0)?.substring(1); + if (isValidHttpUrl(url)) { + confirmSelectWeb(url); + } else { + toast.error( + $i18n.t( + 'Oops! Looks like the URL is invalid. Please double-check and try again.' + ) + ); + } + }} + > + <div class=" font-medium text-black dark:text-gray-100 line-clamp-1"> + {prompt.split(' ')?.at(0)?.substring(1)} + </div> + + <div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div> + </button> + {/if} + </div> + </div> + </div> + </div> +{/if} diff --git a/src/lib/components/chat/MessageInput/FilesOverlay.svelte b/src/lib/components/chat/MessageInput/FilesOverlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a8acc6a3e22309d9840331bd93e0c434e6745274 --- /dev/null +++ b/src/lib/components/chat/MessageInput/FilesOverlay.svelte @@ -0,0 +1,35 @@ +<script lang="ts"> + import { showSidebar } from '$lib/stores'; + import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; + + export let show = false; + let overlayElement = null; + + $: if (show && overlayElement) { + document.body.appendChild(overlayElement); + document.body.style.overflow = 'hidden'; + } else if (overlayElement) { + document.body.removeChild(overlayElement); + document.body.style.overflow = 'unset'; + } +</script> + +{#if show} + <div + bind:this={overlayElement} + class="fixed {$showSidebar + ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]' + : 'left-0'} fixed top-0 right-0 bottom-0 w-full h-full flex z-[9999] touch-none pointer-events-none" + id="dropzone" + role="region" + aria-label="Drag and Drop Container" + > + <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> + <div class="m-auto pt-64 flex flex-col justify-center"> + <div class="max-w-md"> + <AddFilesPlaceholder /> + </div> + </div> + </div> + </div> +{/if} diff --git a/src/lib/components/chat/MessageInput/InputMenu.svelte b/src/lib/components/chat/MessageInput/InputMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10a0cf87e2d6cbdd228e8f92ae3e0ba56865b187 --- /dev/null +++ b/src/lib/components/chat/MessageInput/InputMenu.svelte @@ -0,0 +1,112 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import DocumentArrowUpSolid from '$lib/components/icons/DocumentArrowUpSolid.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + import GlobeAltSolid from '$lib/components/icons/GlobeAltSolid.svelte'; + import { config } from '$lib/stores'; + import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte'; + + const i18n = getContext('i18n'); + + export let uploadFilesHandler: Function; + + export let selectedToolIds: string[] = []; + export let webSearchEnabled: boolean; + + export let tools = {}; + export let onClose: Function; + + $: tools = Object.fromEntries( + Object.keys(tools).map((toolId) => [ + toolId, + { + ...tools[toolId], + enabled: selectedToolIds.includes(toolId) + } + ]) + ); + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[200px] rounded-xl px-1 py-1 border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={15} + alignOffset={-8} + side="top" + align="start" + transition={flyAndScale} + > + {#if Object.keys(tools).length > 0} + <div class=" max-h-28 overflow-y-auto scrollbar-hidden"> + {#each Object.keys(tools) as toolId} + <div + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl" + > + <div class="flex-1 flex items-center gap-2"> + <WrenchSolid /> + <Tooltip content={tools[toolId]?.description ?? ''} className="flex-1"> + <div class=" line-clamp-1">{tools[toolId].name}</div> + </Tooltip> + </div> + + <Switch + bind:state={tools[toolId].enabled} + on:change={(e) => { + selectedToolIds = e.detail + ? [...selectedToolIds, toolId] + : selectedToolIds.filter((id) => id !== toolId); + }} + /> + </div> + {/each} + </div> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + {/if} + + {#if $config?.features?.enable_web_search} + <div + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer rounded-xl" + > + <div class="flex-1 flex items-center gap-2"> + <GlobeAltSolid /> + <div class=" line-clamp-1">{$i18n.t('Web Search')}</div> + </div> + + <Switch bind:state={webSearchEnabled} /> + </div> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + {/if} + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl" + on:click={() => { + uploadFilesHandler(); + }} + > + <DocumentArrowUpSolid /> + <div class=" line-clamp-1">{$i18n.t('Upload Files')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/chat/MessageInput/Models.svelte b/src/lib/components/chat/MessageInput/Models.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fb28cee59b3632e0e71b3f03c7f9471be71eb168 --- /dev/null +++ b/src/lib/components/chat/MessageInput/Models.svelte @@ -0,0 +1,176 @@ +<script lang="ts"> + import { createEventDispatcher } from 'svelte'; + + import { generatePrompt } from '$lib/apis/ollama'; + import { models } from '$lib/stores'; + import { splitStream } from '$lib/utils'; + import { tick, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + export let prompt = ''; + export let user = null; + + export let chatInputPlaceholder = ''; + export let messages = []; + + let selectedIdx = 0; + let filteredModels = []; + + $: filteredModels = $models + .filter((p) => + p.name.toLowerCase().includes(prompt.toLowerCase().split(' ')?.at(0)?.substring(1) ?? '') + ) + .sort((a, b) => a.name.localeCompare(b.name)); + + $: if (prompt) { + selectedIdx = 0; + } + + export const selectUp = () => { + selectedIdx = Math.max(0, selectedIdx - 1); + }; + + export const selectDown = () => { + selectedIdx = Math.min(selectedIdx + 1, filteredModels.length - 1); + }; + + const confirmSelect = async (model) => { + prompt = ''; + dispatch('select', model); + }; + + const confirmSelectCollaborativeChat = async (model) => { + // dispatch('select', model); + prompt = ''; + user = JSON.parse(JSON.stringify(model.name)); + await tick(); + + chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name }); + + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + chatInputElement?.focus(); + await tick(); + + const convoText = messages.reduce((a, message, i, arr) => { + return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; + }, ''); + + const res = await generatePrompt(localStorage.token, model.name, convoText); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + + if ('detail' in data) { + throw data; + } + + if ('id' in data) { + console.log(data); + } else { + if (data.done == false) { + if (prompt == '' && data.response == '\n') { + continue; + } else { + prompt += data.response; + console.log(data.response); + chatInputElement.scrollTop = chatInputElement.scrollHeight; + await tick(); + } + } + } + } + } + } catch (error) { + console.log(error); + if ('detail' in error) { + toast.error(error.detail); + } + break; + } + } + } else { + if (res !== null) { + const error = await res.json(); + console.log(error); + if ('detail' in error) { + toast.error(error.detail); + } else { + toast.error(error.error); + } + } else { + toast.error( + $i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' }) + ); + } + } + + chatInputPlaceholder = ''; + + console.log(user); + }; +</script> + +{#if prompt.charAt(0) === '@'} + {#if filteredModels.length > 0} + <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"> + <div class="flex w-full dark:border dark:border-gray-850 rounded-lg"> + <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center"> + <div class=" text-lg font-semibold mt-2">@</div> + </div> + + <div + class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100" + > + <div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden"> + {#each filteredModels as model, modelIdx} + <button + class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx + ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button' + : ''}" + type="button" + on:click={() => { + confirmSelect(model); + }} + on:mousemove={() => { + selectedIdx = modelIdx; + }} + on:focus={() => {}} + > + <div class=" font-medium text-black dark:text-gray-100 line-clamp-1"> + {model.name} + </div> + + <!-- <div class=" text-xs text-gray-600 line-clamp-1"> + {doc.title} + </div> --> + </button> + {/each} + </div> + </div> + </div> + </div> + {/if} +{/if} diff --git a/src/lib/components/chat/MessageInput/PromptCommands.svelte b/src/lib/components/chat/MessageInput/PromptCommands.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4dd8d3302454d6976c2b1966664cff7259fa504a --- /dev/null +++ b/src/lib/components/chat/MessageInput/PromptCommands.svelte @@ -0,0 +1,155 @@ +<script lang="ts"> + import { prompts } from '$lib/stores'; + import { findWordIndices } from '$lib/utils'; + import { tick, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + export let files; + export let prompt = ''; + let selectedCommandIdx = 0; + let filteredPromptCommands = []; + + $: filteredPromptCommands = $prompts + .filter((p) => p.command.toLowerCase().includes(prompt.toLowerCase())) + .sort((a, b) => a.title.localeCompare(b.title)); + + $: if (prompt) { + selectedCommandIdx = 0; + } + + export const selectUp = () => { + selectedCommandIdx = Math.max(0, selectedCommandIdx - 1); + }; + + export const selectDown = () => { + selectedCommandIdx = Math.min(selectedCommandIdx + 1, filteredPromptCommands.length - 1); + }; + + const confirmCommand = async (command) => { + let text = command.content; + + if (command.content.includes('{{CLIPBOARD}}')) { + const clipboardText = await navigator.clipboard.readText().catch((err) => { + toast.error($i18n.t('Failed to read clipboard contents')); + return '{{CLIPBOARD}}'; + }); + + console.log(clipboardText); + + const clipboardItems = await navigator.clipboard.read(); + + let imageUrl = null; + for (const item of clipboardItems) { + // Check for known image types + for (const type of item.types) { + if (type.startsWith('image/')) { + const blob = await item.getType(type); + imageUrl = URL.createObjectURL(blob); + console.log(`Image URL (${type}): ${imageUrl}`); + } + } + } + + if (imageUrl) { + files = [ + ...files, + { + type: 'image', + url: imageUrl + } + ]; + } + + text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText); + } + + prompt = text; + + const chatInputElement = document.getElementById('chat-textarea'); + + await tick(); + + chatInputElement.style.height = ''; + chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px'; + + chatInputElement?.focus(); + + await tick(); + + const words = findWordIndices(prompt); + + if (words.length > 0) { + const word = words.at(0); + chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1); + } + }; +</script> + +{#if filteredPromptCommands.length > 0} + <div class="pl-1 pr-12 mb-3 text-left w-full absolute bottom-0 left-0 right-0 z-10"> + <div class="flex w-full dark:border dark:border-gray-850 rounded-lg"> + <div class=" bg-gray-50 dark:bg-gray-850 w-10 rounded-l-lg text-center"> + <div class=" text-lg font-semibold mt-2">/</div> + </div> + + <div + class="max-h-60 flex flex-col w-full rounded-r-lg bg-white dark:bg-gray-900 dark:text-gray-100" + > + <div class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden"> + {#each filteredPromptCommands as command, commandIdx} + <button + class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx + ? ' bg-gray-50 dark:bg-gray-850 selected-command-option-button' + : ''}" + type="button" + on:click={() => { + confirmCommand(command); + }} + on:mousemove={() => { + selectedCommandIdx = commandIdx; + }} + on:focus={() => {}} + > + <div class=" font-medium text-black dark:text-gray-100"> + {command.command} + </div> + + <div class=" text-xs text-gray-600 dark:text-gray-100"> + {command.title} + </div> + </button> + {/each} + </div> + + <div + class=" px-2 pb-1 text-xs text-gray-600 dark:text-gray-100 bg-white dark:bg-gray-900 rounded-br-xl flex items-center space-x-1" + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-3 h-3" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" + /> + </svg> + </div> + + <div class="line-clamp-1"> + {$i18n.t( + 'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.' + )} + </div> + </div> + </div> + </div> + </div> +{/if} diff --git a/src/lib/components/chat/MessageInput/Suggestions.svelte b/src/lib/components/chat/MessageInput/Suggestions.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4e660c791938cdb6ab762e7617c21e0ae9c5680c --- /dev/null +++ b/src/lib/components/chat/MessageInput/Suggestions.svelte @@ -0,0 +1,118 @@ +<script lang="ts"> + import Bolt from '$lib/components/icons/Bolt.svelte'; + import { onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + export let submitPrompt: Function; + export let suggestionPrompts = []; + + let prompts = []; + + $: prompts = suggestionPrompts + .reduce((acc, current) => [...acc, ...[current]], []) + .sort(() => Math.random() - 0.5); + // suggestionPrompts.length <= 4 + // ? suggestionPrompts + // : suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4); + + onMount(() => { + const containerElement = document.getElementById('suggestions-container'); + + if (containerElement) { + containerElement.addEventListener('wheel', function (event) { + if (event.deltaY !== 0) { + // If scrolling vertically, prevent default behavior + event.preventDefault(); + // Adjust horizontal scroll position based on vertical scroll + containerElement.scrollLeft += event.deltaY; + } + }); + } + }); +</script> + +{#if prompts.length > 0} + <div class="mb-2 flex gap-1 text-sm font-medium items-center text-gray-400 dark:text-gray-600"> + <Bolt /> + {$i18n.t('Suggested')} + </div> +{/if} + +<div class="w-full"> + <div + class="relative w-full flex gap-2 snap-x snap-mandatory md:snap-none overflow-x-auto tabs" + id="suggestions-container" + > + {#each prompts as prompt, promptIdx} + <div class="snap-center shrink-0"> + <button + class="flex flex-col flex-1 shrink-0 w-64 justify-between h-36 p-5 px-6 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-3xl transition group" + on:click={() => { + submitPrompt(prompt.content); + }} + > + <div class="flex flex-col text-left"> + {#if prompt.title && prompt.title[0] !== ''} + <div + class=" font-medium dark:text-gray-300 dark:group-hover:text-gray-200 transition" + > + {prompt.title[0]} + </div> + <div class="text-sm text-gray-600 font-normal line-clamp-2">{prompt.title[1]}</div> + {:else} + <div + class=" text-sm font-medium dark:text-gray-300 dark:group-hover:text-gray-100 transition line-clamp-2" + > + {prompt.content} + </div> + {/if} + </div> + + <div class="w-full flex justify-between"> + <div + class="text-xs text-gray-400 group-hover:text-gray-500 dark:text-gray-600 dark:group-hover:text-gray-500 transition self-center" + > + {$i18n.t('Prompt')} + </div> + + <div + class="self-end p-1 rounded-lg text-gray-300 group-hover:text-gray-800 dark:text-gray-700 dark:group-hover:text-gray-100 transition" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M8 14a.75.75 0 0 1-.75-.75V4.56L4.03 7.78a.75.75 0 0 1-1.06-1.06l4.5-4.5a.75.75 0 0 1 1.06 0l4.5 4.5a.75.75 0 0 1-1.06 1.06L8.75 4.56v8.69A.75.75 0 0 1 8 14Z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + </button> + </div> + {/each} + + <!-- <div class="snap-center shrink-0"> + <img + class="shrink-0 w-80 h-40 rounded-lg shadow-xl bg-white" + src="https://images.unsplash.com/photo-1604999565976-8913ad2ddb7c?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=320&h=160&q=80" + /> + </div> --> + </div> +</div> + +<style> + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +</style> diff --git a/src/lib/components/chat/MessageInput/VoiceRecording.svelte b/src/lib/components/chat/MessageInput/VoiceRecording.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b2e859649381099ff0c47951f4e934b48af0dd5b --- /dev/null +++ b/src/lib/components/chat/MessageInput/VoiceRecording.svelte @@ -0,0 +1,458 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { createEventDispatcher, tick, getContext } from 'svelte'; + import { config, settings } from '$lib/stores'; + import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils'; + + import { transcribeAudio } from '$lib/apis/audio'; + + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + export let recording = false; + + let loading = false; + let confirmed = false; + + let durationSeconds = 0; + let durationCounter = null; + + let transcription = ''; + + const startDurationCounter = () => { + durationCounter = setInterval(() => { + durationSeconds++; + }, 1000); + }; + + const stopDurationCounter = () => { + clearInterval(durationCounter); + durationSeconds = 0; + }; + + $: if (recording) { + startRecording(); + } else { + stopRecording(); + } + + const formatSeconds = (seconds) => { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : remainingSeconds; + return `${minutes}:${formattedSeconds}`; + }; + + let speechRecognition; + + let mediaRecorder; + let audioChunks = []; + + const MIN_DECIBELS = -45; + const VISUALIZER_BUFFER_LENGTH = 300; + + let visualizerData = Array(VISUALIZER_BUFFER_LENGTH).fill(0); + + // Function to calculate the RMS level from time domain data + const calculateRMS = (data: Uint8Array) => { + let sumSquares = 0; + for (let i = 0; i < data.length; i++) { + const normalizedValue = (data[i] - 128) / 128; // Normalize the data + sumSquares += normalizedValue * normalizedValue; + } + return Math.sqrt(sumSquares / data.length); + }; + + const normalizeRMS = (rms) => { + rms = rms * 10; + const exp = 1.5; // Adjust exponent value; values greater than 1 expand larger numbers more and compress smaller numbers more + const scaledRMS = Math.pow(rms, exp); + + // Scale between 0.01 (1%) and 1.0 (100%) + return Math.min(1.0, Math.max(0.01, scaledRMS)); + }; + + const analyseAudio = (stream) => { + const audioContext = new AudioContext(); + const audioStreamSource = audioContext.createMediaStreamSource(stream); + + const analyser = audioContext.createAnalyser(); + analyser.minDecibels = MIN_DECIBELS; + audioStreamSource.connect(analyser); + + const bufferLength = analyser.frequencyBinCount; + + const domainData = new Uint8Array(bufferLength); + const timeDomainData = new Uint8Array(analyser.fftSize); + + let lastSoundTime = Date.now(); + + const detectSound = () => { + const processFrame = () => { + if (!recording || loading) return; + + if (recording && !loading) { + analyser.getByteTimeDomainData(timeDomainData); + analyser.getByteFrequencyData(domainData); + + // Calculate RMS level from time domain data + const rmsLevel = calculateRMS(timeDomainData); + // Push the calculated decibel level to visualizerData + visualizerData.push(normalizeRMS(rmsLevel)); + + // Ensure visualizerData array stays within the buffer length + if (visualizerData.length >= VISUALIZER_BUFFER_LENGTH) { + visualizerData.shift(); + } + + visualizerData = visualizerData; + + // if (domainData.some((value) => value > 0)) { + // lastSoundTime = Date.now(); + // } + + // if (recording && Date.now() - lastSoundTime > 3000) { + // if ($settings?.speechAutoSend ?? false) { + // confirmRecording(); + // } + // } + } + + window.requestAnimationFrame(processFrame); + }; + + window.requestAnimationFrame(processFrame); + }; + + detectSound(); + }; + + const transcribeHandler = async (audioBlob) => { + // Create a blob from the audio chunks + + await tick(); + const file = blobToFile(audioBlob, 'recording.wav'); + + const res = await transcribeAudio(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + console.log(res.text); + dispatch('confirm', res.text); + } + }; + + const saveRecording = (blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + document.body.appendChild(a); + a.style = 'display: none'; + a.href = url; + a.download = 'recording.wav'; + a.click(); + window.URL.revokeObjectURL(url); + }; + + const startRecording = async () => { + startDurationCounter(); + + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + mediaRecorder.onstart = () => { + console.log('Recording started'); + audioChunks = []; + analyseAudio(stream); + }; + mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data); + mediaRecorder.onstop = async () => { + console.log('Recording stopped'); + if (($settings?.audio?.stt?.engine ?? '') === 'web') { + audioChunks = []; + } else { + if (confirmed) { + const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); + + await transcribeHandler(audioBlob); + + confirmed = false; + loading = false; + } + audioChunks = []; + recording = false; + } + }; + mediaRecorder.start(); + if ($config.audio.stt.engine === 'web' || ($settings?.audio?.stt?.engine ?? '') === 'web') { + if ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) { + // Create a SpeechRecognition object + speechRecognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); + + // Set continuous to true for continuous recognition + speechRecognition.continuous = true; + + // Set the timeout for turning off the recognition after inactivity (in milliseconds) + const inactivityTimeout = 2000; // 3 seconds + + let timeoutId; + // Start recognition + speechRecognition.start(); + + // Event triggered when speech is recognized + speechRecognition.onresult = async (event) => { + // Clear the inactivity timeout + clearTimeout(timeoutId); + + // Handle recognized speech + console.log(event); + const transcript = event.results[Object.keys(event.results).length - 1][0].transcript; + + transcription = `${transcription}${transcript}`; + + await tick(); + document.getElementById('chat-textarea')?.focus(); + + // Restart the inactivity timeout + timeoutId = setTimeout(() => { + console.log('Speech recognition turned off due to inactivity.'); + speechRecognition.stop(); + }, inactivityTimeout); + }; + + // Event triggered when recognition is ended + speechRecognition.onend = function () { + // Restart recognition after it ends + console.log('recognition ended'); + + confirmRecording(); + dispatch('confirm', transcription); + + confirmed = false; + loading = false; + }; + + // Event triggered when an error occurs + speechRecognition.onerror = function (event) { + console.log(event); + toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error })); + dispatch('cancel'); + + stopRecording(); + }; + } + } + }; + + const stopRecording = async () => { + if (recording && mediaRecorder) { + await mediaRecorder.stop(); + } + stopDurationCounter(); + audioChunks = []; + }; + + const confirmRecording = async () => { + loading = true; + confirmed = true; + + if (recording && mediaRecorder) { + await mediaRecorder.stop(); + } + clearInterval(durationCounter); + }; +</script> + +<div + class="{loading + ? ' bg-gray-100/50 dark:bg-gray-850/50' + : 'bg-indigo-300/10 dark:bg-indigo-500/10 '} rounded-full flex p-2.5" +> + <div class="flex items-center mr-1"> + <button + type="button" + class="p-1.5 + + {loading + ? ' bg-gray-200 dark:bg-gray-700/50' + : 'bg-indigo-400/20 text-indigo-600 dark:text-indigo-300 '} + + + rounded-full" + on:click={async () => { + dispatch('cancel'); + stopRecording(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="3" + stroke="currentColor" + class="size-4" + > + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> + </svg> + </button> + </div> + + <div + class="flex flex-1 self-center items-center justify-between ml-2 mx-1 overflow-hidden h-6" + dir="rtl" + > + <div class="flex-1 flex items-center gap-0.5 h-6"> + {#each visualizerData.slice().reverse() as rms} + <div + class="w-[2px] + + {loading + ? ' bg-gray-500 dark:bg-gray-400 ' + : 'bg-indigo-500 dark:bg-indigo-400 '} + + inline-block h-full" + style="height: {Math.min(100, Math.max(14, rms * 100))}%;" + /> + {/each} + </div> + </div> + + <div class=" mx-1.5 pr-1 flex justify-center items-center"> + <div + class="text-sm + + + {loading ? ' text-gray-500 dark:text-gray-400 ' : ' text-indigo-400 '} + font-medium flex-1 mx-auto text-center" + > + {formatSeconds(durationSeconds)} + </div> + </div> + + <div class="flex items-center mr-1"> + {#if loading} + <div class=" text-gray-500 rounded-full cursor-not-allowed"> + <svg + width="24" + height="24" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + fill="currentColor" + ><style> + .spinner_OSmW { + transform-origin: center; + animation: spinner_T6mA 0.75s step-end infinite; + } + @keyframes spinner_T6mA { + 8.3% { + transform: rotate(30deg); + } + 16.6% { + transform: rotate(60deg); + } + 25% { + transform: rotate(90deg); + } + 33.3% { + transform: rotate(120deg); + } + 41.6% { + transform: rotate(150deg); + } + 50% { + transform: rotate(180deg); + } + 58.3% { + transform: rotate(210deg); + } + 66.6% { + transform: rotate(240deg); + } + 75% { + transform: rotate(270deg); + } + 83.3% { + transform: rotate(300deg); + } + 91.6% { + transform: rotate(330deg); + } + 100% { + transform: rotate(360deg); + } + } + </style><g class="spinner_OSmW" + ><rect x="11" y="1" width="2" height="5" opacity=".14" /><rect + x="11" + y="1" + width="2" + height="5" + transform="rotate(30 12 12)" + opacity=".29" + /><rect + x="11" + y="1" + width="2" + height="5" + transform="rotate(60 12 12)" + opacity=".43" + /><rect + x="11" + y="1" + width="2" + height="5" + transform="rotate(90 12 12)" + opacity=".57" + /><rect + x="11" + y="1" + width="2" + height="5" + transform="rotate(120 12 12)" + opacity=".71" + /><rect + x="11" + y="1" + width="2" + height="5" + transform="rotate(150 12 12)" + opacity=".86" + /><rect x="11" y="1" width="2" height="5" transform="rotate(180 12 12)" /></g + ></svg + > + </div> + {:else} + <button + type="button" + class="p-1.5 bg-indigo-500 text-white dark:bg-indigo-500 dark:text-blue-950 rounded-full" + on:click={async () => { + await confirmRecording(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.5" + stroke="currentColor" + class="size-4" + > + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> + </svg> + </button> + {/if} + </div> +</div> + +<style> + .visualizer { + display: flex; + height: 100%; + } + + .visualizer-bar { + width: 2px; + background-color: #4a5aba; /* or whatever color you need */ + } +</style> diff --git a/src/lib/components/chat/Messages.svelte b/src/lib/components/chat/Messages.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e46e931432061e11bb4a2fb64bf079b69ea91b87 --- /dev/null +++ b/src/lib/components/chat/Messages.svelte @@ -0,0 +1,397 @@ +<script lang="ts"> + import { v4 as uuidv4 } from 'uuid'; + import { chats, config, settings, user as _user, mobile } from '$lib/stores'; + import { tick, getContext, onMount } from 'svelte'; + + import { toast } from 'svelte-sonner'; + import { getChatList, updateChatById } from '$lib/apis/chats'; + + import UserMessage from './Messages/UserMessage.svelte'; + import ResponseMessage from './Messages/ResponseMessage.svelte'; + import Placeholder from './Messages/Placeholder.svelte'; + import Spinner from '../common/Spinner.svelte'; + import { imageGenerations } from '$lib/apis/images'; + import { copyToClipboard, findWordIndices } from '$lib/utils'; + import CompareMessages from './Messages/CompareMessages.svelte'; + import { stringify } from 'postcss'; + + const i18n = getContext('i18n'); + + export let chatId = ''; + export let readOnly = false; + export let sendPrompt: Function; + export let continueGeneration: Function; + export let regenerateResponse: Function; + export let chatActionHandler: Function; + + export let user = $_user; + export let prompt; + export let processing = ''; + export let bottomPadding = false; + export let autoScroll; + export let history = {}; + export let messages = []; + + export let selectedModels; + + $: if (autoScroll && bottomPadding) { + (async () => { + await tick(); + scrollToBottom(); + })(); + } + + const scrollToBottom = () => { + const element = document.getElementById('messages-container'); + element.scrollTop = element.scrollHeight; + }; + + const copyToClipboardWithToast = async (text) => { + const res = await copyToClipboard(text); + if (res) { + toast.success($i18n.t('Copying to clipboard was successful!')); + } + }; + + const confirmEditMessage = async (messageId, content) => { + let userPrompt = content; + let userMessageId = uuidv4(); + + let userMessage = { + id: userMessageId, + parentId: history.messages[messageId].parentId, + childrenIds: [], + role: 'user', + content: userPrompt, + ...(history.messages[messageId].files && { files: history.messages[messageId].files }), + models: selectedModels.filter((m, mIdx) => selectedModels.indexOf(m) === mIdx) + }; + + let messageParentId = history.messages[messageId].parentId; + + if (messageParentId !== null) { + history.messages[messageParentId].childrenIds = [ + ...history.messages[messageParentId].childrenIds, + userMessageId + ]; + } + + history.messages[userMessageId] = userMessage; + history.currentId = userMessageId; + + await tick(); + await sendPrompt(userPrompt, userMessageId); + }; + + const updateChatMessages = async () => { + await tick(); + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + + await chats.set(await getChatList(localStorage.token)); + }; + + const confirmEditResponseMessage = async (messageId, content) => { + history.messages[messageId].originalContent = history.messages[messageId].content; + history.messages[messageId].content = content; + + await updateChatMessages(); + }; + + const rateMessage = async (messageId, rating) => { + history.messages[messageId].annotation = { + ...history.messages[messageId].annotation, + rating: rating + }; + + await updateChatMessages(); + }; + + const showPreviousMessage = async (message) => { + if (message.parentId !== null) { + let messageId = + history.messages[message.parentId].childrenIds[ + Math.max(history.messages[message.parentId].childrenIds.indexOf(message.id) - 1, 0) + ]; + + if (message.id !== messageId) { + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + } + } else { + let childrenIds = Object.values(history.messages) + .filter((message) => message.parentId === null) + .map((message) => message.id); + let messageId = childrenIds[Math.max(childrenIds.indexOf(message.id) - 1, 0)]; + + if (message.id !== messageId) { + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + } + } + + await tick(); + + const element = document.getElementById('messages-container'); + autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; + + setTimeout(() => { + scrollToBottom(); + }, 100); + }; + + const showNextMessage = async (message) => { + if (message.parentId !== null) { + let messageId = + history.messages[message.parentId].childrenIds[ + Math.min( + history.messages[message.parentId].childrenIds.indexOf(message.id) + 1, + history.messages[message.parentId].childrenIds.length - 1 + ) + ]; + + if (message.id !== messageId) { + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + } + } else { + let childrenIds = Object.values(history.messages) + .filter((message) => message.parentId === null) + .map((message) => message.id); + let messageId = + childrenIds[Math.min(childrenIds.indexOf(message.id) + 1, childrenIds.length - 1)]; + + if (message.id !== messageId) { + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + } + } + + await tick(); + + const element = document.getElementById('messages-container'); + autoScroll = element.scrollHeight - element.scrollTop <= element.clientHeight + 50; + + setTimeout(() => { + scrollToBottom(); + }, 100); + }; + + const deleteMessageHandler = async (messageId) => { + const messageToDelete = history.messages[messageId]; + + const parentMessageId = messageToDelete.parentId; + const childMessageIds = messageToDelete.childrenIds ?? []; + + const hasDescendantMessages = childMessageIds.some( + (childId) => history.messages[childId]?.childrenIds?.length > 0 + ); + + history.currentId = parentMessageId; + await tick(); + + // Remove the message itself from the parent message's children array + history.messages[parentMessageId].childrenIds = history.messages[ + parentMessageId + ].childrenIds.filter((id) => id !== messageId); + + await tick(); + + childMessageIds.forEach((childId) => { + const childMessage = history.messages[childId]; + + if (childMessage && childMessage.childrenIds) { + if (childMessage.childrenIds.length === 0 && !hasDescendantMessages) { + // If there are no other responses/prompts + history.messages[parentMessageId].childrenIds = []; + } else { + childMessage.childrenIds.forEach((grandChildId) => { + if (history.messages[grandChildId]) { + history.messages[grandChildId].parentId = parentMessageId; + history.messages[parentMessageId].childrenIds.push(grandChildId); + } + }); + } + } + + // Remove child message id from the parent message's children array + history.messages[parentMessageId].childrenIds = history.messages[ + parentMessageId + ].childrenIds.filter((id) => id !== childId); + }); + + await tick(); + + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }; +</script> + +<div class="h-full flex"> + {#if messages.length == 0} + <Placeholder + modelIds={selectedModels} + submitPrompt={async (p) => { + let text = p; + + if (p.includes('{{CLIPBOARD}}')) { + const clipboardText = await navigator.clipboard.readText().catch((err) => { + toast.error($i18n.t('Failed to read clipboard contents')); + return '{{CLIPBOARD}}'; + }); + + text = p.replaceAll('{{CLIPBOARD}}', clipboardText); + } + + prompt = text; + + await tick(); + + const chatInputElement = document.getElementById('chat-textarea'); + if (chatInputElement) { + prompt = p; + + chatInputElement.style.height = ''; + chatInputElement.style.height = Math.min(chatInputElement.scrollHeight, 200) + 'px'; + chatInputElement.focus(); + + const words = findWordIndices(prompt); + + if (words.length > 0) { + const word = words.at(0); + chatInputElement.setSelectionRange(word?.startIndex, word.endIndex + 1); + } + } + + await tick(); + }} + /> + {:else} + <div class="w-full pt-2"> + {#key chatId} + {#each messages as message, messageIdx} + <div class=" w-full {messageIdx === messages.length - 1 ? ' pb-12' : ''}"> + <div + class="flex flex-col justify-between px-5 mb-3 {$settings?.widescreenMode ?? null + ? 'max-w-full' + : 'max-w-5xl'} mx-auto rounded-lg group" + > + {#if message.role === 'user'} + <UserMessage + on:delete={() => deleteMessageHandler(message.id)} + {user} + {readOnly} + {message} + isFirstMessage={messageIdx === 0} + siblings={message.parentId !== null + ? history.messages[message.parentId]?.childrenIds ?? [] + : Object.values(history.messages) + .filter((message) => message.parentId === null) + .map((message) => message.id) ?? []} + {confirmEditMessage} + {showPreviousMessage} + {showNextMessage} + copyToClipboard={copyToClipboardWithToast} + /> + {:else if $mobile || (history.messages[message.parentId]?.models?.length ?? 1) === 1} + {#key message.id && history.currentId} + <ResponseMessage + {message} + siblings={history.messages[message.parentId]?.childrenIds ?? []} + isLastMessage={messageIdx + 1 === messages.length} + {readOnly} + {updateChatMessages} + {confirmEditResponseMessage} + {showPreviousMessage} + {showNextMessage} + {rateMessage} + copyToClipboard={copyToClipboardWithToast} + {continueGeneration} + {regenerateResponse} + on:action={async (e) => { + await chatActionHandler(chatId, e.detail, message.model, message.id); + }} + on:save={async (e) => { + console.log('save', e); + + const message = e.detail; + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }} + /> + {/key} + {:else} + {#key message.parentId} + <CompareMessages + bind:history + {messages} + {readOnly} + {chatId} + parentMessage={history.messages[message.parentId]} + {messageIdx} + {updateChatMessages} + {confirmEditResponseMessage} + {rateMessage} + copyToClipboard={copyToClipboardWithToast} + {continueGeneration} + {regenerateResponse} + on:change={async () => { + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + + if (autoScroll) { + const element = document.getElementById('messages-container'); + autoScroll = + element.scrollHeight - element.scrollTop <= element.clientHeight + 50; + setTimeout(() => { + scrollToBottom(); + }, 100); + } + }} + /> + {/key} + {/if} + </div> + </div> + {/each} + + {#if bottomPadding} + <div class=" pb-6" /> + {/if} + {/key} + </div> + {/if} +</div> diff --git a/src/lib/components/chat/Messages/CitationsModal.svelte b/src/lib/components/chat/Messages/CitationsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..83b82f8f86eda66252cedab2019fa1584623fb23 --- /dev/null +++ b/src/lib/components/chat/Messages/CitationsModal.svelte @@ -0,0 +1,92 @@ +<script lang="ts"> + import { getContext, onMount, tick } from 'svelte'; + import Modal from '$lib/components/common/Modal.svelte'; + const i18n = getContext('i18n'); + + export let show = false; + export let citation; + + let mergedDocuments = []; + + $: if (citation) { + mergedDocuments = citation.document?.map((c, i) => { + return { + source: citation.source, + document: c, + metadata: citation.metadata?.[i] + }; + }); + } +</script> + +<Modal size="lg" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center capitalize"> + {$i18n.t('Citation')} + </div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-6 pb-5 md:space-x-4"> + <div + class="flex flex-col w-full dark:text-gray-200 overflow-y-scroll max-h-[22rem] scrollbar-hidden" + > + {#each mergedDocuments as document, documentIdx} + <div class="flex flex-col w-full"> + <div class="text-sm font-medium dark:text-gray-300"> + {$i18n.t('Source')} + </div> + + {#if document.source?.name} + <div class="text-sm dark:text-gray-400"> + <a + href={document?.metadata?.file_id + ? `/api/v1/files/${document?.metadata?.file_id}/content` + : document.source.name.includes('http') + ? document.source.name + : `#`} + target="_blank" + > + {document?.metadata?.name ?? document.source.name} + </a> + </div> + {:else} + <div class="text-sm dark:text-gray-400"> + {$i18n.t('No source available')} + </div> + {/if} + </div> + <div class="flex flex-col w-full"> + <div class=" text-sm font-medium dark:text-gray-300"> + {$i18n.t('Content')} + </div> + <pre class="text-sm dark:text-gray-400 whitespace-pre-line"> + {document.document} + </pre> + </div> + + {#if documentIdx !== mergedDocuments.length - 1} + <hr class=" dark:border-gray-850 my-3" /> + {/if} + {/each} + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/chat/Messages/CodeBlock.svelte b/src/lib/components/chat/Messages/CodeBlock.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1139affb3f8b0443ff2f986cec86a028437e03a6 --- /dev/null +++ b/src/lib/components/chat/Messages/CodeBlock.svelte @@ -0,0 +1,273 @@ +<script lang="ts"> + import Spinner from '$lib/components/common/Spinner.svelte'; + import { copyToClipboard } from '$lib/utils'; + import hljs from 'highlight.js'; + import 'highlight.js/styles/github-dark.min.css'; + import { loadPyodide } from 'pyodide'; + import { onMount, tick } from 'svelte'; + import PyodideWorker from '$lib/workers/pyodide.worker?worker'; + + export let id = ''; + + export let lang = ''; + export let code = ''; + + let highlightedCode = null; + let executing = false; + + let stdout = null; + let stderr = null; + let result = null; + + let copied = false; + + const copyCode = async () => { + copied = true; + await copyToClipboard(code); + + setTimeout(() => { + copied = false; + }, 1000); + }; + + const checkPythonCode = (str) => { + // Check if the string contains typical Python syntax characters + const pythonSyntax = [ + 'def ', + 'else:', + 'elif ', + 'try:', + 'except:', + 'finally:', + 'yield ', + 'lambda ', + 'assert ', + 'nonlocal ', + 'del ', + 'True', + 'False', + 'None', + ' and ', + ' or ', + ' not ', + ' in ', + ' is ', + ' with ' + ]; + + for (let syntax of pythonSyntax) { + if (str.includes(syntax)) { + return true; + } + } + + // If none of the above conditions met, it's probably not Python code + return false; + }; + + const executePython = async (code) => { + if (!code.includes('input') && !code.includes('matplotlib')) { + executePythonAsWorker(code); + } else { + result = null; + stdout = null; + stderr = null; + + executing = true; + + document.pyodideMplTarget = document.getElementById(`plt-canvas-${id}`); + + let pyodide = await loadPyodide({ + indexURL: '/pyodide/', + stdout: (text) => { + console.log('Python output:', text); + + if (stdout) { + stdout += `${text}\n`; + } else { + stdout = `${text}\n`; + } + }, + stderr: (text) => { + console.log('An error occured:', text); + if (stderr) { + stderr += `${text}\n`; + } else { + stderr = `${text}\n`; + } + }, + packages: ['micropip'] + }); + + try { + const micropip = pyodide.pyimport('micropip'); + + // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json'); + + let packages = [ + code.includes('requests') ? 'requests' : null, + code.includes('bs4') ? 'beautifulsoup4' : null, + code.includes('numpy') ? 'numpy' : null, + code.includes('pandas') ? 'pandas' : null, + code.includes('matplotlib') ? 'matplotlib' : null, + code.includes('sklearn') ? 'scikit-learn' : null, + code.includes('scipy') ? 'scipy' : null, + code.includes('re') ? 'regex' : null, + code.includes('seaborn') ? 'seaborn' : null + ].filter(Boolean); + + console.log(packages); + await micropip.install(packages); + + result = await pyodide.runPythonAsync(`from js import prompt +def input(p): + return prompt(p) +__builtins__.input = input`); + + result = await pyodide.runPython(code); + + if (!result) { + result = '[NO OUTPUT]'; + } + + console.log(result); + console.log(stdout); + console.log(stderr); + + const pltCanvasElement = document.getElementById(`plt-canvas-${id}`); + + if (pltCanvasElement?.innerHTML !== '') { + pltCanvasElement.classList.add('pt-4'); + } + } catch (error) { + console.error('Error:', error); + stderr = error; + } + + executing = false; + } + }; + + const executePythonAsWorker = async (code) => { + result = null; + stdout = null; + stderr = null; + + executing = true; + + let packages = [ + code.includes('requests') ? 'requests' : null, + code.includes('bs4') ? 'beautifulsoup4' : null, + code.includes('numpy') ? 'numpy' : null, + code.includes('pandas') ? 'pandas' : null, + code.includes('sklearn') ? 'scikit-learn' : null, + code.includes('scipy') ? 'scipy' : null, + code.includes('re') ? 'regex' : null, + code.includes('seaborn') ? 'seaborn' : null + ].filter(Boolean); + + console.log(packages); + + const pyodideWorker = new PyodideWorker(); + + pyodideWorker.postMessage({ + id: id, + code: code, + packages: packages + }); + + setTimeout(() => { + if (executing) { + executing = false; + stderr = 'Execution Time Limit Exceeded'; + pyodideWorker.terminate(); + } + }, 60000); + + pyodideWorker.onmessage = (event) => { + console.log('pyodideWorker.onmessage', event); + const { id, ...data } = event.data; + + console.log(id, data); + + data['stdout'] && (stdout = data['stdout']); + data['stderr'] && (stderr = data['stderr']); + data['result'] && (result = data['result']); + + executing = false; + }; + + pyodideWorker.onerror = (event) => { + console.log('pyodideWorker.onerror', event); + executing = false; + }; + }; + + let debounceTimeout; + $: if (code) { + // Function to perform the code highlighting + const highlightCode = () => { + highlightedCode = hljs.highlightAuto(code, hljs.getLanguage(lang)?.aliases).value || code; + }; + + // Clear the previous timeout if it exists + clearTimeout(debounceTimeout); + + // Set a new timeout to debounce the code highlighting + debounceTimeout = setTimeout(highlightCode, 10); + } +</script> + +<div class="mb-4" dir="ltr"> + <div + class="flex justify-between bg-[#202123] text-white text-xs px-4 pt-1 pb-0.5 rounded-t-lg overflow-x-auto" + > + <div class="p-1">{@html lang}</div> + + <div class="flex items-center"> + {#if lang.toLowerCase() === 'python' || lang.toLowerCase() === 'py' || (lang === '' && checkPythonCode(code))} + {#if executing} + <div class="copy-code-button bg-none border-none p-1 cursor-not-allowed">Running</div> + {:else} + <button + class="copy-code-button bg-none border-none p-1" + on:click={() => { + executePython(code); + }}>Run</button + > + {/if} + {/if} + <button class="copy-code-button bg-none border-none p-1" on:click={copyCode} + >{copied ? 'Copied' : 'Copy Code'}</button + > + </div> + </div> + + <pre + class=" hljs p-4 px-5 overflow-x-auto" + style="border-top-left-radius: 0px; border-top-right-radius: 0px; {(executing || + stdout || + stderr || + result) && + 'border-bottom-left-radius: 0px; border-bottom-right-radius: 0px;'}"><code + class="language-{lang} rounded-t-none whitespace-pre" + >{#if highlightedCode}{@html highlightedCode}{:else}{code}{/if}</code + ></pre> + + <div + id="plt-canvas-{id}" + class="bg-[#202123] text-white max-w-full overflow-x-auto scrollbar-hidden" + /> + + {#if executing} + <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg"> + <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div> + <div class="text-sm">Running...</div> + </div> + {:else if stdout || stderr || result} + <div class="bg-[#202123] text-white px-4 py-4 rounded-b-lg"> + <div class=" text-gray-500 text-xs mb-1">STDOUT/STDERR</div> + <div class="text-sm">{stdout || stderr || result}</div> + </div> + {/if} +</div> diff --git a/src/lib/components/chat/Messages/CompareMessages.svelte b/src/lib/components/chat/Messages/CompareMessages.svelte new file mode 100644 index 0000000000000000000000000000000000000000..27fefb6cb4230b81e401fe757413eb189eb4ed97 --- /dev/null +++ b/src/lib/components/chat/Messages/CompareMessages.svelte @@ -0,0 +1,167 @@ +<script lang="ts"> + import { createEventDispatcher } from 'svelte'; + + import { updateChatById } from '$lib/apis/chats'; + import { onMount, tick } from 'svelte'; + import ResponseMessage from './ResponseMessage.svelte'; + + export let chatId; + + export let history; + export let messages = []; + export let messageIdx; + + export let parentMessage; + + export let readOnly = false; + + export let updateChatMessages: Function; + export let confirmEditResponseMessage: Function; + export let rateMessage: Function; + + export let copyToClipboard: Function; + export let continueGeneration: Function; + export let regenerateResponse: Function; + + const dispatch = createEventDispatcher(); + + let currentMessageId; + + let groupedMessagesIdx = {}; + let groupedMessages = {}; + + $: groupedMessages = parentMessage?.models.reduce((a, model) => { + const modelMessages = parentMessage?.childrenIds + .map((id) => history.messages[id]) + .filter((m) => m.model === model); + + return { + ...a, + [model]: { messages: modelMessages } + }; + }, {}); + + const showPreviousMessage = (model) => { + groupedMessagesIdx[model] = Math.max(0, groupedMessagesIdx[model] - 1); + let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id; + + console.log(messageId); + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + + dispatch('change'); + }; + + const showNextMessage = (model) => { + groupedMessagesIdx[model] = Math.min( + groupedMessages[model].messages.length - 1, + groupedMessagesIdx[model] + 1 + ); + + let messageId = groupedMessages[model].messages[groupedMessagesIdx[model]].id; + console.log(messageId); + + let messageChildrenIds = history.messages[messageId].childrenIds; + + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + + dispatch('change'); + }; + + onMount(async () => { + await tick(); + currentMessageId = messages[messageIdx].id; + + for (const model of parentMessage?.models) { + const idx = groupedMessages[model].messages.findIndex((m) => m.id === currentMessageId); + + if (idx !== -1) { + groupedMessagesIdx[model] = idx; + } else { + groupedMessagesIdx[model] = 0; + } + } + }); +</script> + +<div> + <div + class="flex snap-x snap-mandatory overflow-x-auto scrollbar-hidden" + id="responses-container-{parentMessage.id}" + > + {#key currentMessageId} + {#each Object.keys(groupedMessages) as model} + {#if groupedMessagesIdx[model] !== undefined && groupedMessages[model].messages.length > 0} + <!-- svelte-ignore a11y-no-static-element-interactions --> + <!-- svelte-ignore a11y-click-events-have-key-events --> + {@const message = groupedMessages[model].messages[groupedMessagesIdx[model]]} + + <div + class=" snap-center min-w-80 w-full max-w-full m-1 border {history.messages[ + currentMessageId + ].model === model + ? 'border-gray-100 dark:border-gray-800 border-[1.5px]' + : 'border-gray-50 dark:border-gray-850 '} transition p-5 rounded-3xl" + on:click={() => { + if (currentMessageId != message.id) { + currentMessageId = message.id; + let messageId = message.id; + console.log(messageId); + + // + let messageChildrenIds = history.messages[messageId].childrenIds; + while (messageChildrenIds.length !== 0) { + messageId = messageChildrenIds.at(-1); + messageChildrenIds = history.messages[messageId].childrenIds; + } + + history.currentId = messageId; + dispatch('change'); + } + }} + > + <ResponseMessage + message={groupedMessages[model].messages[groupedMessagesIdx[model]]} + siblings={groupedMessages[model].messages.map((m) => m.id)} + isLastMessage={true} + {updateChatMessages} + {confirmEditResponseMessage} + showPreviousMessage={() => showPreviousMessage(model)} + showNextMessage={() => showNextMessage(model)} + {readOnly} + {rateMessage} + {copyToClipboard} + {continueGeneration} + regenerateResponse={async (message) => { + regenerateResponse(message); + await tick(); + groupedMessagesIdx[model] = groupedMessages[model].messages.length - 1; + }} + on:save={async (e) => { + console.log('save', e); + + const message = e.detail; + history.messages[message.id] = message; + await updateChatById(localStorage.token, chatId, { + messages: messages, + history: history + }); + }} + /> + </div> + {/if} + {/each} + {/key} + </div> +</div> diff --git a/src/lib/components/chat/Messages/Name.svelte b/src/lib/components/chat/Messages/Name.svelte new file mode 100644 index 0000000000000000000000000000000000000000..53879b24adeb3a8b86ec8e956cf36cd6d7e1dfab --- /dev/null +++ b/src/lib/components/chat/Messages/Name.svelte @@ -0,0 +1,3 @@ +<div class=" self-center font-semibold mb-0.5 line-clamp-1 contents"> + <slot /> +</div> diff --git a/src/lib/components/chat/Messages/Placeholder.svelte b/src/lib/components/chat/Messages/Placeholder.svelte new file mode 100644 index 0000000000000000000000000000000000000000..34038a60835703ba2c2c36ad442e136106d7da23 --- /dev/null +++ b/src/lib/components/chat/Messages/Placeholder.svelte @@ -0,0 +1,121 @@ +<script lang="ts"> + import { WEBUI_BASE_URL } from '$lib/constants'; + import { marked } from 'marked'; + + import { config, user, models as _models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + + import { blur, fade } from 'svelte/transition'; + + import Suggestions from '../MessageInput/Suggestions.svelte'; + import { sanitizeResponseContent } from '$lib/utils'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let modelIds = []; + export let models = []; + + export let submitPrompt; + + let mounted = false; + let selectedModelIdx = 0; + + $: if (modelIds.length > 0) { + selectedModelIdx = models.length - 1; + } + + $: models = modelIds.map((id) => $_models.find((m) => m.id === id)); + + onMount(() => { + mounted = true; + }); +</script> + +{#key mounted} + <div class="m-auto w-full max-w-6xl px-8 lg:px-20 pb-10"> + <div class="flex justify-start"> + <div class="flex -space-x-4 mb-1" in:fade={{ duration: 200 }}> + {#each models as model, modelIdx} + <button + on:click={() => { + selectedModelIdx = modelIdx; + }} + > + <Tooltip + content={marked.parse( + sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '') + )} + placement="right" + > + <img + crossorigin="anonymous" + src={model?.info?.meta?.profile_image_url ?? + ($i18n.language === 'dg-DG' + ? `/doge.png` + : `${WEBUI_BASE_URL}/static/favicon.png`)} + class=" size-[2.7rem] rounded-full border-[1px] border-gray-200 dark:border-none" + alt="logo" + draggable="false" + /> + </Tooltip> + </button> + {/each} + </div> + </div> + + <div + class=" mt-2 mb-4 text-3xl text-gray-800 dark:text-gray-100 font-semibold text-left flex items-center gap-4 font-primary" + > + <div> + <div class=" capitalize line-clamp-1" in:fade={{ duration: 200 }}> + {#if models[selectedModelIdx]?.info} + {models[selectedModelIdx]?.info?.name} + {:else} + {$i18n.t('Hello, {{name}}', { name: $user.name })} + {/if} + </div> + + <div in:fade={{ duration: 200, delay: 200 }}> + {#if models[selectedModelIdx]?.info?.meta?.description ?? null} + <div + class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown" + > + {@html marked.parse( + sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description) + )} + </div> + {#if models[selectedModelIdx]?.info?.meta?.user} + <div class="mt-0.5 text-sm font-normal text-gray-400 dark:text-gray-500"> + By + {#if models[selectedModelIdx]?.info?.meta?.user.community} + <a + href="https://openwebui.com/m/{models[selectedModelIdx]?.info?.meta?.user + .username}" + >{models[selectedModelIdx]?.info?.meta?.user.name + ? models[selectedModelIdx]?.info?.meta?.user.name + : `@${models[selectedModelIdx]?.info?.meta?.user.username}`}</a + > + {:else} + {models[selectedModelIdx]?.info?.meta?.user.name} + {/if} + </div> + {/if} + {:else} + <div class=" font-medium text-gray-400 dark:text-gray-500 line-clamp-1 font-p"> + {$i18n.t('How can I help you today?')} + </div> + {/if} + </div> + </div> + </div> + + <div class=" w-full font-primary" in:fade={{ duration: 200, delay: 300 }}> + <Suggestions + suggestionPrompts={models[selectedModelIdx]?.info?.meta?.suggestion_prompts ?? + $config.default_prompt_suggestions} + {submitPrompt} + /> + </div> + </div> +{/key} diff --git a/src/lib/components/chat/Messages/ProfileImage.svelte b/src/lib/components/chat/Messages/ProfileImage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9d9f67a8d1be5467c6bc94aaa08b9db7be90a5e6 --- /dev/null +++ b/src/lib/components/chat/Messages/ProfileImage.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import { settings } from '$lib/stores'; + import { WEBUI_BASE_URL } from '$lib/constants'; + + export let className = 'size-8'; + + export let src = '/user.png'; +</script> + +<div class={`flex-shrink-0 ${($settings?.chatDirection ?? 'LTR') === 'LTR' ? 'mr-3' : 'ml-3'}`}> + <img + crossorigin="anonymous" + src={src.startsWith(WEBUI_BASE_URL) || + src.startsWith('https://www.gravatar.com/avatar/') || + src.startsWith('data:') || + src.startsWith('/') + ? src + : `/user.png`} + class=" {className} object-cover rounded-full -translate-y-[1px]" + alt="profile" + draggable="false" + /> +</div> diff --git a/src/lib/components/chat/Messages/RateComment.svelte b/src/lib/components/chat/Messages/RateComment.svelte new file mode 100644 index 0000000000000000000000000000000000000000..78eddecd92e01ca898af7de5497a35dda4ec0e5e --- /dev/null +++ b/src/lib/components/chat/Messages/RateComment.svelte @@ -0,0 +1,129 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + + import { createEventDispatcher, onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + export let messageId = null; + export let show = false; + export let message; + + let LIKE_REASONS = []; + let DISLIKE_REASONS = []; + + function loadReasons() { + LIKE_REASONS = [ + $i18n.t('Accurate information'), + $i18n.t('Followed instructions perfectly'), + $i18n.t('Showcased creativity'), + $i18n.t('Positive attitude'), + $i18n.t('Attention to detail'), + $i18n.t('Thorough explanation'), + $i18n.t('Other') + ]; + + DISLIKE_REASONS = [ + $i18n.t("Don't like the style"), + $i18n.t('Not factually correct'), + $i18n.t("Didn't fully follow instructions"), + $i18n.t("Refused when it shouldn't have"), + $i18n.t('Being lazy'), + $i18n.t('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 ?? ''; + loadReasons(); + }); + + const submitHandler = () => { + console.log('submitHandler'); + + message.annotation.reason = selectedReason; + message.annotation.comment = comment; + + dispatch('submit'); + + toast.success($i18n.t('Thanks for your feedback!')); + show = false; + }; +</script> + +<div + class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850" + id="message-feedback-{messageId}" +> + <div class="flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('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 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason === + reason + ? 'bg-gray-200 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={$i18n.t('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(); + }} + > + {$i18n.t('Submit')} + </button> + </div> +</div> diff --git a/src/lib/components/chat/Messages/ResponseMessage.svelte b/src/lib/components/chat/Messages/ResponseMessage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..52c313ada7ab2f4ae39f48170e3cf3d94d14458a --- /dev/null +++ b/src/lib/components/chat/Messages/ResponseMessage.svelte @@ -0,0 +1,1093 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { marked } from 'marked'; + import tippy from 'tippy.js'; + import auto_render from 'katex/dist/contrib/auto-render.mjs'; + import 'katex/dist/katex.min.css'; + import mermaid from 'mermaid'; + + import { fade } from 'svelte/transition'; + import { createEventDispatcher } from 'svelte'; + import { onMount, tick, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + import { config, models, settings, user } from '$lib/stores'; + import { synthesizeOpenAISpeech } from '$lib/apis/audio'; + import { imageGenerations } from '$lib/apis/images'; + import { + approximateToHumanReadable, + extractSentences, + replaceTokens, + revertSanitizedResponseContent, + sanitizeResponseContent + } from '$lib/utils'; + import { WEBUI_BASE_URL } from '$lib/constants'; + + import Name from './Name.svelte'; + import ProfileImage from './ProfileImage.svelte'; + import Skeleton from './Skeleton.svelte'; + import CodeBlock from './CodeBlock.svelte'; + import Image from '$lib/components/common/Image.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import RateComment from './RateComment.svelte'; + import CitationsModal from '$lib/components/chat/Messages/CitationsModal.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import WebSearchResults from './ResponseMessage/WebSearchResults.svelte'; + import Sparkles from '$lib/components/icons/Sparkles.svelte'; + + export let message; + export let siblings; + + export let isLastMessage = true; + + export let readOnly = false; + + export let updateChatMessages: Function; + export let confirmEditResponseMessage: Function; + export let showPreviousMessage: Function; + export let showNextMessage: Function; + export let rateMessage: Function; + + export let copyToClipboard: Function; + export let continueGeneration: Function; + export let regenerateResponse: Function; + export let chatActionHandler: Function; + + let model = null; + $: model = $models.find((m) => m.id === message.model); + + let edit = false; + let editedContent = ''; + let editTextAreaElement: HTMLTextAreaElement; + let tooltipInstance = null; + + let sentencesAudio = {}; + let speaking = null; + let speakingIdx = null; + + let loadingSpeech = false; + let generatingImage = false; + + let showRateComment = false; + let showCitationModal = false; + + let selectedCitation = null; + + $: tokens = marked.lexer( + replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name) + ); + + const renderer = new marked.Renderer(); + + // For code blocks with simple backticks + renderer.codespan = (code) => { + return `<code>${code.replaceAll('&', '&')}</code>`; + }; + + // Open all links in a new tab/window (from https://github.com/markedjs/marked/issues/655#issuecomment-383226346) + const origLinkRenderer = renderer.link; + renderer.link = (href, title, text) => { + const html = origLinkRenderer.call(renderer, href, title, text); + return html.replace(/^<a /, '<a target="_blank" rel="nofollow" '); + }; + + const { extensions, ...defaults } = marked.getDefaults() as marked.MarkedOptions & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + extensions: any; + }; + + $: if (message) { + renderStyling(); + } + + const renderStyling = async () => { + await tick(); + + if (tooltipInstance) { + tooltipInstance[0]?.destroy(); + } + + renderLatex(); + + if (message.info) { + let tooltipContent = ''; + if (message.info.openai) { + tooltipContent = `prompt_tokens: ${message.info.prompt_tokens ?? 'N/A'}<br/> + completion_tokens: ${message.info.completion_tokens ?? 'N/A'}<br/> + total_tokens: ${message.info.total_tokens ?? 'N/A'}`; + } else { + tooltipContent = `response_token/s: ${ + `${ + Math.round( + ((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100 + ) / 100 + } tokens` ?? 'N/A' + }<br/> + prompt_token/s: ${ + Math.round( + ((message.info.prompt_eval_count ?? 0) / + (message.info.prompt_eval_duration / 1000000000)) * + 100 + ) / 100 ?? 'N/A' + } tokens<br/> + total_duration: ${ + Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ?? + 'N/A' + }ms<br/> + load_duration: ${ + Math.round(((message.info.load_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' + }ms<br/> + prompt_eval_count: ${message.info.prompt_eval_count ?? 'N/A'}<br/> + prompt_eval_duration: ${ + Math.round(((message.info.prompt_eval_duration ?? 0) / 1000000) * 100) / + 100 ?? 'N/A' + }ms<br/> + eval_count: ${message.info.eval_count ?? 'N/A'}<br/> + eval_duration: ${ + Math.round(((message.info.eval_duration ?? 0) / 1000000) * 100) / 100 ?? 'N/A' + }ms<br/> + approximate_total: ${approximateToHumanReadable(message.info.total_duration)}`; + } + tooltipInstance = tippy(`#info-${message.id}`, { + content: `<span class="text-xs" id="tooltip-${message.id}">${tooltipContent}</span>`, + allowHTML: true, + theme: 'dark', + arrow: false, + offset: [0, 4] + }); + } + }; + + const renderLatex = () => { + let chatMessageElements = document + .getElementById(`message-${message.id}`) + ?.getElementsByClassName('chat-assistant'); + + if (chatMessageElements) { + for (const element of chatMessageElements) { + auto_render(element, { + // customised options + // • auto-render specific keys, e.g.: + delimiters: [ + { left: '$$', right: '$$', display: false }, + { left: '$ ', right: ' $', display: false }, + { left: '\\pu{', right: '}', display: false }, + { left: '\\ce{', right: '}', display: false }, + { left: '\\(', right: '\\)', display: false }, + { left: '( ', right: ' )', display: false }, + { left: '\\[', right: '\\]', display: false }, + { left: '[ ', right: ' ]', display: false } + ], + // • rendering keys, e.g.: + throwOnError: false + }); + } + } + }; + + const playAudio = (idx) => { + return new Promise((res) => { + speakingIdx = idx; + const audio = sentencesAudio[idx]; + audio.play(); + audio.onended = async (e) => { + await new Promise((r) => setTimeout(r, 300)); + + if (Object.keys(sentencesAudio).length - 1 === idx) { + speaking = null; + } + + res(e); + }; + }); + }; + + const toggleSpeakMessage = async () => { + if (speaking) { + try { + speechSynthesis.cancel(); + + sentencesAudio[speakingIdx].pause(); + sentencesAudio[speakingIdx].currentTime = 0; + } catch {} + + speaking = null; + speakingIdx = null; + } else { + if ((message?.content ?? '').trim() !== '') { + speaking = true; + + if ($config.audio.tts.engine !== '') { + loadingSpeech = true; + + const sentences = extractSentences(message.content).reduce((mergedTexts, currentText) => { + const lastIndex = mergedTexts.length - 1; + if (lastIndex >= 0) { + const previousText = mergedTexts[lastIndex]; + const wordCount = previousText.split(/\s+/).length; + if (wordCount < 2) { + mergedTexts[lastIndex] = previousText + ' ' + currentText; + } else { + mergedTexts.push(currentText); + } + } else { + mergedTexts.push(currentText); + } + return mergedTexts; + }, []); + + console.log(sentences); + + if (sentences.length > 0) { + sentencesAudio = sentences.reduce((a, e, i, arr) => { + a[i] = null; + return a; + }, {}); + + let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately + + for (const [idx, sentence] of sentences.entries()) { + const res = await synthesizeOpenAISpeech( + localStorage.token, + $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, + sentence + ).catch((error) => { + toast.error(error); + + speaking = null; + loadingSpeech = false; + + return null; + }); + + if (res) { + const blob = await res.blob(); + const blobUrl = URL.createObjectURL(blob); + const audio = new Audio(blobUrl); + sentencesAudio[idx] = audio; + loadingSpeech = false; + lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); + } + } + } else { + speaking = null; + loadingSpeech = false; + } + } else { + let voices = []; + const getVoicesLoop = setInterval(async () => { + voices = await speechSynthesis.getVoices(); + if (voices.length > 0) { + clearInterval(getVoicesLoop); + + const voice = + voices + ?.filter( + (v) => + v.voiceURI === ($settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice) + ) + ?.at(0) ?? undefined; + + console.log(voice); + + const speak = new SpeechSynthesisUtterance(message.content); + + console.log(speak); + + speak.onend = () => { + speaking = null; + if ($settings.conversationMode) { + document.getElementById('voice-input-button')?.click(); + } + }; + + if (voice) { + speak.voice = voice; + } + + speechSynthesis.speak(speak); + } + }, 100); + } + } else { + toast.error($i18n.t('No content to speak')); + } + } + }; + + const editMessageHandler = async () => { + edit = true; + editedContent = message.content; + + await tick(); + + editTextAreaElement.style.height = ''; + editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`; + }; + + const editMessageConfirmHandler = async () => { + if (editedContent === '') { + editedContent = ' '; + } + + confirmEditResponseMessage(message.id, editedContent); + + edit = false; + editedContent = ''; + + await tick(); + renderStyling(); + }; + + const cancelEditMessage = async () => { + edit = false; + editedContent = ''; + await tick(); + renderStyling(); + }; + + const generateImage = async (message) => { + generatingImage = true; + const res = await imageGenerations(localStorage.token, message.content).catch((error) => { + toast.error(error); + }); + console.log(res); + + if (res) { + message.files = res.map((image) => ({ + type: 'image', + url: `${image.url}` + })); + + dispatch('save', message); + } + + generatingImage = false; + }; + + $: if (!edit) { + (async () => { + await tick(); + renderStyling(); + + await mermaid.run({ + querySelector: '.mermaid' + }); + })(); + } + + onMount(async () => { + await tick(); + renderStyling(); + + await mermaid.run({ + querySelector: '.mermaid' + }); + }); +</script> + +<CitationsModal bind:show={showCitationModal} citation={selectedCitation} /> + +{#key message.id} + <div + class=" flex w-full message-{message.id}" + id="message-{message.id}" + dir={$settings.chatDirection} + > + <ProfileImage + src={model?.info?.meta?.profile_image_url ?? + ($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)} + /> + + <div class="w-full overflow-hidden pl-1"> + <Name> + {model?.name ?? message.model} + + {#if message.timestamp} + <span + class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium uppercase" + > + {dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))} + </span> + {/if} + </Name> + + {#if (message?.files ?? []).filter((f) => f.type === 'image').length > 0} + <div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap"> + {#each message.files as file} + <div> + {#if file.type === 'image'} + <Image src={file.url} /> + {/if} + </div> + {/each} + </div> + {/if} + + <div + class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-headings:-mb-4 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line" + > + <div> + {#if (message?.statusHistory ?? [...(message?.status ? [message?.status] : [])]).length > 0} + {@const status = ( + message?.statusHistory ?? [...(message?.status ? [message?.status] : [])] + ).at(-1)} + <div class="flex items-center gap-2 pt-1 pb-1"> + {#if status.done === false} + <div class=""> + <Spinner className="size-4" /> + </div> + {/if} + + {#if status?.action === 'web_search' && status?.urls} + <WebSearchResults {status}> + <div class="flex flex-col justify-center -space-y-0.5"> + <div class="text-base line-clamp-1 text-wrap"> + {status?.description} + </div> + </div> + </WebSearchResults> + {:else} + <div class="flex flex-col justify-center -space-y-0.5"> + <div class=" text-gray-500 dark:text-gray-500 text-base line-clamp-1 text-wrap"> + {status?.description} + </div> + </div> + {/if} + </div> + {/if} + + {#if edit === true} + <div class="w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 my-2"> + <textarea + id="message-edit-{message.id}" + bind:this={editTextAreaElement} + class=" bg-transparent outline-none w-full resize-none" + bind:value={editedContent} + on:input={(e) => { + e.target.style.height = ''; + e.target.style.height = `${e.target.scrollHeight}px`; + }} + on:keydown={(e) => { + if (e.key === 'Escape') { + document.getElementById('close-edit-message-button')?.click(); + } + + const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey; + const isEnterPressed = e.key === 'Enter'; + + if (isCmdOrCtrlPressed && isEnterPressed) { + document.getElementById('save-edit-message-button')?.click(); + } + }} + /> + + <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> + <button + id="close-edit-message-button" + class="px-4 py-2 bg-white hover:bg-gray-100 text-gray-800 transition rounded-3xl" + on:click={() => { + cancelEditMessage(); + }} + > + {$i18n.t('Cancel')} + </button> + + <button + id="save-edit-message-button" + class=" px-4 py-2 bg-gray-900 hover:bg-gray-850 text-gray-100 transition rounded-3xl" + on:click={() => { + editMessageConfirmHandler(); + }} + > + {$i18n.t('Save')} + </button> + </div> + </div> + {:else} + <div class="w-full"> + {#if message.content === '' && !message.error} + <Skeleton /> + {:else if message.content && message.error !== true} + <!-- always show message contents even if there's an error --> + <!-- unless message.error === true which is legacy error handling, where the error message is stored in message.content --> + {#each tokens as token, tokenIdx} + {#if token.type === 'code'} + {#if token.lang === 'mermaid'} + <pre class="mermaid">{revertSanitizedResponseContent(token.text)}</pre> + {:else} + <CodeBlock + id={`${message.id}-${tokenIdx}`} + lang={token?.lang ?? ''} + code={revertSanitizedResponseContent(token?.text ?? '')} + /> + {/if} + {:else} + {@html marked.parse(token.raw, { + ...defaults, + gfm: true, + breaks: true, + renderer + })} + {/if} + {/each} + {/if} + + {#if message.error} + <div + class="flex mt-2 mb-4 space-x-2 border px-4 py-3 border-red-800 bg-red-800/30 font-medium rounded-lg" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5 self-center" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" + /> + </svg> + + <div class=" self-center"> + {message?.error?.content ?? message.content} + </div> + </div> + {/if} + + {#if message.citations} + <div class="mt-1 mb-2 w-full flex gap-1 items-center flex-wrap"> + {#each message.citations.reduce((acc, citation) => { + citation.document.forEach((document, index) => { + const metadata = citation.metadata?.[index]; + const id = metadata?.source ?? 'N/A'; + let source = citation?.source; + + if (metadata?.name) { + source = { ...source, name: metadata.name }; + } + + // Check if ID looks like a URL + if (id.startsWith('http://') || id.startsWith('https://')) { + source = { name: id }; + } + + const existingSource = acc.find((item) => item.id === id); + + if (existingSource) { + existingSource.document.push(document); + existingSource.metadata.push(metadata); + } else { + acc.push( { id: id, source: source, document: [document], metadata: metadata ? [metadata] : [] } ); + } + }); + return acc; + }, []) as citation, idx} + <div class="flex gap-1 text-xs font-semibold"> + <button + class="flex dark:text-gray-300 py-1 px-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-xl" + on:click={() => { + showCitationModal = true; + selectedCitation = citation; + }} + > + <div class="bg-white dark:bg-gray-700 rounded-full size-4"> + {idx + 1} + </div> + <div class="flex-1 mx-2 line-clamp-1"> + {citation.source.name} + </div> + </button> + </div> + {/each} + </div> + {/if} + + {#if message.done || siblings.length > 1} + <div + class=" flex justify-start overflow-x-auto buttons text-gray-600 dark:text-gray-500" + > + {#if siblings.length > 1} + <div class="flex self-center min-w-fit" dir="ltr"> + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showPreviousMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 19.5 8.25 12l7.5-7.5" + /> + </svg> + </button> + + <div + class="text-sm tracking-widest font-semibold self-center dark:text-gray-100 min-w-fit" + > + {siblings.indexOf(message.id) + 1}/{siblings.length} + </div> + + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showNextMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m8.25 4.5 7.5 7.5-7.5 7.5" + /> + </svg> + </button> + </div> + {/if} + + {#if message.done} + {#if !readOnly} + <Tooltip content={$i18n.t('Edit')} placement="bottom"> + <button + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition" + on:click={() => { + editMessageHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + </button> + </Tooltip> + {/if} + + <Tooltip content={$i18n.t('Copy')} placement="bottom"> + <button + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button" + on:click={() => { + copyToClipboard(message.content); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" + /> + </svg> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Read Aloud')} placement="bottom"> + <button + id="speak-button-{message.id}" + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition" + on:click={() => { + if (!loadingSpeech) { + toggleSpeakMessage(message); + } + }} + > + {#if loadingSpeech} + <svg + class=" w-4 h-4" + fill="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_S1WN { + animation: spinner_MGfb 0.8s linear infinite; + animation-delay: -0.8s; + } + .spinner_Km9P { + animation-delay: -0.65s; + } + .spinner_JApP { + animation-delay: -0.5s; + } + @keyframes spinner_MGfb { + 93.75%, + 100% { + opacity: 0.2; + } + } + </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle + class="spinner_S1WN spinner_Km9P" + cx="12" + cy="12" + r="3" + /><circle + class="spinner_S1WN spinner_JApP" + cx="20" + cy="12" + r="3" + /></svg + > + {:else if speaking} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z" + /> + </svg> + {/if} + </button> + </Tooltip> + + {#if $config?.features.enable_image_generation && !readOnly} + <Tooltip content={$i18n.t('Generate Image')} placement="bottom"> + <button + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition" + on:click={() => { + if (!generatingImage) { + generateImage(message); + } + }} + > + {#if generatingImage} + <svg + class=" w-4 h-4" + fill="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_S1WN { + animation: spinner_MGfb 0.8s linear infinite; + animation-delay: -0.8s; + } + .spinner_Km9P { + animation-delay: -0.65s; + } + .spinner_JApP { + animation-delay: -0.5s; + } + @keyframes spinner_MGfb { + 93.75%, + 100% { + opacity: 0.2; + } + } + </style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle + class="spinner_S1WN spinner_Km9P" + cx="12" + cy="12" + r="3" + /><circle + class="spinner_S1WN spinner_JApP" + cx="20" + cy="12" + r="3" + /></svg + > + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" + /> + </svg> + {/if} + </button> + </Tooltip> + {/if} + + {#if message.info} + <Tooltip content={$i18n.t('Generation Info')} placement="bottom"> + <button + class=" {isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap" + on:click={() => { + console.log(message); + }} + id="info-{message.id}" + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" + /> + </svg> + </button> + </Tooltip> + {/if} + + {#if !readOnly} + <Tooltip content={$i18n.t('Good Response')} placement="bottom"> + <button + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message + ?.annotation?.rating ?? null) === 1 + ? 'bg-gray-100 dark:bg-gray-800' + : ''} dark:hover:text-white hover:text-black transition" + on:click={() => { + rateMessage(message.id, 1); + showRateComment = true; + + window.setTimeout(() => { + document + .getElementById(`message-feedback-${message.id}`) + ?.scrollIntoView(); + }, 0); + }} + > + <svg + stroke="currentColor" + fill="none" + stroke-width="2.3" + viewBox="0 0 24 24" + stroke-linecap="round" + stroke-linejoin="round" + class="w-4 h-4" + xmlns="http://www.w3.org/2000/svg" + ><path + d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" + /></svg + > + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Bad Response')} placement="bottom"> + <button + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(message + ?.annotation?.rating ?? null) === -1 + ? 'bg-gray-100 dark:bg-gray-800' + : ''} dark:hover:text-white hover:text-black transition" + on:click={() => { + rateMessage(message.id, -1); + showRateComment = true; + window.setTimeout(() => { + document + .getElementById(`message-feedback-${message.id}`) + ?.scrollIntoView(); + }, 0); + }} + > + <svg + stroke="currentColor" + fill="none" + stroke-width="2.3" + viewBox="0 0 24 24" + stroke-linecap="round" + stroke-linejoin="round" + class="w-4 h-4" + xmlns="http://www.w3.org/2000/svg" + ><path + d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" + /></svg + > + </button> + </Tooltip> + + {#if isLastMessage} + <Tooltip content={$i18n.t('Continue Response')} placement="bottom"> + <button + type="button" + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button" + on:click={() => { + continueGeneration(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" + /> + </svg> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Regenerate')} placement="bottom"> + <button + type="button" + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button" + on:click={() => { + showRateComment = false; + regenerateResponse(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" + /> + </svg> + </button> + </Tooltip> + + {#each model?.actions ?? [] as action} + <Tooltip content={action.name} placement="bottom"> + <button + type="button" + class="{isLastMessage + ? 'visible' + : 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button" + on:click={() => { + dispatch('action', action.id); + }} + > + {#if action.icon_url} + <img + src={action.icon_url} + class="w-4 h-4 {action.icon_url.includes('svg') + ? 'dark:invert-[80%]' + : ''}" + style="fill: currentColor;" + alt={action.name} + /> + {:else} + <Sparkles strokeWidth="2.1" className="size-4" /> + {/if} + </button> + </Tooltip> + {/each} + {/if} + {/if} + {/if} + </div> + {/if} + + {#if message.done && showRateComment} + <RateComment + messageId={message.id} + bind:show={showRateComment} + bind:message + on:submit={() => { + updateChatMessages(); + }} + /> + {/if} + </div> + {/if} + </div> + </div> + </div> + </div> +{/key} + +<style> + .buttons::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .buttons { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +</style> diff --git a/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d4416014af847f88158fbb1a497bf99137d00928 --- /dev/null +++ b/src/lib/components/chat/Messages/ResponseMessage/WebSearchResults.svelte @@ -0,0 +1,90 @@ +<script lang="ts"> + import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; + import ChevronUp from '$lib/components/icons/ChevronUp.svelte'; + import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte'; + import Collapsible from '$lib/components/common/Collapsible.svelte'; + + export let status = { urls: [], query: '' }; + let state = false; +</script> + +<Collapsible bind:open={state} className="w-full space-y-1"> + <div + class="flex items-center gap-2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition" + > + <slot /> + + {#if state} + <ChevronUp strokeWidth="3.5" className="size-3.5 " /> + {:else} + <ChevronDown strokeWidth="3.5" className="size-3.5 " /> + {/if} + </div> + <div class="text-sm border border-gray-300/30 dark:border-gray-700/50 rounded-xl" slot="content"> + {#if status?.query} + <a + href="https://www.google.com/search?q={status.query}" + target="_blank" + class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline" + > + <div class="flex gap-2 items-center"> + <MagnifyingGlass /> + + <div class=" line-clamp-1"> + {status.query} + </div> + </div> + + <div + class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition" + > + <!-- --> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z" + clip-rule="evenodd" + /> + </svg> + </div> + </a> + {/if} + + {#each status.urls as url, urlIdx} + <a + href={url} + target="_blank" + class="flex w-full items-center p-3 px-4 {urlIdx === status.urls.length - 1 + ? '' + : 'border-b border-gray-300/30 dark:border-gray-700/50'} group/item justify-between font-normal text-gray-800 dark:text-gray-300" + > + <div class=" line-clamp-1"> + {url} + </div> + + <div + class=" ml-1 text-white dark:text-gray-900 group-hover/item:text-gray-600 dark:group-hover/item:text-white transition" + > + <!-- --> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z" + clip-rule="evenodd" + /> + </svg> + </div> + </a> + {/each} + </div> +</Collapsible> diff --git a/src/lib/components/chat/Messages/Skeleton.svelte b/src/lib/components/chat/Messages/Skeleton.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8a11cd7a678c7286fb622731bb5717d99e135c66 --- /dev/null +++ b/src/lib/components/chat/Messages/Skeleton.svelte @@ -0,0 +1,19 @@ +<div class="w-full mt-2 mb-4"> + <div class="animate-pulse flex w-full"> + <div class="space-y-2 w-full"> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded mr-14" /> + + <div class="grid grid-cols-3 gap-4"> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" /> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" /> + </div> + <div class="grid grid-cols-4 gap-4"> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1" /> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-2" /> + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded col-span-1 mr-4" /> + </div> + + <div class="h-2 bg-gray-200 dark:bg-gray-600 rounded" /> + </div> + </div> +</div> diff --git a/src/lib/components/chat/Messages/UserMessage.svelte b/src/lib/components/chat/Messages/UserMessage.svelte new file mode 100644 index 0000000000000000000000000000000000000000..748fcd61b3231020bdb008a01c3f9ff170af5ae8 --- /dev/null +++ b/src/lib/components/chat/Messages/UserMessage.svelte @@ -0,0 +1,368 @@ +<script lang="ts"> + import dayjs from 'dayjs'; + + import { tick, createEventDispatcher, getContext } from 'svelte'; + import Name from './Name.svelte'; + import ProfileImage from './ProfileImage.svelte'; + import { models, settings } from '$lib/stores'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + import { user as _user } from '$lib/stores'; + import { getFileContentById } from '$lib/apis/files'; + import FileItem from '$lib/components/common/FileItem.svelte'; + + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + export let user; + export let message; + export let siblings; + export let isFirstMessage: boolean; + export let readOnly: boolean; + + export let confirmEditMessage: Function; + export let showPreviousMessage: Function; + export let showNextMessage: Function; + export let copyToClipboard: Function; + + let edit = false; + let editedContent = ''; + let messageEditTextAreaElement: HTMLTextAreaElement; + const editMessageHandler = async () => { + edit = true; + editedContent = message.content; + + await tick(); + + messageEditTextAreaElement.style.height = ''; + messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`; + + messageEditTextAreaElement?.focus(); + }; + + const editMessageConfirmHandler = async () => { + confirmEditMessage(message.id, editedContent); + + edit = false; + editedContent = ''; + }; + + const cancelEditMessage = () => { + edit = false; + editedContent = ''; + }; + + const deleteMessageHandler = async () => { + dispatch('delete', message.id); + }; +</script> + +<div class=" flex w-full user-message" dir={$settings.chatDirection}> + {#if !($settings?.chatBubble ?? true)} + <ProfileImage + src={message.user + ? $models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ?? '/user.png' + : user?.profile_image_url ?? '/user.png'} + /> + {/if} + <div class="w-full overflow-hidden pl-1"> + {#if !($settings?.chatBubble ?? true)} + <div> + <Name> + {#if message.user} + {$i18n.t('You')} + <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span> + {:else if $settings.showUsername || $_user.name !== user.name} + {user.name} + {:else} + {$i18n.t('You')} + {/if} + + {#if message.timestamp} + <span + class=" invisible group-hover:visible text-gray-400 text-xs font-medium uppercase" + > + {dayjs(message.timestamp * 1000).format($i18n.t('h:mm a'))} + </span> + {/if} + </Name> + </div> + {/if} + + <div + class="prose chat-{message.role} w-full max-w-full flex flex-col justify-end dark:prose-invert prose-headings:my-0 prose-p:my-0 prose-p:-mb-4 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-6 prose-li:-mb-4 whitespace-pre-line" + > + {#if message.files} + <div class="mt-2.5 mb-1 w-full flex flex-col justify-end overflow-x-auto gap-1 flex-wrap"> + {#each message.files as file} + <div class={$settings?.chatBubble ?? true ? 'self-end' : ''}> + {#if file.type === 'image'} + <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> + {:else} + <FileItem + url={file.url} + name={file.name} + type={file.type} + colorClassName="bg-white dark:bg-gray-850 " + /> + {/if} + </div> + {/each} + </div> + {/if} + + {#if edit === true} + <div class=" w-full bg-gray-50 dark:bg-gray-800 rounded-3xl px-5 py-3 mb-2"> + <textarea + id="message-edit-{message.id}" + bind:this={messageEditTextAreaElement} + class=" bg-transparent outline-none w-full resize-none" + bind:value={editedContent} + on:input={(e) => { + e.target.style.height = ''; + e.target.style.height = `${e.target.scrollHeight}px`; + }} + on:keydown={(e) => { + if (e.key === 'Escape') { + document.getElementById('close-edit-message-button')?.click(); + } + + const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey; + const isEnterPressed = e.key === 'Enter'; + + if (isCmdOrCtrlPressed && isEnterPressed) { + document.getElementById('save-edit-message-button')?.click(); + } + }} + /> + + <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium"> + <button + id="close-edit-message-button" + class="px-4 py-2 bg-white dark:bg-gray-900 hover:bg-gray-100 text-gray-800 dark:text-gray-100 transition rounded-3xl" + on:click={() => { + cancelEditMessage(); + }} + > + {$i18n.t('Cancel')} + </button> + + <button + id="save-edit-message-button" + class=" px-4 py-2 bg-gray-900 dark:bg-white hover:bg-gray-850 text-gray-100 dark:text-gray-800 transition rounded-3xl" + on:click={() => { + editMessageConfirmHandler(); + }} + > + {$i18n.t('Send')} + </button> + </div> + </div> + {:else} + <div class="w-full"> + <div class="flex {$settings?.chatBubble ?? true ? 'justify-end' : ''} mb-2"> + <div + class="rounded-3xl {$settings?.chatBubble ?? true + ? `max-w-[90%] px-5 py-2 bg-gray-50 dark:bg-gray-850 ${ + message.files ? 'rounded-tr-lg' : '' + }` + : ''} " + > + <pre id="user-message">{message.content}</pre> + </div> + </div> + + <div + class=" flex {$settings?.chatBubble ?? true + ? 'justify-end' + : ''} text-gray-600 dark:text-gray-500" + > + {#if !($settings?.chatBubble ?? true)} + {#if siblings.length > 1} + <div class="flex self-center" dir="ltr"> + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showPreviousMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 19.5 8.25 12l7.5-7.5" + /> + </svg> + </button> + + <div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"> + {siblings.indexOf(message.id) + 1}/{siblings.length} + </div> + + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showNextMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m8.25 4.5 7.5 7.5-7.5 7.5" + /> + </svg> + </button> + </div> + {/if} + {/if} + {#if !readOnly} + <Tooltip content={$i18n.t('Edit')} placement="bottom"> + <button + class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition edit-user-message-button" + on:click={() => { + editMessageHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + </button> + </Tooltip> + {/if} + + <Tooltip content={$i18n.t('Copy')} placement="bottom"> + <button + class="invisible group-hover:visible p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition" + on:click={() => { + copyToClipboard(message.content); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2.3" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" + /> + </svg> + </button> + </Tooltip> + + {#if !isFirstMessage && !readOnly} + <Tooltip content={$i18n.t('Delete')} placement="bottom"> + <button + class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" + on:click={() => { + deleteMessageHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </Tooltip> + {/if} + + {#if $settings?.chatBubble ?? true} + {#if siblings.length > 1} + <div class="flex self-center" dir="ltr"> + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showPreviousMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 19.5 8.25 12l7.5-7.5" + /> + </svg> + </button> + + <div class="text-sm tracking-widest font-semibold self-center dark:text-gray-100"> + {siblings.indexOf(message.id) + 1}/{siblings.length} + </div> + + <button + class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition" + on:click={() => { + showNextMessage(message); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + stroke-width="2.5" + class="size-3.5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m8.25 4.5 7.5 7.5-7.5 7.5" + /> + </svg> + </button> + </div> + {/if} + {/if} + </div> + </div> + {/if} + </div> + </div> +</div> diff --git a/src/lib/components/chat/ModelSelector.svelte b/src/lib/components/chat/ModelSelector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c045d138e5e8f2f5acaf565f293c03a750a78da6 --- /dev/null +++ b/src/lib/components/chat/ModelSelector.svelte @@ -0,0 +1,109 @@ +<script lang="ts"> + import { models, showSettings, settings, user, mobile } from '$lib/stores'; + import { onMount, tick, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import Selector from './ModelSelector/Selector.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + + import { setDefaultModels } from '$lib/apis/configs'; + import { updateUserSettings } from '$lib/apis/users'; + + const i18n = getContext('i18n'); + + export let selectedModels = ['']; + export let disabled = false; + + export let showSetDefault = true; + + const saveDefaultModel = async () => { + const hasEmptyModel = selectedModels.filter((it) => it === ''); + if (hasEmptyModel.length) { + toast.error($i18n.t('Choose a model before saving...')); + return; + } + settings.set({ ...$settings, models: selectedModels }); + await updateUserSettings(localStorage.token, { ui: $settings }); + + toast.success($i18n.t('Default model updated')); + }; + + $: if (selectedModels.length > 0 && $models.length > 0) { + selectedModels = selectedModels.map((model) => + $models.map((m) => m.id).includes(model) ? model : '' + ); + } +</script> + +<div class="flex flex-col w-full items-start"> + {#each selectedModels as selectedModel, selectedModelIdx} + <div class="flex w-full max-w-fit"> + <div class="overflow-hidden w-full"> + <div class="mr-1 max-w-full"> + <Selector + placeholder={$i18n.t('Select a model')} + items={$models.map((model) => ({ + value: model.id, + label: model.name, + model: model + }))} + bind:value={selectedModel} + /> + </div> + </div> + + {#if selectedModelIdx === 0} + <div class=" self-center mr-2 disabled:text-gray-600 disabled:hover:text-gray-600"> + <Tooltip content={$i18n.t('Add Model')}> + <button + class=" " + {disabled} + on:click={() => { + selectedModels = [...selectedModels, '']; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-3.5" + > + <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m6-6H6" /> + </svg> + </button> + </Tooltip> + </div> + {:else} + <div class=" self-center disabled:text-gray-600 disabled:hover:text-gray-600 mr-2"> + <Tooltip content={$i18n.t('Remove Model')}> + <button + {disabled} + on:click={() => { + selectedModels.splice(selectedModelIdx, 1); + selectedModels = selectedModels; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-3.5" + > + <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" /> + </svg> + </button> + </Tooltip> + </div> + {/if} + </div> + {/each} +</div> + +{#if showSetDefault && !$mobile} + <div class="text-left mt-0.5 ml-1 text-[0.7rem] text-gray-500 font-primary"> + <button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button> + </div> +{/if} diff --git a/src/lib/components/chat/ModelSelector/Selector.svelte b/src/lib/components/chat/ModelSelector/Selector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e443e1f13b1bbb113feb5678311d3a9ff957f9f8 --- /dev/null +++ b/src/lib/components/chat/ModelSelector/Selector.svelte @@ -0,0 +1,518 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { marked } from 'marked'; + + import { flyAndScale } from '$lib/utils/transitions'; + import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; + + import ChevronDown from '$lib/components/icons/ChevronDown.svelte'; + import Check from '$lib/components/icons/Check.svelte'; + import Search from '$lib/components/icons/Search.svelte'; + + import { deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama'; + + import { user, MODEL_DOWNLOAD_POOL, models, mobile } from '$lib/stores'; + import { toast } from 'svelte-sonner'; + import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils'; + import { getModels } from '$lib/apis'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let value = ''; + export let placeholder = 'Select a model'; + export let searchEnabled = true; + export let searchPlaceholder = $i18n.t('Search a model'); + + export let items: { + label: string; + value: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + } = []; + + export let className = 'w-[32rem]'; + + let show = false; + + let selectedModel = ''; + $: selectedModel = items.find((item) => item.value === value) ?? ''; + + let searchValue = ''; + let ollamaVersion = null; + + let selectedModelIdx = 0; + + $: filteredItems = items.filter( + (item) => + (searchValue + ? item.value.toLowerCase().includes(searchValue.toLowerCase()) || + item.label.toLowerCase().includes(searchValue.toLowerCase()) || + (item.model?.info?.meta?.tags ?? []).some((tag) => + tag.name.toLowerCase().includes(searchValue.toLowerCase()) + ) + : true) && !(item.model?.info?.meta?.hidden ?? false) + ); + + const pullModelHandler = async () => { + const sanitizedModelTag = searchValue.trim().replace(/^ollama\s+(run|pull)\s+/, ''); + + console.log($MODEL_DOWNLOAD_POOL); + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) { + toast.error( + $i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, { + modelTag: sanitizedModelTag + }) + ); + return; + } + if (Object.keys($MODEL_DOWNLOAD_POOL).length === 3) { + toast.error( + $i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.') + ); + return; + } + + const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch( + (error) => { + toast.error(error); + return null; + } + ); + + if (res) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + abortController: controller, + reader, + done: false + } + }); + + while (true) { + try { + const { value, done } = await reader.read(); + if (done) break; + + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line); + console.log(data); + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if (data.digest) { + let downloadProgress = 0; + if (data.completed) { + downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + downloadProgress = 100; + } + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + pullProgress: downloadProgress, + digest: data.digest + } + }); + } else { + toast.success(data.status); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + done: data.status === 'success' + } + }); + } + } + } + } + } catch (error) { + console.log(error); + if (typeof error !== 'string') { + error = error.message; + } + + toast.error(error); + // opts.callback({ success: false, error, modelName: opts.modelName }); + break; + } + } + + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) { + toast.success( + $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { + modelName: sanitizedModelTag + }) + ); + + models.set(await getModels(localStorage.token)); + } else { + toast.error($i18n.t('Download canceled')); + } + + delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag]; + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + } + }; + + onMount(async () => { + ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); + }); + + const cancelModelPullHandler = async (model: string) => { + const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model]; + if (abortController) { + abortController.abort(); + } + if (reader) { + await reader.cancel(); + delete $MODEL_DOWNLOAD_POOL[model]; + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + await deleteModel(localStorage.token, model); + toast.success(`${model} download has been canceled`); + } + }; +</script> + +<DropdownMenu.Root + bind:open={show} + onOpenChange={async () => { + searchValue = ''; + selectedModelIdx = 0; + window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); + }} + closeFocus={false} +> + <DropdownMenu.Trigger class="relative w-full font-primary" aria-label={placeholder}> + <div + class="flex w-full text-left px-0.5 outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none" + > + {#if selectedModel} + {selectedModel.label} + {:else} + {placeholder} + {/if} + <ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" /> + </div> + </DropdownMenu.Trigger> + + <DropdownMenu.Content + class=" z-40 {$mobile + ? `w-full` + : `${className}`} max-w-[calc(100vw-1rem)] justify-start rounded-xl bg-white dark:bg-gray-850 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none" + transition={flyAndScale} + side={$mobile ? 'bottom' : 'bottom-start'} + sideOffset={4} + > + <slot> + {#if searchEnabled} + <div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3"> + <Search className="size-4" strokeWidth="2.5" /> + + <input + id="model-search-input" + bind:value={searchValue} + class="w-full text-sm bg-transparent outline-none" + placeholder={searchPlaceholder} + autocomplete="off" + on:keydown={(e) => { + if (e.code === 'Enter' && filteredItems.length > 0) { + value = filteredItems[selectedModelIdx].value; + show = false; + return; // dont need to scroll on selection + } else if (e.code === 'ArrowDown') { + selectedModelIdx = Math.min(selectedModelIdx + 1, filteredItems.length - 1); + } else if (e.code === 'ArrowUp') { + selectedModelIdx = Math.max(selectedModelIdx - 1, 0); + } else { + // if the user types something, reset to the top selection. + selectedModelIdx = 0; + } + + const item = document.querySelector(`[data-arrow-selected="true"]`); + item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' }); + }} + /> + </div> + + <hr class="border-gray-100 dark:border-gray-800" /> + {/if} + + <div class="px-3 my-2 max-h-64 overflow-y-auto scrollbar-hidden group"> + {#each filteredItems as item, index} + <button + aria-label="model-item" + class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted {index === + selectedModelIdx + ? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent' + : ''}" + data-arrow-selected={index === selectedModelIdx} + on:click={() => { + value = item.value; + selectedModelIdx = index; + + show = false; + }} + > + <div class="flex flex-col"> + {#if $mobile && (item?.model?.info?.meta?.tags ?? []).length > 0} + <div class="flex gap-0.5 self-start h-full mb-0.5 -translate-x-1"> + {#each item.model?.info?.meta.tags as tag} + <div + class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + {tag.name} + </div> + {/each} + </div> + {/if} + <div class="flex items-center gap-2"> + <div class="flex items-center min-w-fit"> + <div class="line-clamp-1"> + <div class="flex items-center min-w-fit"> + <img + src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'} + alt="Model" + class="rounded-full size-5 flex items-center mr-2" + /> + {item.label} + </div> + </div> + {#if item.model.owned_by === 'ollama' && (item.model.ollama?.details?.parameter_size ?? '') !== ''} + <div class="flex ml-1 items-center translate-y-[0.5px]"> + <Tooltip + content={`${ + item.model.ollama?.details?.quantization_level + ? item.model.ollama?.details?.quantization_level + ' ' + : '' + }${ + item.model.ollama?.size + ? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)` + : '' + }`} + className="self-end" + > + <span + class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1" + >{item.model.ollama?.details?.parameter_size ?? ''}</span + > + </Tooltip> + </div> + {/if} + </div> + + <!-- {JSON.stringify(item.info)} --> + + {#if item.model.owned_by === 'openai'} + <Tooltip content={`${'External'}`}> + <div class="translate-y-[1px]"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-3" + > + <path + fill-rule="evenodd" + d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" + clip-rule="evenodd" + /> + </svg> + </div> + </Tooltip> + {/if} + + {#if item.model?.info?.meta?.description} + <Tooltip + content={`${marked.parse( + sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll( + '\n', + '<br>' + ) + )}`} + > + <div class=" translate-y-[1px]"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" + /> + </svg> + </div> + </Tooltip> + {/if} + + {#if !$mobile && (item?.model?.info?.meta?.tags ?? []).length > 0} + <div class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px]"> + {#each item.model?.info?.meta.tags as tag} + <Tooltip content={tag.name}> + <div + class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + {tag.name} + </div> + </Tooltip> + {/each} + </div> + {/if} + </div> + </div> + + {#if value === item.value} + <div class="ml-auto pl-2"> + <Check /> + </div> + {/if} + </button> + {:else} + <div> + <div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100"> + {$i18n.t('No results found')} + </div> + </div> + {/each} + + {#if !(searchValue.trim() in $MODEL_DOWNLOAD_POOL) && searchValue && ollamaVersion && $user.role === 'admin'} + <button + class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-[highlighted]:bg-muted" + on:click={() => { + pullModelHandler(); + }} + > + {$i18n.t(`Pull "{{searchValue}}" from Ollama.com`, { searchValue: searchValue })} + </button> + {/if} + + {#each Object.keys($MODEL_DOWNLOAD_POOL) as model} + <div + class="flex w-full justify-between font-medium select-none rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 rounded-lg cursor-pointer data-[highlighted]:bg-muted" + > + <div class="flex"> + <div class="-ml-2 mr-2.5 translate-y-0.5"> + <svg + class="size-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + + <div class="flex flex-col self-start"> + <div class="line-clamp-1"> + Downloading "{model}" {'pullProgress' in $MODEL_DOWNLOAD_POOL[model] + ? `(${$MODEL_DOWNLOAD_POOL[model].pullProgress}%)` + : ''} + </div> + + {#if 'digest' in $MODEL_DOWNLOAD_POOL[model] && $MODEL_DOWNLOAD_POOL[model].digest} + <div class="-mt-1 h-fit text-[0.7rem] dark:text-gray-500 line-clamp-1"> + {$MODEL_DOWNLOAD_POOL[model].digest} + </div> + {/if} + </div> + </div> + + <div class="mr-2 translate-y-0.5"> + <Tooltip content={$i18n.t('Cancel')}> + <button + class="text-gray-800 dark:text-gray-100" + on:click={() => { + cancelModelPullHandler(model); + }} + > + <svg + class="w-4 h-4 text-gray-800 dark:text-white" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="currentColor" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M6 18 17.94 6M18 18 6.06 6" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + {/each} + </div> + + <div class="hidden w-[42rem]" /> + <div class="hidden w-[32rem]" /> + </slot> + </DropdownMenu.Content> +</DropdownMenu.Root> + +<style> + .scrollbar-hidden:active::-webkit-scrollbar-thumb, + .scrollbar-hidden:focus::-webkit-scrollbar-thumb, + .scrollbar-hidden:hover::-webkit-scrollbar-thumb { + visibility: visible; + } + .scrollbar-hidden::-webkit-scrollbar-thumb { + visibility: hidden; + } +</style> diff --git a/src/lib/components/chat/Settings/About.svelte b/src/lib/components/chat/Settings/About.svelte new file mode 100644 index 0000000000000000000000000000000000000000..11177d45b47de5601f5a7bd37c87ae7f8cb5d0e9 --- /dev/null +++ b/src/lib/components/chat/Settings/About.svelte @@ -0,0 +1,144 @@ +<script lang="ts"> + import { getVersionUpdates } from '$lib/apis'; + import { getOllamaVersion } from '$lib/apis/ollama'; + import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants'; + import { WEBUI_NAME, config, showChangelog } from '$lib/stores'; + import { compareVersion } from '$lib/utils'; + import { onMount, getContext } from 'svelte'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + let ollamaVersion = ''; + + let updateAvailable = null; + let version = { + current: '', + latest: '' + }; + + const checkForVersionUpdates = async () => { + updateAvailable = null; + version = await getVersionUpdates(localStorage.token).catch((error) => { + return { + current: WEBUI_VERSION, + latest: WEBUI_VERSION + }; + }); + + console.log(version); + + updateAvailable = compareVersion(version.latest, version.current); + console.log(updateAvailable); + }; + + onMount(async () => { + ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { + return ''; + }); + + checkForVersionUpdates(); + }); +</script> + +<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6"> + <div class=" space-y-3"> + <div> + <div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center"> + <div> + {$WEBUI_NAME} + {$i18n.t('Version')} + </div> + </div> + <div class="flex w-full justify-between items-center"> + <div class="flex flex-col text-xs text-gray-700 dark:text-gray-200"> + <div class="flex gap-1"> + <Tooltip content={WEBUI_BUILD_HASH}> + v{WEBUI_VERSION} + </Tooltip> + + <a + href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}" + target="_blank" + > + {updateAvailable === null + ? $i18n.t('Checking for updates...') + : updateAvailable + ? `(v${version.latest} ${$i18n.t('available!')})` + : $i18n.t('(latest)')} + </a> + </div> + + <button + class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500" + on:click={() => { + showChangelog.set(true); + }} + > + <div>{$i18n.t("See what's new")}</div> + </button> + </div> + + <button + class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium" + on:click={() => { + checkForVersionUpdates(); + }} + > + {$i18n.t('Check for updates')} + </button> + </div> + </div> + + {#if ollamaVersion} + <hr class=" dark:border-gray-850" /> + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div> + <div class="flex w-full"> + <div class="flex-1 text-xs text-gray-700 dark:text-gray-200"> + {ollamaVersion ?? 'N/A'} + </div> + </div> + </div> + {/if} + + <hr class=" dark:border-gray-850" /> + + <div class="flex space-x-1"> + <a href="https://discord.gg/5rJgQTnV4s" target="_blank"> + <img + alt="Discord" + src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white" + /> + </a> + + <a href="https://twitter.com/OpenWebUI" target="_blank"> + <img + alt="X (formerly Twitter) Follow" + src="https://img.shields.io/twitter/follow/OpenWebUI" + /> + </a> + + <a href="https://github.com/open-webui/open-webui" target="_blank"> + <img + alt="Github Repo" + src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github" + /> + </a> + </div> + + <div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> + {#if !$WEBUI_NAME.includes('Open WebUI')} + <span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> - + {/if} + {$i18n.t('Created by')} + <a + class=" text-gray-500 dark:text-gray-300 font-medium" + href="https://github.com/tjbck" + target="_blank">Timothy J. Baek</a + > + </div> + </div> +</div> diff --git a/src/lib/components/chat/Settings/Account.svelte b/src/lib/components/chat/Settings/Account.svelte new file mode 100644 index 0000000000000000000000000000000000000000..177bacb75d72a039b191d37a5ca4289f79dc2e36 --- /dev/null +++ b/src/lib/components/chat/Settings/Account.svelte @@ -0,0 +1,412 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { onMount, getContext } from 'svelte'; + + import { user } from '$lib/stores'; + import { updateUserProfile, createAPIKey, getAPIKey } from '$lib/apis/auths'; + + 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'; + import SensitiveInput from '$lib/components/common/SensitiveInput.svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let profileImageUrl = ''; + let name = ''; + + let showAPIKeys = false; + + let JWTTokenCopied = false; + + let APIKey = ''; + let APIKeyCopied = false; + + 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); + } + ); + + if (updatedUser) { + await user.set(updatedUser); + return true; + } + return false; + }; + + const createAPIKeyHandler = async () => { + APIKey = await createAPIKey(localStorage.token); + if (APIKey) { + toast.success($i18n.t('API Key created.')); + } else { + toast.error($i18n.t('Failed to create API Key.')); + } + }; + + onMount(async () => { + name = $user.name; + profileImageUrl = $user.profile_image_url; + + APIKey = await getAPIKey(localStorage.token).catch((error) => { + console.log(error); + return ''; + }); + }); +</script> + +<div class="flex flex-col h-full justify-between text-sm"> + <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]"> + <input + id="profile-image-input" + bind:this={profileImageInputElement} + type="file" + hidden + accept="image/*" + on:change={(e) => { + const files = profileImageInputElement.files ?? []; + let reader = new FileReader(); + reader.onload = (event) => { + let originalImageUrl = `${event.target.result}`; + + const img = new Image(); + img.src = originalImageUrl; + + img.onload = function () { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate the aspect ratio of the image + const aspectRatio = img.width / img.height; + + // Calculate the new width and height to fit within 100x100 + let newWidth, newHeight; + if (aspectRatio > 1) { + newWidth = 100 * aspectRatio; + newHeight = 100; + } else { + newWidth = 100; + newHeight = 100 / aspectRatio; + } + + // Set the canvas size + canvas.width = 100; + canvas.height = 100; + + // Calculate the position to center the image + const offsetX = (100 - newWidth) / 2; + const offsetY = (100 - newHeight) / 2; + + // Draw the image on the canvas + ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); + + // Get the base64 representation of the compressed image + const compressedSrc = canvas.toDataURL('image/jpeg'); + + // Display the compressed image + profileImageUrl = compressedSrc; + + profileImageInputElement.files = null; + }; + }; + + if ( + files.length > 0 && + ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(files[0]['type']) + ) { + reader.readAsDataURL(files[0]); + } + }} + /> + + <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 mt-2"> + <button + class="relative rounded-full dark:bg-gray-700" + type="button" + on:click={() => { + profileImageInputElement.click(); + }} + > + <img + src={profileImageUrl !== '' ? profileImageUrl : generateInitialsImage(name)} + alt="profile" + class=" rounded-full size-16 object-cover" + /> + + <div + class="absolute flex justify-center rounded-full bottom-0 left-0 right-0 top-0 h-full w-full overflow-hidden bg-gray-700 bg-fixed opacity-0 transition duration-300 ease-in-out hover:opacity-50" + > + <div class="my-auto text-gray-100"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" + /> + </svg> + </div> + </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-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="pt-0.5"> + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs font-medium">{$i18n.t('Name')}</div> + + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + bind:value={name} + required + /> + </div> + </div> + </div> + </div> + + <div class="py-0.5"> + <UpdatePassword /> + </div> + + <hr class=" dark:border-gray-850 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"> + <div class="self-center text-xs font-medium">{$i18n.t('JWT Token')}</div> + </div> + + <div class="flex mt-2"> + <SensitiveInput value={localStorage.token} readOnly={true} /> + + <button + class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" + on:click={() => { + copyToClipboard(localStorage.token); + JWTTokenCopied = true; + setTimeout(() => { + JWTTokenCopied = false; + }, 2000); + }} + > + {#if JWTTokenCopied} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + </div> + </div> + <div class="justify-between w-full"> + <div class="flex justify-between w-full"> + <div class="self-center text-xs font-medium">{$i18n.t('API Key')}</div> + </div> + + <div class="flex mt-2"> + {#if APIKey} + <SensitiveInput value={APIKey} readOnly={true} /> + + <button + class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" + on:click={() => { + copyToClipboard(APIKey); + APIKeyCopied = true; + setTimeout(() => { + APIKeyCopied = false; + }, 2000); + }} + > + {#if APIKeyCopied} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + + <Tooltip content={$i18n.t('Create new key')}> + <button + class=" px-1.5 py-1 dark:hover:bg-gray-850transition rounded-lg" + on:click={() => { + createAPIKeyHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" + /> + </svg> + </button> + </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-850 transition" + on:click={() => { + createAPIKeyHandler(); + }} + > + <Plus strokeWidth="2" className=" size-3.5" /> + + {$i18n.t('Create new secret key')}</button + > + {/if} + </div> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + on:click={async () => { + const res = await submitHandler(); + + if (res) { + saveHandler(); + } + }} + > + {$i18n.t('Save')} + </button> + </div> +</div> diff --git a/src/lib/components/chat/Settings/Account/UpdatePassword.svelte b/src/lib/components/chat/Settings/Account/UpdatePassword.svelte new file mode 100644 index 0000000000000000000000000000000000000000..175ee61ebbed6d21c0bd6fb650e7cc04bd3b3176 --- /dev/null +++ b/src/lib/components/chat/Settings/Account/UpdatePassword.svelte @@ -0,0 +1,109 @@ +<script lang="ts"> + import { getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import { updateUserPassword } from '$lib/apis/auths'; + + const i18n = getContext('i18n'); + + let show = false; + let currentPassword = ''; + let newPassword = ''; + let newPasswordConfirm = ''; + + const updatePasswordHandler = async () => { + if (newPassword === newPasswordConfirm) { + const res = await updateUserPassword(localStorage.token, currentPassword, newPassword).catch( + (error) => { + toast.error(error); + return null; + } + ); + + if (res) { + toast.success($i18n.t('Successfully updated.')); + } + + currentPassword = ''; + newPassword = ''; + newPasswordConfirm = ''; + } else { + toast.error( + `The passwords you entered don't quite match. Please double-check and try again.` + ); + newPassword = ''; + newPasswordConfirm = ''; + } + }; +</script> + +<form + class="flex flex-col text-sm" + on:submit|preventDefault={() => { + updatePasswordHandler(); + }} +> + <div class="flex justify-between items-center text-sm"> + <div class=" font-medium">{$i18n.t('Change Password')}</div> + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + show = !show; + }}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button + > + </div> + + {#if show} + <div class=" py-2.5 space-y-1.5"> + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div> + + <div class="flex-1"> + <input + class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" + type="password" + bind:value={currentPassword} + autocomplete="current-password" + required + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</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" + type="password" + bind:value={newPassword} + autocomplete="new-password" + required + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</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" + type="password" + bind:value={newPasswordConfirm} + autocomplete="off" + required + /> + </div> + </div> + </div> + + <div class="mt-3 flex justify-end"> + <button + class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium" + > + {$i18n.t('Update password')} + </button> + </div> + {/if} +</form> diff --git a/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fc83f4534e77b8bb7ecc174ef1482b4484ef5feb --- /dev/null +++ b/src/lib/components/chat/Settings/Advanced/AdvancedParams.svelte @@ -0,0 +1,853 @@ +<script lang="ts"> + import Switch from '$lib/components/common/Switch.svelte'; + import { getContext, createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let admin = false; + + export let params = { + // Advanced + seed: null, + stop: null, + temperature: null, + frequency_penalty: null, + repeat_last_n: null, + mirostat: null, + mirostat_eta: null, + mirostat_tau: null, + top_k: null, + top_p: null, + tfs_z: null, + num_ctx: null, + num_batch: null, + num_keep: null, + max_tokens: null, + use_mmap: null, + use_mlock: null, + num_thread: null, + template: null + }; + + let customFieldName = ''; + let customFieldValue = ''; + + $: if (params) { + dispatch('change', params); + } +</script> + +<div class=" space-y-1 text-xs"> + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Seed')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.seed = (params?.seed ?? null) === null ? 0 : null; + }} + > + {#if (params?.seed ?? null) === null} + <span class="ml-2 self-center"> {$i18n.t('Default')} </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if (params?.seed ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder="Enter Seed" + bind:value={params.seed} + autocomplete="off" + min="0" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Stop Sequence')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.stop = (params?.stop ?? null) === null ? '' : null; + }} + > + {#if (params?.stop ?? null) === null} + <span class="ml-2 self-center"> {$i18n.t('Default')} </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if (params?.stop ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter stop sequence')} + bind:value={params.stop} + autocomplete="off" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.temperature = (params?.temperature ?? null) === null ? 0.8 : null; + }} + > + {#if (params?.temperature ?? null) === null} + <span class="ml-2 self-center"> {$i18n.t('Default')} </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if (params?.temperature ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="1" + step="0.05" + bind:value={params.temperature} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.temperature} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="1" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.mirostat = (params?.mirostat ?? null) === null ? 0 : null; + }} + > + {#if (params?.mirostat ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.mirostat ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="2" + step="1" + bind:value={params.mirostat} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.mirostat} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="2" + step="1" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.mirostat_eta = (params?.mirostat_eta ?? null) === null ? 0.1 : null; + }} + > + {#if (params?.mirostat_eta ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.mirostat_eta ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="1" + step="0.05" + bind:value={params.mirostat_eta} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.mirostat_eta} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="1" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.mirostat_tau = (params?.mirostat_tau ?? null) === null ? 5.0 : null; + }} + > + {#if (params?.mirostat_tau ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.mirostat_tau ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="10" + step="0.5" + bind:value={params.mirostat_tau} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.mirostat_tau} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="10" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.top_k = (params?.top_k ?? null) === null ? 40 : null; + }} + > + {#if (params?.top_k ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.top_k ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="100" + step="0.5" + bind:value={params.top_k} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.top_k} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="100" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.top_p = (params?.top_p ?? null) === null ? 0.9 : null; + }} + > + {#if (params?.top_p ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.top_p ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="1" + step="0.05" + bind:value={params.top_p} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.top_p} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="1" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Frequency Penalty')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.frequency_penalty = (params?.frequency_penalty ?? null) === null ? 1.1 : null; + }} + > + {#if (params?.frequency_penalty ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.frequency_penalty ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="2" + step="0.05" + bind:value={params.frequency_penalty} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.frequency_penalty} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="2" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.repeat_last_n = (params?.repeat_last_n ?? null) === null ? 64 : null; + }} + > + {#if (params?.repeat_last_n ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.repeat_last_n ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="-1" + max="128" + step="1" + bind:value={params.repeat_last_n} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.repeat_last_n} + type="number" + class=" bg-transparent text-center w-14" + min="-1" + max="128" + step="1" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.tfs_z = (params?.tfs_z ?? null) === null ? 1 : null; + }} + > + {#if (params?.tfs_z ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.tfs_z ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="0" + max="2" + step="0.05" + bind:value={params.tfs_z} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div> + <input + bind:value={params.tfs_z} + type="number" + class=" bg-transparent text-center w-14" + min="0" + max="2" + step="any" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.num_ctx = (params?.num_ctx ?? null) === null ? 2048 : null; + }} + > + {#if (params?.num_ctx ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.num_ctx ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="-1" + max="10240000" + step="1" + bind:value={params.num_ctx} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={params.num_ctx} + type="number" + class=" bg-transparent text-center w-14" + min="-1" + step="1" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Batch Size (num_batch)')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.num_batch = (params?.num_batch ?? null) === null ? 512 : null; + }} + > + {#if (params?.num_batch ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.num_batch ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="256" + max="8192" + step="256" + bind:value={params.num_batch} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={params.num_batch} + type="number" + class=" bg-transparent text-center w-14" + min="256" + step="256" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Tokens To Keep On Context Refresh (num_keep)')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.num_keep = (params?.num_keep ?? null) === null ? 24 : null; + }} + > + {#if (params?.num_keep ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.num_keep ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="-1" + max="10240000" + step="1" + bind:value={params.num_keep} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={params.num_keep} + type="number" + class=" bg-transparent text-center w-14" + min="-1" + step="1" + /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens (num_predict)')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.max_tokens = (params?.max_tokens ?? null) === null ? 128 : null; + }} + > + {#if (params?.max_tokens ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.max_tokens ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="-2" + max="16000" + step="1" + bind:value={params.max_tokens} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={params.max_tokens} + type="number" + class=" bg-transparent text-center w-14" + min="-2" + max="16000" + step="1" + /> + </div> + </div> + {/if} + </div> + + {#if admin} + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('use_mmap (Ollama)')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.use_mmap = (params?.use_mmap ?? null) === null ? true : null; + }} + > + {#if (params?.use_mmap ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.use_mmap ?? null) !== null} + <div class="flex justify-between items-center mt-1"> + <div class="text-xs text-gray-500"> + {params.use_mmap ? 'Enabled' : 'Disabled'} + </div> + + <div class=" pr-2"> + <Switch bind:state={params.use_mmap} /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('use_mlock (Ollama)')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.use_mlock = (params?.use_mlock ?? null) === null ? true : null; + }} + > + {#if (params?.use_mlock ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.use_mlock ?? null) !== null} + <div class="flex justify-between items-center mt-1"> + <div class="text-xs text-gray-500"> + {params.use_mlock ? 'Enabled' : 'Disabled'} + </div> + + <div class=" pr-2"> + <Switch bind:state={params.use_mlock} /> + </div> + </div> + {/if} + </div> + + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('num_thread (Ollama)')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.num_thread = (params?.num_thread ?? null) === null ? 2 : null; + }} + > + {#if (params?.num_thread ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.num_thread ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <input + id="steps-range" + type="range" + min="1" + max="256" + step="1" + bind:value={params.num_thread} + class="w-full h-2 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" + /> + </div> + <div class=""> + <input + bind:value={params.num_thread} + type="number" + class=" bg-transparent text-center w-14" + min="1" + max="256" + step="1" + /> + </div> + </div> + {/if} + </div> + + <!-- <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Template')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition flex-shrink-0 outline-none" + type="button" + on:click={() => { + params.template = (params?.template ?? null) === null ? '' : null; + }} + > + {#if (params?.template ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (params?.template ?? null) !== null} + <div class="flex mt-0.5 space-x-2"> + <div class=" flex-1"> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1" + placeholder="Write your model template content here" + rows="4" + bind:value={params.template} + /> + </div> + </div> + {/if} + </div> --> + {/if} +</div> diff --git a/src/lib/components/chat/Settings/Audio.svelte b/src/lib/components/chat/Settings/Audio.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2876321aa65f69ba5ec936b2de469c8a47964d2b --- /dev/null +++ b/src/lib/components/chat/Settings/Audio.svelte @@ -0,0 +1,215 @@ +<script lang="ts"> + import { user, settings, config } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import Switch from '$lib/components/common/Switch.svelte'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let saveSettings: Function; + + // Audio + let conversationMode = false; + let speechAutoSend = false; + let responseAutoPlayback = false; + let nonLocalVoices = false; + + let STTEngine = ''; + + let voices = []; + let voice = ''; + + const getOpenAIVoices = () => { + voices = [ + { name: 'alloy' }, + { name: 'echo' }, + { name: 'fable' }, + { name: 'onyx' }, + { name: 'nova' }, + { name: 'shimmer' } + ]; + }; + + const getWebAPIVoices = () => { + const getVoicesLoop = setInterval(async () => { + voices = await speechSynthesis.getVoices(); + + // do your loop + if (voices.length > 0) { + clearInterval(getVoicesLoop); + } + }, 100); + }; + + const toggleResponseAutoPlayback = async () => { + responseAutoPlayback = !responseAutoPlayback; + saveSettings({ responseAutoPlayback: responseAutoPlayback }); + }; + + const toggleSpeechAutoSend = async () => { + speechAutoSend = !speechAutoSend; + saveSettings({ speechAutoSend: speechAutoSend }); + }; + + onMount(async () => { + conversationMode = $settings.conversationMode ?? false; + speechAutoSend = $settings.speechAutoSend ?? false; + responseAutoPlayback = $settings.responseAutoPlayback ?? false; + + STTEngine = $settings?.audio?.stt?.engine ?? ''; + voice = $settings?.audio?.tts?.voice ?? $config.audio.tts.voice ?? ''; + nonLocalVoices = $settings.audio?.tts?.nonLocalVoices ?? false; + + if ($config.audio.tts.engine === 'openai') { + getOpenAIVoices(); + } else { + getWebAPIVoices(); + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + saveSettings({ + audio: { + stt: { + engine: STTEngine !== '' ? STTEngine : undefined + }, + tts: { + voice: voice !== '' ? voice : undefined, + nonLocalVoices: $config.audio.tts.engine === '' ? nonLocalVoices : undefined + } + } + }); + dispatch('save'); + }} +> + <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]"> + <div> + <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div> + + {#if $config.audio.stt.engine !== 'web'} + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={STTEngine} + placeholder="Select an engine" + > + <option value="">{$i18n.t('Default')}</option> + <option value="web">{$i18n.t('Web API')}</option> + </select> + </div> + </div> + {/if} + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Instant Auto-Send After Voice Transcription')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleSpeechAutoSend(); + }} + type="button" + > + {#if speechAutoSend === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleResponseAutoPlayback(); + }} + type="button" + > + {#if responseAutoPlayback === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <hr class=" dark:border-gray-850" /> + + {#if $config.audio.tts.engine === ''} + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={voice} + > + <option value="" selected={voice !== ''}>{$i18n.t('Default')}</option> + {#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice} + <option + value={_voice.name} + class="bg-gray-100 dark:bg-gray-700" + selected={voice === _voice.name}>{_voice.name}</option + > + {/each} + </select> + </div> + </div> + <div class="flex items-center justify-between my-1.5"> + <div class="text-xs"> + {$i18n.t('Allow non-local voices')} + </div> + + <div class="mt-1"> + <Switch bind:state={nonLocalVoices} /> + </div> + </div> + </div> + {:else if $config.audio.tts.engine !== ''} + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div> + <div class="flex w-full"> + <div class="flex-1"> + <input + list="voice-list" + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={voice} + placeholder="Select a voice" + /> + + <datalist id="voice-list"> + {#each voices as voice} + <option value={voice.name} /> + {/each} + </datalist> + </div> + </div> + </div> + {/if} + </div> + + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/chat/Settings/Chats.svelte b/src/lib/components/chat/Settings/Chats.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0b9238e3dc45cd774818db225b6d799e8877acf --- /dev/null +++ b/src/lib/components/chat/Settings/Chats.svelte @@ -0,0 +1,396 @@ +<script lang="ts"> + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { chats, user, settings } from '$lib/stores'; + + import { + archiveAllChats, + createNewChat, + deleteAllChats, + getAllChats, + getAllUserChats, + getChatList + } from '$lib/apis/chats'; + import { getImportOrigin, convertOpenAIChats } from '$lib/utils'; + import { onMount, getContext } from 'svelte'; + import { goto } from '$app/navigation'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + export let saveSettings: Function; + // Chats + let saveChatHistory = true; + let importFiles; + + let showArchiveConfirm = false; + let showDeleteConfirm = false; + + let chatImportInputElement: HTMLInputElement; + + $: if (importFiles) { + console.log(importFiles); + + let reader = new FileReader(); + reader.onload = (event) => { + let chats = JSON.parse(event.target.result); + console.log(chats); + if (getImportOrigin(chats) == 'openai') { + try { + chats = convertOpenAIChats(chats); + } catch (error) { + console.log('Unable to import chats:', error); + } + } + importChats(chats); + }; + + if (importFiles.length > 0) { + reader.readAsText(importFiles[0]); + } + } + + const importChats = async (_chats) => { + for (const chat of _chats) { + console.log(chat); + + if (chat.chat) { + await createNewChat(localStorage.token, chat.chat); + } else { + await createNewChat(localStorage.token, chat); + } + } + + await chats.set(await getChatList(localStorage.token)); + }; + + const exportChats = async () => { + let blob = new Blob([JSON.stringify(await getAllChats(localStorage.token))], { + type: 'application/json' + }); + saveAs(blob, `chat-export-${Date.now()}.json`); + }; + + const archiveAllChatsHandler = async () => { + await goto('/'); + await archiveAllChats(localStorage.token).catch((error) => { + toast.error(error); + }); + await chats.set(await getChatList(localStorage.token)); + }; + + const deleteAllChatsHandler = async () => { + await goto('/'); + await deleteAllChats(localStorage.token).catch((error) => { + toast.error(error); + }); + await chats.set(await getChatList(localStorage.token)); + }; + + const toggleSaveChatHistory = async () => { + saveChatHistory = !saveChatHistory; + console.log(saveChatHistory); + + if (saveChatHistory === false) { + await goto('/'); + } + saveSettings({ saveChatHistory: saveChatHistory }); + }; + + onMount(async () => { + saveChatHistory = $settings.saveChatHistory ?? true; + }); +</script> + +<div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]"> + <div class=" space-y-2"> + <div + class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition" + > + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + toggleSaveChatHistory(); + }} + > + {#if saveChatHistory === true} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> + <path + fill-rule="evenodd" + d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + clip-rule="evenodd" + /> + </svg> + + <span class="ml-2 self-center"> {$i18n.t('On')} </span> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" + clip-rule="evenodd" + /> + <path + d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" + /> + </svg> + + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + + <div class="text-xs text-left w-full font-medium mt-0.5"> + {$i18n.t('This setting does not sync across browsers or devices.')} + </div> + </div> + + <hr class=" dark:border-gray-850" /> + + <div class="flex flex-col"> + <input + id="chat-import-input" + bind:this={chatImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + /> + <button + class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + chatImportInputElement.click(); + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div> + </button> + <button + class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + exportChats(); + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div> + </button> + </div> + + <hr class=" dark:border-gray-850" /> + + <div class="flex flex-col"> + {#if showArchiveConfirm} + <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> + <div class="flex items-center space-x-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> + <path + fill-rule="evenodd" + d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" + clip-rule="evenodd" + /> + </svg> + <span>{$i18n.t('Are you sure?')}</span> + </div> + + <div class="flex space-x-1.5 items-center"> + <button + class="hover:text-white transition" + on:click={() => { + archiveAllChatsHandler(); + showArchiveConfirm = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + </button> + <button + class="hover:text-white transition" + on:click={() => { + showArchiveConfirm = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + </div> + {:else} + <button + class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + showArchiveConfirm = true; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + d="M3.375 3C2.339 3 1.5 3.84 1.5 4.875v.75c0 1.036.84 1.875 1.875 1.875h17.25c1.035 0 1.875-.84 1.875-1.875v-.75C22.5 3.839 21.66 3 20.625 3H3.375Z" + /> + <path + fill-rule="evenodd" + d="m3.087 9 .54 9.176A3 3 0 0 0 6.62 21h10.757a3 3 0 0 0 2.995-2.824L20.913 9H3.087Zm6.163 3.75A.75.75 0 0 1 10 12h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Archive All Chats')}</div> + </button> + {/if} + + {#if showDeleteConfirm} + <div class="flex justify-between rounded-md items-center py-2 px-3.5 w-full transition"> + <div class="flex items-center space-x-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3Z" /> + <path + fill-rule="evenodd" + d="M13 6H3v6a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V6ZM5.72 7.47a.75.75 0 0 1 1.06 0L8 8.69l1.22-1.22a.75.75 0 1 1 1.06 1.06L9.06 9.75l1.22 1.22a.75.75 0 1 1-1.06 1.06L8 10.81l-1.22 1.22a.75.75 0 0 1-1.06-1.06l1.22-1.22-1.22-1.22a.75.75 0 0 1 0-1.06Z" + clip-rule="evenodd" + /> + </svg> + <span>{$i18n.t('Are you sure?')}</span> + </div> + + <div class="flex space-x-1.5 items-center"> + <button + class="hover:text-white transition" + on:click={() => { + deleteAllChatsHandler(); + showDeleteConfirm = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + </button> + <button + class="hover:text-white transition" + on:click={() => { + showDeleteConfirm = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + </div> + {:else} + <button + class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" + on:click={() => { + showDeleteConfirm = true; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm7 7a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1 0-1.5h4.5A.75.75 0 0 1 11 9Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center text-sm font-medium">{$i18n.t('Delete All Chats')}</div> + </button> + {/if} + </div> + </div> +</div> diff --git a/src/lib/components/chat/Settings/General.svelte b/src/lib/components/chat/Settings/General.svelte new file mode 100644 index 0000000000000000000000000000000000000000..427bc4a7c7b78c84d444e6b1947d9dcd609bba0d --- /dev/null +++ b/src/lib/components/chat/Settings/General.svelte @@ -0,0 +1,334 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { createEventDispatcher, onMount, getContext } from 'svelte'; + import { getLanguages } from '$lib/i18n'; + const dispatch = createEventDispatcher(); + + import { models, settings, theme, user } from '$lib/stores'; + + const i18n = getContext('i18n'); + + import AdvancedParams from './Advanced/AdvancedParams.svelte'; + + export let saveSettings: Function; + export let getModels: Function; + + // General + let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark']; + let selectedTheme = 'system'; + + let languages = []; + let lang = $i18n.language; + let notificationEnabled = false; + let system = ''; + + let showAdvanced = false; + + const toggleNotification = async () => { + const permission = await Notification.requestPermission(); + + if (permission === 'granted') { + notificationEnabled = !notificationEnabled; + saveSettings({ notificationEnabled: notificationEnabled }); + } else { + toast.error( + $i18n.t( + 'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.' + ) + ); + } + }; + + // Advanced + let requestFormat = ''; + let keepAlive = null; + + let params = { + // Advanced + seed: null, + temperature: null, + frequency_penalty: null, + repeat_last_n: null, + mirostat: null, + mirostat_eta: null, + mirostat_tau: null, + top_k: null, + top_p: null, + stop: null, + tfs_z: null, + num_ctx: null, + num_batch: null, + num_keep: null, + max_tokens: null + }; + + const toggleRequestFormat = async () => { + if (requestFormat === '') { + requestFormat = 'json'; + } else { + requestFormat = ''; + } + + saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined }); + }; + + onMount(async () => { + selectedTheme = localStorage.theme ?? 'system'; + + languages = await getLanguages(); + + notificationEnabled = $settings.notificationEnabled ?? false; + system = $settings.system ?? ''; + + requestFormat = $settings.requestFormat ?? ''; + keepAlive = $settings.keepAlive ?? null; + + params = { ...params, ...$settings.params }; + params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null; + }); + + const applyTheme = (_theme: string) => { + let themeToApply = _theme === 'oled-dark' ? 'dark' : _theme; + + if (_theme === 'system') { + themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + if (themeToApply === 'dark' && !_theme.includes('oled')) { + document.documentElement.style.setProperty('--color-gray-800', '#333'); + document.documentElement.style.setProperty('--color-gray-850', '#262626'); + document.documentElement.style.setProperty('--color-gray-900', '#171717'); + document.documentElement.style.setProperty('--color-gray-950', '#0d0d0d'); + } + + themes + .filter((e) => e !== themeToApply) + .forEach((e) => { + e.split(' ').forEach((e) => { + document.documentElement.classList.remove(e); + }); + }); + + themeToApply.split(' ').forEach((e) => { + document.documentElement.classList.add(e); + }); + + console.log(_theme); + }; + + const themeChangeHandler = (_theme: string) => { + theme.set(_theme); + localStorage.setItem('theme', _theme); + if (_theme.includes('oled')) { + document.documentElement.style.setProperty('--color-gray-800', '#101010'); + document.documentElement.style.setProperty('--color-gray-850', '#050505'); + document.documentElement.style.setProperty('--color-gray-900', '#000000'); + document.documentElement.style.setProperty('--color-gray-950', '#000000'); + document.documentElement.classList.add('dark'); + } + applyTheme(_theme); + }; +</script> + +<div class="flex flex-col h-full justify-between text-sm"> + <div class=" pr-1.5 overflow-y-scroll max-h-[25rem]"> + <div class=""> + <div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div> + + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div> + <div class="flex items-center relative"> + <select + class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" + bind:value={selectedTheme} + placeholder="Select a theme" + on:change={() => themeChangeHandler(selectedTheme)} + > + <option value="system">⚙️ {$i18n.t('System')}</option> + <option value="dark">🌑 {$i18n.t('Dark')}</option> + <option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option> + <option value="light">☀️ {$i18n.t('Light')}</option> + <option value="her">🌷 Her</option> + <!-- <option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option> + <option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option> --> + </select> + </div> + </div> + + <div class=" flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div> + <div class="flex items-center relative"> + <select + class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right" + bind:value={lang} + placeholder="Select a language" + on:change={(e) => { + $i18n.changeLanguage(lang); + }} + > + {#each languages as language} + <option value={language['code']}>{language['title']}</option> + {/each} + </select> + </div> + </div> + {#if $i18n.language === 'en-US'} + <div class="mb-2 text-xs text-gray-400 dark:text-gray-500"> + Couldn't find your language? + <a + class=" text-gray-300 font-medium underline" + href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization" + target="_blank" + > + Help us translate Open WebUI! + </a> + </div> + {/if} + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Notifications')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleNotification(); + }} + type="button" + > + {#if notificationEnabled === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + </div> + + <hr class=" dark:border-gray-850 my-3" /> + + <div> + <div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div> + <textarea + bind:value={system} + class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + rows="4" + /> + </div> + + <div class="mt-2 space-y-3 pr-1.5"> + <div class="flex justify-between items-center text-sm"> + <div class=" font-medium">{$i18n.t('Advanced Parameters')}</div> + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + showAdvanced = !showAdvanced; + }}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button + > + </div> + + {#if showAdvanced} + <AdvancedParams admin={$user?.role === 'admin'} bind:params /> + <hr class=" dark:border-gray-850" /> + + <div class=" py-1 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + keepAlive = keepAlive === null ? '5m' : null; + }} + > + {#if keepAlive === null} + <span class="ml-2 self-center"> {$i18n.t('Default')} </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if keepAlive !== null} + <div class="flex mt-1 space-x-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")} + bind:value={keepAlive} + /> + </div> + {/if} + </div> + + <div> + <div class=" py-1 flex w-full justify-between"> + <div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleRequestFormat(); + }} + > + {#if requestFormat === ''} + <span class="ml-2 self-center"> {$i18n.t('Default')} </span> + {:else if requestFormat === 'json'} + <!-- <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4 self-center" + > + <path + d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z" + /> + </svg> --> + <span class="ml-2 self-center"> {$i18n.t('JSON')} </span> + {/if} + </button> + </div> + </div> + {/if} + </div> + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + on:click={() => { + saveSettings({ + system: system !== '' ? system : undefined, + params: { + seed: (params.seed !== null ? params.seed : undefined) ?? undefined, + stop: params.stop ? params.stop.split(',').filter((e) => e) : undefined, + temperature: params.temperature !== null ? params.temperature : undefined, + frequency_penalty: + params.frequency_penalty !== null ? params.frequency_penalty : undefined, + repeat_last_n: params.repeat_last_n !== null ? params.repeat_last_n : undefined, + mirostat: params.mirostat !== null ? params.mirostat : undefined, + mirostat_eta: params.mirostat_eta !== null ? params.mirostat_eta : undefined, + mirostat_tau: params.mirostat_tau !== null ? params.mirostat_tau : undefined, + top_k: params.top_k !== null ? params.top_k : undefined, + top_p: params.top_p !== null ? params.top_p : undefined, + tfs_z: params.tfs_z !== null ? params.tfs_z : undefined, + num_ctx: params.num_ctx !== null ? params.num_ctx : undefined, + num_batch: params.num_batch !== null ? params.num_batch : undefined, + num_keep: params.num_keep !== null ? params.num_keep : undefined, + max_tokens: params.max_tokens !== null ? params.max_tokens : undefined, + use_mmap: params.use_mmap !== null ? params.use_mmap : undefined, + use_mlock: params.use_mlock !== null ? params.use_mlock : undefined, + num_thread: params.num_thread !== null ? params.num_thread : undefined + }, + keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined + }); + dispatch('save'); + }} + > + {$i18n.t('Save')} + </button> + </div> +</div> diff --git a/src/lib/components/chat/Settings/Interface.svelte b/src/lib/components/chat/Settings/Interface.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e6eefaefcd4e2891a8945cbac34e26670c9bc0a5 --- /dev/null +++ b/src/lib/components/chat/Settings/Interface.svelte @@ -0,0 +1,464 @@ +<script lang="ts"> + import { getBackendConfig } from '$lib/apis'; + import { setDefaultPromptSuggestions } from '$lib/apis/configs'; + import { config, models, settings, user } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import { updateUserInfo } from '$lib/apis/users'; + import { getUserPosition } from '$lib/utils'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let saveSettings: Function; + + let backgroundImageUrl = null; + let inputFiles = null; + let filesInputElement; + + // Addons + let titleAutoGenerate = true; + let responseAutoCopy = false; + let widescreenMode = false; + let splitLargeChunks = false; + let userLocation = false; + + // Interface + let defaultModelId = ''; + let showUsername = false; + + let chatBubble = true; + let chatDirection: 'LTR' | 'RTL' = 'LTR'; + + let showEmojiInCall = false; + let voiceInterruption = false; + + const toggleSplitLargeChunks = async () => { + splitLargeChunks = !splitLargeChunks; + saveSettings({ splitLargeChunks: splitLargeChunks }); + }; + + const togglewidescreenMode = async () => { + widescreenMode = !widescreenMode; + saveSettings({ widescreenMode: widescreenMode }); + }; + + const toggleChatBubble = async () => { + chatBubble = !chatBubble; + saveSettings({ chatBubble: chatBubble }); + }; + + const toggleShowUsername = async () => { + showUsername = !showUsername; + saveSettings({ showUsername: showUsername }); + }; + + const toggleEmojiInCall = async () => { + showEmojiInCall = !showEmojiInCall; + saveSettings({ showEmojiInCall: showEmojiInCall }); + }; + + const toggleVoiceInterruption = async () => { + voiceInterruption = !voiceInterruption; + saveSettings({ voiceInterruption: voiceInterruption }); + }; + + const toggleUserLocation = async () => { + userLocation = !userLocation; + + if (userLocation) { + const position = await getUserPosition().catch((error) => { + toast.error(error.message); + return null; + }); + + if (position) { + await updateUserInfo(localStorage.token, { location: position }); + toast.success($i18n.t('User location successfully retrieved.')); + } else { + userLocation = false; + } + } + + saveSettings({ userLocation }); + }; + + const toggleTitleAutoGenerate = async () => { + titleAutoGenerate = !titleAutoGenerate; + saveSettings({ + title: { + ...$settings.title, + auto: titleAutoGenerate + } + }); + }; + + const toggleResponseAutoCopy = async () => { + const permission = await navigator.clipboard + .readText() + .then(() => { + return 'granted'; + }) + .catch(() => { + return ''; + }); + + console.log(permission); + + if (permission === 'granted') { + responseAutoCopy = !responseAutoCopy; + saveSettings({ responseAutoCopy: responseAutoCopy }); + } else { + toast.error( + $i18n.t( + 'Clipboard write permission denied. Please check your browser settings to grant the necessary access.' + ) + ); + } + }; + + const toggleChangeChatDirection = async () => { + chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR'; + saveSettings({ chatDirection }); + }; + + const updateInterfaceHandler = async () => { + saveSettings({ + models: [defaultModelId] + }); + }; + + onMount(async () => { + titleAutoGenerate = $settings?.title?.auto ?? true; + + responseAutoCopy = $settings.responseAutoCopy ?? false; + showUsername = $settings.showUsername ?? false; + + showEmojiInCall = $settings.showEmojiInCall ?? false; + voiceInterruption = $settings.voiceInterruption ?? false; + + chatBubble = $settings.chatBubble ?? true; + widescreenMode = $settings.widescreenMode ?? false; + splitLargeChunks = $settings.splitLargeChunks ?? false; + chatDirection = $settings.chatDirection ?? 'LTR'; + userLocation = $settings.userLocation ?? false; + + defaultModelId = $settings?.models?.at(0) ?? ''; + if ($config?.default_models) { + defaultModelId = $config.default_models.split(',')[0]; + } + + backgroundImageUrl = $settings.backgroundImageUrl ?? null; + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + updateInterfaceHandler(); + dispatch('save'); + }} +> + <input + bind:this={filesInputElement} + bind:files={inputFiles} + type="file" + hidden + accept="image/*" + on:change={() => { + let reader = new FileReader(); + reader.onload = (event) => { + let originalImageUrl = `${event.target.result}`; + + backgroundImageUrl = originalImageUrl; + saveSettings({ backgroundImageUrl }); + }; + + if ( + inputFiles && + inputFiles.length > 0 && + ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) + ) { + reader.readAsDataURL(inputFiles[0]); + } else { + console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); + inputFiles = null; + } + }} + /> + + <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden"> + <div class=" space-y-1 mb-3"> + <div class="mb-2"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-sm font-medium">{$i18n.t('Default Model')}</div> + </div> + </div> + + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={defaultModelId} + placeholder="Select a model" + > + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {#each $models.filter((model) => model.id) as model} + <option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option> + {/each} + </select> + </div> + </div> + <hr class=" dark:border-gray-850" /> + + <div> + <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleChatBubble(); + }} + type="button" + > + {#if chatBubble === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + {#if !$settings.chatBubble} + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs"> + {$i18n.t('Display the username instead of You in the Chat')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleShowUsername(); + }} + type="button" + > + {#if showUsername === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + {/if} + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + togglewidescreenMode(); + }} + type="button" + > + {#if widescreenMode === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Chat direction')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={toggleChangeChatDirection} + type="button" + > + {#if chatDirection === 'LTR'} + <span class="ml-2 self-center">{$i18n.t('LTR')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('RTL')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs"> + {$i18n.t('Fluidly stream large external response chunks')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleSplitLargeChunks(); + }} + type="button" + > + {#if splitLargeChunks === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs"> + {$i18n.t('Chat Background Image')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + if (backgroundImageUrl !== null) { + backgroundImageUrl = null; + saveSettings({ backgroundImageUrl }); + } else { + filesInputElement.click(); + } + }} + type="button" + > + {#if backgroundImageUrl !== null} + <span class="ml-2 self-center">{$i18n.t('Reset')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Upload')}</span> + {/if} + </button> + </div> + </div> + + <div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleTitleAutoGenerate(); + }} + type="button" + > + {#if titleAutoGenerate === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs"> + {$i18n.t('Response AutoCopy to Clipboard')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleResponseAutoCopy(); + }} + type="button" + > + {#if responseAutoCopy === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleUserLocation(); + }} + type="button" + > + {#if userLocation === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleVoiceInterruption(); + }} + type="button" + > + {#if voiceInterruption === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + toggleEmojiInCall(); + }} + type="button" + > + {#if showEmojiInCall === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + </div> + </div> + + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/chat/Settings/Models.svelte b/src/lib/components/chat/Settings/Models.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ee825dfa75f109a466b573c0878b83e331730dcb --- /dev/null +++ b/src/lib/components/chat/Settings/Models.svelte @@ -0,0 +1,1075 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + + import { + createModel, + deleteModel, + downloadModel, + getOllamaUrls, + getOllamaVersion, + pullModel, + uploadModel, + getOllamaConfig + } from '$lib/apis/ollama'; + + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, models, MODEL_DOWNLOAD_POOL, user, config } from '$lib/stores'; + import { splitStream } from '$lib/utils'; + import { onMount, getContext } from 'svelte'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + + const i18n = getContext('i18n'); + + export let getModels: Function; + + let modelUploadInputElement: HTMLInputElement; + + // Models + + let ollamaEnabled = null; + + let OLLAMA_URLS = []; + let selectedOllamaUrlIdx: string | null = null; + + let updateModelId = null; + let updateProgress = null; + + let showExperimentalOllama = false; + + let ollamaVersion = null; + const MAX_PARALLEL_DOWNLOADS = 3; + + let modelTransferring = false; + let modelTag = ''; + + let createModelLoading = false; + let createModelTag = ''; + let createModelContent = ''; + let createModelDigest = ''; + let createModelPullProgress = null; + + let digest = ''; + let pullProgress = null; + + let modelUploadMode = 'file'; + let modelInputFile: File[] | null = null; + let modelFileUrl = ''; + let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`; + let modelFileDigest = ''; + + let uploadProgress = null; + let uploadMessage = ''; + + let deleteModelTag = ''; + + const updateModelsHandler = async () => { + for (const model of $models.filter( + (m) => + !(m?.preset ?? false) && + m.owned_by === 'ollama' && + (selectedOllamaUrlIdx === null + ? true + : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx)) + )) { + console.log(model); + + updateModelId = model.id; + const [res, controller] = await pullModel( + localStorage.token, + model.id, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + try { + const { value, done } = await reader.read(); + if (done) break; + + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line); + + console.log(data); + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + if (data.status) { + if (data.digest) { + updateProgress = 0; + if (data.completed) { + updateProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + updateProgress = 100; + } + } else { + toast.success(data.status); + } + } + } + } + } catch (error) { + console.log(error); + } + } + } + } + + updateModelId = null; + updateProgress = null; + }; + + const pullModelHandler = async () => { + const sanitizedModelTag = modelTag.trim().replace(/^ollama\s+(run|pull)\s+/, ''); + console.log($MODEL_DOWNLOAD_POOL); + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag]) { + toast.error( + $i18n.t(`Model '{{modelTag}}' is already in queue for downloading.`, { + modelTag: sanitizedModelTag + }) + ); + return; + } + if (Object.keys($MODEL_DOWNLOAD_POOL).length === MAX_PARALLEL_DOWNLOADS) { + toast.error( + $i18n.t('Maximum of 3 models can be downloaded simultaneously. Please try again later.') + ); + return; + } + + const [res, controller] = await pullModel(localStorage.token, sanitizedModelTag, '0').catch( + (error) => { + toast.error(error); + return null; + } + ); + + if (res) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + abortController: controller, + reader, + done: false + } + }); + + while (true) { + try { + const { value, done } = await reader.read(); + if (done) break; + + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line); + console.log(data); + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if (data.digest) { + let downloadProgress = 0; + if (data.completed) { + downloadProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + downloadProgress = 100; + } + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + pullProgress: downloadProgress, + digest: data.digest + } + }); + } else { + toast.success(data.status); + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL, + [sanitizedModelTag]: { + ...$MODEL_DOWNLOAD_POOL[sanitizedModelTag], + done: data.status === 'success' + } + }); + } + } + } + } + } catch (error) { + console.log(error); + if (typeof error !== 'string') { + error = error.message; + } + + toast.error(error); + // opts.callback({ success: false, error, modelName: opts.modelName }); + } + } + + console.log($MODEL_DOWNLOAD_POOL[sanitizedModelTag]); + + if ($MODEL_DOWNLOAD_POOL[sanitizedModelTag].done) { + toast.success( + $i18n.t(`Model '{{modelName}}' has been successfully downloaded.`, { + modelName: sanitizedModelTag + }) + ); + + models.set(await getModels(localStorage.token)); + } else { + toast.error($i18n.t('Download canceled')); + } + + delete $MODEL_DOWNLOAD_POOL[sanitizedModelTag]; + + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + } + + modelTag = ''; + modelTransferring = false; + }; + + const uploadModelHandler = async () => { + modelTransferring = true; + + let uploaded = false; + let fileResponse = null; + let name = ''; + + if (modelUploadMode === 'file') { + const file = modelInputFile ? modelInputFile[0] : null; + + if (file) { + uploadMessage = 'Uploading...'; + + fileResponse = await uploadModel(localStorage.token, file, selectedOllamaUrlIdx).catch( + (error) => { + toast.error(error); + return null; + } + ); + } + } else { + uploadProgress = 0; + fileResponse = await downloadModel( + localStorage.token, + modelFileUrl, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + } + + if (fileResponse && fileResponse.ok) { + const reader = fileResponse.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + let data = JSON.parse(line.replace(/^data: /, '')); + + if (data.progress) { + if (uploadMessage) { + uploadMessage = ''; + } + uploadProgress = data.progress; + } + + if (data.error) { + throw data.error; + } + + if (data.done) { + modelFileDigest = data.blob; + name = data.name; + uploaded = true; + } + } + } + } catch (error) { + console.log(error); + } + } + } else { + const error = await fileResponse?.json(); + toast.error(error?.detail ?? error); + } + + if (uploaded) { + const res = await createModel( + localStorage.token, + `${name}:latest`, + `FROM @${modelFileDigest}\n${modelFileContent}` + ); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + console.log(data); + + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if ( + !data.digest && + !data.status.includes('writing') && + !data.status.includes('sha256') + ) { + toast.success(data.status); + } else { + if (data.digest) { + digest = data.digest; + + if (data.completed) { + pullProgress = Math.round((data.completed / data.total) * 1000) / 10; + } else { + pullProgress = 100; + } + } + } + } + } + } + } catch (error) { + console.log(error); + toast.error(error); + } + } + } + } + + modelFileUrl = ''; + + if (modelUploadInputElement) { + modelUploadInputElement.value = ''; + } + modelInputFile = null; + modelTransferring = false; + uploadProgress = null; + + models.set(await getModels()); + }; + + const deleteModelHandler = async () => { + const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch( + (error) => { + toast.error(error); + } + ); + + if (res) { + toast.success($i18n.t(`Deleted {{deleteModelTag}}`, { deleteModelTag })); + } + + deleteModelTag = ''; + models.set(await getModels()); + }; + + const cancelModelPullHandler = async (model: string) => { + const { reader, abortController } = $MODEL_DOWNLOAD_POOL[model]; + if (abortController) { + abortController.abort(); + } + if (reader) { + await reader.cancel(); + delete $MODEL_DOWNLOAD_POOL[model]; + MODEL_DOWNLOAD_POOL.set({ + ...$MODEL_DOWNLOAD_POOL + }); + await deleteModel(localStorage.token, model); + toast.success(`${model} download has been canceled`); + } + }; + + const createModelHandler = async () => { + createModelLoading = true; + const res = await createModel( + localStorage.token, + createModelTag, + createModelContent, + selectedOllamaUrlIdx + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + let data = JSON.parse(line); + console.log(data); + + if (data.error) { + throw data.error; + } + if (data.detail) { + throw data.detail; + } + + if (data.status) { + if ( + !data.digest && + !data.status.includes('writing') && + !data.status.includes('sha256') + ) { + toast.success(data.status); + } else { + if (data.digest) { + createModelDigest = data.digest; + + if (data.completed) { + createModelPullProgress = + Math.round((data.completed / data.total) * 1000) / 10; + } else { + createModelPullProgress = 100; + } + } + } + } + } + } + } catch (error) { + console.log(error); + toast.error(error); + } + } + } + + models.set(await getModels()); + + createModelLoading = false; + + createModelTag = ''; + createModelContent = ''; + createModelDigest = ''; + createModelPullProgress = null; + }; + + onMount(async () => { + const ollamaConfig = await getOllamaConfig(localStorage.token); + + if (ollamaConfig.ENABLE_OLLAMA_API) { + ollamaEnabled = true; + + await Promise.all([ + (async () => { + OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => { + toast.error(error); + return []; + }); + + if (OLLAMA_URLS.length > 0) { + selectedOllamaUrlIdx = 0; + } + })(), + (async () => { + ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); + })() + ]); + } else { + ollamaEnabled = false; + toast.error($i18n.t('Ollama API is disabled')); + } + }); +</script> + +<div class="flex flex-col h-full justify-between text-sm"> + <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[27rem]"> + {#if ollamaEnabled} + {#if ollamaVersion !== null} + <div class="space-y-2 pr-1.5"> + <div class="text-sm font-medium">{$i18n.t('Manage Ollama Models')}</div> + + {#if OLLAMA_URLS.length > 0} + <div class="flex gap-2"> + <div class="flex-1 pb-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedOllamaUrlIdx} + placeholder={$i18n.t('Select an Ollama instance')} + > + {#each OLLAMA_URLS as url, idx} + <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option> + {/each} + </select> + </div> + + <div> + <div class="flex w-full justify-end"> + <Tooltip content="Update All Models" placement="top"> + <button + class="p-2.5 flex gap-2 items-center bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + updateModelsHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7 1a.75.75 0 0 1 .75.75V6h-1.5V1.75A.75.75 0 0 1 7 1ZM6.25 6v2.94L5.03 7.72a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l2.5-2.5a.75.75 0 1 0-1.06-1.06L7.75 8.94V6H10a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.25Z" + /> + <path + d="M4.268 14A2 2 0 0 0 6 15h6a2 2 0 0 0 2-2v-3a2 2 0 0 0-1-1.732V11a3 3 0 0 1-3 3H4.268Z" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + </div> + + {#if updateModelId} + Updating "{updateModelId}" {updateProgress ? `(${updateProgress}%)` : ''} + {/if} + {/if} + + <div class="space-y-2"> + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Pull a model from Ollama.com')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { + modelTag: 'mistral:7b' + })} + bind:value={modelTag} + /> + </div> + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + pullModelHandler(); + }} + disabled={modelTransferring} + > + {#if modelTransferring} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + </div> + + <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('To access the available model names for downloading,')} + <a + class=" text-gray-500 dark:text-gray-300 font-medium underline" + href="https://ollama.com/library" + target="_blank">{$i18n.t('click here.')}</a + > + </div> + + {#if Object.keys($MODEL_DOWNLOAD_POOL).length > 0} + {#each Object.keys($MODEL_DOWNLOAD_POOL) as model} + {#if 'pullProgress' in $MODEL_DOWNLOAD_POOL[model]} + <div class="flex flex-col"> + <div class="font-medium mb-1">{model}</div> + <div class=""> + <div class="flex flex-row justify-between space-x-4 pr-2"> + <div class=" flex-1"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max( + 15, + $MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0 + )}%" + > + {$MODEL_DOWNLOAD_POOL[model].pullProgress ?? 0}% + </div> + </div> + + <Tooltip content={$i18n.t('Cancel')}> + <button + class="text-gray-800 dark:text-gray-100" + on:click={() => { + cancelModelPullHandler(model); + }} + > + <svg + class="w-4 h-4 text-gray-800 dark:text-white" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + fill="currentColor" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="2" + d="M6 18 17.94 6M18 18 6.06 6" + /> + </svg> + </button> + </Tooltip> + </div> + {#if 'digest' in $MODEL_DOWNLOAD_POOL[model]} + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {$MODEL_DOWNLOAD_POOL[model].digest} + </div> + {/if} + </div> + </div> + {/if} + {/each} + {/if} + </div> + + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Delete a model')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2"> + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={deleteModelTag} + placeholder={$i18n.t('Select a model')} + > + {#if !deleteModelTag} + <option value="" disabled selected>{$i18n.t('Select a model')}</option> + {/if} + {#each $models.filter((m) => !(m?.preset ?? false) && m.owned_by === 'ollama' && (selectedOllamaUrlIdx === null ? true : (m?.ollama?.urls ?? []).includes(selectedOllamaUrlIdx))) as model} + <option value={model.name} class="bg-gray-100 dark:bg-gray-700" + >{model.name + + ' (' + + (model.ollama.size / 1024 ** 3).toFixed(1) + + ' GB)'}</option + > + {/each} + </select> + </div> + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition" + on:click={() => { + deleteModelHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + </div> + + <div> + <div class=" mb-2 text-sm font-medium">{$i18n.t('Create a model')}</div> + <div class="flex w-full"> + <div class="flex-1 mr-2 flex flex-col gap-2"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Enter model tag (e.g. {{modelTag}})', { + modelTag: 'my-modelfile' + })} + bind:value={createModelTag} + disabled={createModelLoading} + /> + + <textarea + bind:value={createModelContent} + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none scrollbar-hidden" + rows="6" + placeholder={`TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`} + disabled={createModelLoading} + /> + </div> + + <div class="flex self-start"> + <button + class="px-2.5 py-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition disabled:cursor-not-allowed" + on:click={() => { + createModelHandler(); + }} + disabled={createModelLoading} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + </button> + </div> + </div> + + {#if createModelDigest !== ''} + <div class="flex flex-col mt-1"> + <div class="font-medium mb-1">{createModelTag}</div> + <div class=""> + <div class="flex flex-row justify-between space-x-4 pr-2"> + <div class=" flex-1"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max(15, createModelPullProgress ?? 0)}%" + > + {createModelPullProgress ?? 0}% + </div> + </div> + </div> + {#if createModelDigest} + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {createModelDigest} + </div> + {/if} + </div> + </div> + {/if} + </div> + + <div class="pt-1"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-sm font-medium">{$i18n.t('Experimental')}</div> + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + showExperimentalOllama = !showExperimentalOllama; + }}>{showExperimentalOllama ? $i18n.t('Hide') : $i18n.t('Show')}</button + > + </div> + </div> + + {#if showExperimentalOllama} + <form + on:submit|preventDefault={() => { + uploadModelHandler(); + }} + > + <div class=" mb-2 flex w-full justify-between"> + <div class=" text-sm font-medium">{$i18n.t('Upload a GGUF model')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + if (modelUploadMode === 'file') { + modelUploadMode = 'url'; + } else { + modelUploadMode = 'file'; + } + }} + type="button" + > + {#if modelUploadMode === 'file'} + <span class="ml-2 self-center">{$i18n.t('File Mode')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('URL Mode')}</span> + {/if} + </button> + </div> + + <div class="flex w-full mb-1.5"> + <div class="flex flex-col w-full"> + {#if modelUploadMode === 'file'} + <div + class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}" + > + <input + id="model-upload-input" + bind:this={modelUploadInputElement} + type="file" + bind:files={modelInputFile} + on:change={() => { + console.log(modelInputFile); + }} + accept=".gguf,.safetensors" + required + hidden + /> + + <button + type="button" + class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850" + on:click={() => { + modelUploadInputElement.click(); + }} + > + {#if modelInputFile && modelInputFile.length > 0} + {modelInputFile[0].name} + {:else} + {$i18n.t('Click here to select')} + {/if} + </button> + </div> + {:else} + <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}"> + <input + class="w-full rounded-lg text-left py-2 px-4 bg-white dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !== + '' + ? 'mr-2' + : ''}" + type="url" + required + bind:value={modelFileUrl} + placeholder={$i18n.t('Type Hugging Face Resolve (Download) URL')} + /> + </div> + {/if} + </div> + + {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} + <button + class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg disabled:cursor-not-allowed transition" + type="submit" + disabled={modelTransferring} + > + {#if modelTransferring} + <div class="self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + > + <style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style> + <path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /> + <path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /> + </svg> + </div> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + {/if} + </button> + {/if} + </div> + + {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} + <div> + <div> + <div class=" my-2.5 text-sm font-medium">{$i18n.t('Modelfile Content')}</div> + <textarea + bind:value={modelFileContent} + class="w-full rounded-lg py-2 px-4 text-sm bg-gray-100 dark:text-gray-100 dark:bg-gray-850 outline-none resize-none" + rows="6" + /> + </div> + </div> + {/if} + <div class=" mt-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('To access the GGUF models available for downloading,')} + <a + class=" text-gray-500 dark:text-gray-300 font-medium underline" + href="https://huggingface.co/models?search=gguf" + target="_blank">{$i18n.t('click here.')}</a + > + </div> + + {#if uploadMessage} + <div class="mt-2"> + <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> + + <div class="w-full rounded-full dark:bg-gray-800"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: 100%" + > + {uploadMessage} + </div> + </div> + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {modelFileDigest} + </div> + </div> + {:else if uploadProgress !== null} + <div class="mt-2"> + <div class=" mb-2 text-xs">{$i18n.t('Upload Progress')}</div> + + <div class="w-full rounded-full dark:bg-gray-800"> + <div + class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" + style="width: {Math.max(15, uploadProgress ?? 0)}%" + > + {uploadProgress ?? 0}% + </div> + </div> + <div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> + {modelFileDigest} + </div> + </div> + {/if} + </form> + {/if} + </div> + </div> + {:else if ollamaVersion === false} + <div>Ollama Not Detected</div> + {:else} + <div class="flex h-full justify-center"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + {:else if ollamaEnabled === false} + <div>{$i18n.t('Ollama API is disabled')}</div> + {:else} + <div class="flex h-full justify-center"> + <div class="my-auto"> + <Spinner className="size-6" /> + </div> + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/chat/Settings/Personalization.svelte b/src/lib/components/chat/Settings/Personalization.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5d86c1fbda73ffa08f11716efb9b04f70a7b5da3 --- /dev/null +++ b/src/lib/components/chat/Settings/Personalization.svelte @@ -0,0 +1,98 @@ +<script lang="ts"> + import { getBackendConfig } from '$lib/apis'; + import { setDefaultPromptSuggestions } from '$lib/apis/configs'; + import Switch from '$lib/components/common/Switch.svelte'; + import { config, models, settings, user } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; + import { toast } from 'svelte-sonner'; + import ManageModal from './Personalization/ManageModal.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let saveSettings: Function; + + let showManageModal = false; + + // Addons + let enableMemory = false; + + onMount(async () => { + enableMemory = $settings?.memory ?? false; + }); +</script> + +<ManageModal bind:show={showManageModal} /> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + dispatch('save'); + }} +> + <div class=" pr-1.5 py-1 overflow-y-scroll max-h-[25rem]"> + <div> + <div class="flex items-center justify-between mb-1"> + <Tooltip + content={$i18n.t( + 'This is an experimental feature, it may not function as expected and is subject to change at any time.' + )} + > + <div class="text-sm font-medium"> + {$i18n.t('Memory')} + + <span class=" text-xs text-gray-500">({$i18n.t('Experimental')})</span> + </div> + </Tooltip> + + <div class=""> + <Switch + bind:state={enableMemory} + on:change={async () => { + saveSettings({ memory: enableMemory }); + }} + /> + </div> + </div> + </div> + + <div class="text-xs text-gray-600 dark:text-gray-400"> + <div> + {$i18n.t( + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you." + )} + </div> + + <!-- <div class="mt-3"> + To understand what LLM remembers or teach it something new, just chat with it: + + <div>- “Remember that I like concise responses.”</div> + <div>- “I just got a puppy!”</div> + <div>- “What do you remember about me?”</div> + <div>- “Where did we leave off on my last project?”</div> + </div> --> + </div> + + <div class="mt-3 mb-1 ml-1"> + <button + type="button" + class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl" + on:click={() => { + showManageModal = true; + }} + > + {$i18n.t('Manage')} + </button> + </div> + </div> + + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte b/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6e16576b0c3b883304bcad5c7c663e0154446ca2 --- /dev/null +++ b/src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte @@ -0,0 +1,126 @@ +<script> + import { createEventDispatcher, getContext } from 'svelte'; + + import Modal from '$lib/components/common/Modal.svelte'; + import { addNewMemory, updateMemoryById } from '$lib/apis/memories'; + import { toast } from 'svelte-sonner'; + + const dispatch = createEventDispatcher(); + + export let show; + const i18n = getContext('i18n'); + + let loading = false; + let content = ''; + + const submitHandler = async () => { + loading = true; + + const res = await addNewMemory(localStorage.token, content).catch((error) => { + toast.error(error); + + return null; + }); + + if (res) { + console.log(res); + toast.success($i18n.t('Memory added successfully')); + content = ''; + show = false; + dispatch('save'); + } + + loading = false; + }; +</script> + +<Modal bind:show size="sm"> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center"> + {$i18n.t('Add Memory')} + </div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class=""> + <textarea + bind:value={content} + class=" bg-transparent w-full text-sm resize-none rounded-xl p-3 outline outline-1 outline-gray-100 dark:outline-gray-800" + rows="3" + placeholder={$i18n.t('Enter a detail about yourself for your LLMs to recall')} + /> + + <div class="text-xs text-gray-500"> + ⓘ {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')} + </div> + </div> + + <div class="flex justify-end pt-1 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading + ? ' cursor-not-allowed' + : ''}" + type="submit" + disabled={loading} + > + {$i18n.t('Add')} + + {#if loading} + <div class="ml-2 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/chat/Settings/Personalization/EditMemoryModal.svelte b/src/lib/components/chat/Settings/Personalization/EditMemoryModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..773309ff9e12db5d4d53c1714a5819ea6b52786b --- /dev/null +++ b/src/lib/components/chat/Settings/Personalization/EditMemoryModal.svelte @@ -0,0 +1,136 @@ +<script> + import { createEventDispatcher, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + import { updateMemoryById } from '$lib/apis/memories'; + + import Modal from '$lib/components/common/Modal.svelte'; + + const dispatch = createEventDispatcher(); + + export let show; + export let memory = {}; + + const i18n = getContext('i18n'); + + let loading = false; + let content = ''; + + $: if (show) { + setContent(); + } + + const setContent = () => { + content = memory.content; + }; + + const submitHandler = async () => { + loading = true; + + const res = await updateMemoryById(localStorage.token, memory.id, content).catch((error) => { + toast.error(error); + + return null; + }); + + if (res) { + console.log(res); + toast.success($i18n.t('Memory updated successfully')); + dispatch('save'); + show = false; + } + + loading = false; + }; +</script> + +<Modal bind:show size="sm"> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center"> + {$i18n.t('Edit Memory')} + </div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class=""> + <textarea + bind:value={content} + class=" bg-transparent w-full text-sm resize-none rounded-xl p-3 outline outline-1 outline-gray-100 dark:outline-gray-800" + rows="3" + placeholder={$i18n.t('Enter a detail about yourself for your LLMs to recall')} + /> + + <div class="text-xs text-gray-500"> + ⓘ {$i18n.t('Refer to yourself as "User" (e.g., "User is learning Spanish")')} + </div> + </div> + + <div class="flex justify-end pt-1 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading + ? ' cursor-not-allowed' + : ''}" + type="submit" + disabled={loading} + > + {$i18n.t('Update')} + + {#if loading} + <div class="ml-2 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/chat/Settings/Personalization/ManageModal.svelte b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..92b4936e91b4ca8eb75e5f7ed990ebc1d2cdc0fe --- /dev/null +++ b/src/lib/components/chat/Settings/Personalization/ManageModal.svelte @@ -0,0 +1,208 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { getContext, createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import Modal from '$lib/components/common/Modal.svelte'; + import AddMemoryModal from './AddMemoryModal.svelte'; + import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import { error } from '@sveltejs/kit'; + import EditMemoryModal from './EditMemoryModal.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + + let memories = []; + let loading = true; + + let showAddMemoryModal = false; + let showEditMemoryModal = false; + + let selectedMemory = null; + + $: if (show && memories.length === 0 && loading) { + (async () => { + memories = await getMemories(localStorage.token); + loading = false; + })(); + } +</script> + +<Modal size="xl" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1"> + <div class=" text-lg font-medium self-center">{$i18n.t('Memory')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col w-full px-5 pb-5 dark:text-gray-200"> + <div + class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6 h-[28rem] max-h-screen outline outline-1 rounded-xl outline-gray-100 dark:outline-gray-800 mb-4 mt-1" + > + {#if memories.length > 0} + <div class="text-left text-sm w-full mb-4 overflow-y-scroll"> + <div class="relative overflow-x-auto"> + <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> + <thead + class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800" + > + <tr> + <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> + <th scope="col" class="px-3 py-2 hidden md:flex"> + {$i18n.t('Last Modified')} + </th> + <th scope="col" class="px-3 py-2 text-right" /> + </tr> + </thead> + <tbody> + {#each memories as memory} + <tr class="border-b dark:border-gray-800 items-center"> + <td class="px-3 py-1"> + <div class="line-clamp-1"> + {memory.content} + </div> + </td> + <td class=" px-3 py-1 hidden md:flex h-[2.5rem]"> + <div class="my-auto whitespace-nowrap"> + {dayjs(memory.updated_at * 1000).format( + $i18n.t('MMMM DD, YYYY hh:mm:ss A') + )} + </div> + </td> + <td class="px-3 py-1"> + <div class="flex justify-end w-full"> + <Tooltip content="Edit"> + <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={() => { + selectedMemory = memory; + showEditMemoryModal = true; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4 s-FoVA_WMOgxUD" + ><path + stroke-linecap="round" + stroke-linejoin="round" + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + class="s-FoVA_WMOgxUD" + /></svg + > + </button> + </Tooltip> + + <Tooltip content="Delete"> + <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 () => { + const res = await deleteMemoryById( + localStorage.token, + memory.id + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Memory deleted successfully')); + memories = await getMemories(localStorage.token); + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </Tooltip> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + </div> + </div> + {:else} + <div class="text-center flex h-full text-sm w-full"> + <div class=" my-auto pb-10 px-4 w-full text-gray-500"> + {$i18n.t('Memories accessible by LLMs will be shown here.')} + </div> + </div> + {/if} + </div> + <div class="flex text-sm font-medium gap-1.5"> + <button + class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl" + on:click={() => { + showAddMemoryModal = true; + }}>{$i18n.t('Add Memory')}</button + > + <button + class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl" + on:click={async () => { + const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Memory cleared successfully')); + memories = []; + } + }}>{$i18n.t('Clear memory')}</button + > + </div> + </div> + </div> +</Modal> + +<AddMemoryModal + bind:show={showAddMemoryModal} + on:save={async () => { + memories = await getMemories(localStorage.token); + }} +/> + +<EditMemoryModal + bind:show={showEditMemoryModal} + memory={selectedMemory} + on:save={async () => { + memories = await getMemories(localStorage.token); + }} +/> diff --git a/src/lib/components/chat/Settings/Valves.svelte b/src/lib/components/chat/Settings/Valves.svelte new file mode 100644 index 0000000000000000000000000000000000000000..579779162cb135295c01e45ac3235e7ce9d1c4f5 --- /dev/null +++ b/src/lib/components/chat/Settings/Valves.svelte @@ -0,0 +1,190 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + + import { config, functions, models, settings, tools, user } from '$lib/stores'; + import { createEventDispatcher, onMount, getContext, tick } from 'svelte'; + + import { + getUserValvesSpecById as getToolUserValvesSpecById, + getUserValvesById as getToolUserValvesById, + updateUserValvesById as updateToolUserValvesById + } from '$lib/apis/tools'; + import { + getUserValvesSpecById as getFunctionUserValvesSpecById, + getUserValvesById as getFunctionUserValvesById, + updateUserValvesById as updateFunctionUserValvesById + } from '$lib/apis/functions'; + + import ManageModal from './Personalization/ManageModal.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + import Valves from '$lib/components/common/Valves.svelte'; + + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let saveSettings: Function; + + let tab = 'tools'; + let selectedId = ''; + + let loading = false; + + let valvesSpec = null; + let valves = {}; + + const getUserValves = async () => { + loading = true; + if (tab === 'tools') { + valves = await getToolUserValvesById(localStorage.token, selectedId); + valvesSpec = await getToolUserValvesSpecById(localStorage.token, selectedId); + } else if (tab === 'functions') { + valves = await getFunctionUserValvesById(localStorage.token, selectedId); + valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, selectedId); + } + + if (valvesSpec) { + // Convert array to string + for (const property in valvesSpec.properties) { + if (valvesSpec.properties[property]?.type === 'array') { + valves[property] = (valves[property] ?? []).join(','); + } + } + } + + loading = false; + }; + + const submitHandler = async () => { + if (valvesSpec) { + // Convert string to array + for (const property in valvesSpec.properties) { + if (valvesSpec.properties[property]?.type === 'array') { + valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim()); + } + } + + if (tab === 'tools') { + const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch( + (error) => { + toast.error(error); + return null; + } + ); + + if (res) { + toast.success($i18n.t('Valves updated')); + valves = res; + } + } else if (tab === 'functions') { + const res = await updateFunctionUserValvesById( + localStorage.token, + selectedId, + valves + ).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Valves updated')); + valves = res; + } + } + } + }; + + $: if (tab) { + selectedId = ''; + } + + $: if (selectedId) { + getUserValves(); + } +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + submitHandler(); + dispatch('save'); + }} +> + <div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]"> + <div> + <div class="flex items-center justify-between mb-2"> + <Tooltip content=""> + <div class="text-sm font-medium"> + {$i18n.t('Manage Valves')} + </div> + </Tooltip> + + <div class=" self-end"> + <select + class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right" + bind:value={tab} + placeholder="Select" + > + <option value="tools">{$i18n.t('Tools')}</option> + <option value="functions">{$i18n.t('Functions')}</option> + </select> + </div> + </div> + </div> + + <div class="space-y-1"> + <div class="flex gap-2"> + <div class="flex-1"> + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + bind:value={selectedId} + on:change={async () => { + await tick(); + }} + > + {#if tab === 'tools'} + <option value="" selected disabled class="bg-gray-100 dark:bg-gray-700" + >{$i18n.t('Select a tool')}</option + > + + {#each $tools as tool, toolIdx} + <option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option> + {/each} + {:else if tab === 'functions'} + <option value="" selected disabled class="bg-gray-100 dark:bg-gray-700" + >{$i18n.t('Select a function')}</option + > + + {#each $functions as func, funcIdx} + <option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option> + {/each} + {/if} + </select> + </div> + </div> + </div> + + {#if selectedId} + <hr class="dark:border-gray-800 my-3 w-full" /> + + <div> + {#if !loading} + <Valves {valvesSpec} bind:valves /> + {:else} + <Spinner className="size-5" /> + {/if} + </div> + {/if} + </div> + + <div class="flex justify-end text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..66f0efad5d9eee637ef2424523438d2f81e7c93c --- /dev/null +++ b/src/lib/components/chat/SettingsModal.svelte @@ -0,0 +1,395 @@ +<script lang="ts"> + import { getContext, tick } from 'svelte'; + import { toast } from 'svelte-sonner'; + import { models, settings, user } from '$lib/stores'; + import { updateUserSettings } from '$lib/apis/users'; + import { getModels as _getModels } from '$lib/apis'; + import { goto } from '$app/navigation'; + + import Modal from '../common/Modal.svelte'; + import Account from './Settings/Account.svelte'; + import About from './Settings/About.svelte'; + import General from './Settings/General.svelte'; + import Interface from './Settings/Interface.svelte'; + import Audio from './Settings/Audio.svelte'; + import Chats from './Settings/Chats.svelte'; + import User from '../icons/User.svelte'; + import Personalization from './Settings/Personalization.svelte'; + import Valves from './Settings/Valves.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + + const saveSettings = async (updated) => { + console.log(updated); + await settings.set({ ...$settings, ...updated }); + await models.set(await getModels()); + await updateUserSettings(localStorage.token, { ui: $settings }); + }; + + const getModels = async () => { + return await _getModels(localStorage.token); + }; + + let selectedTab = 'general'; + + // Function to handle sideways scrolling + const scrollHandler = (event) => { + const settingsTabsContainer = document.getElementById('settings-tabs-container'); + if (settingsTabsContainer) { + event.preventDefault(); // Prevent default vertical scrolling + settingsTabsContainer.scrollLeft += event.deltaY; // Scroll sideways + } + }; + + const addScrollListener = async () => { + await tick(); + const settingsTabsContainer = document.getElementById('settings-tabs-container'); + if (settingsTabsContainer) { + settingsTabsContainer.addEventListener('wheel', scrollHandler); + } + }; + + const removeScrollListener = async () => { + await tick(); + const settingsTabsContainer = document.getElementById('settings-tabs-container'); + if (settingsTabsContainer) { + settingsTabsContainer.removeEventListener('wheel', scrollHandler); + } + }; + + $: if (show) { + addScrollListener(); + } else { + removeScrollListener(); + } +</script> + +<Modal bind:show> + <div class="text-gray-700 dark:text-gray-100"> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1"> + <div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4"> + <div + id="settings-tabs-container" + class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0" + > + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'general' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'general'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M8.34 1.804A1 1 0 019.32 1h1.36a1 1 0 01.98.804l.295 1.473c.497.144.971.342 1.416.587l1.25-.834a1 1 0 011.262.125l.962.962a1 1 0 01.125 1.262l-.834 1.25c.245.445.443.919.587 1.416l1.473.294a1 1 0 01.804.98v1.361a1 1 0 01-.804.98l-1.473.295a6.95 6.95 0 01-.587 1.416l.834 1.25a1 1 0 01-.125 1.262l-.962.962a1 1 0 01-1.262.125l-1.25-.834a6.953 6.953 0 01-1.416.587l-.294 1.473a1 1 0 01-.98.804H9.32a1 1 0 01-.98-.804l-.295-1.473a6.957 6.957 0 01-1.416-.587l-1.25.834a1 1 0 01-1.262-.125l-.962-.962a1 1 0 01-.125-1.262l.834-1.25a6.957 6.957 0 01-.587-1.416l-1.473-.294A1 1 0 011 10.68V9.32a1 1 0 01.804-.98l1.473-.295c.144-.497.342-.971.587-1.416l-.834-1.25a1 1 0 01.125-1.262l.962-.962A1 1 0 015.38 3.03l1.25.834a6.957 6.957 0 011.416-.587l.294-1.473zM13 10a3 3 0 11-6 0 3 3 0 016 0z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('General')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'interface' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'interface'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Interface')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'personalization' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'personalization'; + }} + > + <div class=" self-center mr-2"> + <User /> + </div> + <div class=" self-center">{$i18n.t('Personalization')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'audio' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'audio'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M7.557 2.066A.75.75 0 0 1 8 2.75v10.5a.75.75 0 0 1-1.248.56L3.59 11H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.59l3.162-2.81a.75.75 0 0 1 .805-.124ZM12.95 3.05a.75.75 0 1 0-1.06 1.06 5.5 5.5 0 0 1 0 7.78.75.75 0 1 0 1.06 1.06 7 7 0 0 0 0-9.9Z" + /> + <path + d="M10.828 5.172a.75.75 0 1 0-1.06 1.06 2.5 2.5 0 0 1 0 3.536.75.75 0 1 0 1.06 1.06 4 4 0 0 0 0-5.656Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Audio')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'valves' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'valves'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Valves')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'chats' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'chats'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M8 2C4.262 2 1 4.57 1 8c0 1.86.98 3.486 2.455 4.566a3.472 3.472 0 0 1-.469 1.26.75.75 0 0 0 .713 1.14 6.961 6.961 0 0 0 3.06-1.06c.403.062.818.094 1.241.094 3.738 0 7-2.57 7-6s-3.262-6-7-6ZM5 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Zm7-1a1 1 0 1 1-2 0 1 1 0 0 1 2 0ZM8 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Chats')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'account' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'account'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M15 8A7 7 0 1 1 1 8a7 7 0 0 1 14 0Zm-5-2a2 2 0 1 1-4 0 2 2 0 0 1 4 0ZM8 9c-1.825 0-3.422.977-4.295 2.437A5.49 5.49 0 0 0 8 13.5a5.49 5.49 0 0 0 4.294-2.063A4.997 4.997 0 0 0 8 9Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Account')}</div> + </button> + + {#if $user.role === 'admin'} + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'admin' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={async () => { + await goto('/admin/settings'); + show = false; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Admin Settings')}</div> + </button> + {/if} + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'about' + ? 'bg-gray-200 dark:bg-gray-800' + : ' hover:bg-gray-100 dark:hover:bg-gray-850'}" + on:click={() => { + selectedTab = 'about'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('About')}</div> + </button> + </div> + <div class="flex-1 md:min-h-[28rem]"> + {#if selectedTab === 'general'} + <General + {getModels} + {saveSettings} + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'interface'} + <Interface + {saveSettings} + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'personalization'} + <Personalization + {saveSettings} + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'audio'} + <Audio + {saveSettings} + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'valves'} + <Valves + {saveSettings} + on:save={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'chats'} + <Chats {saveSettings} /> + {:else if selectedTab === 'account'} + <Account + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'about'} + <About /> + {/if} + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/chat/ShareChatModal.svelte b/src/lib/components/chat/ShareChatModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e6ccc1323f2c7d6ccb38f58a286e25ff7399ac3c --- /dev/null +++ b/src/lib/components/chat/ShareChatModal.svelte @@ -0,0 +1,202 @@ +<script lang="ts"> + import { getContext, onMount } from 'svelte'; + import { models, config } from '$lib/stores'; + + import { toast } from 'svelte-sonner'; + import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; + import { copyToClipboard } from '$lib/utils'; + + import Modal from '../common/Modal.svelte'; + import Link from '../icons/Link.svelte'; + + export let chatId; + + let chat = null; + let shareUrl = null; + const i18n = getContext('i18n'); + + const shareLocalChat = async () => { + const _chat = chat; + + const sharedChat = await shareChatById(localStorage.token, chatId); + shareUrl = `${window.location.origin}/s/${sharedChat.id}`; + console.log(shareUrl); + chat = await getChatById(localStorage.token, chatId); + + return shareUrl; + }; + + const shareChat = async () => { + const _chat = chat.chat; + console.log('share', _chat); + + toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + const url = 'https://openwebui.com'; + // const url = 'http://localhost:5173'; + + const tab = await window.open(`${url}/chats/upload`, '_blank'); + window.addEventListener( + 'message', + (event) => { + if (event.origin !== url) return; + if (event.data === 'loaded') { + tab.postMessage( + JSON.stringify({ + chat: _chat, + models: $models.filter((m) => _chat.models.includes(m.id)) + }), + '*' + ); + } + }, + false + ); + }; + + export let show = false; + + const isDifferentChat = (_chat) => { + if (!chat) { + return true; + } + if (!_chat) { + return false; + } + return chat.id !== _chat.id || chat.share_id !== _chat.share_id; + }; + + $: if (show) { + (async () => { + if (chatId) { + const _chat = await getChatById(localStorage.token, chatId); + if (isDifferentChat(_chat)) { + chat = _chat; + } + } else { + chat = null; + console.log(chat); + } + })(); + } +</script> + +<Modal bind:show size="sm"> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-0.5"> + <div class=" text-lg font-medium self-center">{$i18n.t('Share Chat')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + {#if chat} + <div class="px-5 pt-4 pb-5 w-full flex flex-col justify-center"> + <div class=" text-sm dark:text-gray-300 mb-1"> + {#if chat.share_id} + <a href="/s/{chat.share_id}" target="_blank" + >{$i18n.t('You have shared this chat')} + <span class=" underline">{$i18n.t('before')}</span>.</a + > + {$i18n.t('Click here to')} + <button + class="underline" + on:click={async () => { + const res = await deleteSharedChatById(localStorage.token, chatId); + + if (res) { + chat = await getChatById(localStorage.token, chatId); + } + }} + >{$i18n.t('delete this link')} + </button> + {$i18n.t('and create a new shared link.')} + {:else} + {$i18n.t( + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat." + )} + {/if} + </div> + + <div class="flex justify-end"> + <div class="flex flex-col items-end space-x-1 mt-1.5"> + <div class="flex gap-1"> + {#if $config?.features.enable_community_sharing} + <button + class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white" + type="button" + on:click={() => { + shareChat(); + show = false; + }} + > + {$i18n.t('Share to OpenWebUI Community')} + </button> + {/if} + + <button + class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white" + type="button" + id="copy-and-share-chat-button" + on:click={async () => { + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + + if (isSafari) { + // Oh, Safari, you're so special, let's give you some extra love and attention + console.log('isSafari'); + + const getUrlPromise = async () => { + const url = await shareLocalChat(); + return new Blob([url], { type: 'text/plain' }); + }; + + navigator.clipboard + .write([ + new ClipboardItem({ + 'text/plain': getUrlPromise() + }) + ]) + .then(() => { + console.log('Async: Copying to clipboard was successful!'); + return true; + }) + .catch((error) => { + console.error('Async: Could not copy text: ', error); + return false; + }); + } else { + copyToClipboard(await shareLocalChat()); + } + + toast.success($i18n.t('Copied shared chat URL to clipboard!')); + show = false; + }} + > + <Link /> + + {#if chat.share_id} + {$i18n.t('Update and Copy Link')} + {:else} + {$i18n.t('Copy Link')} + {/if} + </button> + </div> + </div> + </div> + </div> + {/if} + </div> +</Modal> diff --git a/src/lib/components/chat/ShortcutsModal.svelte b/src/lib/components/chat/ShortcutsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9729add4ab61737a2ae6aeab01dc7cd5ed8a22ae --- /dev/null +++ b/src/lib/components/chat/ShortcutsModal.svelte @@ -0,0 +1,287 @@ +<script lang="ts"> + import { getContext } from 'svelte'; + import Modal from '../common/Modal.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; +</script> + +<Modal bind:show> + <div class="text-gray-700 dark:text-gray-100"> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4"> + <div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <div class="flex flex-col space-y-3 w-full self-start"> + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Open new chat')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + O + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Focus chat input')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Esc + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Copy last code block')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + ; + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Copy last response')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + C + </div> + </div> + </div> + </div> + + <div class="flex flex-col space-y-3 w-full self-start"> + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Toggle settings')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + . + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Toggle sidebar')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + S + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Delete chat')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Shift + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + ⌫ + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm">{$i18n.t('Show shortcuts')}</div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + Ctrl/⌘ + </div> + + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + / + </div> + </div> + </div> + </div> + </div> + </div> + + <div class=" flex justify-between dark:text-gray-300 px-5"> + <div class=" text-lg font-medium self-center">{$i18n.t('Input commands')}</div> + </div> + + <div class="flex flex-col md:flex-row w-full p-5 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <div class="flex flex-col space-y-3 w-full self-start"> + <div class="w-full flex justify-between items-center"> + <div class=" text-sm"> + {$i18n.t('Attach file')} + </div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + # + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm"> + {$i18n.t('Add custom prompt')} + </div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + / + </div> + </div> + </div> + + <div class="w-full flex justify-between items-center"> + <div class=" text-sm"> + {$i18n.t('Select model')} + </div> + + <div class="flex space-x-1 text-xs"> + <div + class=" h-fit py-1 px-2 flex items-center justify-center rounded border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300" + > + @ + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/chat/TagChatModal.svelte b/src/lib/components/chat/TagChatModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..53adac40e95c04d54ec29a762f64ec586329d41e --- /dev/null +++ b/src/lib/components/chat/TagChatModal.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + import { getContext } from 'svelte'; + import Modal from '../common/Modal.svelte'; + + import Tags from '../common/Tags.svelte'; + + const i18n = getContext('i18n'); + + export let tags; + export let deleteTag: Function; + export let addTag: Function; + + export let show = false; +</script> + +<Modal bind:show size="xs"> + <div class="px-4 pt-4 pb-5 w-full flex flex-col justify-center"> + <Tags {tags} {deleteTag} {addTag} /> + </div> +</Modal> diff --git a/src/lib/components/chat/Tags.svelte b/src/lib/components/chat/Tags.svelte new file mode 100644 index 0000000000000000000000000000000000000000..cb4e546411502939c25b148e856f9d02fc2de53d --- /dev/null +++ b/src/lib/components/chat/Tags.svelte @@ -0,0 +1,77 @@ +<script> + import { + addTagById, + deleteTagById, + getAllChatTags, + getChatList, + getChatListByTagName, + getTagsById, + updateChatById + } from '$lib/apis/chats'; + import { tags as _tags, chats, pinnedChats } from '$lib/stores'; + import { createEventDispatcher, onMount } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import Tags from '../common/Tags.svelte'; + + export let chatId = ''; + let tags = []; + + const getTags = async () => { + return ( + await getTagsById(localStorage.token, chatId).catch(async (error) => { + return []; + }) + ).filter((tag) => tag.name !== 'pinned'); + }; + + const addTag = async (tagName) => { + const res = await addTagById(localStorage.token, chatId, tagName); + tags = await getTags(); + + await updateChatById(localStorage.token, chatId, { + tags: tags + }); + + _tags.set(await getAllChatTags(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + }; + + const deleteTag = async (tagName) => { + const res = await deleteTagById(localStorage.token, chatId, tagName); + tags = await getTags(); + + await updateChatById(localStorage.token, chatId, { + tags: tags + }); + + console.log($_tags); + await _tags.set(await getAllChatTags(localStorage.token)); + + console.log($_tags); + + if ($_tags.map((t) => t.name).includes(tagName)) { + if (tagName === 'pinned') { + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + } else { + await chats.set(await getChatListByTagName(localStorage.token, tagName)); + } + + if ($chats.find((chat) => chat.id === chatId)) { + dispatch('close'); + } + } else { + await chats.set(await getChatList(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + } + }; + + onMount(async () => { + if (chatId) { + tags = await getTags(); + } + }); +</script> + +<Tags {tags} {deleteTag} {addTag} /> diff --git a/src/lib/components/common/Banner.svelte b/src/lib/components/common/Banner.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a11df90579e1bf6581f4661912563d9e9927f313 --- /dev/null +++ b/src/lib/components/common/Banner.svelte @@ -0,0 +1,126 @@ +<script lang="ts"> + import type { Banner } from '$lib/types'; + import { onMount, createEventDispatcher } from 'svelte'; + import { fade } from 'svelte/transition'; + + const dispatch = createEventDispatcher(); + + export let banner: Banner = { + id: '', + type: 'info', + title: '', + content: '', + url: '', + dismissable: true, + timestamp: Math.floor(Date.now() / 1000) + }; + + export let dismissed = false; + + let mounted = false; + + const classNames: Record<string, string> = { + info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ', + success: 'bg-green-500/20 text-green-700 dark:text-green-200', + warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200', + error: 'bg-red-500/20 text-red-700 dark:text-red-200' + }; + + const dismiss = (id) => { + dismissed = true; + dispatch('dismiss', id); + }; + + onMount(() => { + mounted = true; + }); +</script> + +{#if !dismissed} + {#if mounted} + <div + class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-50 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-30" + transition:fade={{ delay: 100, duration: 300 }} + > + <div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5"> + <div class="flex justify-between self-start"> + <div + class=" text-xs font-bold {classNames[banner.type] ?? + classNames['info']} w-fit px-2 rounded uppercase line-clamp-1 mr-0.5" + > + {banner.type} + </div> + + {#if banner.url} + <div class="flex md:hidden group w-fit md:items-center"> + <a + class="text-gray-700 dark:text-white text-xs font-semibold underline" + href="/assets/files/whitepaper.pdf" + target="_blank">Learn More</a + > + + <div + class=" ml-1 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white" + > + <!-- --> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + {/if} + </div> + + <div class="flex-1 text-xs text-gray-700 dark:text-white"> + {banner.content} + </div> + </div> + + {#if banner.url} + <div class="hidden md:flex group w-fit md:items-center"> + <a + class="text-gray-700 dark:text-white text-xs font-semibold underline" + href="/" + target="_blank">Learn More</a + > + + <div class=" ml-1 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white"> + <!-- --> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + {/if} + <div class="flex self-start"> + {#if banner.dismissible} + <button + on:click={() => { + dismiss(banner.id); + }} + class=" -mt-1 -mb-2 -translate-y-[1px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white" + >×</button + > + {/if} + </div> + </div> + {/if} +{/if} diff --git a/src/lib/components/common/Checkbox.svelte b/src/lib/components/common/Checkbox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3d43a69509db387136209b670ba6d2eea873546f --- /dev/null +++ b/src/lib/components/common/Checkbox.svelte @@ -0,0 +1,71 @@ +<script lang="ts"> + import { createEventDispatcher } from 'svelte'; + const dispatch = createEventDispatcher(); + + export let state = 'unchecked'; + export let indeterminate = false; + + let _state = 'unchecked'; + + $: _state = state; +</script> + +<button + class=" outline -outline-offset-1 outline-[1.5px] outline-gray-200 dark:outline-gray-600 {state !== + 'unchecked' + ? 'bg-black outline-black ' + : 'hover:outline-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800'} text-white transition-all rounded inline-block w-3.5 h-3.5 relative" + on:click={() => { + if (_state === 'unchecked') { + _state = 'checked'; + dispatch('change', _state); + } else if (_state === 'checked') { + _state = 'unchecked'; + if (!indeterminate) { + dispatch('change', _state); + } + } else if (indeterminate) { + _state = 'checked'; + dispatch('change', _state); + } + }} + type="button" +> + <div class="top-0 left-0 absolute w-full flex justify-center"> + {#if _state === 'checked'} + <svg + class="w-3.5 h-3.5" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="3" + d="m5 12 4.7 4.5 9.3-9" + /> + </svg> + {:else if indeterminate} + <svg + class="w-3 h-3.5 text-gray-800 dark:text-white" + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <path + stroke="currentColor" + stroke-linecap="round" + stroke-linejoin="round" + stroke-width="3" + d="M5 12h14" + /> + </svg> + {/if} + </div> + + <!-- {checked} --> +</button> diff --git a/src/lib/components/common/CodeEditor.svelte b/src/lib/components/common/CodeEditor.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9453c6ce32381cc27ccf23e9e3f6ea4a63e20ebe --- /dev/null +++ b/src/lib/components/common/CodeEditor.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import { basicSetup, EditorView } from 'codemirror'; + import { keymap, placeholder } from '@codemirror/view'; + import { Compartment, EditorState } from '@codemirror/state'; + + import { acceptCompletion } from '@codemirror/autocomplete'; + import { indentWithTab } from '@codemirror/commands'; + + import { indentUnit } from '@codemirror/language'; + import { python } from '@codemirror/lang-python'; + import { oneDark } from '@codemirror/theme-one-dark'; + + import { onMount, createEventDispatcher, getContext } from 'svelte'; + import { formatPythonCode } from '$lib/apis/utils'; + import { toast } from 'svelte-sonner'; + + const dispatch = createEventDispatcher(); + const i18n = getContext('i18n'); + + export let boilerplate = ''; + export let value = ''; + + let codeEditor; + + let isDarkMode = false; + let editorTheme = new Compartment(); + + export const formatPythonCodeHandler = async () => { + if (codeEditor) { + const res = await formatPythonCode(value).catch((error) => { + toast.error(error); + return null; + }); + + if (res && res.code) { + const formattedCode = res.code; + codeEditor.dispatch({ + changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }] + }); + + toast.success($i18n.t('Code formatted successfully')); + return true; + } + return false; + } + return false; + }; + + let extensions = [ + basicSetup, + keymap.of([{ key: 'Tab', run: acceptCompletion }, indentWithTab]), + python(), + indentUnit.of(' '), + placeholder('Enter your code here...'), + EditorView.updateListener.of((e) => { + if (e.docChanged) { + value = e.state.doc.toString(); + } + }), + editorTheme.of([]) + ]; + + onMount(() => { + console.log(value); + if (value === '') { + value = boilerplate; + } + + // Check if html class has dark mode + isDarkMode = document.documentElement.classList.contains('dark'); + + // python code editor, highlight python code + codeEditor = new EditorView({ + state: EditorState.create({ + doc: value, + extensions: extensions + }), + parent: document.getElementById('code-textarea') + }); + + if (isDarkMode) { + codeEditor.dispatch({ + effects: editorTheme.reconfigure(oneDark) + }); + } + + // listen to html class changes this should fire only when dark mode is toggled + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && mutation.attributeName === 'class') { + const _isDarkMode = document.documentElement.classList.contains('dark'); + + if (_isDarkMode !== isDarkMode) { + isDarkMode = _isDarkMode; + if (_isDarkMode) { + codeEditor.dispatch({ + effects: editorTheme.reconfigure(oneDark) + }); + } else { + codeEditor.dispatch({ + effects: editorTheme.reconfigure() + }); + } + } + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + + const keydownHandler = async (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 's') { + e.preventDefault(); + dispatch('save'); + } + + // Format code when Ctrl + Shift + F is pressed + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'f') { + e.preventDefault(); + await formatPythonCodeHandler(); + } + }; + + document.addEventListener('keydown', keydownHandler); + + return () => { + observer.disconnect(); + document.removeEventListener('keydown', keydownHandler); + }; + }); +</script> + +<div id="code-textarea" class="h-full w-full" /> diff --git a/src/lib/components/common/Collapsible.svelte b/src/lib/components/common/Collapsible.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c7497d83518f289870fd5ef47e7adcfb0ace21d4 --- /dev/null +++ b/src/lib/components/common/Collapsible.svelte @@ -0,0 +1,18 @@ +<script lang="ts"> + import { slide } from 'svelte/transition'; + import { quintOut } from 'svelte/easing'; + export let open = false; + export let className = ''; +</script> + +<div class={className}> + <button on:click={() => (open = !open)}> + <slot /> + </button> + + {#if open} + <div transition:slide={{ duration: 300, easing: quintOut, axis: 'y' }}> + <slot name="content" /> + </div> + {/if} +</div> diff --git a/src/lib/components/common/ConfirmDialog.svelte b/src/lib/components/common/ConfirmDialog.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c0ba3783c92b50d795b791226631c2be058f315a --- /dev/null +++ b/src/lib/components/common/ConfirmDialog.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import { onMount, getContext, createEventDispatcher } from 'svelte'; + import { fade } from 'svelte/transition'; + const i18n = getContext('i18n'); + + import { flyAndScale } from '$lib/utils/transitions'; + + const dispatch = createEventDispatcher(); + + export let title = ''; + export let message = ''; + + export let cancelLabel = $i18n.t('Cancel'); + export let confirmLabel = $i18n.t('Confirm'); + + export let input = false; + export let inputPlaceholder = ''; + export let inputValue = ''; + + export let show = false; + + let modalElement = null; + let mounted = false; + + 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'; + } + } +</script> + +{#if show} + <!-- 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-[9999] overflow-hidden overscroll-contain" + in:fade={{ duration: 10 }} + on:mousedown={() => { + show = false; + }} + > + <div + class=" m-auto rounded-2xl max-w-full w-[32rem] mx-2 bg-gray-50 dark:bg-gray-950 shadow-3xl border border-gray-850" + in:flyAndScale + on:mousedown={(e) => { + e.stopPropagation(); + }} + > + <div class="px-[1.75rem] py-6"> + <div class=" text-lg font-semibold dark:text-gray-200 mb-2.5"> + {#if title !== ''} + {title} + {:else} + {$i18n.t('Confirm your action')} + {/if} + </div> + + <slot> + <div class=" text-sm text-gray-500"> + {#if message !== ''} + {message} + {:else} + {$i18n.t('This action cannot be undone. Do you wish to continue?')} + {/if} + + {#if input} + <textarea + bind:value={inputValue} + placeholder={inputPlaceholder ? inputPlaceholder : $i18n.t('Enter your message')} + class="w-full mt-2 rounded-lg px-4 py-2 text-sm dark:text-gray-300 dark:bg-gray-900 outline-none resize-none" + rows="3" + required + /> + {/if} + </div> + </slot> + + <div class="mt-6 flex justify-between gap-1.5"> + <button + class="bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white font-medium w-full py-2.5 rounded-lg transition" + on:click={() => { + show = false; + dispatch('cancel'); + }} + type="button" + > + {cancelLabel} + </button> + <button + class="bg-gray-900 hover:bg-gray-850 text-gray-100 dark:bg-gray-100 dark:hover:bg-white dark:text-gray-800 font-medium w-full py-2.5 rounded-lg transition" + on:click={() => { + show = false; + dispatch('confirm', inputValue); + }} + type="button" + > + {confirmLabel} + </button> + </div> + </div> + </div> + </div> +{/if} + +<style> + .modal-content { + animation: scaleUp 0.1s ease-out forwards; + } + + @keyframes scaleUp { + from { + transform: scale(0.985); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } + } +</style> diff --git a/src/lib/components/common/Dropdown.svelte b/src/lib/components/common/Dropdown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e8e1eb8b5abb3645eba8d849607535e85d16311e --- /dev/null +++ b/src/lib/components/common/Dropdown.svelte @@ -0,0 +1,44 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { createEventDispatcher } from 'svelte'; + + import { flyAndScale } from '$lib/utils/transitions'; + + export let show = false; + const dispatch = createEventDispatcher(); +</script> + +<DropdownMenu.Root + bind:open={show} + closeFocus={false} + onOpenChange={(state) => { + dispatch('change', state); + }} + typeahead={false} +> + <DropdownMenu.Trigger> + <slot /> + </DropdownMenu.Trigger> + + <slot name="content"> + <DropdownMenu.Content + class="w-full max-w-[130px] rounded-lg px-1 py-1.5 border border-gray-700 z-50 bg-gray-850 text-white" + sideOffset={8} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium"> + <div class="flex items-center">Profile</div> + </DropdownMenu.Item> + + <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium"> + <div class="flex items-center">Profile</div> + </DropdownMenu.Item> + + <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium"> + <div class="flex items-center">Profile</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </slot> +</DropdownMenu.Root> diff --git a/src/lib/components/common/FileItem.svelte b/src/lib/components/common/FileItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6ae011caea0a6022ee3d886b640b80c8a78cef2e --- /dev/null +++ b/src/lib/components/common/FileItem.svelte @@ -0,0 +1,136 @@ +<script lang="ts"> + import { createEventDispatcher, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let className = 'w-72'; + export let colorClassName = 'bg-white dark:bg-gray-800'; + export let url: string | null = null; + + export let clickHandler: Function | null = null; + + export let dismissible = false; + export let status = 'processed'; + + export let name: string; + export let type: string; +</script> + +<div class="relative group"> + <button + class="h-14 {className} flex items-center space-x-3 {colorClassName} rounded-xl border border-gray-100 dark:border-gray-800 text-left" + type="button" + on:click={async () => { + if (clickHandler === null) { + if (url) { + if (type === 'file') { + window.open(`${url}/content`, '_blank').focus(); + } else { + window.open(`${url}`, '_blank').focus(); + } + } + } else { + clickHandler(); + } + }} + > + <div class="p-4 py-[1.1rem] bg-red-400 text-white rounded-l-xl"> + {#if status === 'processed'} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class=" size-5" + > + <path + fill-rule="evenodd" + d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" + clip-rule="evenodd" + /> + <path + d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" + /> + </svg> + {:else} + <svg + class=" size-5 translate-y-[0.5px]" + fill="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_qM83 { + animation: spinner_8HQG 1.05s infinite; + } + .spinner_oXPr { + animation-delay: 0.1s; + } + .spinner_ZTLf { + animation-delay: 0.2s; + } + @keyframes spinner_8HQG { + 0%, + 57.14% { + animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); + transform: translate(0); + } + 28.57% { + animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); + transform: translateY(-6px); + } + 100% { + transform: translate(0); + } + } + </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle + class="spinner_qM83 spinner_oXPr" + cx="12" + cy="12" + r="2.5" + /><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg + > + {/if} + </div> + + <div class="flex flex-col justify-center -space-y-0.5 pl-1.5 pr-4 w-full"> + <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1"> + {name} + </div> + + <div class=" text-gray-500 text-xs"> + {#if type === 'file'} + {$i18n.t('File')} + {:else if type === 'doc'} + {$i18n.t('Document')} + {:else if type === 'collection'} + {$i18n.t('Collection')} + {:else} + <span class=" capitalize">{type}</span> + {/if} + </div> + </div> + </button> + + {#if dismissible} + <div class=" absolute -top-1 -right-1"> + <button + class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" + type="button" + on:click={() => { + dispatch('dismiss'); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + {/if} +</div> diff --git a/src/lib/components/common/Image.svelte b/src/lib/components/common/Image.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dc65df87de2027c0837550f9d352fab4e314581a --- /dev/null +++ b/src/lib/components/common/Image.svelte @@ -0,0 +1,23 @@ +<script lang="ts"> + import { WEBUI_BASE_URL } from '$lib/constants'; + import ImagePreview from './ImagePreview.svelte'; + + export let src = ''; + export let alt = ''; + + let _src = ''; + + $: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src; + + let showImagePreview = false; +</script> + +<ImagePreview bind:show={showImagePreview} src={_src} {alt} /> +<button + on:click={() => { + console.log('image preview'); + showImagePreview = true; + }} +> + <img src={_src} {alt} class=" max-h-96 rounded-lg" draggable="false" data-cy="image" /> +</button> diff --git a/src/lib/components/common/ImagePreview.svelte b/src/lib/components/common/ImagePreview.svelte new file mode 100644 index 0000000000000000000000000000000000000000..16253b8a2728c90da2d7c1f30bc5067276df1b58 --- /dev/null +++ b/src/lib/components/common/ImagePreview.svelte @@ -0,0 +1,100 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + + export let show = false; + export let src = ''; + export let alt = ''; + + let mounted = false; + + const downloadImage = (url, filename) => { + fetch(url) + .then((response) => response.blob()) + .then((blob) => { + const objectUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = objectUrl; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(objectUrl); + }) + .catch((error) => console.error('Error downloading image:', error)); + }; + + 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'; + } + } +</script> + +{#if show} + <!-- svelte-ignore a11y-click-events-have-key-events --> + <!-- svelte-ignore a11y-no-static-element-interactions --> + <div + class="fixed top-0 right-0 left-0 bottom-0 bg-black text-white w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" + > + <div class=" absolute left-0 w-full flex justify-between"> + <div> + <button + class=" p-5" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="w-6 h-6" + > + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> + </svg> + </button> + </div> + + <div> + <button + class=" p-5" + on:click={() => { + downloadImage(src, src.substring(src.lastIndexOf('/') + 1)); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-6 h-6" + > + <path + d="M10.75 2.75a.75.75 0 0 0-1.5 0v8.614L6.295 8.235a.75.75 0 1 0-1.09 1.03l4.25 4.5a.75.75 0 0 0 1.09 0l4.25-4.5a.75.75 0 0 0-1.09-1.03l-2.955 3.129V2.75Z" + /> + <path + d="M3.5 12.75a.75.75 0 0 0-1.5 0v2.5A2.75 2.75 0 0 0 4.75 18h10.5A2.75 2.75 0 0 0 18 15.25v-2.5a.75.75 0 0 0-1.5 0v2.5c0 .69-.56 1.25-1.25 1.25H4.75c-.69 0-1.25-.56-1.25-1.25v-2.5Z" + /> + </svg> + </button> + </div> + </div> + <img {src} {alt} class=" mx-auto h-full object-scale-down" /> + </div> +{/if} diff --git a/src/lib/components/common/Modal.svelte b/src/lib/components/common/Modal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..08264f0fc99c1dc04d02b7ce7ff3e6d3fb0d48df --- /dev/null +++ b/src/lib/components/common/Modal.svelte @@ -0,0 +1,92 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import { fade } from 'svelte/transition'; + + import { flyAndScale } from '$lib/utils/transitions'; + + export let show = true; + export let size = 'md'; + + let modalElement = null; + let mounted = false; + + const sizeToWidth = (size) => { + if (size === 'xs') { + return 'w-[16rem]'; + } else if (size === 'sm') { + return 'w-[30rem]'; + } else if (size === 'md') { + return 'w-[48rem]'; + } else { + return 'w-[56rem]'; + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isTopModal()) { + console.log('Escape'); + show = false; + } + }; + + const isTopModal = () => { + const modals = document.getElementsByClassName('modal'); + return modals.length && modals[modals.length - 1] === modalElement; + }; + + onMount(() => { + mounted = true; + }); + + $: if (show && modalElement) { + document.body.appendChild(modalElement); + window.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } else if (modalElement) { + window.removeEventListener('keydown', handleKeyDown); + document.body.removeChild(modalElement); + document.body.style.overflow = 'unset'; + } +</script> + +{#if show} + <!-- svelte-ignore a11y-click-events-have-key-events --> + <!-- svelte-ignore a11y-no-static-element-interactions --> + <div + bind:this={modalElement} + class="modal fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain" + in:fade={{ duration: 10 }} + on:mousedown={() => { + show = false; + }} + > + <div + class=" m-auto rounded-2xl max-w-full {sizeToWidth( + size + )} mx-2 bg-gray-50 dark:bg-gray-900 shadow-3xl max-h-[100dvh] overflow-y-auto scrollbar-hidden" + in:flyAndScale + on:mousedown={(e) => { + e.stopPropagation(); + }} + > + <slot /> + </div> + </div> +{/if} + +<style> + .modal-content { + animation: scaleUp 0.1s ease-out forwards; + } + + @keyframes scaleUp { + from { + transform: scale(0.985); + opacity: 0; + } + to { + transform: scale(1); + opacity: 1; + } + } +</style> diff --git a/src/lib/components/common/Overlay.svelte b/src/lib/components/common/Overlay.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d38358212ae03a767b4718c74124af0d78b4bffb --- /dev/null +++ b/src/lib/components/common/Overlay.svelte @@ -0,0 +1,33 @@ +<script> + import Spinner from './Spinner.svelte'; + + export let show = false; + export let content = ''; + + export let opacity = 1; +</script> + +<div class="relative"> + {#if show} + <div class="absolute w-full h-full flex"> + <div + class="absolute rounded" + style="inset: -10px; opacity: {opacity}; backdrop-filter: blur(5px);" + /> + + <div class="flex w-full flex-col justify-center"> + <div class=" py-3"> + <Spinner className="ml-2" /> + </div> + + {#if content !== ''} + <div class="text-center text-gray-100 text-xs font-medium z-50"> + {content} + </div> + {/if} + </div> + </div> + {/if} + + <slot /> +</div> diff --git a/src/lib/components/common/Pagination.svelte b/src/lib/components/common/Pagination.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5ef7e795386c1549c470c398bc9a3c482800181e --- /dev/null +++ b/src/lib/components/common/Pagination.svelte @@ -0,0 +1,42 @@ +<script lang="ts"> + import { Pagination } from 'bits-ui'; + import { createEventDispatcher } from 'svelte'; + + import ChevronLeft from '../icons/ChevronLeft.svelte'; + import ChevronRight from '../icons/ChevronRight.svelte'; + + export let page = 0; + export let count = 0; + export let perPage = 20; +</script> + +<div class="flex justify-center"> + <Pagination.Root bind:page {count} {perPage} let:pages> + <div class="my-2 flex items-center"> + <Pagination.PrevButton + class="mr-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent" + > + <ChevronLeft className="size-4" strokeWidth="2" /> + </Pagination.PrevButton> + <div class="flex items-center gap-2.5"> + {#each pages as page (page.key)} + {#if page.type === 'ellipsis'} + <div class="text-sm font-medium text-foreground-alt">...</div> + {:else} + <Pagination.Page + {page} + class="inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-sm font-medium hover:bg-dark-10 active:scale-98 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent data-[selected]:bg-black data-[selected]:text-gray-100 data-[selected]:hover:bg-black dark:data-[selected]:bg-white dark:data-[selected]:text-gray-900 dark:data-[selected]:hover:bg-white" + > + {page.value} + </Pagination.Page> + {/if} + {/each} + </div> + <Pagination.NextButton + class="ml-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent" + > + <ChevronRight className="size-4" strokeWidth="2" /> + </Pagination.NextButton> + </div> + </Pagination.Root> +</div> diff --git a/src/lib/components/common/Selector.svelte b/src/lib/components/common/Selector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a449f5ca80d5d082e8a122e5c2d05b4d4571bf05 --- /dev/null +++ b/src/lib/components/common/Selector.svelte @@ -0,0 +1,95 @@ +<script lang="ts"> + import { Select } from 'bits-ui'; + + import { flyAndScale } from '$lib/utils/transitions'; + + import { createEventDispatcher } from 'svelte'; + import ChevronDown from '../icons/ChevronDown.svelte'; + import Check from '../icons/Check.svelte'; + import Search from '../icons/Search.svelte'; + + const dispatch = createEventDispatcher(); + + export let value = ''; + export let placeholder = 'Select a model'; + export let searchEnabled = true; + export let searchPlaceholder = 'Search a model'; + + export let items = [ + { value: 'mango', label: 'Mango' }, + { value: 'watermelon', label: 'Watermelon' }, + { value: 'apple', label: 'Apple' }, + { value: 'pineapple', label: 'Pineapple' }, + { value: 'orange', label: 'Orange' } + ]; + + let searchValue = ''; + + $: filteredItems = searchValue + ? items.filter((item) => item.value.toLowerCase().includes(searchValue.toLowerCase())) + : items; +</script> + +<Select.Root + {items} + onOpenChange={() => { + searchValue = ''; + }} + selected={items.find((item) => item.value === value)} + onSelectedChange={(selectedItem) => { + value = selectedItem.value; + }} +> + <Select.Trigger class="relative w-full" aria-label={placeholder}> + <Select.Value + class="inline-flex h-input px-0.5 w-full outline-none bg-transparent truncate text-lg font-semibold placeholder-gray-400 focus:outline-none" + {placeholder} + /> + <ChevronDown className="absolute end-2 top-1/2 -translate-y-[45%] size-3.5" strokeWidth="2.5" /> + </Select.Trigger> + <Select.Content + class="w-full rounded-lg bg-white dark:bg-gray-900 dark:text-white shadow-lg border border-gray-300/30 dark:border-gray-700/40 outline-none" + transition={flyAndScale} + sideOffset={4} + > + <slot> + {#if searchEnabled} + <div class="flex items-center gap-2.5 px-5 mt-3.5 mb-3"> + <Search className="size-4" strokeWidth="2.5" /> + + <input + bind:value={searchValue} + class="w-full text-sm bg-transparent outline-none" + placeholder={searchPlaceholder} + /> + </div> + + <hr class="border-gray-100 dark:border-gray-800" /> + {/if} + + <div class="px-3 my-2 max-h-80 overflow-y-auto"> + {#each filteredItems as item} + <Select.Item + class="flex w-full font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-none transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg cursor-pointer data-[highlighted]:bg-muted" + value={item.value} + label={item.label} + > + {item.label} + + {#if value === item.value} + <div class="ml-auto"> + <Check /> + </div> + {/if} + </Select.Item> + {:else} + <div> + <div class="block px-5 py-2 text-sm text-gray-700 dark:text-gray-100"> + No results found + </div> + </div> + {/each} + </div> + </slot> + </Select.Content> +</Select.Root> diff --git a/src/lib/components/common/SensitiveInput.svelte b/src/lib/components/common/SensitiveInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a1d7b9248957cb06941217da79d1d2a8d2bed442 --- /dev/null +++ b/src/lib/components/common/SensitiveInput.svelte @@ -0,0 +1,63 @@ +<script lang="ts"> + export let value: string = ''; + export let placeholder = ''; + export let required = true; + export let readOnly = false; + export let outerClassName = 'flex flex-1'; + export let inputClassName = + 'w-full rounded-l-lg py-2 pl-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-none'; + export let showButtonClassName = 'px-2 transition rounded-r-lg bg-gray-50 dark:bg-gray-850'; + + let show = false; +</script> + +<div class={outerClassName}> + <input + class={inputClassName} + {placeholder} + bind:value + required={required && !readOnly} + disabled={readOnly} + autocomplete="off" + {...{ type: show ? 'text' : 'password' }} + /> + <button + class={showButtonClassName} + on:click={(e) => { + e.preventDefault(); + show = !show; + }} + > + {#if show} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z" + clip-rule="evenodd" + /> + <path + d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z" + /> + </svg> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" /> + <path + fill-rule="evenodd" + d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> +</div> diff --git a/src/lib/components/common/Spinner.svelte b/src/lib/components/common/Spinner.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a22f56dcb2c0d9c4451f42876902777db0b128ad --- /dev/null +++ b/src/lib/components/common/Spinner.svelte @@ -0,0 +1,25 @@ +<script lang="ts"> + export let className: string = 'size-5'; +</script> + +<div class="flex justify-center text-center"> + <svg class={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > +</div> diff --git a/src/lib/components/common/Switch.svelte b/src/lib/components/common/Switch.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0f8f4546033ae84345c558333af4c306e12dd562 --- /dev/null +++ b/src/lib/components/common/Switch.svelte @@ -0,0 +1,22 @@ +<script lang="ts"> + import { createEventDispatcher, tick } from 'svelte'; + import { Switch } from 'bits-ui'; + export let state = true; + + const dispatch = createEventDispatcher(); +</script> + +<Switch.Root + bind:checked={state} + onCheckedChange={async (e) => { + await tick(); + dispatch('change', e); + }} + class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] transition {state + ? ' bg-emerald-600' + : 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800" +> + <Switch.Thumb + class="pointer-events-none block size-4 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini " + /> +</Switch.Root> diff --git a/src/lib/components/common/Tags.svelte b/src/lib/components/common/Tags.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bc1e7b5b383d43b7c43cb5fe60005211678fdd8e --- /dev/null +++ b/src/lib/components/common/Tags.svelte @@ -0,0 +1,28 @@ +<script lang="ts"> + import TagInput from './Tags/TagInput.svelte'; + import TagList from './Tags/TagList.svelte'; + import { getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + export let tags = []; + + export let deleteTag: Function; + export let addTag: Function; +</script> + +<div class="flex flex-row flex-wrap gap-1 line-clamp-1"> + <TagList + {tags} + on:delete={(e) => { + deleteTag(e.detail); + }} + /> + + <TagInput + label={tags.length == 0 ? $i18n.t('Add Tags') : ''} + on:add={(e) => { + addTag(e.detail); + }} + /> +</div> diff --git a/src/lib/components/common/Tags/TagInput.svelte b/src/lib/components/common/Tags/TagInput.svelte new file mode 100644 index 0000000000000000000000000000000000000000..dbda5a175cc01c2e8af6aa735f518d2d1480b42e --- /dev/null +++ b/src/lib/components/common/Tags/TagInput.svelte @@ -0,0 +1,88 @@ +<script lang="ts"> + import { createEventDispatcher, getContext } from 'svelte'; + import { tags } from '$lib/stores'; + import { toast } from 'svelte-sonner'; + const dispatch = createEventDispatcher(); + + const i18n = getContext('i18n'); + + export let label = ''; + let showTagInput = false; + let tagName = ''; + + const addTagHandler = async () => { + tagName = tagName.trim(); + if (tagName !== '') { + dispatch('add', tagName); + tagName = ''; + showTagInput = false; + } else { + toast.error($i18n.t(`Invalid Tag`)); + } + }; +</script> + +<div class="px-0.5 flex {showTagInput ? 'flex-row-reverse' : ''}"> + {#if showTagInput} + <div class="flex items-center"> + <input + bind:value={tagName} + class=" px-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-none line-clamp-1 w-[5.5rem]" + placeholder={$i18n.t('Add a tag')} + list="tagOptions" + on:keydown={(event) => { + if (event.key === 'Enter') { + addTagHandler(); + } + }} + /> + <datalist id="tagOptions"> + {#each $tags as tag} + <option value={tag.name} /> + {/each} + </datalist> + + <button type="button" aria-label={$i18n.t('Save Tag')} on:click={addTagHandler}> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + stroke-width="2" + class="w-3 h-3" + > + <path + fill-rule="evenodd" + d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z" + clip-rule="evenodd" + /> + </svg> + </button> + </div> + {/if} + + <button + class=" cursor-pointer self-center p-0.5 flex h-fit items-center dark:hover:bg-gray-700 rounded-full transition border dark:border-gray-600 border-dashed" + type="button" + aria-label={$i18n.t('Add Tag')} + on:click={() => { + showTagInput = !showTagInput; + }} + > + <div class=" m-auto self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-3 h-3 {showTagInput ? 'rotate-45' : ''} transition-all transform" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </div> + </button> + + {#if label && !showTagInput} + <span class="text-xs pl-2 self-center">{label}</span> + {/if} +</div> diff --git a/src/lib/components/common/Tags/TagList.svelte b/src/lib/components/common/Tags/TagList.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f1a23f6dc7eb2bb3968206bf57800dda3446767f --- /dev/null +++ b/src/lib/components/common/Tags/TagList.svelte @@ -0,0 +1,34 @@ +<script lang="ts"> + import { createEventDispatcher } from 'svelte'; + const dispatch = createEventDispatcher(); + + export let tags = []; +</script> + +{#each tags as tag} + <div + class="px-2 py-[0.5px] gap-0.5 flex justify-between h-fit items-center rounded-full transition border dark:border-gray-800 dark:text-white" + > + <div class=" text-[0.7rem] font-medium self-center line-clamp-1"> + {tag.name} + </div> + <button + class="h-full flex self-center cursor-pointer" + on:click={() => { + dispatch('delete', tag.name); + }} + type="button" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="size-3 m-auto self-center translate-y-[0.3px] translate-x-[3px]" + > + <path + d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" + /> + </svg> + </button> + </div> +{/each} diff --git a/src/lib/components/common/Tooltip.svelte b/src/lib/components/common/Tooltip.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1198935b147dab2d9c03d99a0eddaf39838d0b14 --- /dev/null +++ b/src/lib/components/common/Tooltip.svelte @@ -0,0 +1,46 @@ +<script lang="ts"> + import { onDestroy } from 'svelte'; + import { marked } from 'marked'; + + import tippy from 'tippy.js'; + import { roundArrow } from 'tippy.js'; + + export let placement = 'top'; + export let content = `I'm a tooltip!`; + export let touch = true; + export let className = 'flex'; + export let theme = ''; + + let tooltipElement; + let tooltipInstance; + + $: if (tooltipElement && content) { + if (tooltipInstance) { + tooltipInstance.setContent(content); + } else { + tooltipInstance = tippy(tooltipElement, { + content: content, + placement: placement, + allowHTML: true, + touch: touch, + ...(theme !== '' ? { theme } : { theme: 'dark' }), + arrow: false, + offset: [0, 4] + }); + } + } else if (tooltipInstance && content === '') { + if (tooltipInstance) { + tooltipInstance.destroy(); + } + } + + onDestroy(() => { + if (tooltipInstance) { + tooltipInstance.destroy(); + } + }); +</script> + +<div bind:this={tooltipElement} aria-label={content} class={className}> + <slot /> +</div> diff --git a/src/lib/components/common/Valves.svelte b/src/lib/components/common/Valves.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f15a4fb23a572cba1b27a24082cc5effd92df2c5 --- /dev/null +++ b/src/lib/components/common/Valves.svelte @@ -0,0 +1,95 @@ +<script> + import { onMount, getContext } from 'svelte'; + const i18n = getContext('i18n'); + + import Switch from './Switch.svelte'; + + export let valvesSpec = null; + export let valves = {}; +</script> + +{#if valvesSpec} + {#each Object.keys(valvesSpec.properties) as property, idx} + <div class=" py-0.5 w-full justify-between"> + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {valvesSpec.properties[property].title} + + {#if (valvesSpec?.required ?? []).includes(property)} + <span class=" text-gray-500">*required</span> + {/if} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + valves[property] = + (valves[property] ?? null) === null + ? valvesSpec.properties[property]?.default ?? '' + : null; + }} + > + {#if (valves[property] ?? null) === null} + <span class="ml-2 self-center"> + {#if (valvesSpec?.required ?? []).includes(property)} + {$i18n.t('None')} + {:else} + {$i18n.t('Default')} + {/if} + </span> + {:else} + <span class="ml-2 self-center"> {$i18n.t('Custom')} </span> + {/if} + </button> + </div> + + {#if (valves[property] ?? null) !== null} + <!-- {valves[property]} --> + <div class="flex mt-0.5 mb-1.5 space-x-2"> + <div class=" flex-1"> + {#if valvesSpec.properties[property]?.enum ?? null} + <select + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800" + bind:value={valves[property]} + > + {#each valvesSpec.properties[property].enum as option} + <option value={option} selected={option === valves[property]}> + {option} + </option> + {/each} + </select> + {:else if (valvesSpec.properties[property]?.type ?? null) === 'boolean'} + <div class="flex justify-between items-center"> + <div class="text-xs text-gray-500"> + {valves[property] ? 'Enabled' : 'Disabled'} + </div> + + <div class=" pr-2"> + <Switch bind:state={valves[property]} /> + </div> + </div> + {:else} + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none border border-gray-100 dark:border-gray-800" + type="text" + placeholder={valvesSpec.properties[property].title} + bind:value={valves[property]} + autocomplete="off" + required + /> + {/if} + </div> + </div> + {/if} + + {#if (valvesSpec.properties[property]?.description ?? null) !== null} + <div class="text-xs text-gray-500"> + {valvesSpec.properties[property].description} + </div> + {/if} + </div> + {/each} +{:else} + <div class="text-sm">No valves</div> +{/if} diff --git a/src/lib/components/documents/AddDocModal.svelte b/src/lib/components/documents/AddDocModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10164be97d86f227635eb94cc3cca12c42527e55 --- /dev/null +++ b/src/lib/components/documents/AddDocModal.svelte @@ -0,0 +1,169 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { onMount, getContext } from 'svelte'; + + import { createNewDoc, getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; + import Modal from '../common/Modal.svelte'; + import { documents } from '$lib/stores'; + import TagInput from '../common/Tags/TagInput.svelte'; + import Tags from '../common/Tags.svelte'; + import { addTagById } from '$lib/apis/chats'; + import { uploadDocToVectorDB } from '$lib/apis/rag'; + import { transformFileName } from '$lib/utils'; + import { SUPPORTED_FILE_EXTENSIONS, SUPPORTED_FILE_TYPE } from '$lib/constants'; + + const i18n = getContext('i18n'); + + export let show = false; + export let uploadDoc: Function; + let uploadDocInputElement: HTMLInputElement; + let inputFiles; + let tags = []; + + let doc = { + name: '', + title: '', + content: null + }; + + const submitHandler = async () => { + if (inputFiles && inputFiles.length > 0) { + for (const file of inputFiles) { + console.log(file, file.name.split('.').at(-1)); + if ( + SUPPORTED_FILE_TYPE.includes(file['type']) || + SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) + ) { + uploadDoc(file, tags); + } else { + toast.error( + `Unknown File Type '${file['type']}', but accepting and treating as plain text` + ); + uploadDoc(file, tags); + } + } + + inputFiles = null; + uploadDocInputElement.value = ''; + } else { + toast.error($i18n.t(`File not found.`)); + } + + show = false; + documents.set(await getDocs(localStorage.token)); + }; + + const addTagHandler = async (tagName) => { + if (!tags.find((tag) => tag.name === tagName) && tagName !== '') { + tags = [...tags, { name: tagName }]; + } else { + console.log('tag already exists'); + } + }; + + const deleteTagHandler = async (tagName) => { + tags = tags.filter((tag) => tag.name !== tagName); + }; + + onMount(() => {}); +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4"> + <div class=" text-lg font-medium self-center">{$i18n.t('Add Docs')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + <div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="mb-3 w-full"> + <input + id="upload-doc-input" + bind:this={uploadDocInputElement} + hidden + bind:files={inputFiles} + type="file" + multiple + /> + + <button + class="w-full text-sm font-medium py-3 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 text-center rounded-xl" + type="button" + on:click={() => { + uploadDocInputElement.click(); + }} + > + {#if inputFiles} + {inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. + {:else} + {$i18n.t('Click here to select documents.')} + {/if} + </button> + </div> + + <div class=" flex flex-col space-y-1.5"> + <div class="flex flex-col w-full"> + <div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div> + + <Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> + </div> + </div> + + <div class="flex justify-end pt-5 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/documents/EditDocModal.svelte b/src/lib/components/documents/EditDocModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..47577bd482bb8e811caf275f575224fd484073be --- /dev/null +++ b/src/lib/components/documents/EditDocModal.svelte @@ -0,0 +1,181 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { onMount, getContext } from 'svelte'; + + import { getDocs, tagDocByName, updateDocByName } from '$lib/apis/documents'; + import Modal from '../common/Modal.svelte'; + import { documents } from '$lib/stores'; + import TagInput from '../common/Tags/TagInput.svelte'; + import Tags from '../common/Tags.svelte'; + import { addTagById } from '$lib/apis/chats'; + + const i18n = getContext('i18n'); + + export let show = false; + export let selectedDoc; + + let tags = []; + + let doc = { + name: '', + title: '', + content: null + }; + + const submitHandler = async () => { + const res = await updateDocByName(localStorage.token, selectedDoc.name, { + title: doc.title, + name: doc.name + }).catch((error) => { + toast.error(error); + }); + + if (res) { + show = false; + + documents.set(await getDocs(localStorage.token)); + } + }; + + const addTagHandler = async (tagName) => { + if (!tags.find((tag) => tag.name === tagName) && tagName !== '') { + tags = [...tags, { name: tagName }]; + + await tagDocByName(localStorage.token, doc.name, { + name: doc.name, + tags: tags + }); + + documents.set(await getDocs(localStorage.token)); + } else { + console.log('tag already exists'); + } + }; + + const deleteTagHandler = async (tagName) => { + tags = tags.filter((tag) => tag.name !== tagName); + + await tagDocByName(localStorage.token, doc.name, { + name: doc.name, + tags: tags + }); + + documents.set(await getDocs(localStorage.token)); + }; + + onMount(() => { + if (selectedDoc) { + doc = JSON.parse(JSON.stringify(selectedDoc)); + + tags = doc?.content?.tags ?? []; + } + }); +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4"> + <div class=" text-lg font-medium self-center">{$i18n.t('Edit Doc')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + <div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class=" flex flex-col space-y-1.5"> + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name Tag')}</div> + + <div class="flex flex-1"> + <div + class="bg-gray-200 dark:bg-gray-800 font-semibold px-3 py-0.5 border border-r-0 dark:border-gray-800 rounded-l-xl flex items-center" + > + # + </div> + <input + class="w-full rounded-r-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 disabled:text-gray-500 dark:disabled:text-gray-500 outline-none" + type="text" + bind:value={doc.name} + autocomplete="off" + required + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-1 text-xs text-gray-500">{$i18n.t('Title')}</div> + + <div class="flex-1"> + <input + class="w-full rounded-xl py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + bind:value={doc.title} + autocomplete="off" + required + /> + </div> + </div> + + <div class="flex flex-col w-full"> + <div class=" mb-2 text-xs text-gray-500">{$i18n.t('Tags')}</div> + + <Tags {tags} addTag={addTagHandler} deleteTag={deleteTagHandler} /> + </div> + </div> + + <div class="flex justify-end pt-5 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/documents/Settings/ChunkParams.svelte b/src/lib/components/documents/Settings/ChunkParams.svelte new file mode 100644 index 0000000000000000000000000000000000000000..330930fe6745ca07d3b60a42e4949d7d380eb723 --- /dev/null +++ b/src/lib/components/documents/Settings/ChunkParams.svelte @@ -0,0 +1,126 @@ +<script lang="ts"> + import { getDocs } from '$lib/apis/documents'; + import { + getRAGConfig, + updateRAGConfig, + getQuerySettings, + scanDocs, + updateQuerySettings, + resetVectorDB, + getEmbeddingConfig, + updateEmbeddingConfig, + getRerankingConfig, + updateRerankingConfig + } from '$lib/apis/rag'; + + import { documents, models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let scanDirLoading = false; + let updateEmbeddingModelLoading = false; + let updateRerankingModelLoading = false; + + let showResetConfirm = false; + + let chunkSize = 0; + let chunkOverlap = 0; + let pdfExtractImages = true; + + const submitHandler = async () => { + const res = await updateRAGConfig(localStorage.token, { + pdf_extract_images: pdfExtractImages, + chunk: { + chunk_overlap: chunkOverlap, + chunk_size: chunkSize + } + }); + }; + + onMount(async () => { + const res = await getRAGConfig(localStorage.token); + + if (res) { + pdfExtractImages = res.pdf_extract_images; + + chunkSize = res.chunk.chunk_size; + chunkOverlap = res.chunk.chunk_overlap; + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + submitHandler(); + saveHandler(); + }} +> + <div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]"> + <div class=" "> + <div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> + + <div class=" flex"> + <div class=" flex w-full justify-between"> + <div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Chunk Size')}</div> + + <div class="self-center p-3"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Chunk Size')} + bind:value={chunkSize} + autocomplete="off" + min="0" + /> + </div> + </div> + + <div class="flex w-full"> + <div class=" self-center text-xs font-medium min-w-fit"> + {$i18n.t('Chunk Overlap')} + </div> + + <div class="self-center p-3"> + <input + class="w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Chunk Overlap')} + bind:value={chunkOverlap} + autocomplete="off" + min="0" + /> + </div> + </div> + </div> + + <div class="pr-2"> + <div class="flex justify-between items-center text-xs"> + <div class=" text-xs font-medium">{$i18n.t('PDF Extract Images (OCR)')}</div> + + <button + class=" text-xs font-medium text-gray-500" + type="button" + on:click={() => { + pdfExtractImages = !pdfExtractImages; + }}>{pdfExtractImages ? $i18n.t('On') : $i18n.t('Off')}</button + > + </div> + </div> + </div> + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/documents/Settings/QueryParams.svelte b/src/lib/components/documents/Settings/QueryParams.svelte new file mode 100644 index 0000000000000000000000000000000000000000..140afed6c65e8ab7747a10f9390e3c33680bd36d --- /dev/null +++ b/src/lib/components/documents/Settings/QueryParams.svelte @@ -0,0 +1,119 @@ +<script lang="ts"> + import { getDocs } from '$lib/apis/documents'; + import { + getRAGConfig, + updateRAGConfig, + getQuerySettings, + scanDocs, + updateQuerySettings, + resetVectorDB, + getEmbeddingConfig, + updateEmbeddingConfig, + getRerankingConfig, + updateRerankingConfig + } from '$lib/apis/rag'; + + import { documents, models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let querySettings = { + template: '', + r: 0.0, + k: 4, + hybrid: false + }; + + const submitHandler = async () => { + querySettings = await updateQuerySettings(localStorage.token, querySettings); + }; + + onMount(async () => { + querySettings = await getQuerySettings(localStorage.token); + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={() => { + submitHandler(); + saveHandler(); + }} +> + <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]"> + <div class=" "> + <div class=" text-sm font-medium">{$i18n.t('Query Params')}</div> + + <div class=" flex"> + <div class=" flex w-full justify-between"> + <div class="self-center text-xs font-medium min-w-fit">{$i18n.t('Top K')}</div> + + <div class="self-center p-3"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + placeholder={$i18n.t('Enter Top K')} + bind:value={querySettings.k} + autocomplete="off" + min="0" + /> + </div> + </div> + + {#if querySettings.hybrid === true} + <div class="flex w-full"> + <div class=" self-center text-xs font-medium min-w-fit"> + {$i18n.t('Minimum Score')} + </div> + + <div class="self-center p-3"> + <input + class=" w-full rounded-lg py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="number" + step="0.01" + placeholder={$i18n.t('Enter Score')} + bind:value={querySettings.r} + autocomplete="off" + min="0.0" + title={$i18n.t('The score should be a value between 0.0 (0%) and 1.0 (100%).')} + /> + </div> + </div> + {/if} + </div> + + {#if querySettings.hybrid === true} + <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t( + 'Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.' + )} + </div> + + <hr class=" dark:border-gray-850 my-3" /> + {/if} + + <div> + <div class=" mb-2.5 text-sm font-medium">{$i18n.t('RAG Template')}</div> + <textarea + bind:value={querySettings.template} + class="w-full rounded-lg px-4 py-3 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none" + rows="4" + /> + </div> + </div> + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/documents/Settings/WebParams.svelte b/src/lib/components/documents/Settings/WebParams.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b5a4d1679d11e35ce51042b338b7c0dc4e6c48d1 --- /dev/null +++ b/src/lib/components/documents/Settings/WebParams.svelte @@ -0,0 +1,285 @@ +<script lang="ts"> + import { getRAGConfig, updateRAGConfig } from '$lib/apis/rag'; + import Switch from '$lib/components/common/Switch.svelte'; + + import { documents, models } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + export let saveHandler: Function; + + let webConfig = null; + let webSearchEngines = ['searxng', 'google_pse', 'brave', 'serpstack', 'serper', 'serply']; + + let youtubeLanguage = 'en'; + let youtubeTranslation = null; + + const submitHandler = async () => { + const res = await updateRAGConfig(localStorage.token, { + web: webConfig, + youtube: { + language: youtubeLanguage.split(',').map((lang) => lang.trim()), + translation: youtubeTranslation + } + }); + }; + + onMount(async () => { + const res = await getRAGConfig(localStorage.token); + + if (res) { + webConfig = res.web; + + youtubeLanguage = res.youtube.language.join(','); + youtubeTranslation = res.youtube.translation; + } + }); +</script> + +<form + class="flex flex-col h-full justify-between space-y-3 text-sm" + on:submit|preventDefault={async () => { + await submitHandler(); + saveHandler(); + }} +> + <div class=" space-y-3 pr-1.5 overflow-y-scroll h-full max-h-[22rem]"> + {#if webConfig} + <div> + <div class=" mb-1 text-sm font-medium"> + {$i18n.t('Web Search')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Enable Web Search')} + </div> + + <Switch bind:state={webConfig.search.enabled} /> + </div> + </div> + + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium">{$i18n.t('Web Search Engine')}</div> + <div class="flex items-center relative"> + <select + class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right" + bind:value={webConfig.search.engine} + placeholder={$i18n.t('Select a engine')} + required + > + <option disabled selected value="">{$i18n.t('Select a engine')}</option> + {#each webSearchEngines as engine} + <option value={engine}>{engine}</option> + {/each} + </select> + </div> + </div> + + {#if webConfig.search.engine !== ''} + <div class="mt-1.5"> + {#if webConfig.search.engine === 'searxng'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Searxng Query URL')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Searxng Query URL')} + bind:value={webConfig.search.searxng_query_url} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'google_pse'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Google PSE API Key')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Google PSE API Key')} + bind:value={webConfig.search.google_pse_api_key} + autocomplete="off" + /> + </div> + </div> + </div> + <div class="mt-1.5"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Google PSE Engine Id')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Google PSE Engine Id')} + bind:value={webConfig.search.google_pse_engine_id} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'brave'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Brave Search API Key')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Brave Search API Key')} + bind:value={webConfig.search.brave_search_api_key} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'serpstack'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Serpstack API Key')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Serpstack API Key')} + bind:value={webConfig.search.serpstack_api_key} + autocomplete="off" + /> + </div> + </div> + </div> + {:else if webConfig.search.engine === 'serper'} + <div> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Serper API Key')} + </div> + + <div class="flex w-full"> + <div class="flex-1"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter Serper API Key')} + bind:value={webConfig.search.serper_api_key} + autocomplete="off" + /> + </div> + </div> + </div> + {/if} + </div> + {/if} + + {#if webConfig.search.enabled} + <div class="mt-2 flex gap-2 mb-1"> + <div class="w-full"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Search Result Count')} + </div> + + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Search Result Count')} + bind:value={webConfig.search.result_count} + required + /> + </div> + + <div class="w-full"> + <div class=" self-center text-xs font-medium mb-1"> + {$i18n.t('Concurrent Requests')} + </div> + + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + placeholder={$i18n.t('Concurrent Requests')} + bind:value={webConfig.search.concurrent_requests} + required + /> + </div> + </div> + {/if} + </div> + + <hr class=" dark:border-gray-850 my-2" /> + + <div> + <div class=" mb-1 text-sm font-medium"> + {$i18n.t('Web Loader Settings')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" self-center text-xs font-medium"> + {$i18n.t('Bypass SSL verification for Websites')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + on:click={() => { + webConfig.ssl_verification = !webConfig.ssl_verification; + submitHandler(); + }} + type="button" + > + {#if webConfig.ssl_verification === true} + <span class="ml-2 self-center">{$i18n.t('On')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Off')}</span> + {/if} + </button> + </div> + </div> + + <div class=" mt-2 mb-1 text-sm font-medium"> + {$i18n.t('Youtube Loader Settings')} + </div> + + <div> + <div class=" py-0.5 flex w-full justify-between"> + <div class=" w-20 text-xs font-medium self-center">{$i18n.t('Language')}</div> + <div class=" flex-1 self-center"> + <input + class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" + type="text" + placeholder={$i18n.t('Enter language codes')} + bind:value={youtubeLanguage} + autocomplete="off" + /> + </div> + </div> + </div> + </div> + {/if} + </div> + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> +</form> diff --git a/src/lib/components/documents/SettingsModal.svelte b/src/lib/components/documents/SettingsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b2201c9ec431a0f04d801910cfa21ad0930d0545 --- /dev/null +++ b/src/lib/components/documents/SettingsModal.svelte @@ -0,0 +1,187 @@ +<script> + import { getContext, tick } from 'svelte'; + import Modal from '../common/Modal.svelte'; + import General from './Settings/General.svelte'; + import ChunkParams from './Settings/ChunkParams.svelte'; + import QueryParams from './Settings/QueryParams.svelte'; + import WebParams from './Settings/WebParams.svelte'; + import { toast } from 'svelte-sonner'; + import { config } from '$lib/stores'; + import { getBackendConfig } from '$lib/apis'; + + const i18n = getContext('i18n'); + + export let show = false; + + let selectedTab = 'general'; +</script> + +<Modal bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4"> + <div class=" text-lg font-medium self-center">{$i18n.t('Document Settings')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full p-4 md:space-x-4"> + <div + class="tabs flex flex-row overflow-x-auto space-x-1 md:space-x-0 md:space-y-1 md:flex-col flex-1 md:flex-none md:w-40 dark:text-gray-200 text-xs text-left mb-3 md:mb-0" + > + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'general' + ? 'bg-gray-200 dark:bg-gray-700' + : ' hover:bg-gray-300 dark:hover:bg-gray-800'}" + on:click={() => { + selectedTab = 'general'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('General')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'chunk' + ? 'bg-gray-200 dark:bg-gray-700' + : ' hover:bg-gray-300 dark:hover:bg-gray-800'}" + on:click={() => { + selectedTab = 'chunk'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875ZM12.75 12a.75.75 0 0 0-1.5 0v2.25H9a.75.75 0 0 0 0 1.5h2.25V18a.75.75 0 0 0 1.5 0v-2.25H15a.75.75 0 0 0 0-1.5h-2.25V12Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Chunk Params')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'query' + ? 'bg-gray-200 dark:bg-gray-700' + : ' hover:bg-gray-300 dark:hover:bg-gray-800'}" + on:click={() => { + selectedTab = 'query'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4 h-4" + > + <path d="M11.625 16.5a1.875 1.875 0 1 0 0-3.75 1.875 1.875 0 0 0 0 3.75Z" /> + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6 16.5c.66 0 1.277-.19 1.797-.518l1.048 1.048a.75.75 0 0 0 1.06-1.06l-1.047-1.048A3.375 3.375 0 1 0 11.625 18Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Query Params')}</div> + </button> + + <button + class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === + 'web' + ? 'bg-gray-200 dark:bg-gray-700' + : ' hover:bg-gray-300 dark:hover:bg-gray-800'}" + on:click={() => { + selectedTab = 'web'; + }} + > + <div class=" self-center mr-2"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z" + /> + </svg> + </div> + <div class=" self-center">{$i18n.t('Web Params')}</div> + </button> + </div> + <div class="flex-1 md:min-h-[380px]"> + {#if selectedTab === 'general'} + <General + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'chunk'} + <ChunkParams + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'query'} + <QueryParams + saveHandler={() => { + toast.success($i18n.t('Settings saved successfully!')); + }} + /> + {:else if selectedTab === 'web'} + <WebParams + saveHandler={async () => { + toast.success($i18n.t('Settings saved successfully!')); + + await tick(); + await config.set(await getBackendConfig()); + }} + /> + {/if} + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/icons/AdjustmentsHorizontal.svelte b/src/lib/components/icons/AdjustmentsHorizontal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4cc509e8460877f15613d0e45575ba1a9425b7ad --- /dev/null +++ b/src/lib/components/icons/AdjustmentsHorizontal.svelte @@ -0,0 +1,17 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + stroke="currentColor" + fill="currentColor" + class={className} + stroke-width={strokeWidth} +> + <path + d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z" + /> +</svg> diff --git a/src/lib/components/icons/ArchiveBox.svelte b/src/lib/components/icons/ArchiveBox.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d4f6e201abf2c226a8b820ab21a5c65c204087b4 --- /dev/null +++ b/src/lib/components/icons/ArchiveBox.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-3.5'; + export let strokeWidth = '2.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" + /> +</svg> diff --git a/src/lib/components/icons/ArrowDownTray.svelte b/src/lib/components/icons/ArrowDownTray.svelte new file mode 100644 index 0000000000000000000000000000000000000000..55620e9feaf4b1d92cd8373a1167ecc9ddfef289 --- /dev/null +++ b/src/lib/components/icons/ArrowDownTray.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" + /> +</svg> diff --git a/src/lib/components/icons/Bolt.svelte b/src/lib/components/icons/Bolt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..681ef53c9562d600163a16923500b55ed2f3fddc --- /dev/null +++ b/src/lib/components/icons/Bolt.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-3'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" + /> +</svg> diff --git a/src/lib/components/icons/Bookmark.svelte b/src/lib/components/icons/Bookmark.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ea8028457d0f4fdf08398513e192d69aa97f7c5f --- /dev/null +++ b/src/lib/components/icons/Bookmark.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z" + /> +</svg> diff --git a/src/lib/components/icons/BookmarkSlash.svelte b/src/lib/components/icons/BookmarkSlash.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6b80ea3cab1b437c51834d5bb12464bdfc55169a --- /dev/null +++ b/src/lib/components/icons/BookmarkSlash.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m3 3 1.664 1.664M21 21l-1.5-1.5m-5.485-1.242L12 17.25 4.5 21V8.742m.164-4.078a2.15 2.15 0 0 1 1.743-1.342 48.507 48.507 0 0 1 11.186 0c1.1.128 1.907 1.077 1.907 2.185V19.5M4.664 4.664 19.5 19.5" + /> +</svg> diff --git a/src/lib/components/icons/ChatBubble.svelte b/src/lib/components/icons/ChatBubble.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7ece2c4c7a658fd9d161da7a1f99b6f382dcfb63 --- /dev/null +++ b/src/lib/components/icons/ChatBubble.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M8.625 12a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H8.25m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0H12m4.125 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 0 1-2.555-.337A5.972 5.972 0 0 1 5.41 20.97a5.969 5.969 0 0 1-.474-.065 4.48 4.48 0 0 0 .978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25Z" + /> +</svg> diff --git a/src/lib/components/icons/ChatBubbles.svelte b/src/lib/components/icons/ChatBubbles.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1651746fd6fbedd745cdbeb7f078872ff3f1e036 --- /dev/null +++ b/src/lib/components/icons/ChatBubbles.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" + /> +</svg> diff --git a/src/lib/components/icons/ChatMenu.svelte b/src/lib/components/icons/ChatMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..673e643b1ecd2a6e981e32d1ba1e35878d985a7e --- /dev/null +++ b/src/lib/components/icons/ChatMenu.svelte @@ -0,0 +1,124 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import Star from '$lib/components/icons/Star.svelte'; + + const i18n = getContext('i18n'); + + export let pinHandler: Function; + export let shareHandler: Function; + export let cloneChatHandler: Function; + export let archiveChatHandler: Function; + export let renameHandler: Function; + export let deleteHandler: Function; + export let onClose: Function; + + export let chatId = ''; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + pinHandler(); + }} + > + <Star strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Pin')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + renameHandler(); + }} + > + <Pencil strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Rename')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneChatHandler(); + }} + > + <DocumentDuplicate strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + archiveChatHandler(); + }} + > + <ArchiveBox strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Archive')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" /> + + <div class="flex p-1"> + <Tags + {chatId} + on:close={() => { + show = false; + onClose(); + }} + /> + </div> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/icons/Check.svelte b/src/lib/components/icons/Check.svelte new file mode 100644 index 0000000000000000000000000000000000000000..37eb9d7139f53afd199c2e011ae6481b6286250a --- /dev/null +++ b/src/lib/components/icons/Check.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" /> +</svg> diff --git a/src/lib/components/icons/ChevronDown.svelte b/src/lib/components/icons/ChevronDown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..16686ea3a3be64ce629c8d020764f14f8b4184ae --- /dev/null +++ b/src/lib/components/icons/ChevronDown.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> +</svg> diff --git a/src/lib/components/icons/ChevronLeft.svelte b/src/lib/components/icons/ChevronLeft.svelte new file mode 100644 index 0000000000000000000000000000000000000000..78ee64d2422a4729cbaa28fd7d86696781f848ab --- /dev/null +++ b/src/lib/components/icons/ChevronLeft.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /> +</svg> diff --git a/src/lib/components/icons/ChevronRight.svelte b/src/lib/components/icons/ChevronRight.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7daf4a14a5297f72d9aa1fcddedb6d17eb556456 --- /dev/null +++ b/src/lib/components/icons/ChevronRight.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /> +</svg> diff --git a/src/lib/components/icons/ChevronUp.svelte b/src/lib/components/icons/ChevronUp.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6fe7ca1ccd0473c8dbd1d32bdc4ffb8abeb6d0b6 --- /dev/null +++ b/src/lib/components/icons/ChevronUp.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="m4.5 15.75 7.5-7.5 7.5 7.5" /> +</svg> diff --git a/src/lib/components/icons/ChevronUpDown.svelte b/src/lib/components/icons/ChevronUpDown.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7f23435a2d0ca45e0295994428e467722cc79a93 --- /dev/null +++ b/src/lib/components/icons/ChevronUpDown.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M8.25 15 12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" + /> +</svg> diff --git a/src/lib/components/icons/DocumentArrowUpSolid.svelte b/src/lib/components/icons/DocumentArrowUpSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2690f55370964ddc86ee805145e29d470b25d8d8 --- /dev/null +++ b/src/lib/components/icons/DocumentArrowUpSolid.svelte @@ -0,0 +1,14 @@ +<script lang="ts"> + export let className = 'size-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}> + <path + fill-rule="evenodd" + d="M5.625 1.5H9a3.75 3.75 0 0 1 3.75 3.75v1.875c0 1.036.84 1.875 1.875 1.875H16.5a3.75 3.75 0 0 1 3.75 3.75v7.875c0 1.035-.84 1.875-1.875 1.875H5.625a1.875 1.875 0 0 1-1.875-1.875V3.375c0-1.036.84-1.875 1.875-1.875Zm6.905 9.97a.75.75 0 0 0-1.06 0l-3 3a.75.75 0 1 0 1.06 1.06l1.72-1.72V18a.75.75 0 0 0 1.5 0v-4.19l1.72 1.72a.75.75 0 1 0 1.06-1.06l-3-3Z" + clip-rule="evenodd" + /> + <path + d="M14.25 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 16.5 7.5h-1.875a.375.375 0 0 1-.375-.375V5.25Z" + /> +</svg> diff --git a/src/lib/components/icons/DocumentDuplicate.svelte b/src/lib/components/icons/DocumentDuplicate.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a208fefc8ac1ecc3acedf9d6a0aa1013ea6bab18 --- /dev/null +++ b/src/lib/components/icons/DocumentDuplicate.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75" + /> +</svg> diff --git a/src/lib/components/icons/EllipsisHorizontal.svelte b/src/lib/components/icons/EllipsisHorizontal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6a7d532e38cc572fadb6d43cf7f64527f1437c1f --- /dev/null +++ b/src/lib/components/icons/EllipsisHorizontal.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" + /> +</svg> diff --git a/src/lib/components/icons/EllipsisVertical.svelte b/src/lib/components/icons/EllipsisVertical.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7ccbea2947395ce6cc584917c886bc44169af935 --- /dev/null +++ b/src/lib/components/icons/EllipsisVertical.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" + /> +</svg> diff --git a/src/lib/components/icons/GarbageBin.svelte b/src/lib/components/icons/GarbageBin.svelte new file mode 100644 index 0000000000000000000000000000000000000000..31530fc7210733a9fb8f8cc0064c5854a4859ec6 --- /dev/null +++ b/src/lib/components/icons/GarbageBin.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" + /> +</svg> diff --git a/src/lib/components/icons/GlobeAlt.svelte b/src/lib/components/icons/GlobeAlt.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d2f86f43801f5baaa7a6a83cbdc386a06c0305a4 --- /dev/null +++ b/src/lib/components/icons/GlobeAlt.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418" + /> +</svg> diff --git a/src/lib/components/icons/GlobeAltSolid.svelte b/src/lib/components/icons/GlobeAltSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7054d311f82bb8a8ed946abc4e81e0b258ddbdf8 --- /dev/null +++ b/src/lib/components/icons/GlobeAltSolid.svelte @@ -0,0 +1,9 @@ +<script lang="ts"> + export let className = 'size-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}> + <path + d="M21.721 12.752a9.711 9.711 0 0 0-.945-5.003 12.754 12.754 0 0 1-4.339 2.708 18.991 18.991 0 0 1-.214 4.772 17.165 17.165 0 0 0 5.498-2.477ZM14.634 15.55a17.324 17.324 0 0 0 .332-4.647c-.952.227-1.945.347-2.966.347-1.021 0-2.014-.12-2.966-.347a17.515 17.515 0 0 0 .332 4.647 17.385 17.385 0 0 0 5.268 0ZM9.772 17.119a18.963 18.963 0 0 0 4.456 0A17.182 17.182 0 0 1 12 21.724a17.18 17.18 0 0 1-2.228-4.605ZM7.777 15.23a18.87 18.87 0 0 1-.214-4.774 12.753 12.753 0 0 1-4.34-2.708 9.711 9.711 0 0 0-.944 5.004 17.165 17.165 0 0 0 5.498 2.477ZM21.356 14.752a9.765 9.765 0 0 1-7.478 6.817 18.64 18.64 0 0 0 1.988-4.718 18.627 18.627 0 0 0 5.49-2.098ZM2.644 14.752c1.682.971 3.53 1.688 5.49 2.099a18.64 18.64 0 0 0 1.988 4.718 9.765 9.765 0 0 1-7.478-6.816ZM13.878 2.43a9.755 9.755 0 0 1 6.116 3.986 11.267 11.267 0 0 1-3.746 2.504 18.63 18.63 0 0 0-2.37-6.49ZM12 2.276a17.152 17.152 0 0 1 2.805 7.121c-.897.23-1.837.353-2.805.353-.968 0-1.908-.122-2.805-.353A17.151 17.151 0 0 1 12 2.276ZM10.122 2.43a18.629 18.629 0 0 0-2.37 6.49 11.266 11.266 0 0 1-3.746-2.504 9.754 9.754 0 0 1 6.116-3.985Z" + /> +</svg> diff --git a/src/lib/components/icons/Headphone.svelte b/src/lib/components/icons/Headphone.svelte new file mode 100644 index 0000000000000000000000000000000000000000..10902a7cced4fb8dfae3cb2e3eecd802529c0b30 --- /dev/null +++ b/src/lib/components/icons/Headphone.svelte @@ -0,0 +1,20 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '0'; +</script> + +<svg + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + fill="currentColor" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + fill-rule="evenodd" + d="M12 5a7 7 0 0 0-7 7v1.17c.313-.11.65-.17 1-.17h2a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H6a3 3 0 0 1-3-3v-6a9 9 0 0 1 18 0v6a3 3 0 0 1-3 3h-2a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1h2c.35 0 .687.06 1 .17V12a7 7 0 0 0-7-7Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/Heart.svelte b/src/lib/components/icons/Heart.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ba042ff6552108af5ec0e3aa75cdfb7a868c631c --- /dev/null +++ b/src/lib/components/icons/Heart.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z" + /> +</svg> diff --git a/src/lib/components/icons/Keyboard.svelte b/src/lib/components/icons/Keyboard.svelte new file mode 100644 index 0000000000000000000000000000000000000000..baf633c0d6c4b998557335472015f3df8ddf2027 --- /dev/null +++ b/src/lib/components/icons/Keyboard.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '2'; +</script> + +<svg + aria-hidden="true" + xmlns="http://www.w3.org/2000/svg" + fill="currentColor" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + class={className} +> + <path + fill-rule="evenodd" + d="M2 7a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7Zm5.01 1H5v2.01h2.01V8Zm3 0H8v2.01h2.01V8Zm3 0H11v2.01h2.01V8Zm3 0H14v2.01h2.01V8Zm3 0H17v2.01h2.01V8Zm-12 3H5v2.01h2.01V11Zm3 0H8v2.01h2.01V11Zm3 0H11v2.01h2.01V11Zm3 0H14v2.01h2.01V11Zm3 0H17v2.01h2.01V11Zm-12 3H5v2.01h2.01V14ZM8 14l-.001 2 8.011.01V14H8Zm11.01 0H17v2.01h2.01V14Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/Lifebuoy.svelte b/src/lib/components/icons/Lifebuoy.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d8c49b0ae3733e58eac0774bdb1d841097bbc8e0 --- /dev/null +++ b/src/lib/components/icons/Lifebuoy.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '2'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.712 4.33a9.027 9.027 0 0 1 1.652 1.306c.51.51.944 1.064 1.306 1.652M16.712 4.33l-3.448 4.138m3.448-4.138a9.014 9.014 0 0 0-9.424 0M19.67 7.288l-4.138 3.448m4.138-3.448a9.014 9.014 0 0 1 0 9.424m-4.138-5.976a3.736 3.736 0 0 0-.88-1.388 3.737 3.737 0 0 0-1.388-.88m2.268 2.268a3.765 3.765 0 0 1 0 2.528m-2.268-4.796a3.765 3.765 0 0 0-2.528 0m4.796 4.796c-.181.506-.475.982-.88 1.388a3.736 3.736 0 0 1-1.388.88m2.268-2.268 4.138 3.448m0 0a9.027 9.027 0 0 1-1.306 1.652c-.51.51-1.064.944-1.652 1.306m0 0-3.448-4.138m3.448 4.138a9.014 9.014 0 0 1-9.424 0m5.976-4.138a3.765 3.765 0 0 1-2.528 0m0 0a3.736 3.736 0 0 1-1.388-.88 3.737 3.737 0 0 1-.88-1.388m2.268 2.268L7.288 19.67m0 0a9.024 9.024 0 0 1-1.652-1.306 9.027 9.027 0 0 1-1.306-1.652m0 0 4.138-3.448M4.33 16.712a9.014 9.014 0 0 1 0-9.424m4.138 5.976a3.765 3.765 0 0 1 0-2.528m0 0c.181-.506.475-.982.88-1.388a3.736 3.736 0 0 1 1.388-.88m-2.268 2.268L4.33 7.288m6.406 1.18L7.288 4.33m0 0a9.024 9.024 0 0 0-1.652 1.306A9.025 9.025 0 0 0 4.33 7.288" + /> +</svg> diff --git a/src/lib/components/icons/Link.svelte b/src/lib/components/icons/Link.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9f1a7231105c131d01f76aaa115546af7618401a --- /dev/null +++ b/src/lib/components/icons/Link.svelte @@ -0,0 +1,16 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}> + <path + fill-rule="evenodd" + d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/MagnifyingGlass.svelte b/src/lib/components/icons/MagnifyingGlass.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a61186b5d0f40ce156632a8c3172e5a79a72816f --- /dev/null +++ b/src/lib/components/icons/MagnifyingGlass.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-4'; + export let strokeWidth = '2'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" + /> +</svg> diff --git a/src/lib/components/icons/MenuLines.svelte b/src/lib/components/icons/MenuLines.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c2b27c45b72ca615c5d8608986aead7983fcd7e9 --- /dev/null +++ b/src/lib/components/icons/MenuLines.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'size-5'; + export let strokeWidth = '2'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" + /> +</svg> diff --git a/src/lib/components/icons/Pencil.svelte b/src/lib/components/icons/Pencil.svelte new file mode 100644 index 0000000000000000000000000000000000000000..42b60916c4bb4aaf2d71c9684e764dd85fb54e51 --- /dev/null +++ b/src/lib/components/icons/Pencil.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> +</svg> diff --git a/src/lib/components/icons/Plus.svelte b/src/lib/components/icons/Plus.svelte new file mode 100644 index 0000000000000000000000000000000000000000..bcfe4a8b23a9e78c62eece472f840a20f4b269e8 --- /dev/null +++ b/src/lib/components/icons/Plus.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> +</svg> diff --git a/src/lib/components/icons/QuestionMarkCircle.svelte b/src/lib/components/icons/QuestionMarkCircle.svelte new file mode 100644 index 0000000000000000000000000000000000000000..79c2e7d84c2e7a2f1271cfc0dcc644cbdf24127b --- /dev/null +++ b/src/lib/components/icons/QuestionMarkCircle.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '2'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" + /> +</svg> diff --git a/src/lib/components/icons/Search.svelte b/src/lib/components/icons/Search.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c2dc55845d54f1ad900ea75f3be5674b264d1955 --- /dev/null +++ b/src/lib/components/icons/Search.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" + /> +</svg> diff --git a/src/lib/components/icons/Share.svelte b/src/lib/components/icons/Share.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f098995c68da47b9261d492033b60780a033c587 --- /dev/null +++ b/src/lib/components/icons/Share.svelte @@ -0,0 +1,11 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}> + <path + fill-rule="evenodd" + d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/Sparkles.svelte b/src/lib/components/icons/Sparkles.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0f9034d261fa9f424a2e5b74d0366e0eeb539361 --- /dev/null +++ b/src/lib/components/icons/Sparkles.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M9.813 15.904 9 18.75l-.813-2.846a4.5 4.5 0 0 0-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 0 0 3.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 0 0 3.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 0 0-3.09 3.09ZM18.259 8.715 18 9.75l-.259-1.035a3.375 3.375 0 0 0-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 0 0 2.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 0 0 2.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 0 0-2.456 2.456ZM16.894 20.567 16.5 21.75l-.394-1.183a2.25 2.25 0 0 0-1.423-1.423L13.5 18.75l1.183-.394a2.25 2.25 0 0 0 1.423-1.423l.394-1.183.394 1.183a2.25 2.25 0 0 0 1.423 1.423l1.183.394-1.183.394a2.25 2.25 0 0 0-1.423 1.423Z" + /> +</svg> diff --git a/src/lib/components/icons/Star.svelte b/src/lib/components/icons/Star.svelte new file mode 100644 index 0000000000000000000000000000000000000000..45faf808b679a4f2c9485eeeeab250de8b1ba2b7 --- /dev/null +++ b/src/lib/components/icons/Star.svelte @@ -0,0 +1,19 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; + export let strokeWidth = '1.5'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z" + /> +</svg> diff --git a/src/lib/components/icons/User.svelte b/src/lib/components/icons/User.svelte new file mode 100644 index 0000000000000000000000000000000000000000..71a87a2f52b1b194d9d72779a227464eb155f0d5 --- /dev/null +++ b/src/lib/components/icons/User.svelte @@ -0,0 +1,11 @@ +<script lang="ts"> + export let className = 'w-4 h-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}> + <path + fill-rule="evenodd" + d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/WrenchSolid.svelte b/src/lib/components/icons/WrenchSolid.svelte new file mode 100644 index 0000000000000000000000000000000000000000..66a2113018376da328c13f146ebbadf8c5511794 --- /dev/null +++ b/src/lib/components/icons/WrenchSolid.svelte @@ -0,0 +1,11 @@ +<script lang="ts"> + export let className = 'size-4'; +</script> + +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}> + <path + fill-rule="evenodd" + d="M12 6.75a5.25 5.25 0 0 1 6.775-5.025.75.75 0 0 1 .313 1.248l-3.32 3.319c.063.475.276.934.641 1.299.365.365.824.578 1.3.64l3.318-3.319a.75.75 0 0 1 1.248.313 5.25 5.25 0 0 1-5.472 6.756c-1.018-.086-1.87.1-2.309.634L7.344 21.3A3.298 3.298 0 1 1 2.7 16.657l8.684-7.151c.533-.44.72-1.291.634-2.309A5.342 5.342 0 0 1 12 6.75ZM4.117 19.125a.75.75 0 0 1 .75-.75h.008a.75.75 0 0 1 .75.75v.008a.75.75 0 0 1-.75.75h-.008a.75.75 0 0 1-.75-.75v-.008Z" + clip-rule="evenodd" + /> +</svg> diff --git a/src/lib/components/icons/XMark.svelte b/src/lib/components/icons/XMark.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b75be506ca797fdbb687b27be4118dadaeef4d4e --- /dev/null +++ b/src/lib/components/icons/XMark.svelte @@ -0,0 +1,15 @@ +<script lang="ts"> + export let className = 'size-3.5'; + export let strokeWidth = '2'; +</script> + +<svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width={strokeWidth} + stroke="currentColor" + class={className} +> + <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" /> +</svg> diff --git a/src/lib/components/layout/Help.svelte b/src/lib/components/layout/Help.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9e0a18582e54ff1b2e32a0d30b9d730f12ee3272 --- /dev/null +++ b/src/lib/components/layout/Help.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + import { onMount, tick, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + import ShortcutsModal from '../chat/ShortcutsModal.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + import HelpMenu from './Help/HelpMenu.svelte'; + + let showShortcuts = false; +</script> + +<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-20"> + <button + id="show-shortcuts-button" + class="hidden" + on:click={() => { + showShortcuts = !showShortcuts; + }} + /> + + <HelpMenu + showDocsHandler={() => { + showShortcuts = !showShortcuts; + }} + showShortcutsHandler={() => { + showShortcuts = !showShortcuts; + }} + > + <Tooltip content={$i18n.t('Help')} placement="left"> + <button + class="text-gray-600 dark:text-gray-300 bg-gray-300/20 size-5 flex items-center justify-center text-[0.7rem] rounded-full" + > + ? + </button> + </Tooltip> + </HelpMenu> +</div> + +<ShortcutsModal bind:show={showShortcuts} /> diff --git a/src/lib/components/layout/Help/HelpMenu.svelte b/src/lib/components/layout/Help/HelpMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..7371f629c86e9079288f2a68610aec124198cfba --- /dev/null +++ b/src/lib/components/layout/Help/HelpMenu.svelte @@ -0,0 +1,60 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { getContext } from 'svelte'; + + import { showSettings } from '$lib/stores'; + import { flyAndScale } from '$lib/utils/transitions'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte'; + import Lifebuoy from '$lib/components/icons/Lifebuoy.svelte'; + import Keyboard from '$lib/components/icons/Keyboard.svelte'; + const i18n = getContext('i18n'); + + export let showDocsHandler: Function; + export let showShortcutsHandler: Function; + + export let onClose: Function = () => {}; +</script> + +<Dropdown + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <slot /> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg" + sideOffset={4} + side="top" + align="end" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + id="chat-share-button" + on:click={() => { + window.open('https://docs.openwebui.com', '_blank'); + }} + > + <QuestionMarkCircle className="size-5" /> + <div class="flex items-center">{$i18n.t('Documentation')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + id="chat-share-button" + on:click={() => { + showShortcutsHandler(); + }} + > + <Keyboard className="size-5" /> + <div class="flex items-center">{$i18n.t('Keyboard shortcuts')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/layout/Navbar.svelte b/src/lib/components/layout/Navbar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8b5e94e89e9fedfbf07951685fe49ebe3f49ca38 --- /dev/null +++ b/src/lib/components/layout/Navbar.svelte @@ -0,0 +1,177 @@ +<script lang="ts"> + import { getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + import { + WEBUI_NAME, + chatId, + mobile, + settings, + showArchivedChats, + showSettings, + showSidebar, + user + } from '$lib/stores'; + + import { slide } from 'svelte/transition'; + import ShareChatModal from '../chat/ShareChatModal.svelte'; + import ModelSelector from '../chat/ModelSelector.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + import Menu from './Navbar/Menu.svelte'; + import { page } from '$app/stores'; + import UserMenu from './Sidebar/UserMenu.svelte'; + import MenuLines from '../icons/MenuLines.svelte'; + import AdjustmentsHorizontal from '../icons/AdjustmentsHorizontal.svelte'; + + const i18n = getContext('i18n'); + + export let initNewChat: Function; + export let title: string = $WEBUI_NAME; + export let shareEnabled: boolean = false; + + export let chat; + export let selectedModels; + + export let showModelSelector = true; + export let showControls = false; + + let showShareChatModal = false; + let showDownloadChatModal = false; +</script> + +<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} /> +<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-10"> + <div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]"> + <div class="flex items-center w-full max-w-full"> + <div + class="{$showSidebar + ? 'md:hidden' + : ''} mr-3 self-start flex flex-none items-center text-gray-600 dark:text-gray-400" + > + <button + id="sidebar-toggle-button" + class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition" + on:click={() => { + showSidebar.set(!$showSidebar); + }} + > + <div class=" m-auto self-center"> + <MenuLines /> + </div> + </button> + </div> + + <div class="flex-1 overflow-hidden max-w-full"> + {#if showModelSelector} + <ModelSelector bind:selectedModels showSetDefault={!shareEnabled} /> + {/if} + </div> + + <div class="self-start flex flex-none items-center text-gray-600 dark:text-gray-400"> + <!-- <div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" /> --> + + {#if shareEnabled && chat && chat.id} + <Menu + {chat} + {shareEnabled} + shareHandler={() => { + showShareChatModal = !showShareChatModal; + }} + downloadHandler={() => { + showDownloadChatModal = !showDownloadChatModal; + }} + > + <button + class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition" + id="chat-context-menu-button" + > + <div class=" m-auto self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" + /> + </svg> + </div> + </button> + </Menu> + {/if} + + <Tooltip content={$i18n.t('Controls')}> + <button + class=" flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-50 dark:hover:bg-gray-850 transition" + on:click={() => { + showControls = !showControls; + }} + > + <div class=" m-auto self-center"> + <AdjustmentsHorizontal className=" size-5" strokeWidth="0.5" /> + </div> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('New Chat')}> + <button + id="new-chat-button" + class=" flex {$showSidebar + ? 'md:hidden' + : ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition" + on:click={() => { + initNewChat(); + }} + > + <div class=" m-auto self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" + /> + <path + d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" + /> + </svg> + </div> + </button> + </Tooltip> + + {#if $user !== undefined} + <UserMenu + className="max-w-[200px]" + role={$user.role} + on:show={(e) => { + if (e.detail === 'archived-chat') { + showArchivedChats.set(true); + } + }} + > + <button + class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition" + aria-label="User Menu" + > + <div class=" self-center"> + <img + src={$user.profile_image_url} + class="size-6 object-cover rounded-full" + alt="User profile" + draggable="false" + /> + </div> + </button> + </UserMenu> + {/if} + </div> + </div> + </div> +</nav> diff --git a/src/lib/components/layout/Navbar/Menu.svelte b/src/lib/components/layout/Navbar/Menu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..03e36d8e9c42cdb0aa03073eee3009a1b963d906 --- /dev/null +++ b/src/lib/components/layout/Navbar/Menu.svelte @@ -0,0 +1,208 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { getContext } from 'svelte'; + + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { showSettings } from '$lib/stores'; + import { flyAndScale } from '$lib/utils/transitions'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + + import { downloadChatAsPDF } from '$lib/apis/utils'; + + const i18n = getContext('i18n'); + + export let shareEnabled: boolean = false; + export let shareHandler: Function; + export let downloadHandler: Function; + + // export let tagHandler: Function; + + export let chat; + export let onClose: Function = () => {}; + + const downloadTxt = async () => { + const _chat = chat.chat; + console.log('download', chat); + + const chatText = _chat.messages.reduce((a, message, i, arr) => { + return `${a}### ${message.role.toUpperCase()}\n${message.content}\n\n`; + }, ''); + + let blob = new Blob([chatText], { + type: 'text/plain' + }); + + saveAs(blob, `chat-${_chat.title}.txt`); + }; + + const downloadPdf = async () => { + const _chat = chat.chat; + console.log('download', chat); + + const blob = await downloadChatAsPDF(_chat); + + // Create a URL for the blob + const url = window.URL.createObjectURL(blob); + + // Create a link element to trigger the download + const a = document.createElement('a'); + a.href = url; + a.download = `chat-${_chat.title}.pdf`; + + // Append the link to the body and click it programmatically + document.body.appendChild(a); + a.click(); + + // Remove the link from the body + document.body.removeChild(a); + + // Revoke the URL to release memory + window.URL.revokeObjectURL(url); + }; + + const downloadJSONExport = async () => { + let blob = new Blob([JSON.stringify([chat])], { + type: 'application/json' + }); + saveAs(blob, `chat-export-${Date.now()}.json`); + }; +</script> + +<Dropdown + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <slot /> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[200px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg" + sideOffset={8} + side="bottom" + align="end" + transition={flyAndScale} + > + <!-- <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-800 rounded-md" + on:click={async () => { + await showSettings.set(!$showSettings); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> + </svg> + <div class="flex items-center">{$i18n.t('Settings')}</div> + </DropdownMenu.Item> --> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + id="chat-share-button" + on:click={() => { + shareHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + fill-rule="evenodd" + d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z" + clip-rule="evenodd" + /> + </svg> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + <!-- <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" + on:click={() => { + downloadHandler(); + }} + /> --> + <DropdownMenu.Sub> + <DropdownMenu.SubTrigger + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + > + <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="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" + /> + </svg> + + <div class="flex items-center">{$i18n.t('Download')}</div> + </DropdownMenu.SubTrigger> + <DropdownMenu.SubContent + class="w-full rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg" + transition={flyAndScale} + sideOffset={8} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + downloadJSONExport(); + }} + > + <div class="flex items-center line-clamp-1">{$i18n.t('Export chat (.json)')}</div> + </DropdownMenu.Item> + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + downloadTxt(); + }} + > + <div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + downloadPdf(); + }} + > + <div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div> + </DropdownMenu.Item> + </DropdownMenu.SubContent> + </DropdownMenu.Sub> + + <hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" /> + + <div class="flex p-1"> + <Tags chatId={chat.id} /> + </div> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/layout/Overlay/AccountPending.svelte b/src/lib/components/layout/Overlay/AccountPending.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c272963e9b8738ba50645a2024046845590f9913 --- /dev/null +++ b/src/lib/components/layout/Overlay/AccountPending.svelte @@ -0,0 +1,62 @@ +<script lang="ts"> + import { getAdminDetails } from '$lib/apis/auths'; + import { onMount, tick, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + let adminDetails = null; + + onMount(async () => { + adminDetails = await getAdminDetails(localStorage.token).catch((err) => { + console.error(err); + return null; + }); + }); +</script> + +<div class="fixed w-full h-full flex z-[999]"> + <div + class="absolute w-full h-full backdrop-blur-lg bg-white/10 dark:bg-gray-900/50 flex justify-center" + > + <div class="m-auto pb-10 flex flex-col justify-center"> + <div class="max-w-md"> + <div class="text-center dark:text-white text-2xl font-medium z-50"> + {$i18n.t('Account Activation Pending')}<br /> + {$i18n.t('Contact Admin for WebUI Access')} + </div> + + <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full"> + {$i18n.t('Your account status is currently pending activation.')}<br /> + {$i18n.t( + 'To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.' + )} + </div> + + {#if adminDetails} + <div class="mt-4 text-sm font-medium text-center"> + <div>{$i18n.t('Admin')}: {adminDetails.name} ({adminDetails.email})</div> + </div> + {/if} + + <div class=" mt-6 mx-auto relative group w-fit"> + <button + class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 text-gray-700 transition font-medium text-sm" + on:click={async () => { + location.href = '/'; + }} + > + {$i18n.t('Check Again')} + </button> + + <button + class="text-xs text-center w-full mt-2 text-gray-400 underline" + on:click={async () => { + localStorage.removeItem('token'); + location.href = '/auth'; + }}>{$i18n.t('Sign Out')}</button + > + </div> + </div> + </div> + </div> +</div> diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte new file mode 100644 index 0000000000000000000000000000000000000000..49a0e2ebba55d6e1aa448ef8a94b21925e7d3939 --- /dev/null +++ b/src/lib/components/layout/Sidebar.svelte @@ -0,0 +1,577 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { goto } from '$app/navigation'; + import { + user, + chats, + settings, + showSettings, + chatId, + tags, + showSidebar, + mobile, + showArchivedChats, + pinnedChats + } from '$lib/stores'; + import { onMount, getContext, tick } from 'svelte'; + + const i18n = getContext('i18n'); + + import { updateUserSettings } from '$lib/apis/users'; + import { + deleteChatById, + getChatList, + getChatById, + getChatListByTagName, + updateChatById, + getAllChatTags, + archiveChatById, + cloneChatById + } from '$lib/apis/chats'; + import { WEBUI_BASE_URL } from '$lib/constants'; + + import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte'; + import UserMenu from './Sidebar/UserMenu.svelte'; + import ChatItem from './Sidebar/ChatItem.svelte'; + import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const BREAKPOINT = 768; + + let navElement; + let search = ''; + + let shiftKey = false; + + let selectedChatId = null; + let deleteChat = null; + + let showDeleteConfirm = false; + let showDropdown = false; + + let filteredChatList = []; + + $: filteredChatList = $chats.filter((chat) => { + if (search === '') { + return true; + } else { + let title = chat.title.toLowerCase(); + const query = search.toLowerCase(); + + let contentMatches = false; + // Access the messages within chat.chat.messages + if (chat.chat && chat.chat.messages && Array.isArray(chat.chat.messages)) { + contentMatches = chat.chat.messages.some((message) => { + // Check if message.content exists and includes the search query + return message.content && message.content.toLowerCase().includes(query); + }); + } + + return title.includes(query) || contentMatches; + } + }); + + onMount(async () => { + mobile.subscribe((e) => { + if ($showSidebar && e) { + showSidebar.set(false); + } + + if (!$showSidebar && !e) { + showSidebar.set(true); + } + }); + + showSidebar.set(window.innerWidth > BREAKPOINT); + + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + await chats.set(await getChatList(localStorage.token)); + + let touchstart; + let touchend; + + function checkDirection() { + const screenWidth = window.innerWidth; + const swipeDistance = Math.abs(touchend.screenX - touchstart.screenX); + if (touchstart.clientX < 40 && swipeDistance >= screenWidth / 8) { + if (touchend.screenX < touchstart.screenX) { + showSidebar.set(false); + } + if (touchend.screenX > touchstart.screenX) { + showSidebar.set(true); + } + } + } + + const onTouchStart = (e) => { + touchstart = e.changedTouches[0]; + console.log(touchstart.clientX); + }; + + const onTouchEnd = (e) => { + touchend = e.changedTouches[0]; + checkDirection(); + }; + + const onKeyDown = (e) => { + if (e.key === 'Shift') { + shiftKey = true; + } + }; + + const onKeyUp = (e) => { + if (e.key === 'Shift') { + shiftKey = false; + } + }; + + const onFocus = () => {}; + + const onBlur = () => { + shiftKey = false; + selectedChatId = null; + }; + + window.addEventListener('keydown', onKeyDown); + window.addEventListener('keyup', onKeyUp); + + window.addEventListener('touchstart', onTouchStart); + window.addEventListener('touchend', onTouchEnd); + + window.addEventListener('focus', onFocus); + window.addEventListener('blur', onBlur); + + return () => { + window.removeEventListener('keydown', onKeyDown); + window.removeEventListener('keyup', onKeyUp); + + window.removeEventListener('touchstart', onTouchStart); + window.removeEventListener('touchend', onTouchEnd); + + window.removeEventListener('focus', onFocus); + window.removeEventListener('blur', onBlur); + }; + }); + + // Helper function to fetch and add chat content to each chat + const enrichChatsWithContent = async (chatList) => { + const enrichedChats = await Promise.all( + chatList.map(async (chat) => { + const chatDetails = await getChatById(localStorage.token, chat.id).catch((error) => null); // Handle error or non-existent chat gracefully + if (chatDetails) { + chat.chat = chatDetails.chat; // Assuming chatDetails.chat contains the chat content + } + return chat; + }) + ); + + await chats.set(enrichedChats); + }; + + const saveSettings = async (updated) => { + await settings.set({ ...$settings, ...updated }); + await updateUserSettings(localStorage.token, { ui: $settings }); + location.href = '/'; + }; + + const deleteChatHandler = async (id) => { + const res = await deleteChatById(localStorage.token, id).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + if ($chatId === id) { + await chatId.set(''); + await tick(); + goto('/'); + } + await chats.set(await getChatList(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + } + }; +</script> + +<ArchivedChatsModal + bind:show={$showArchivedChats} + on:change={async () => { + await chats.set(await getChatList(localStorage.token)); + }} +/> + +<DeleteConfirmDialog + bind:show={showDeleteConfirm} + title={$i18n.t('Delete chat?')} + on:confirm={() => { + deleteChatHandler(deleteChat.id); + }} +> + <div class=" text-sm text-gray-500"> + {$i18n.t('This will delete')} <span class=" font-semibold">{deleteChat.title}</span>. + </div> +</DeleteConfirmDialog> + +<!-- svelte-ignore a11y-no-static-element-interactions --> + +{#if $showSidebar} + <div + class=" fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain" + on:mousedown={() => { + showSidebar.set(!$showSidebar); + }} + /> +{/if} + +<div + bind:this={navElement} + id="sidebar" + class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar + ? 'md:relative w-[260px]' + : '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl + " + data-state={$showSidebar} +> + <div + class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar + ? '' + : 'invisible'}" + > + <div class="px-2.5 flex justify-between space-x-1 text-gray-600 dark:text-gray-400"> + <a + id="sidebar-new-chat-button" + class="flex flex-1 justify-between rounded-xl px-2 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition" + href="/" + draggable="false" + on:click={async () => { + selectedChatId = null; + await goto('/'); + const newChatButton = document.getElementById('new-chat-button'); + setTimeout(() => { + newChatButton?.click(); + if ($mobile) { + showSidebar.set(false); + } + }, 0); + }} + > + <div class="self-center mx-1.5"> + <img + crossorigin="anonymous" + src="{WEBUI_BASE_URL}/static/favicon.png" + class=" size-6 -translate-x-1.5 rounded-full" + alt="logo" + /> + </div> + <div class=" self-center font-medium text-sm text-gray-850 dark:text-white font-primary"> + {$i18n.t('New Chat')} + </div> + <div class="self-center ml-auto"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="size-5" + > + <path + d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z" + /> + <path + d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z" + /> + </svg> + </div> + </a> + + <button + class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-900 transition" + on:click={() => { + showSidebar.set(!$showSidebar); + }} + > + <div class=" m-auto self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" + /> + </svg> + </div> + </button> + </div> + + {#if $user?.role === 'admin'} + <div class="px-2.5 flex justify-center text-gray-800 dark:text-gray-200"> + <a + class="flex-grow flex space-x-3 rounded-xl px-2.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition" + href="/workspace" + on:click={() => { + selectedChatId = null; + chatId.set(''); + + if ($mobile) { + showSidebar.set(false); + } + }} + draggable="false" + > + <div class="self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="size-[1.1rem]" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z" + /> + </svg> + </div> + + <div class="flex self-center"> + <div class=" self-center font-medium text-sm font-primary">{$i18n.t('Workspace')}</div> + </div> + </a> + </div> + {/if} + + <div class="relative flex flex-col flex-1 overflow-y-auto"> + {#if !($settings.saveChatHistory ?? true)} + <div class="absolute z-40 w-full h-full bg-gray-50/90 dark:bg-black/90 flex justify-center"> + <div class=" text-left px-5 py-2"> + <div class=" font-medium">{$i18n.t('Chat History is off for this browser.')}</div> + <div class="text-xs mt-2"> + {$i18n.t( + "When history is turned off, new chats on this browser won't appear in your history on any of your devices." + )} + <span class=" font-semibold" + >{$i18n.t('This setting does not sync across browsers or devices.')}</span + > + </div> + + <div class="mt-3"> + <button + class="flex justify-center items-center space-x-1.5 px-3 py-2.5 rounded-lg text-xs bg-gray-100 hover:bg-gray-200 transition text-gray-800 font-medium w-full" + type="button" + on:click={() => { + saveSettings({ + saveChatHistory: true + }); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-3 h-3" + > + <path + fill-rule="evenodd" + d="M8 1a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0v-6.5A.75.75 0 0 1 8 1ZM4.11 3.05a.75.75 0 0 1 0 1.06 5.5 5.5 0 1 0 7.78 0 .75.75 0 0 1 1.06-1.06 7 7 0 1 1-9.9 0 .75.75 0 0 1 1.06 0Z" + clip-rule="evenodd" + /> + </svg> + + <div>{$i18n.t('Enable Chat History')}</div> + </button> + </div> + </div> + </div> + {/if} + + <div class="px-2 mt-0.5 mb-2 flex justify-center space-x-2"> + <div class="flex w-full rounded-xl" id="chat-search"> + <div class="self-center pl-3 py-2 rounded-l-xl bg-transparent"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + + <input + class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm bg-transparent dark:text-gray-300 outline-none" + placeholder={$i18n.t('Search')} + bind:value={search} + on:focus={() => { + enrichChatsWithContent($chats); + }} + /> + </div> + </div> + + {#if $tags.filter((t) => t.name !== 'pinned').length > 0} + <div class="px-2.5 mb-2 flex gap-1 flex-wrap"> + <button + class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full" + on:click={async () => { + await chats.set(await getChatList(localStorage.token)); + }} + > + {$i18n.t('all')} + </button> + {#each $tags.filter((t) => t.name !== 'pinned') as tag} + <button + class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full" + on:click={async () => { + let chatIds = await getChatListByTagName(localStorage.token, tag.name); + if (chatIds.length === 0) { + await tags.set(await getAllChatTags(localStorage.token)); + chatIds = await getChatList(localStorage.token); + } + await chats.set(chatIds); + }} + > + {tag.name} + </button> + {/each} + </div> + {/if} + + {#if $pinnedChats.length > 0} + <div class="pl-2 py-2 flex flex-col space-y-1"> + <div class=""> + <div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5"> + {$i18n.t('Pinned')} + </div> + + {#each $pinnedChats as chat, idx} + <ChatItem + {chat} + {shiftKey} + selected={selectedChatId === chat.id} + on:select={() => { + selectedChatId = chat.id; + }} + on:unselect={() => { + selectedChatId = null; + }} + on:delete={(e) => { + if ((e?.detail ?? '') === 'shift') { + deleteChatHandler(chat.id); + } else { + deleteChat = chat; + showDeleteConfirm = true; + } + }} + /> + {/each} + </div> + </div> + {/if} + + <div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden"> + {#each filteredChatList as chat, idx} + {#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)} + <div + class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0 + ? '' + : 'pt-5'} pb-0.5" + > + {$i18n.t(chat.time_range)} + <!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed): + {$i18n.t('Today')} + {$i18n.t('Yesterday')} + {$i18n.t('Previous 7 days')} + {$i18n.t('Previous 30 days')} + {$i18n.t('January')} + {$i18n.t('February')} + {$i18n.t('March')} + {$i18n.t('April')} + {$i18n.t('May')} + {$i18n.t('June')} + {$i18n.t('July')} + {$i18n.t('August')} + {$i18n.t('September')} + {$i18n.t('October')} + {$i18n.t('November')} + {$i18n.t('December')} + --> + </div> + {/if} + + <ChatItem + {chat} + {shiftKey} + selected={selectedChatId === chat.id} + on:select={() => { + selectedChatId = chat.id; + }} + on:unselect={() => { + selectedChatId = null; + }} + on:delete={(e) => { + if ((e?.detail ?? '') === 'shift') { + deleteChatHandler(chat.id); + } else { + deleteChat = chat; + showDeleteConfirm = true; + } + }} + /> + {/each} + </div> + </div> + + <div class="px-2.5"> + <!-- <hr class=" border-gray-900 mb-1 w-full" /> --> + + <div class="flex flex-col font-primary"> + {#if $user !== undefined} + <UserMenu + role={$user.role} + on:show={(e) => { + if (e.detail === 'archived-chat') { + showArchivedChats.set(true); + } + }} + > + <button + class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition" + on:click={() => { + showDropdown = !showDropdown; + }} + > + <div class=" self-center mr-3"> + <img + src={$user.profile_image_url} + class=" max-w-[30px] object-cover rounded-full" + alt="User profile" + /> + </div> + <div class=" self-center font-medium">{$user.name}</div> + </button> + </UserMenu> + {/if} + </div> + </div> + </div> +</div> + +<style> + .scrollbar-hidden:active::-webkit-scrollbar-thumb, + .scrollbar-hidden:focus::-webkit-scrollbar-thumb, + .scrollbar-hidden:hover::-webkit-scrollbar-thumb { + visibility: visible; + } + .scrollbar-hidden::-webkit-scrollbar-thumb { + visibility: hidden; + } +</style> diff --git a/src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte b/src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..80e3f1579040a266b35cab6f7e9927f0ad8b0597 --- /dev/null +++ b/src/lib/components/layout/Sidebar/ArchivedChatsModal.svelte @@ -0,0 +1,222 @@ +<script lang="ts"> + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + import { toast } from 'svelte-sonner'; + import dayjs from 'dayjs'; + import { getContext, createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import Modal from '$lib/components/common/Modal.svelte'; + import { + archiveChatById, + deleteChatById, + getAllArchivedChats, + getArchivedChatList + } from '$lib/apis/chats'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + + let searchValue = ''; + + let chats = []; + + const unarchiveChatHandler = async (chatId) => { + const res = await archiveChatById(localStorage.token, chatId).catch((error) => { + toast.error(error); + }); + + chats = await getArchivedChatList(localStorage.token); + + dispatch('change'); + }; + + const deleteChatHandler = async (chatId) => { + const res = await deleteChatById(localStorage.token, chatId).catch((error) => { + toast.error(error); + }); + + chats = await getArchivedChatList(localStorage.token); + }; + + const exportChatsHandler = async () => { + const chats = await getAllArchivedChats(localStorage.token); + let blob = new Blob([JSON.stringify(chats)], { + type: 'application/json' + }); + saveAs(blob, `archived-chat-export-${Date.now()}.json`); + }; + + $: if (show) { + (async () => { + chats = await getArchivedChatList(localStorage.token); + })(); + } +</script> + +<Modal size="lg" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1"> + <div class=" text-lg font-medium self-center">{$i18n.t('Archived Chats')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col w-full px-5 pb-4 dark:text-gray-200"> + <div class=" flex w-full mt-2 space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={searchValue} + placeholder={$i18n.t('Search Chats')} + /> + </div> + </div> + <hr class=" dark:border-gray-850 my-2" /> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + {#if chats.length > 0} + <div class="w-full"> + <div class="text-left text-sm w-full mb-3 max-h-[22rem] overflow-y-scroll"> + <div class="relative overflow-x-auto"> + <table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto"> + <thead + class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800" + > + <tr> + <th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th> + <th scope="col" class="px-3 py-2 hidden md:flex"> + {$i18n.t('Created At')} + </th> + <th scope="col" class="px-3 py-2 text-right" /> + </tr> + </thead> + <tbody> + {#each chats.filter((c) => searchValue === '' || c.title + .toLowerCase() + .includes(searchValue.toLowerCase())) as chat, idx} + <tr + class="bg-transparent {idx !== chats.length - 1 && + 'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs" + > + <td class="px-3 py-1 w-2/3"> + <a href="/c/{chat.id}" target="_blank"> + <div class=" underline line-clamp-1"> + {chat.title} + </div> + </a> + </td> + + <td class=" px-3 py-1 hidden md:flex h-[2.5rem]"> + <div class="my-auto"> + {dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))} + </div> + </td> + + <td class="px-3 py-1 text-right"> + <div class="flex justify-end w-full"> + <Tooltip content="Unarchive Chat"> + <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 () => { + unarchiveChatHandler(chat.id); + }} + > + <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="M9 8.25H7.5a2.25 2.25 0 0 0-2.25 2.25v9a2.25 2.25 0 0 0 2.25 2.25h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25H15m0-3-3-3m0 0-3 3m3-3V15" + /> + </svg> + </button> + </Tooltip> + + <Tooltip content="Delete Chat"> + <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 () => { + deleteChatHandler(chat.id); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </Tooltip> + </div> + </td> + </tr> + {/each} + </tbody> + </table> + </div> + </div> + + <div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2 m-1"> + <button + class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl" + on:click={() => { + exportChatsHandler(); + }}>Export All Archived Chats</button + > + </div> + </div> + {:else} + <div class="text-left text-sm w-full mb-8"> + {$i18n.t('You have no archived conversations.')} + </div> + {/if} + </div> + </div> + </div> +</Modal> diff --git a/src/lib/components/layout/Sidebar/ChatItem.svelte b/src/lib/components/layout/Sidebar/ChatItem.svelte new file mode 100644 index 0000000000000000000000000000000000000000..129c653fae43d4d8521926df2f1c612f32d27da1 --- /dev/null +++ b/src/lib/components/layout/Sidebar/ChatItem.svelte @@ -0,0 +1,288 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { goto, invalidate, invalidateAll } from '$app/navigation'; + import { onMount, getContext, createEventDispatcher, tick } from 'svelte'; + const i18n = getContext('i18n'); + + const dispatch = createEventDispatcher(); + + import { + archiveChatById, + cloneChatById, + deleteChatById, + getChatList, + getChatListByTagName, + updateChatById + } from '$lib/apis/chats'; + import { chatId, chats, mobile, pinnedChats, showSidebar } from '$lib/stores'; + + import ChatMenu from './ChatMenu.svelte'; + import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + + export let chat; + export let selected = false; + export let shiftKey = false; + + let mouseOver = false; + + let showShareChatModal = false; + let confirmEdit = false; + + let chatTitle = chat.title; + + const editChatTitle = async (id, _title) => { + if (_title === '') { + toast.error($i18n.t('Title cannot be an empty string.')); + } else { + await updateChatById(localStorage.token, id, { + title: _title + }); + await chats.set(await getChatList(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + } + }; + + const cloneChatHandler = async (id) => { + const res = await cloneChatById(localStorage.token, id).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + goto(`/c/${res.id}`); + await chats.set(await getChatList(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + } + }; + + const archiveChatHandler = async (id) => { + await archiveChatById(localStorage.token, id); + await chats.set(await getChatList(localStorage.token)); + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + }; + + const focusEdit = async (node: HTMLInputElement) => { + node.focus(); + }; +</script> + +<ShareChatModal bind:show={showShareChatModal} chatId={chat.id} /> + +<div class=" w-full pr-2 relative group"> + {#if confirmEdit} + <div + class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit + ? 'bg-gray-200 dark:bg-gray-900' + : selected + ? 'bg-gray-100 dark:bg-gray-950' + : 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis" + > + <input + use:focusEdit + bind:value={chatTitle} + class=" bg-transparent w-full outline-none mr-10" + /> + </div> + {:else} + <a + class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit + ? 'bg-gray-200 dark:bg-gray-900' + : selected + ? 'bg-gray-100 dark:bg-gray-950' + : ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis" + href="/c/{chat.id}" + on:click={() => { + dispatch('select'); + + if ($mobile) { + showSidebar.set(false); + } + }} + on:dblclick={() => { + chatTitle = chat.title; + confirmEdit = true; + }} + on:mouseenter={(e) => { + mouseOver = true; + }} + on:mouseleave={(e) => { + mouseOver = false; + }} + on:focus={(e) => {}} + draggable="false" + > + <div class=" flex self-center flex-1 w-full"> + <div class=" text-left self-center overflow-hidden w-full h-[20px]"> + {chat.title} + </div> + </div> + </a> + {/if} + + <!-- svelte-ignore a11y-no-static-element-interactions --> + <div + class=" + {chat.id === $chatId || confirmEdit + ? 'from-gray-200 dark:from-gray-900' + : selected + ? 'from-gray-100 dark:from-gray-950' + : 'invisible group-hover:visible from-gray-100 dark:from-gray-950'} + absolute right-[10px] top-[6px] py-1 pr-2 pl-5 bg-gradient-to-l from-80% + + to-transparent" + on:mouseenter={(e) => { + mouseOver = true; + }} + on:mouseleave={(e) => { + mouseOver = false; + }} + > + {#if confirmEdit} + <div class="flex self-center space-x-1.5 z-10"> + <Tooltip content={$i18n.t('Confirm')}> + <button + class=" self-center dark:hover:text-white transition" + on:click={() => { + editChatTitle(chat.id, chatTitle); + confirmEdit = false; + chatTitle = ''; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z" + clip-rule="evenodd" + /> + </svg> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Cancel')}> + <button + class=" self-center dark:hover:text-white transition" + on:click={() => { + confirmEdit = false; + chatTitle = ''; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </Tooltip> + </div> + {:else if shiftKey && mouseOver} + <div class=" flex items-center self-center space-x-1.5"> + <Tooltip content={$i18n.t('Archive')} className="flex items-center"> + <button + class=" self-center dark:hover:text-white transition" + on:click={() => { + archiveChatHandler(chat.id); + }} + type="button" + > + <ArchiveBox className="size-4 translate-y-[0.5px]" strokeWidth="2" /> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Delete')}> + <button + class=" self-center dark:hover:text-white transition" + on:click={() => { + dispatch('delete', 'shift'); + }} + type="button" + > + <GarbageBin strokeWidth="2" /> + </button> + </Tooltip> + </div> + {:else} + <div class="flex self-center space-x-1 z-10"> + <ChatMenu + chatId={chat.id} + cloneChatHandler={() => { + cloneChatHandler(chat.id); + }} + shareHandler={() => { + showShareChatModal = true; + }} + archiveChatHandler={() => { + archiveChatHandler(chat.id); + }} + renameHandler={() => { + chatTitle = chat.title; + + confirmEdit = true; + }} + deleteHandler={() => { + dispatch('delete'); + }} + onClose={() => { + dispatch('unselect'); + }} + on:change={async () => { + await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned')); + }} + > + <button + aria-label="Chat Menu" + class=" self-center dark:hover:text-white transition" + on:click={() => { + dispatch('select'); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z" + /> + </svg> + </button> + </ChatMenu> + + {#if chat.id === $chatId} + <!-- Shortcut support using "delete-chat-button" id --> + <button + id="delete-chat-button" + class="hidden" + on:click={() => { + dispatch('delete'); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z" + /> + </svg> + </button> + {/if} + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/layout/Sidebar/ChatMenu.svelte b/src/lib/components/layout/Sidebar/ChatMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6e16fa23ac4a0e0c980f2b54d4cd3e6f5c60238a --- /dev/null +++ b/src/lib/components/layout/Sidebar/ChatMenu.svelte @@ -0,0 +1,154 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext, createEventDispatcher } from 'svelte'; + + const dispatch = createEventDispatcher(); + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import Bookmark from '$lib/components/icons/Bookmark.svelte'; + import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte'; + import { addTagById, deleteTagById, getTagsById } from '$lib/apis/chats'; + + const i18n = getContext('i18n'); + + export let shareHandler: Function; + export let cloneChatHandler: Function; + export let archiveChatHandler: Function; + export let renameHandler: Function; + export let deleteHandler: Function; + export let onClose: Function; + + export let chatId = ''; + + let show = false; + let pinned = false; + + const pinHandler = async () => { + if (pinned) { + await deleteTagById(localStorage.token, chatId, 'pinned'); + } else { + await addTagById(localStorage.token, chatId, 'pinned'); + } + dispatch('change'); + }; + + const checkPinned = async () => { + pinned = ( + await getTagsById(localStorage.token, chatId).catch(async (error) => { + return []; + }) + ).find((tag) => tag.name === 'pinned'); + }; + + $: if (show) { + checkPinned(); + } +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[180px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + pinHandler(); + }} + > + {#if pinned} + <BookmarkSlash strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Unpin')}</div> + {:else} + <Bookmark strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Pin')}</div> + {/if} + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + renameHandler(); + }} + > + <Pencil strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Rename')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneChatHandler(); + }} + > + <DocumentDuplicate strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + archiveChatHandler(); + }} + > + <ArchiveBox strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Archive')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" /> + + <div class="flex p-1"> + <Tags + {chatId} + on:close={() => { + show = false; + onClose(); + }} + /> + </div> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/layout/Sidebar/UserMenu.svelte b/src/lib/components/layout/Sidebar/UserMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..6a2bf2294c31531c392f78cd6fc20fe433847e33 --- /dev/null +++ b/src/lib/components/layout/Sidebar/UserMenu.svelte @@ -0,0 +1,204 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { createEventDispatcher, getContext, onMount } from 'svelte'; + + import { flyAndScale } from '$lib/utils/transitions'; + import { goto } from '$app/navigation'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import { showSettings, activeUserCount, USAGE_POOL } from '$lib/stores'; + import { fade, slide } from 'svelte/transition'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let show = false; + export let role = ''; + export let className = 'max-w-[240px]'; + + const dispatch = createEventDispatcher(); +</script> + +<DropdownMenu.Root + bind:open={show} + onOpenChange={(state) => { + dispatch('change', state); + }} +> + <DropdownMenu.Trigger> + <slot /> + </DropdownMenu.Trigger> + + <slot name="content"> + <DropdownMenu.Content + class="w-full {className} text-sm rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow font-primary" + sideOffset={8} + side="bottom" + align="start" + transition={(e) => fade(e, { duration: 100 })} + > + <button + class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" + on:click={async () => { + await showSettings.set(true); + show = false; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" + /> + </svg> + </div> + <div class=" self-center font-medium">{$i18n.t('Settings')}</div> + </button> + + <button + class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" + on:click={() => { + dispatch('show', 'archived-chat'); + show = false; + }} + > + <div class=" self-center mr-3"> + <ArchiveBox className="size-5" strokeWidth="1.5" /> + </div> + <div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div> + </button> + + {#if role === 'admin'} + <button + class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" + on:click={() => { + goto('/playground'); + show = false; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z" + /> + </svg> + </div> + <div class=" self-center font-medium">{$i18n.t('Playground')}</div> + </button> + + <button + class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" + on:click={() => { + goto('/admin'); + show = false; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" + /> + </svg> + </div> + <div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div> + </button> + {/if} + + <hr class=" dark:border-gray-800 my-1.5 p-0" /> + + <button + class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition" + on:click={() => { + localStorage.removeItem('token'); + location.href = '/auth'; + show = false; + }} + > + <div class=" self-center mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + fill-rule="evenodd" + d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z" + clip-rule="evenodd" + /> + <path + fill-rule="evenodd" + d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium">{$i18n.t('Sign Out')}</div> + </button> + + {#if $activeUserCount} + <hr class=" dark:border-gray-800 my-1.5 p-0" /> + + <Tooltip + content={$USAGE_POOL && $USAGE_POOL.length > 0 + ? `${$i18n.t('Running')}: ${$USAGE_POOL.join(', ')} ✨` + : ''} + > + <div class="flex rounded-md py-1.5 px-3 text-xs gap-2.5 items-center"> + <div class=" flex items-center"> + <span class="relative flex size-2"> + <span + class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" + /> + <span class="relative inline-flex rounded-full size-2 bg-green-500" /> + </span> + </div> + + <div class=" "> + <span class=" font-medium"> + {$i18n.t('Active Users')}: + </span> + <span class=" font-semibold"> + {$activeUserCount} + </span> + </div> + </div> + </Tooltip> + {/if} + + <!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium"> + <div class="flex items-center">Profile</div> + </DropdownMenu.Item> --> + </DropdownMenu.Content> + </slot> +</DropdownMenu.Root> diff --git a/src/lib/components/playground/ChatCompletion.svelte b/src/lib/components/playground/ChatCompletion.svelte new file mode 100644 index 0000000000000000000000000000000000000000..25cc99ec41d77e58cfad4268e807c86e03cc1f3f --- /dev/null +++ b/src/lib/components/playground/ChatCompletion.svelte @@ -0,0 +1,111 @@ +<script lang="ts"> + import { onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + export let messages = []; + let textAreaElement: HTMLTextAreaElement; + onMount(() => { + messages.forEach((message, idx) => { + textAreaElement.style.height = ''; + textAreaElement.style.height = textAreaElement.scrollHeight + 'px'; + }); + }); +</script> + +<div class="py-3 space-y-3"> + {#each messages as message, idx} + <div class="flex gap-2 group"> + <div class="flex items-start pt-1"> + <button + class="px-2 py-1 text-sm font-semibold uppercase min-w-[6rem] text-left dark:group-hover:bg-gray-800 rounded-lg transition" + on:click={() => { + message.role = message.role === 'user' ? 'assistant' : 'user'; + }}>{$i18n.t(message.role)}</button + > + </div> + + <div class="flex-1"> + <!-- $i18n.t('a user') --> + <!-- $i18n.t('an assistant') --> + <textarea + id="{message.role}-{idx}-textarea" + bind:this={textAreaElement} + class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden" + placeholder={$i18n.t(`Enter {{role}} message here`, { + role: message.role === 'user' ? $i18n.t('a user') : $i18n.t('an assistant') + })} + rows="1" + on:input={(e) => { + textAreaElement.style.height = ''; + textAreaElement.style.height = textAreaElement.scrollHeight + 'px'; + }} + on:focus={(e) => { + textAreaElement.style.height = ''; + textAreaElement.style.height = textAreaElement.scrollHeight + 'px'; + + // e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; + }} + bind:value={message.content} + /> + </div> + + <div class=" pt-1"> + <button + class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition" + on:click={() => { + messages = messages.filter((message, messageIdx) => messageIdx !== idx); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="2" + stroke="currentColor" + class="w-5 h-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + </svg> + </button> + </div> + </div> + + <hr class=" dark:border-gray-800" /> + {/each} + + <button + class="flex items-center gap-2 px-2 py-1" + on:click={() => { + console.log(messages.at(-1)); + messages.push({ + role: (messages.at(-1)?.role ?? 'assistant') === 'user' ? 'assistant' : 'user', + content: '' + }); + messages = messages; + }} + > + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-5 h-5" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" + /> + </svg> + </div> + + <div class=" text-sm font-medium">{$i18n.t('Add message')}</div> + </button> +</div> diff --git a/src/lib/components/playground/Playground.svelte b/src/lib/components/playground/Playground.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e99014cab661ee541939096db7315c8888caaad3 --- /dev/null +++ b/src/lib/components/playground/Playground.svelte @@ -0,0 +1,399 @@ +<script lang="ts"> + import { goto } from '$app/navigation'; + + import { onMount, tick, getContext } from 'svelte'; + + import { toast } from 'svelte-sonner'; + + import { OLLAMA_API_BASE_URL, OPENAI_API_BASE_URL, WEBUI_API_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, config, user, models, settings } from '$lib/stores'; + + import { generateChatCompletion } from '$lib/apis/ollama'; + import { generateOpenAIChatCompletion } from '$lib/apis/openai'; + + import { splitStream } from '$lib/utils'; + import ChatCompletion from '$lib/components/playground/ChatCompletion.svelte'; + import Selector from '$lib/components/chat/ModelSelector/Selector.svelte'; + + const i18n = getContext('i18n'); + + let mode = 'chat'; + let loaded = false; + let text = ''; + + let selectedModelId = ''; + + let loading = false; + let stopResponseFlag = false; + + let messagesContainerElement: HTMLDivElement; + let textCompletionAreaElement: HTMLTextAreaElement; + + let system = ''; + let messages = [ + { + role: 'user', + content: '' + } + ]; + + const scrollToBottom = () => { + const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement; + + if (element) { + element.scrollTop = element?.scrollHeight; + } + }; + + const stopResponse = () => { + stopResponseFlag = true; + console.log('stopResponse'); + }; + + const textCompletionHandler = async () => { + const model = $models.find((model) => model.id === selectedModelId); + + const [res, controller] = await generateOpenAIChatCompletion( + localStorage.token, + { + model: model.id, + stream: true, + messages: [ + { + role: 'assistant', + content: text + } + ] + }, + model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1` + ); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done || stopResponseFlag) { + if (stopResponseFlag) { + controller.abort('User: Stop Response'); + } + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + if (line === 'data: [DONE]') { + // responseMessage.done = true; + console.log('done'); + } else { + let data = JSON.parse(line.replace(/^data: /, '')); + console.log(data); + + text += data.choices[0].delta.content ?? ''; + } + } + } + } catch (error) { + console.log(error); + } + + scrollToBottom(); + } + } + }; + + const chatCompletionHandler = async () => { + const model = $models.find((model) => model.id === selectedModelId); + + const [res, controller] = await generateOpenAIChatCompletion( + localStorage.token, + { + model: model.id, + stream: true, + messages: [ + system + ? { + role: 'system', + content: system + } + : undefined, + ...messages + ].filter((message) => message) + }, + model?.owned_by === 'openai' ? `${OPENAI_API_BASE_URL}` : `${OLLAMA_API_BASE_URL}/v1` + ); + + let responseMessage; + if (messages.at(-1)?.role === 'assistant') { + responseMessage = messages.at(-1); + } else { + responseMessage = { + role: 'assistant', + content: '' + }; + messages.push(responseMessage); + messages = messages; + } + + await tick(); + const textareaElement = document.getElementById(`assistant-${messages.length - 1}-textarea`); + + if (res && res.ok) { + const reader = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(splitStream('\n')) + .getReader(); + + while (true) { + const { value, done } = await reader.read(); + if (done || stopResponseFlag) { + if (stopResponseFlag) { + controller.abort('User: Stop Response'); + } + break; + } + + try { + let lines = value.split('\n'); + + for (const line of lines) { + if (line !== '') { + console.log(line); + if (line === 'data: [DONE]') { + // responseMessage.done = true; + messages = messages; + } else { + let data = JSON.parse(line.replace(/^data: /, '')); + console.log(data); + + if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { + continue; + } else { + textareaElement.style.height = textareaElement.scrollHeight + 'px'; + + responseMessage.content += data.choices[0].delta.content ?? ''; + messages = messages; + + textareaElement.style.height = textareaElement.scrollHeight + 'px'; + + await tick(); + } + } + } + } + } catch (error) { + console.log(error); + } + + scrollToBottom(); + } + } + }; + + const submitHandler = async () => { + if (selectedModelId) { + loading = true; + + if (mode === 'complete') { + await textCompletionHandler(); + } else if (mode === 'chat') { + await chatCompletionHandler(); + } + + loading = false; + stopResponseFlag = false; + } + }; + + onMount(async () => { + if ($user?.role !== 'admin') { + await goto('/'); + } + + if ($settings?.models) { + selectedModelId = $settings?.models[0]; + } else if ($config?.default_models) { + selectedModelId = $config?.default_models.split(',')[0]; + } else { + selectedModelId = ''; + } + loaded = true; + }); +</script> + +<svelte:head> + <title> + {$i18n.t('Playground')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div class=" flex flex-col justify-between w-full overflow-y-auto h-full"> + <div class="mx-auto w-full md:px-0 h-full"> + <div class=" flex flex-col h-full"> + <div class="flex flex-col justify-between mb-2.5 gap-1"> + <div class="flex justify-between items-center gap-2"> + <div class=" text-lg font-semibold self-center flex"> + {$i18n.t('Playground')} + <span class=" text-xs text-gray-500 self-center ml-1">{$i18n.t('(Beta)')}</span> + </div> + + <div> + <button + class=" flex items-center gap-0.5 text-xs px-2.5 py-0.5 rounded-lg {mode === 'chat' && + 'text-sky-600 dark:text-sky-200 bg-sky-200/30'} {mode === 'complete' && + 'text-green-600 dark:text-green-200 bg-green-200/30'} " + on:click={() => { + if (mode === 'complete') { + mode = 'chat'; + } else { + mode = 'complete'; + } + }} + > + {#if mode === 'complete'} + {$i18n.t('Text Completion')} + {:else if mode === 'chat'} + {$i18n.t('Chat')} + {/if} + + <div> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-3 h-3" + > + <path + fill-rule="evenodd" + d="M5.22 10.22a.75.75 0 0 1 1.06 0L8 11.94l1.72-1.72a.75.75 0 1 1 1.06 1.06l-2.25 2.25a.75.75 0 0 1-1.06 0l-2.25-2.25a.75.75 0 0 1 0-1.06ZM10.78 5.78a.75.75 0 0 1-1.06 0L8 4.06 6.28 5.78a.75.75 0 0 1-1.06-1.06l2.25-2.25a.75.75 0 0 1 1.06 0l2.25 2.25a.75.75 0 0 1 0 1.06Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + </div> + </div> + + <div class="flex flex-col gap-1 w-full"> + <div class="flex w-full"> + <div class="overflow-hidden w-full"> + <div class="max-w-full"> + <Selector + placeholder={$i18n.t('Select a model')} + items={$models.map((model) => ({ + value: model.id, + label: model.name, + model: model + }))} + bind:value={selectedModelId} + /> + </div> + </div> + </div> + + <!-- <button + class=" self-center dark:hover:text-gray-300" + id="open-settings-button" + on:click={async () => {}} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" + /> + </svg> + </button> --> + </div> + </div> + + {#if mode === 'chat'} + <div class="p-1"> + <div class="p-3 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg"> + <div class=" text-sm font-medium">{$i18n.t('System')}</div> + <textarea + id="system-textarea" + class="w-full h-full bg-transparent resize-none outline-none text-sm" + bind:value={system} + placeholder={$i18n.t("You're a helpful assistant.")} + rows="4" + /> + </div> + </div> + {/if} + + <div + class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" + id="messages-container" + bind:this={messagesContainerElement} + > + <div class=" h-full w-full flex flex-col"> + <div class="flex-1 p-1"> + {#if mode === 'complete'} + <textarea + id="text-completion-textarea" + bind:this={textCompletionAreaElement} + class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm" + bind:value={text} + placeholder={$i18n.t("You're a helpful assistant.")} + /> + {:else if mode === 'chat'} + <ChatCompletion bind:messages /> + {/if} + </div> + </div> + </div> + + <div class="pb-3"> + {#if !loading} + <button + class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg" + on:click={() => { + submitHandler(); + }} + > + {$i18n.t('Submit')} + </button> + {:else} + <button + class="px-3 py-1.5 text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-900 transition rounded-lg" + on:click={() => { + stopResponse(); + }} + > + {$i18n.t('Cancel')} + </button> + {/if} + </div> + </div> + </div> +</div> + +<style> + .scrollbar-hidden::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .scrollbar-hidden { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +</style> diff --git a/src/lib/components/playground/TextCompletion.svelte b/src/lib/components/playground/TextCompletion.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/lib/components/workspace/Documents.svelte b/src/lib/components/workspace/Documents.svelte new file mode 100644 index 0000000000000000000000000000000000000000..017034371d7826ea08260f7b889c3820fcf941ea --- /dev/null +++ b/src/lib/components/workspace/Documents.svelte @@ -0,0 +1,622 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { onMount, getContext } from 'svelte'; + import { WEBUI_NAME, documents, showSidebar } from '$lib/stores'; + import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents'; + + import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants'; + import { processDocToVectorDB, uploadDocToVectorDB } from '$lib/apis/rag'; + import { blobToFile, transformFileName } from '$lib/utils'; + + import Checkbox from '$lib/components/common/Checkbox.svelte'; + + import EditDocModal from '$lib/components/documents/EditDocModal.svelte'; + import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte'; + import AddDocModal from '$lib/components/documents/AddDocModal.svelte'; + import { transcribeAudio } from '$lib/apis/audio'; + import { uploadFile } from '$lib/apis/files'; + + const i18n = getContext('i18n'); + + let importFiles = ''; + + let inputFiles = ''; + let query = ''; + let documentsImportInputElement: HTMLInputElement; + let tags = []; + + let showSettingsModal = false; + let showAddDocModal = false; + let showEditDocModal = false; + let selectedDoc; + let selectedTag = ''; + + let dragged = false; + + const deleteDoc = async (name) => { + await deleteDocByName(localStorage.token, name); + await documents.set(await getDocs(localStorage.token)); + }; + + const deleteDocs = async (docs) => { + const res = await Promise.all( + docs.map(async (doc) => { + return await deleteDocByName(localStorage.token, doc.name); + }) + ); + + await documents.set(await getDocs(localStorage.token)); + }; + + const uploadDoc = async (file, tags?: object) => { + console.log(file); + // Check if the file is an audio file and transcribe/convert it to text file + if (['audio/mpeg', 'audio/wav'].includes(file['type'])) { + const transcribeRes = await transcribeAudio(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + if (transcribeRes) { + console.log(transcribeRes); + const blob = new Blob([transcribeRes.text], { type: 'text/plain' }); + file = blobToFile(blob, `${file.name}.txt`); + } + } + + // Upload the file to the server + const uploadedFile = await uploadFile(localStorage.token, file).catch((error) => { + toast.error(error); + return null; + }); + + const res = await processDocToVectorDB(localStorage.token, uploadedFile.id).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + await createNewDoc( + localStorage.token, + res.collection_name, + res.filename, + transformFileName(res.filename), + res.filename, + tags?.length > 0 + ? { + tags: tags + } + : null + ).catch((error) => { + toast.error(error); + return null; + }); + await documents.set(await getDocs(localStorage.token)); + } + }; + + onMount(() => { + documents.subscribe((docs) => { + tags = docs.reduce((a, e, i, arr) => { + return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])]; + }, []); + }); + const dropZone = document.querySelector('body'); + + const onDragOver = (e) => { + e.preventDefault(); + dragged = true; + }; + + const onDragLeave = () => { + dragged = false; + }; + + const onDrop = async (e) => { + e.preventDefault(); + + if (e.dataTransfer?.files) { + let reader = new FileReader(); + + reader.onload = (event) => { + files = [ + ...files, + { + type: 'image', + url: `${event.target.result}` + } + ]; + }; + + const inputFiles = e.dataTransfer?.files; + + if (inputFiles && inputFiles.length > 0) { + for (const file of inputFiles) { + console.log(file, file.name.split('.').at(-1)); + if ( + SUPPORTED_FILE_TYPE.includes(file['type']) || + SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1)) + ) { + uploadDoc(file); + } else { + toast.error( + `Unknown File Type '${file['type']}', but accepting and treating as plain text` + ); + uploadDoc(file); + } + } + } else { + toast.error($i18n.t(`File not found.`)); + } + } + + dragged = false; + }; + + dropZone?.addEventListener('dragover', onDragOver); + dropZone?.addEventListener('drop', onDrop); + dropZone?.addEventListener('dragleave', onDragLeave); + + return () => { + dropZone?.removeEventListener('dragover', onDragOver); + dropZone?.removeEventListener('drop', onDrop); + dropZone?.removeEventListener('dragleave', onDragLeave); + }; + }); + + let filteredDocs; + + $: filteredDocs = $documents.filter( + (doc) => + (selectedTag === '' || + (doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) && + (query === '' || doc.name.includes(query)) + ); +</script> + +<svelte:head> + <title> + {$i18n.t('Documents')} | {$WEBUI_NAME} + </title> +</svelte:head> + +{#if dragged} + <div + class="fixed {$showSidebar + ? 'left-0 md:left-[260px] md:w-[calc(100%-260px)]' + : 'left-0'} w-full h-full flex z-50 touch-none pointer-events-none" + id="dropzone" + role="region" + aria-label="Drag and Drop Container" + > + <div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center"> + <div class="m-auto pt-64 flex flex-col justify-center"> + <div class="max-w-md"> + <AddFilesPlaceholder> + <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> + Drop any files here to add to my documents + </div> + </AddFilesPlaceholder> + </div> + </div> + </div> + </div> +{/if} + +{#key selectedDoc} + <EditDocModal bind:show={showEditDocModal} {selectedDoc} /> +{/key} + +<AddDocModal bind:show={showAddDocModal} {uploadDoc} /> + +<div class="mb-3"> + <div class="flex justify-between items-center"> + <div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div> + </div> +</div> + +<div class=" flex w-full space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={query} + placeholder={$i18n.t('Search Documents')} + /> + </div> + + <div> + <button + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" + aria-label={$i18n.t('Add Docs')} + on:click={() => { + showAddDocModal = true; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + </div> +</div> + +<!-- <div> + <div + class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged && + ' dark:bg-gray-700'} " + role="region" + on:drop={onDrop} + on:dragover={onDragOver} + on:dragleave={onDragLeave} + > + <div class=" pointer-events-none"> + <div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div> + + <div class=" mt-2 text-center text-sm dark:text-gray-200 w-full"> + Drop any files here to add to my documents + </div> + </div> + </div> +</div> --> + +<hr class=" dark:border-gray-850 my-2.5" /> + +{#if tags.length > 0} + <div class="px-2.5 pt-1 flex gap-1 flex-wrap"> + <div class="ml-0.5 pr-3 my-auto flex items-center"> + <Checkbox + state={filteredDocs.filter((doc) => doc?.selected === 'checked').length === + filteredDocs.length + ? 'checked' + : 'unchecked'} + indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 && + filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length} + on:change={(e) => { + if (e.detail === 'checked') { + filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' })); + } else if (e.detail === 'unchecked') { + filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' })); + } + }} + /> + </div> + + {#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0} + <button + class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white" + on:click={async () => { + selectedTag = ''; + // await chats.set(await getChatListByTagName(localStorage.token, tag.name)); + }} + > + <div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div> + </button> + + {#each tags as tag} + <button + class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white" + on:click={async () => { + selectedTag = tag; + // await chats.set(await getChatListByTagName(localStorage.token, tag.name)); + }} + > + <div class=" text-xs font-medium self-center line-clamp-1"> + #{tag} + </div> + </button> + {/each} + {:else} + <div class="flex-1 flex w-full justify-between items-center"> + <div class="text-xs font-medium py-0.5 self-center mr-1"> + {filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected + </div> + + <div class="flex gap-1"> + <!-- <button + class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white" + on:click={async () => { + selectedTag = ''; + // await chats.set(await getChatListByTagName(localStorage.token, tag.name)); + }} + > + <div class=" text-xs font-medium self-center line-clamp-1">add tags</div> + </button> --> + + <button + class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white" + on:click={async () => { + deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked')); + // await chats.set(await getChatListByTagName(localStorage.token, tag.name)); + }} + > + <div class=" text-xs font-medium self-center line-clamp-1"> + {$i18n.t('delete')} + </div> + </button> + </div> + </div> + {/if} + </div> +{/if} + +<div class="my-3 mb-5"> + {#each filteredDocs as doc} + <button + class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" + on:click={() => { + if (doc?.selected === 'checked') { + doc.selected = 'unchecked'; + } else { + doc.selected = 'checked'; + } + }} + > + <div class="my-auto flex items-center"> + <Checkbox state={doc?.selected ?? 'unchecked'} /> + </div> + <div class=" flex flex-1 space-x-4 cursor-pointer w-full"> + <div class=" flex items-center space-x-3"> + <div class="p-2.5 bg-red-400 text-white rounded-lg"> + {#if doc} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-6 h-6" + > + <path + fill-rule="evenodd" + d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" + clip-rule="evenodd" + /> + <path + d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" + /> + </svg> + {:else} + <svg + class=" w-6 h-6 translate-y-[0.5px]" + fill="currentColor" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_qM83 { + animation: spinner_8HQG 1.05s infinite; + } + .spinner_oXPr { + animation-delay: 0.1s; + } + .spinner_ZTLf { + animation-delay: 0.2s; + } + @keyframes spinner_8HQG { + 0%, + 57.14% { + animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1); + transform: translate(0); + } + 28.57% { + animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33); + transform: translateY(-6px); + } + 100% { + transform: translate(0); + } + } + </style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle + class="spinner_qM83 spinner_oXPr" + cx="12" + cy="12" + r="2.5" + /><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg + > + {/if} + </div> + <div class=" self-center flex-1"> + <div class=" font-semibold line-clamp-1">#{doc.name} ({doc.filename})</div> + <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> + {doc.title} + </div> + </div> + </div> + </div> + <div class="flex flex-row space-x-1 self-center"> + <button + class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + aria-label={$i18n.t('Edit Doc')} + on:click={async (e) => { + e.stopPropagation(); + showEditDocModal = !showEditDocModal; + selectedDoc = doc; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + </button> + + <!-- <button + class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl" + type="button" + on:click={() => { + console.log('download file'); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" + /> + <path + d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z" + /> + </svg> + </button> --> + + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + aria-label={$i18n.t('Delete Doc')} + on:click={(e) => { + e.stopPropagation(); + + deleteDoc(doc.name); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" + /> + </svg> + </button> + </div> + </button> + {/each} +</div> + +<div class=" text-gray-500 text-xs mt-1 mb-2"> + ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")} +</div> + +<div class=" flex justify-end w-full mb-2"> + <div class="flex space-x-2"> + <input + id="documents-import-input" + bind:this={documentsImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + on:change={() => { + console.log(importFiles); + + const reader = new FileReader(); + reader.onload = async (event) => { + const savedDocs = JSON.parse(event.target.result); + console.log(savedDocs); + + for (const doc of savedDocs) { + await createNewDoc( + localStorage.token, + doc.collection_name, + doc.filename, + doc.name, + doc.title, + doc.content + ).catch((error) => { + toast.error(error); + return null; + }); + } + + await documents.set(await getDocs(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} + /> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={() => { + documentsImportInputElement.click(); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1"> + {$i18n.t('Import Documents Mapping')} + </div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={async () => { + let blob = new Blob([JSON.stringify($documents)], { + type: 'application/json' + }); + saveAs(blob, `documents-mapping-export-${Date.now()}.json`); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1"> + {$i18n.t('Export Documents Mapping')} + </div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + </div> +</div> diff --git a/src/lib/components/workspace/Functions.svelte b/src/lib/components/workspace/Functions.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0b03f414079a6153fe16df10a4cf4aed7de8ae6c --- /dev/null +++ b/src/lib/components/workspace/Functions.svelte @@ -0,0 +1,508 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { WEBUI_NAME, functions, models } from '$lib/stores'; + import { onMount, getContext, tick } from 'svelte'; + import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; + + import { goto } from '$app/navigation'; + import { + createNewFunction, + deleteFunctionById, + exportFunctions, + getFunctionById, + getFunctions, + toggleFunctionById, + toggleGlobalById + } from '$lib/apis/functions'; + + import ArrowDownTray from '../icons/ArrowDownTray.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + import ConfirmDialog from '../common/ConfirmDialog.svelte'; + import { getModels } from '$lib/apis'; + import FunctionMenu from './Functions/FunctionMenu.svelte'; + import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; + import Switch from '../common/Switch.svelte'; + import ValvesModal from './common/ValvesModal.svelte'; + import ManifestModal from './common/ManifestModal.svelte'; + import Heart from '../icons/Heart.svelte'; + import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + let functionsImportInputElement: HTMLInputElement; + let importFiles; + + let showConfirm = false; + let query = ''; + + let showManifestModal = false; + let showValvesModal = false; + let selectedFunction = null; + + let showDeleteConfirm = false; + + const shareHandler = async (func) => { + const item = await getFunctionById(localStorage.token, func.id).catch((error) => { + toast.error(error); + return null; + }); + + toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + + const url = 'https://openwebui.com'; + + const tab = await window.open(`${url}/functions/create`, '_blank'); + + // Define the event handler function + const messageHandler = (event) => { + if (event.origin !== url) return; + if (event.data === 'loaded') { + tab.postMessage(JSON.stringify(item), '*'); + + // Remove the event listener after handling the message + window.removeEventListener('message', messageHandler); + } + }; + + window.addEventListener('message', messageHandler, false); + console.log(item); + }; + + const cloneHandler = async (func) => { + const _function = await getFunctionById(localStorage.token, func.id).catch((error) => { + toast.error(error); + return null; + }); + + if (_function) { + sessionStorage.function = JSON.stringify({ + ..._function, + id: `${_function.id}_clone`, + name: `${_function.name} (Clone)` + }); + goto('/workspace/functions/create'); + } + }; + + const exportHandler = async (func) => { + const _function = await getFunctionById(localStorage.token, func.id).catch((error) => { + toast.error(error); + return null; + }); + + if (_function) { + let blob = new Blob([JSON.stringify([_function])], { + type: 'application/json' + }); + saveAs(blob, `function-${_function.id}-export-${Date.now()}.json`); + } + }; + + const deleteHandler = async (func) => { + const res = await deleteFunctionById(localStorage.token, func.id).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Function deleted successfully')); + + functions.set(await getFunctions(localStorage.token)); + models.set(await getModels(localStorage.token)); + } + }; + + const toggleGlobalHandler = async (func) => { + const res = await toggleGlobalById(localStorage.token, func.id).catch((error) => { + toast.error(error); + }); + + if (res) { + if (func.is_global) { + func.type === 'filter' + ? toast.success($i18n.t('Filter is now globally enabled')) + : toast.success($i18n.t('Function is now globally enabled')); + } else { + func.type === 'filter' + ? toast.success($i18n.t('Filter is now globally disabled')) + : toast.success($i18n.t('Function is now globally disabled')); + } + + functions.set(await getFunctions(localStorage.token)); + models.set(await getModels(localStorage.token)); + } + }; +</script> + +<svelte:head> + <title> + {$i18n.t('Functions')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div class="mb-3 flex justify-between items-center"> + <div class=" text-lg font-semibold self-center">{$i18n.t('Functions')}</div> +</div> + +<div class=" flex w-full space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={query} + placeholder={$i18n.t('Search Functions')} + /> + </div> + + <div> + <a + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" + href="/workspace/functions/create" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </a> + </div> +</div> +<hr class=" dark:border-gray-850 my-2.5" /> + +<div class="my-3 mb-5"> + {#each $functions.filter((f) => query === '' || f.name + .toLowerCase() + .includes(query.toLowerCase()) || f.id.toLowerCase().includes(query.toLowerCase())) as func} + <div + class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" + > + <a + class=" flex flex-1 space-x-3.5 cursor-pointer w-full" + href={`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`} + > + <div class="flex items-center text-left"> + <div class=" flex-1 self-center pl-1"> + <div class=" font-semibold flex items-center gap-1.5"> + <div + class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + {func.type} + </div> + + {#if func?.meta?.manifest?.version} + <div + class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + v{func?.meta?.manifest?.version ?? ''} + </div> + {/if} + + <div class=" line-clamp-1"> + {func.name} + </div> + </div> + + <div class="flex gap-1.5 px-1"> + <div class=" text-gray-500 text-xs font-medium flex-shrink-0">{func.id}</div> + + <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> + {func.meta.description} + </div> + </div> + </div> + </div> + </a> + <div class="flex flex-row gap-0.5 self-center"> + {#if func?.meta?.manifest?.funding_url ?? false} + <Tooltip content={$i18n.t('Support')}> + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + selectedFunction = func; + showManifestModal = true; + }} + > + <Heart /> + </button> + </Tooltip> + {/if} + + <Tooltip content={$i18n.t('Valves')}> + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + selectedFunction = func; + showValvesModal = true; + }} + > + <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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> + </svg> + </button> + </Tooltip> + + <FunctionMenu + {func} + editHandler={() => { + goto(`/workspace/functions/edit?id=${encodeURIComponent(func.id)}`); + }} + shareHandler={() => { + shareHandler(func); + }} + cloneHandler={() => { + cloneHandler(func); + }} + exportHandler={() => { + exportHandler(func); + }} + deleteHandler={async () => { + selectedFunction = func; + showDeleteConfirm = true; + }} + toggleGlobalHandler={() => { + if (['filter', 'action'].includes(func.type)) { + toggleGlobalHandler(func); + } + }} + onClose={() => {}} + > + <button + class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + > + <EllipsisHorizontal className="size-5" /> + </button> + </FunctionMenu> + + <div class=" self-center mx-1"> + <Tooltip content={func.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}> + <Switch + bind:state={func.is_active} + on:change={async (e) => { + toggleFunctionById(localStorage.token, func.id); + models.set(await getModels(localStorage.token)); + }} + /> + </Tooltip> + </div> + </div> + </div> + {/each} +</div> + +<!-- <div class=" text-gray-500 text-xs mt-1 mb-2"> + ⓘ {$i18n.t( + 'Admins have access to all tools at all times; users need tools assigned per model in the workspace.' + )} +</div> --> + +<div class=" flex justify-end w-full mb-2"> + <div class="flex space-x-2"> + <input + id="documents-import-input" + bind:this={functionsImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + on:change={() => { + console.log(importFiles); + showConfirm = true; + }} + /> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={() => { + functionsImportInputElement.click(); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Functions')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={async () => { + const _functions = await exportFunctions(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (_functions) { + let blob = new Blob([JSON.stringify(_functions)], { + type: 'application/json' + }); + saveAs(blob, `functions-export-${Date.now()}.json`); + } + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Functions')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + </div> +</div> + +<div class=" my-16"> + <div class=" text-lg font-semibold mb-3 line-clamp-1"> + {$i18n.t('Made by OpenWebUI Community')} + </div> + + <a + class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" + href="https://openwebui.com/#open-webui-community" + target="_blank" + > + <div class=" self-center w-10 flex-shrink-0"> + <div + class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6"> + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + + <div class=" self-center"> + <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a function')}</div> + <div class=" text-sm line-clamp-1"> + {$i18n.t('Discover, download, and explore custom functions')} + </div> + </div> + </a> +</div> + +<DeleteConfirmDialog + bind:show={showDeleteConfirm} + title={$i18n.t('Delete function?')} + on:confirm={() => { + deleteHandler(selectedFunction); + }} +> + <div class=" text-sm text-gray-500"> + {$i18n.t('This will delete')} <span class=" font-semibold">{selectedFunction.name}</span>. + </div> +</DeleteConfirmDialog> + +<ManifestModal bind:show={showManifestModal} manifest={selectedFunction?.meta?.manifest ?? {}} /> +<ValvesModal + bind:show={showValvesModal} + type="function" + id={selectedFunction?.id ?? null} + on:save={async () => { + await tick(); + models.set(await getModels(localStorage.token)); + }} +/> + +<ConfirmDialog + bind:show={showConfirm} + on:confirm={() => { + const reader = new FileReader(); + reader.onload = async (event) => { + const _functions = JSON.parse(event.target.result); + console.log(_functions); + + for (const func of _functions) { + const res = await createNewFunction(localStorage.token, func).catch((error) => { + toast.error(error); + return null; + }); + } + + toast.success($i18n.t('Functions imported successfully')); + functions.set(await getFunctions(localStorage.token)); + models.set(await getModels(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} +> + <div class="text-sm text-gray-500"> + <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> + <div>Please carefully review the following warnings:</div> + + <ul class=" mt-1 list-disc pl-4 text-xs"> + <li>{$i18n.t('Functions allow arbitrary code execution.')}</li> + <li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li> + </ul> + </div> + + <div class="my-3"> + {$i18n.t( + 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.' + )} + </div> + </div> +</ConfirmDialog> diff --git a/src/lib/components/workspace/Functions/FunctionEditor.svelte b/src/lib/components/workspace/Functions/FunctionEditor.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0b9fdd55bc96b5ca8a5d50e214969b5714714ecd --- /dev/null +++ b/src/lib/components/workspace/Functions/FunctionEditor.svelte @@ -0,0 +1,395 @@ +<script> + import { getContext, createEventDispatcher, onMount } from 'svelte'; + import { goto } from '$app/navigation'; + + const dispatch = createEventDispatcher(); + const i18n = getContext('i18n'); + + import CodeEditor from '$lib/components/common/CodeEditor.svelte'; + import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + let formElement = null; + let loading = false; + let showConfirm = false; + + export let edit = false; + export let clone = false; + + export let id = ''; + export let name = ''; + export let meta = { + description: '' + }; + export let content = ''; + + $: if (name && !edit && !clone) { + id = name.replace(/\s+/g, '_').toLowerCase(); + } + + let codeEditor; + let boilerplate = `""" +title: Example Filter +author: open-webui +author_url: https://github.com/open-webui +funding_url: https://github.com/open-webui +version: 0.1 +""" + +from pydantic import BaseModel, Field +from typing import Optional + + +class Filter: + class Valves(BaseModel): + priority: int = Field( + default=0, description="Priority level for the filter operations." + ) + max_turns: int = Field( + default=8, description="Maximum allowable conversation turns for a user." + ) + pass + + class UserValves(BaseModel): + max_turns: int = Field( + default=4, description="Maximum allowable conversation turns for a user." + ) + pass + + def __init__(self): + # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom + # implementations, informing the WebUI to defer file-related operations to designated methods within this class. + # Alternatively, you can remove the files directly from the body in from the inlet hook + # self.file_handler = True + + # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings, + # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'. + self.valves = self.Valves() + pass + + def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict: + # Modify the request body or validate it before processing by the chat completion API. + # This function is the pre-processor for the API where various checks on the input can be performed. + # It can also modify the request before sending it to the API. + print(f"inlet:{__name__}") + print(f"inlet:body:{body}") + print(f"inlet:user:{__user__}") + + if __user__.get("role", "admin") in ["user", "admin"]: + messages = body.get("messages", []) + + max_turns = min(__user__["valves"].max_turns, self.valves.max_turns) + if len(messages) > max_turns: + raise Exception( + f"Conversation turn limit exceeded. Max turns: {max_turns}" + ) + + return body + + def outlet(self, body: dict, __user__: Optional[dict] = None) -> dict: + # Modify or analyze the response body after processing by the API. + # This function is the post-processor for the API, which can be used to modify the response + # or perform additional checks and analytics. + print(f"outlet:{__name__}") + print(f"outlet:body:{body}") + print(f"outlet:user:{__user__}") + + return body +`; + + const _boilerplate = `from pydantic import BaseModel +from typing import Optional, Union, Generator, Iterator +from utils.misc import get_last_user_message + +import os +import requests + + +# Filter Class: This class is designed to serve as a pre-processor and post-processor +# for request and response modifications. It checks and transforms requests and responses +# to ensure they meet specific criteria before further processing or returning to the user. +class Filter: + class Valves(BaseModel): + max_turns: int = 4 + pass + + def __init__(self): + # Indicates custom file handling logic. This flag helps disengage default routines in favor of custom + # implementations, informing the WebUI to defer file-related operations to designated methods within this class. + # Alternatively, you can remove the files directly from the body in from the inlet hook + self.file_handler = True + + # Initialize 'valves' with specific configurations. Using 'Valves' instance helps encapsulate settings, + # which ensures settings are managed cohesively and not confused with operational flags like 'file_handler'. + self.valves = self.Valves(**{"max_turns": 2}) + pass + + def inlet(self, body: dict, user: Optional[dict] = None) -> dict: + # Modify the request body or validate it before processing by the chat completion API. + # This function is the pre-processor for the API where various checks on the input can be performed. + # It can also modify the request before sending it to the API. + print(f"inlet:{__name__}") + print(f"inlet:body:{body}") + print(f"inlet:user:{user}") + + if user.get("role", "admin") in ["user", "admin"]: + messages = body.get("messages", []) + if len(messages) > self.valves.max_turns: + raise Exception( + f"Conversation turn limit exceeded. Max turns: {self.valves.max_turns}" + ) + + return body + + def outlet(self, body: dict, user: Optional[dict] = None) -> dict: + # Modify or analyze the response body after processing by the API. + # This function is the post-processor for the API, which can be used to modify the response + # or perform additional checks and analytics. + print(f"outlet:{__name__}") + print(f"outlet:body:{body}") + print(f"outlet:user:{user}") + + messages = [ + { + **message, + "content": f"{message['content']} - @@Modified from Filter Outlet", + } + for message in body.get("messages", []) + ] + + return {"messages": messages} + + + +# Pipe Class: This class functions as a customizable pipeline. +# It can be adapted to work with any external or internal models, +# making it versatile for various use cases outside of just OpenAI models. +class Pipe: + class Valves(BaseModel): + OPENAI_API_BASE_URL: str = "https://api.openai.com/v1" + OPENAI_API_KEY: str = "your-key" + pass + + def __init__(self): + self.type = "manifold" + self.valves = self.Valves() + self.pipes = self.get_openai_models() + pass + + def get_openai_models(self): + if self.valves.OPENAI_API_KEY: + try: + headers = {} + headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + r = requests.get( + f"{self.valves.OPENAI_API_BASE_URL}/models", headers=headers + ) + + models = r.json() + return [ + { + "id": model["id"], + "name": model["name"] if "name" in model else model["id"], + } + for model in models["data"] + if "gpt" in model["id"] + ] + + except Exception as e: + + print(f"Error: {e}") + return [ + { + "id": "error", + "name": "Could not fetch models from OpenAI, please update the API Key in the valves.", + }, + ] + else: + return [] + + def pipe(self, body: dict) -> Union[str, Generator, Iterator]: + # This is where you can add your custom pipelines like RAG. + print(f"pipe:{__name__}") + + if "user" in body: + print(body["user"]) + del body["user"] + + headers = {} + headers["Authorization"] = f"Bearer {self.valves.OPENAI_API_KEY}" + headers["Content-Type"] = "application/json" + + model_id = body["model"][body["model"].find(".") + 1 :] + payload = {**body, "model": model_id} + print(payload) + + try: + r = requests.post( + url=f"{self.valves.OPENAI_API_BASE_URL}/chat/completions", + json=payload, + headers=headers, + stream=True, + ) + + r.raise_for_status() + + if body["stream"]: + return r.iter_lines() + else: + return r.json() + except Exception as e: + return f"Error: {e}" +`; + + const saveHandler = async () => { + loading = true; + dispatch('save', { + id, + name, + meta, + content + }); + }; + + const submitHandler = async () => { + if (codeEditor) { + const res = await codeEditor.formatPythonCodeHandler(); + + if (res) { + console.log('Code formatted successfully'); + saveHandler(); + } + } + }; +</script> + +<div class=" flex flex-col justify-between w-full overflow-y-auto h-full"> + <div class="mx-auto w-full md:px-0 h-full"> + <form + bind:this={formElement} + class=" flex flex-col max-h-[100dvh] h-full" + on:submit|preventDefault={() => { + if (edit) { + submitHandler(); + } else { + showConfirm = true; + } + }} + > + <div class="mb-2.5"> + <button + class="flex space-x-1" + on:click={() => { + goto('/workspace/functions'); + }} + type="button" + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + </div> + + <div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg"> + <div class="w-full mb-2 flex flex-col gap-1.5"> + <div class="flex gap-2 w-full"> + <input + class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t('Function Name (e.g. My Filter)')} + bind:value={name} + required + /> + + <input + class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t('Function ID (e.g. my_filter)')} + bind:value={id} + required + disabled={edit} + /> + </div> + <input + class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t( + 'Function Description (e.g. A filter to remove profanity from text)' + )} + bind:value={meta.description} + required + /> + </div> + + <div class="mb-2 flex-1 overflow-auto h-0 rounded-lg"> + <CodeEditor + bind:value={content} + bind:this={codeEditor} + {boilerplate} + on:save={() => { + if (formElement) { + formElement.requestSubmit(); + } + }} + /> + </div> + + <div class="pb-3 flex justify-between"> + <div class="flex-1 pr-3"> + <div class="text-xs text-gray-500 line-clamp-2"> + <span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span> + {$i18n.t('Functions allow arbitrary code execution')} <br />— + <span class=" font-medium dark:text-gray-400" + >{$i18n.t(`don't install random functions from sources you don't trust.`)}</span + > + </div> + </div> + + <button + class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> + </div> + </form> + </div> +</div> + +<ConfirmDialog + bind:show={showConfirm} + on:confirm={() => { + submitHandler(); + }} +> + <div class="text-sm text-gray-500"> + <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> + <div>{$i18n.t('Please carefully review the following warnings:')}</div> + + <ul class=" mt-1 list-disc pl-4 text-xs"> + <li>{$i18n.t('Functions allow arbitrary code execution.')}</li> + <li>{$i18n.t('Do not install functions from sources you do not fully trust.')}</li> + </ul> + </div> + + <div class="my-3"> + {$i18n.t( + 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.' + )} + </div> + </div> +</ConfirmDialog> diff --git a/src/lib/components/workspace/Functions/FunctionMenu.svelte b/src/lib/components/workspace/Functions/FunctionMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ad82e4a5f39cd92279f98a5e9bc735bf379f0d1a --- /dev/null +++ b/src/lib/components/workspace/Functions/FunctionMenu.svelte @@ -0,0 +1,138 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte'; + + const i18n = getContext('i18n'); + + export let func; + + export let editHandler: Function; + export let shareHandler: Function; + export let cloneHandler: Function; + export let exportHandler: Function; + export let deleteHandler: Function; + export let toggleGlobalHandler: Function; + + export let onClose: Function; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + {#if ['filter', 'action'].includes(func.type)} + <div + class="flex gap-2 justify-between items-center px-3 py-2 text-sm font-medium cursor-pointerrounded-md" + > + <div class="flex gap-2 items-center"> + <GlobeAlt /> + + <div class="flex items-center">{$i18n.t('Global')}</div> + </div> + + <div> + <Switch on:change={toggleGlobalHandler} bind:state={func.is_global} /> + </div> + </div> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + {/if} + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + editHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + + <div class="flex items-center">{$i18n.t('Edit')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneHandler(); + }} + > + <DocumentDuplicate /> + + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + exportHandler(); + }} + > + <ArrowDownTray /> + + <div class="flex items-center">{$i18n.t('Export')}</div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/workspace/Models.svelte b/src/lib/components/workspace/Models.svelte new file mode 100644 index 0000000000000000000000000000000000000000..fde1e77107d95c22c2db9a20e6345d0c85f6a6f8 --- /dev/null +++ b/src/lib/components/workspace/Models.svelte @@ -0,0 +1,527 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import Sortable from 'sortablejs'; + + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { onMount, getContext, tick } from 'svelte'; + + import { WEBUI_NAME, mobile, models, settings, user } from '$lib/stores'; + import { addNewModel, deleteModelById, getModelInfos, updateModelById } from '$lib/apis/models'; + + import { deleteModel } from '$lib/apis/ollama'; + import { goto } from '$app/navigation'; + + import { getModels } from '$lib/apis'; + + import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; + import ModelMenu from './Models/ModelMenu.svelte'; + import ModelDeleteConfirmDialog from '../common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + let showModelDeleteConfirm = false; + + let localModelfiles = []; + + let importFiles; + let modelsImportInputElement: HTMLInputElement; + + let _models = []; + let selectedModel = null; + + let sortable = null; + let searchValue = ''; + + const deleteModelHandler = async (model) => { + console.log(model.info); + if (!model?.info) { + toast.error( + $i18n.t('{{ owner }}: You cannot delete a base model', { + owner: model.owned_by.toUpperCase() + }) + ); + return null; + } + + const res = await deleteModelById(localStorage.token, model.id); + + if (res) { + toast.success($i18n.t(`Deleted {{name}}`, { name: model.id })); + } + + await models.set(await getModels(localStorage.token)); + _models = $models; + }; + + const cloneModelHandler = async (model) => { + if ((model?.info?.base_model_id ?? null) === null) { + toast.error($i18n.t('You cannot clone a base model')); + return; + } else { + sessionStorage.model = JSON.stringify({ + ...model, + id: `${model.id}-clone`, + name: `${model.name} (Clone)` + }); + goto('/workspace/models/create'); + } + }; + + const shareModelHandler = async (model) => { + toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + + const url = 'https://openwebui.com'; + + const tab = await window.open(`${url}/models/create`, '_blank'); + + // Define the event handler function + const messageHandler = (event) => { + if (event.origin !== url) return; + if (event.data === 'loaded') { + tab.postMessage(JSON.stringify(model), '*'); + + // Remove the event listener after handling the message + window.removeEventListener('message', messageHandler); + } + }; + + window.addEventListener('message', messageHandler, false); + }; + + const hideModelHandler = async (model) => { + let info = model.info; + + if (!info) { + info = { + id: model.id, + name: model.name, + meta: { + suggestion_prompts: null + }, + params: {} + }; + } + + info.meta = { + ...info.meta, + hidden: !(info?.meta?.hidden ?? false) + }; + + console.log(info); + + const res = await updateModelById(localStorage.token, info.id, info); + + if (res) { + toast.success( + $i18n.t(`Model {{name}} is now {{status}}`, { + name: info.id, + status: info.meta.hidden ? 'hidden' : 'visible' + }) + ); + } + + await models.set(await getModels(localStorage.token)); + _models = $models; + }; + + const downloadModels = async (models) => { + let blob = new Blob([JSON.stringify(models)], { + type: 'application/json' + }); + saveAs(blob, `models-export-${Date.now()}.json`); + }; + + const exportModelHandler = async (model) => { + let blob = new Blob([JSON.stringify([model])], { + type: 'application/json' + }); + saveAs(blob, `${model.id}-${Date.now()}.json`); + }; + + const positionChangeHanlder = async () => { + // Get the new order of the models + const modelIds = Array.from(document.getElementById('model-list').children).map((child) => + child.id.replace('model-item-', '') + ); + + // Update the position of the models + for (const [index, id] of modelIds.entries()) { + const model = $models.find((m) => m.id === id); + if (model) { + let info = model.info; + + if (!info) { + info = { + id: model.id, + name: model.name, + meta: { + position: index + }, + params: {} + }; + } + + info.meta = { + ...info.meta, + position: index + }; + await updateModelById(localStorage.token, info.id, info); + } + } + + await tick(); + await models.set(await getModels(localStorage.token)); + }; + + onMount(async () => { + // Legacy code to sync localModelfiles with models + _models = $models; + localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); + + if (localModelfiles) { + console.log(localModelfiles); + } + + if (!$mobile) { + // SortableJS + sortable = new Sortable(document.getElementById('model-list'), { + animation: 150, + onUpdate: async (event) => { + console.log(event); + positionChangeHanlder(); + } + }); + } + }); +</script> + +<svelte:head> + <title> + {$i18n.t('Models')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<ModelDeleteConfirmDialog + bind:show={showModelDeleteConfirm} + on:confirm={() => { + deleteModelHandler(selectedModel); + }} +/> + +<div class=" text-lg font-semibold mb-3">{$i18n.t('Models')}</div> + +<div class=" flex w-full space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={searchValue} + placeholder={$i18n.t('Search Models')} + /> + </div> + + <div> + <a + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" + href="/workspace/models/create" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </a> + </div> +</div> +<hr class=" dark:border-gray-850 my-2.5" /> + +<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/models/create"> + <div class=" self-center w-10 flex-shrink-0"> + <div + class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6"> + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + + <div class=" self-center"> + <div class=" font-semibold line-clamp-1">{$i18n.t('Create a model')}</div> + <div class=" text-sm line-clamp-1">{$i18n.t('Customize models for a specific purpose')}</div> + </div> +</a> + +<hr class=" dark:border-gray-850" /> + +<div class=" my-2 mb-5" id="model-list"> + {#each _models.filter((m) => searchValue === '' || m.name + .toLowerCase() + .includes(searchValue.toLowerCase())) as model} + <div + class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" + id="model-item-{model.id}" + > + <a + class=" flex flex-1 space-x-3.5 cursor-pointer w-full" + href={`/?models=${encodeURIComponent(model.id)}`} + > + <div class=" self-start w-8 pt-0.5"> + <div + class=" rounded-full bg-stone-700 {model?.info?.meta?.hidden ?? false + ? 'brightness-90 dark:brightness-50' + : ''} " + > + <img + src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'} + alt="modelfile profile" + class=" rounded-full w-full h-auto object-cover" + /> + </div> + </div> + + <div + class=" flex-1 self-center {model?.info?.meta?.hidden ?? false ? 'text-gray-500' : ''}" + > + <div class=" font-semibold line-clamp-1">{model.name}</div> + <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> + {!!model?.info?.meta?.description ? model?.info?.meta?.description : model.id} + </div> + </div> + </a> + <div class="flex flex-row gap-0.5 self-center"> + <a + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + href={`/workspace/models/edit?id=${encodeURIComponent(model.id)}`} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + /> + </svg> + </a> + + <ModelMenu + {model} + shareHandler={() => { + shareModelHandler(model); + }} + cloneHandler={() => { + cloneModelHandler(model); + }} + exportHandler={() => { + exportModelHandler(model); + }} + hideHandler={() => { + hideModelHandler(model); + }} + deleteHandler={() => { + selectedModel = model; + showModelDeleteConfirm = true; + }} + onClose={() => {}} + > + <button + class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + > + <EllipsisHorizontal className="size-5" /> + </button> + </ModelMenu> + </div> + </div> + {/each} +</div> + +<div class=" flex justify-end w-full mb-3"> + <div class="flex space-x-1"> + <input + id="models-import-input" + bind:this={modelsImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + on:change={() => { + console.log(importFiles); + + let reader = new FileReader(); + reader.onload = async (event) => { + let savedModels = JSON.parse(event.target.result); + console.log(savedModels); + + for (const model of savedModels) { + if (model?.info ?? false) { + if ($models.find((m) => m.id === model.id)) { + await updateModelById(localStorage.token, model.id, model.info).catch((error) => { + return null; + }); + } else { + await addNewModel(localStorage.token, model.info).catch((error) => { + return null; + }); + } + } + } + + await models.set(await getModels(localStorage.token)); + _models = $models; + }; + + reader.readAsText(importFiles[0]); + }} + /> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={() => { + modelsImportInputElement.click(); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Models')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-3.5 h-3.5" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={async () => { + downloadModels($models); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Models')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-3.5 h-3.5" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + </div> + + {#if localModelfiles.length > 0} + <div class="flex"> + <div class=" self-center text-sm font-medium mr-4"> + {localModelfiles.length} Local Modelfiles Detected + </div> + + <div class="flex space-x-1"> + <button + class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex" + on:click={async () => { + downloadModels(localModelfiles); + + localStorage.removeItem('modelfiles'); + localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]'); + }} + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" + /> + </svg> + </div> + </button> + </div> + </div> + {/if} +</div> + +<div class=" my-16"> + <div class=" text-lg font-semibold mb-3 line-clamp-1"> + {$i18n.t('Made by OpenWebUI Community')} + </div> + + <a + class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" + href="https://openwebui.com/#open-webui-community" + target="_blank" + > + <div class=" self-center w-10 flex-shrink-0"> + <div + class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6"> + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + + <div class=" self-center"> + <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a model')}</div> + <div class=" text-sm line-clamp-1"> + {$i18n.t('Discover, download, and explore model presets')} + </div> + </div> + </a> +</div> diff --git a/src/lib/components/workspace/Models/ActionsSelector.svelte b/src/lib/components/workspace/Models/ActionsSelector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8335455edab74179dcec9e736fbe2f88808be5ff --- /dev/null +++ b/src/lib/components/workspace/Models/ActionsSelector.svelte @@ -0,0 +1,59 @@ +<script lang="ts"> + import { getContext, onMount } from 'svelte'; + import Checkbox from '$lib/components/common/Checkbox.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let actions = []; + export let selectedActionIds = []; + + let _actions = {}; + + onMount(() => { + _actions = actions.reduce((acc, action) => { + acc[action.id] = { + ...action, + selected: selectedActionIds.includes(action.id) + }; + + return acc; + }, {}); + }); +</script> + +<div> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Actions')}</div> + </div> + + <div class=" text-xs dark:text-gray-500"> + {$i18n.t('To select actions here, add them to the "Functions" workspace first.')} + </div> + + <div class="flex flex-col"> + {#if actions.length > 0} + <div class=" flex items-center mt-2 flex-wrap"> + {#each Object.keys(_actions) as action, actionIdx} + <div class=" flex items-center gap-2 mr-3"> + <div class="self-center flex items-center"> + <Checkbox + state={_actions[action].selected ? 'checked' : 'unchecked'} + on:change={(e) => { + _actions[action].selected = e.detail === 'checked'; + selectedActionIds = Object.keys(_actions).filter((t) => _actions[t].selected); + }} + /> + </div> + + <div class=" py-0.5 text-sm w-full capitalize font-medium"> + <Tooltip content={_actions[action].meta.description}> + {_actions[action].name} + </Tooltip> + </div> + </div> + {/each} + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/workspace/Models/FiltersSelector.svelte b/src/lib/components/workspace/Models/FiltersSelector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..92f64c2cf084a67b37deae73186b572e695861f9 --- /dev/null +++ b/src/lib/components/workspace/Models/FiltersSelector.svelte @@ -0,0 +1,60 @@ +<script lang="ts"> + import { getContext, onMount } from 'svelte'; + import Checkbox from '$lib/components/common/Checkbox.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + + const i18n = getContext('i18n'); + + export let filters = []; + export let selectedFilterIds = []; + + let _filters = {}; + + onMount(() => { + _filters = filters.reduce((acc, filter) => { + acc[filter.id] = { + ...filter, + selected: selectedFilterIds.includes(filter.id) + }; + + return acc; + }, {}); + }); +</script> + +<div> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Filters')}</div> + </div> + + <div class=" text-xs dark:text-gray-500"> + {$i18n.t('To select filters here, add them to the "Functions" workspace first.')} + </div> + + <!-- TODO: Filer order matters --> + <div class="flex flex-col"> + {#if filters.length > 0} + <div class=" flex items-center mt-2 flex-wrap"> + {#each Object.keys(_filters) as filter, filterIdx} + <div class=" flex items-center gap-2 mr-3"> + <div class="self-center flex items-center"> + <Checkbox + state={_filters[filter].selected ? 'checked' : 'unchecked'} + on:change={(e) => { + _filters[filter].selected = e.detail === 'checked'; + selectedFilterIds = Object.keys(_filters).filter((t) => _filters[t].selected); + }} + /> + </div> + + <div class=" py-0.5 text-sm w-full capitalize font-medium"> + <Tooltip content={_filters[filter].meta.description}> + {_filters[filter].name} + </Tooltip> + </div> + </div> + {/each} + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/workspace/Models/Knowledge.svelte b/src/lib/components/workspace/Models/Knowledge.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1a945a276049ff443537ac2c18160474ce4c428d --- /dev/null +++ b/src/lib/components/workspace/Models/Knowledge.svelte @@ -0,0 +1,106 @@ +<script lang="ts"> + import { getContext } from 'svelte'; + import Selector from './Knowledge/Selector.svelte'; + + export let knowledge = []; + + const i18n = getContext('i18n'); +</script> + +<div> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Knowledge')}</div> + </div> + + <div class=" text-xs dark:text-gray-500"> + {$i18n.t('To add documents here, upload them to the "Documents" workspace first.')} + </div> + + <div class="flex flex-col"> + {#if knowledge.length > 0} + <div class=" flex items-center gap-2 mt-2"> + {#each knowledge as file, fileIdx} + <div class=" relative group"> + <div + class="h-16 w-[15rem] flex items-center space-x-3 px-2.5 dark:bg-gray-600 rounded-xl border border-gray-200 dark:border-none" + > + <div class="p-2.5 bg-red-400 text-white rounded-lg"> + {#if (file?.type ?? 'doc') === 'doc'} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-6" + > + <path + fill-rule="evenodd" + d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" + clip-rule="evenodd" + /> + <path + d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" + /> + </svg> + {:else if file.type === 'collection'} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-6" + > + <path + d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z" + /> + <path + d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z" + /> + </svg> + {/if} + </div> + + <div class="flex flex-col justify-center -space-y-0.5"> + <div class=" dark:text-gray-100 text-sm font-medium line-clamp-1"> + {file?.title ?? `#${file.name}`} + </div> + + <div class=" text-gray-500 text-sm">{$i18n.t(file?.type ?? 'Document')}</div> + </div> + </div> + + <div class=" absolute -top-1 -right-1"> + <button + class=" bg-gray-400 text-white border border-white rounded-full group-hover:visible invisible transition" + type="button" + on:click={() => { + knowledge.splice(fileIdx, 1); + knowledge = knowledge; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + </div> + {/each} + </div> + {/if} + + <div class="flex flex-wrap text-sm font-medium gap-1.5 mt-2"> + <Selector bind:knowledge> + <button + class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl" + type="button">{$i18n.t('Select Documents')}</button + > + </Selector> + </div> + <!-- {knowledge} --> + </div> +</div> diff --git a/src/lib/components/workspace/Models/Knowledge/Selector.svelte b/src/lib/components/workspace/Models/Knowledge/Selector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a09eaf34fa3d65e165dc3caece9f0473dda73d0f --- /dev/null +++ b/src/lib/components/workspace/Models/Knowledge/Selector.svelte @@ -0,0 +1,138 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + + import { documents } from '$lib/stores'; + import { flyAndScale } from '$lib/utils/transitions'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import { onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + export let onClose: Function = () => {}; + + export let knowledge = []; + + let items = []; + + onMount(() => { + let collections = [ + ...($documents.length > 0 + ? [ + { + name: 'All Documents', + type: 'collection', + title: $i18n.t('All Documents'), + collection_names: $documents.map((doc) => doc.collection_name) + } + ] + : []), + ...$documents + .reduce((a, e, i, arr) => { + return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])]; + }, []) + .map((tag) => ({ + name: tag, + type: 'collection', + collection_names: $documents + .filter((doc) => (doc?.content?.tags ?? []).map((tag) => tag.name).includes(tag)) + .map((doc) => doc.collection_name) + })) + ]; + + items = [...collections, ...$documents]; + }); +</script> + +<Dropdown + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <slot /> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[300px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg" + sideOffset={8} + side="bottom" + align="start" + transition={flyAndScale} + > + <div class="max-h-[10rem] overflow-y-scroll"> + {#if items.length === 0} + <div class="text-center text-sm text-gray-500 dark:text-gray-400"> + {$i18n.t('No documents found')} + </div> + {:else} + {#each items as item} + <DropdownMenu.Item + class="flex gap-2.5 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + if (!knowledge.find((k) => k.name === item.name)) { + knowledge = [ + ...knowledge, + { + ...item, + type: item?.type ?? 'doc' + } + ]; + } + }} + > + <div class="flex self-start"> + {#if (item?.type ?? 'doc') === 'doc'} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="w-4" + > + <path + fill-rule="evenodd" + d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" + clip-rule="evenodd" + /> + <path + d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" + /> + </svg> + {:else if item.type === 'collection'} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-4" + > + <path + d="M7.5 3.375c0-1.036.84-1.875 1.875-1.875h.375a3.75 3.75 0 0 1 3.75 3.75v1.875C13.5 8.161 14.34 9 15.375 9h1.875A3.75 3.75 0 0 1 21 12.75v3.375C21 17.16 20.16 18 19.125 18h-9.75A1.875 1.875 0 0 1 7.5 16.125V3.375Z" + /> + <path + d="M15 5.25a5.23 5.23 0 0 0-1.279-3.434 9.768 9.768 0 0 1 6.963 6.963A5.23 5.23 0 0 0 17.25 7.5h-1.875A.375.375 0 0 1 15 7.125V5.25ZM4.875 6H6v10.125A3.375 3.375 0 0 0 9.375 19.5H16.5v1.125c0 1.035-.84 1.875-1.875 1.875h-9.75A1.875 1.875 0 0 1 3 20.625V7.875C3 6.839 3.84 6 4.875 6Z" + /> + </svg> + {/if} + </div> + + <div class="flex items-center"> + <div class="flex flex-col"> + <div + class=" w-fit text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + {item?.type ?? 'Document'} + </div> + + <div class="line-clamp-1 font-medium pr-0.5"> + {item.name} + </div> + </div> + </div> + </DropdownMenu.Item> + {/each} + {/if} + </div> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/workspace/Models/ModelMenu.svelte b/src/lib/components/workspace/Models/ModelMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f3a21d510f7b95a9f92b26c33b12e0d73191f76e --- /dev/null +++ b/src/lib/components/workspace/Models/ModelMenu.svelte @@ -0,0 +1,148 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + + const i18n = getContext('i18n'); + + export let model; + + export let shareHandler: Function; + export let cloneHandler: Function; + export let exportHandler: Function; + + export let hideHandler: Function; + export let deleteHandler: Function; + export let onClose: Function; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneHandler(); + }} + > + <DocumentDuplicate /> + + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + exportHandler(); + }} + > + <ArrowDownTray /> + + <div class="flex items-center">{$i18n.t('Export')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + hideHandler(); + }} + > + {#if model?.info?.meta?.hidden ?? 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="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" + /> + </svg> + {:else} + <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="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> + </svg> + {/if} + + <div class="flex items-center"> + {#if model?.info?.meta?.hidden ?? false} + {$i18n.t('Show Model')} + {:else} + {$i18n.t('Hide Model')} + {/if} + </div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/workspace/Models/ToolsSelector.svelte b/src/lib/components/workspace/Models/ToolsSelector.svelte new file mode 100644 index 0000000000000000000000000000000000000000..5f1b3b482128882f2c076b85f044ed94cfa31798 --- /dev/null +++ b/src/lib/components/workspace/Models/ToolsSelector.svelte @@ -0,0 +1,57 @@ +<script lang="ts"> + import Checkbox from '$lib/components/common/Checkbox.svelte'; + import { getContext, onMount } from 'svelte'; + + export let tools = []; + + let _tools = {}; + + export let selectedToolIds = []; + + const i18n = getContext('i18n'); + + onMount(() => { + _tools = tools.reduce((acc, tool) => { + acc[tool.id] = { + ...tool, + selected: selectedToolIds.includes(tool.id) + }; + + return acc; + }, {}); + }); +</script> + +<div> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Tools')}</div> + </div> + + <div class=" text-xs dark:text-gray-500"> + {$i18n.t('To select toolkits here, add them to the "Tools" workspace first.')} + </div> + + <div class="flex flex-col"> + {#if tools.length > 0} + <div class=" flex items-center mt-2 flex-wrap"> + {#each Object.keys(_tools) as tool, toolIdx} + <div class=" flex items-center gap-2 mr-3"> + <div class="self-center flex items-center"> + <Checkbox + state={_tools[tool].selected ? 'checked' : 'unchecked'} + on:change={(e) => { + _tools[tool].selected = e.detail === 'checked'; + selectedToolIds = Object.keys(_tools).filter((t) => _tools[t].selected); + }} + /> + </div> + + <div class=" py-0.5 text-sm w-full capitalize font-medium"> + {_tools[tool].name} + </div> + </div> + {/each} + </div> + {/if} + </div> +</div> diff --git a/src/lib/components/workspace/Prompts.svelte b/src/lib/components/workspace/Prompts.svelte new file mode 100644 index 0000000000000000000000000000000000000000..c1a7e2e06c8e211094e41b96b7c31f7914fcc1d8 --- /dev/null +++ b/src/lib/components/workspace/Prompts.svelte @@ -0,0 +1,320 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { onMount, getContext } from 'svelte'; + import { WEBUI_NAME, prompts } from '$lib/stores'; + import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; + import { error } from '@sveltejs/kit'; + import { goto } from '$app/navigation'; + import PromptMenu from './Prompts/PromptMenu.svelte'; + import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; + import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + let importFiles = ''; + let query = ''; + let promptsImportInputElement: HTMLInputElement; + + let showDeleteConfirm = false; + let deletePrompt = null; + + const shareHandler = async (prompt) => { + toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + + const url = 'https://openwebui.com'; + + const tab = await window.open(`${url}/prompts/create`, '_blank'); + window.addEventListener( + 'message', + (event) => { + if (event.origin !== url) return; + if (event.data === 'loaded') { + tab.postMessage(JSON.stringify(prompt), '*'); + } + }, + false + ); + }; + + const cloneHandler = async (prompt) => { + sessionStorage.prompt = JSON.stringify(prompt); + goto('/workspace/prompts/create'); + }; + + const exportHandler = async (prompt) => { + let blob = new Blob([JSON.stringify([prompt])], { + type: 'application/json' + }); + saveAs(blob, `prompt-export-${Date.now()}.json`); + }; + + const deleteHandler = async (prompt) => { + const command = prompt.command; + await deletePromptByCommand(localStorage.token, command); + await prompts.set(await getPrompts(localStorage.token)); + }; +</script> + +<svelte:head> + <title> + {$i18n.t('Prompts')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div class="mb-3 flex justify-between items-center"> + <div class=" text-lg font-semibold self-center">{$i18n.t('Prompts')}</div> +</div> + +<div class=" flex w-full space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={query} + placeholder={$i18n.t('Search Prompts')} + /> + </div> + + <div> + <a + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" + href="/workspace/prompts/create" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </a> + </div> +</div> +<hr class=" dark:border-gray-850 my-2.5" /> + +<div class="my-3 mb-5"> + {#each $prompts.filter((p) => query === '' || p.command.includes(query)) as prompt} + <div + class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" + > + <div class=" flex flex-1 space-x-4 cursor-pointer w-full"> + <a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}> + <div class=" flex-1 self-center pl-5"> + <div class=" font-semibold line-clamp-1">{prompt.command}</div> + <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> + {prompt.title} + </div> + </div> + </a> + </div> + <div class="flex flex-row gap-0.5 self-center"> + <a + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + </a> + + <PromptMenu + shareHandler={() => { + shareHandler(prompt); + }} + cloneHandler={() => { + cloneHandler(prompt); + }} + exportHandler={() => { + exportHandler(prompt); + }} + deleteHandler={async () => { + deletePrompt = prompt; + showDeleteConfirm = true; + }} + onClose={() => {}} + > + <button + class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + > + <EllipsisHorizontal className="size-5" /> + </button> + </PromptMenu> + </div> + </div> + {/each} +</div> + +<div class=" flex justify-end w-full mb-3"> + <div class="flex space-x-2"> + <input + id="prompts-import-input" + bind:this={promptsImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + on:change={() => { + console.log(importFiles); + + const reader = new FileReader(); + reader.onload = async (event) => { + const savedPrompts = JSON.parse(event.target.result); + console.log(savedPrompts); + + for (const prompt of savedPrompts) { + await createNewPrompt( + localStorage.token, + prompt.command.charAt(0) === '/' ? prompt.command.slice(1) : prompt.command, + prompt.title, + prompt.content + ).catch((error) => { + toast.error(error); + return null; + }); + } + + await prompts.set(await getPrompts(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} + /> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={() => { + promptsImportInputElement.click(); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Prompts')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={async () => { + // promptsImportInputElement.click(); + let blob = new Blob([JSON.stringify($prompts)], { + type: 'application/json' + }); + saveAs(blob, `prompts-export-${Date.now()}.json`); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Prompts')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <!-- <button + on:click={() => { + loadDefaultPrompts(); + }} + > + dd + </button> --> + </div> +</div> + +<div class=" my-16"> + <div class=" text-lg font-semibold mb-3 line-clamp-1"> + {$i18n.t('Made by OpenWebUI Community')} + </div> + + <a + class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" + href="https://openwebui.com/#open-webui-community" + target="_blank" + > + <div class=" self-center w-10 flex-shrink-0"> + <div + class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6"> + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + + <div class=" self-center"> + <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a prompt')}</div> + <div class=" text-sm line-clamp-1"> + {$i18n.t('Discover, download, and explore custom prompts')} + </div> + </div> + </a> +</div> + +<DeleteConfirmDialog + bind:show={showDeleteConfirm} + title={$i18n.t('Delete prompt?')} + on:confirm={() => { + deleteHandler(deletePrompt); + }} +> + <div class=" text-sm text-gray-500"> + {$i18n.t('This will delete')} <span class=" font-semibold">{deletePrompt.command}</span>. + </div> +</DeleteConfirmDialog> diff --git a/src/lib/components/workspace/Prompts/PromptMenu.svelte b/src/lib/components/workspace/Prompts/PromptMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..9a4177b1edd979597a4c2af4391562eac91f75ac --- /dev/null +++ b/src/lib/components/workspace/Prompts/PromptMenu.svelte @@ -0,0 +1,92 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + + const i18n = getContext('i18n'); + + export let shareHandler: Function; + export let cloneHandler: Function; + export let exportHandler: Function; + export let deleteHandler: Function; + export let onClose: Function; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneHandler(); + }} + > + <DocumentDuplicate /> + + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + exportHandler(); + }} + > + <ArrowDownTray /> + + <div class="flex items-center">{$i18n.t('Export')}</div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/workspace/Tools.svelte b/src/lib/components/workspace/Tools.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8ea5ec582e086ed74bffcbe70fb3fde5e808f63b --- /dev/null +++ b/src/lib/components/workspace/Tools.svelte @@ -0,0 +1,455 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { onMount, getContext } from 'svelte'; + import { WEBUI_NAME, prompts, tools } from '$lib/stores'; + import { createNewPrompt, deletePromptByCommand, getPrompts } from '$lib/apis/prompts'; + + import { goto } from '$app/navigation'; + import { + createNewTool, + deleteToolById, + exportTools, + getToolById, + getTools + } from '$lib/apis/tools'; + import ArrowDownTray from '../icons/ArrowDownTray.svelte'; + import Tooltip from '../common/Tooltip.svelte'; + import ConfirmDialog from '../common/ConfirmDialog.svelte'; + import ToolMenu from './Tools/ToolMenu.svelte'; + import EllipsisHorizontal from '../icons/EllipsisHorizontal.svelte'; + import ValvesModal from './common/ValvesModal.svelte'; + import ManifestModal from './common/ManifestModal.svelte'; + import Heart from '../icons/Heart.svelte'; + import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + let toolsImportInputElement: HTMLInputElement; + let importFiles; + + let showConfirm = false; + let query = ''; + + let showManifestModal = false; + let showValvesModal = false; + let selectedTool = null; + + let showDeleteConfirm = false; + + const shareHandler = async (tool) => { + const item = await getToolById(localStorage.token, tool.id).catch((error) => { + toast.error(error); + return null; + }); + + toast.success($i18n.t('Redirecting you to OpenWebUI Community')); + + const url = 'https://openwebui.com'; + + const tab = await window.open(`${url}/tools/create`, '_blank'); + + // Define the event handler function + const messageHandler = (event) => { + if (event.origin !== url) return; + if (event.data === 'loaded') { + tab.postMessage(JSON.stringify(item), '*'); + + // Remove the event listener after handling the message + window.removeEventListener('message', messageHandler); + } + }; + + window.addEventListener('message', messageHandler, false); + console.log(item); + }; + + const cloneHandler = async (tool) => { + const _tool = await getToolById(localStorage.token, tool.id).catch((error) => { + toast.error(error); + return null; + }); + + if (_tool) { + sessionStorage.tool = JSON.stringify({ + ..._tool, + id: `${_tool.id}_clone`, + name: `${_tool.name} (Clone)` + }); + goto('/workspace/tools/create'); + } + }; + + const exportHandler = async (tool) => { + const _tool = await getToolById(localStorage.token, tool.id).catch((error) => { + toast.error(error); + return null; + }); + + if (_tool) { + let blob = new Blob([JSON.stringify([_tool])], { + type: 'application/json' + }); + saveAs(blob, `tool-${_tool.id}-export-${Date.now()}.json`); + } + }; + + const deleteHandler = async (tool) => { + const res = await deleteToolById(localStorage.token, tool.id).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Tool deleted successfully')); + tools.set(await getTools(localStorage.token)); + } + }; +</script> + +<svelte:head> + <title> + {$i18n.t('Tools')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div class="mb-3 flex justify-between items-center"> + <div class=" text-lg font-semibold self-center">{$i18n.t('Tools')}</div> +</div> + +<div class=" flex w-full space-x-2"> + <div class="flex flex-1"> + <div class=" self-center ml-1 mr-3"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" + clip-rule="evenodd" + /> + </svg> + </div> + <input + class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent" + bind:value={query} + placeholder={$i18n.t('Search Tools')} + /> + </div> + + <div> + <a + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1" + href="/workspace/tools/create" + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </a> + </div> +</div> +<hr class=" dark:border-gray-850 my-2.5" /> + +<div class="my-3 mb-5"> + {#each $tools.filter((t) => query === '' || t.name + .toLowerCase() + .includes(query.toLowerCase()) || t.id.toLowerCase().includes(query.toLowerCase())) as tool} + <div + class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl" + > + <a + class=" flex flex-1 space-x-3.5 cursor-pointer w-full" + href={`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`} + > + <div class="flex items-center text-left"> + <div class=" flex-1 self-center pl-1"> + <div class=" font-semibold flex items-center gap-1.5"> + <div + class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + TOOL + </div> + + {#if tool?.meta?.manifest?.version} + <div + class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200" + > + v{tool?.meta?.manifest?.version ?? ''} + </div> + {/if} + + <div class="line-clamp-1"> + {tool.name} + </div> + </div> + + <div class="flex gap-1.5 px-1"> + <div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div> + + <div class=" text-xs overflow-hidden text-ellipsis line-clamp-1"> + {tool.meta.description} + </div> + </div> + </div> + </div> + </a> + <div class="flex flex-row gap-0.5 self-center"> + {#if tool?.meta?.manifest?.funding_url ?? false} + <Tooltip content="Support"> + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + selectedTool = tool; + showManifestModal = true; + }} + > + <Heart /> + </button> + </Tooltip> + {/if} + + <Tooltip content={$i18n.t('Valves')}> + <button + class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + on:click={() => { + selectedTool = tool; + showValvesModal = true; + }} + > + <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="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" + /> + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" + /> + </svg> + </button> + </Tooltip> + + <ToolMenu + editHandler={() => { + goto(`/workspace/tools/edit?id=${encodeURIComponent(tool.id)}`); + }} + shareHandler={() => { + shareHandler(tool); + }} + cloneHandler={() => { + cloneHandler(tool); + }} + exportHandler={() => { + exportHandler(tool); + }} + deleteHandler={async () => { + selectedTool = tool; + showDeleteConfirm = true; + }} + onClose={() => {}} + > + <button + class="self-center w-fit text-sm p-1.5 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl" + type="button" + > + <EllipsisHorizontal className="size-5" /> + </button> + </ToolMenu> + </div> + </div> + {/each} +</div> + +<div class=" text-gray-500 text-xs mt-1 mb-2"> + ⓘ {$i18n.t( + 'Admins have access to all tools at all times; users need tools assigned per model in the workspace.' + )} +</div> + +<div class=" flex justify-end w-full mb-2"> + <div class="flex space-x-2"> + <input + id="documents-import-input" + bind:this={toolsImportInputElement} + bind:files={importFiles} + type="file" + accept=".json" + hidden + on:change={() => { + console.log(importFiles); + showConfirm = true; + }} + /> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={() => { + toolsImportInputElement.click(); + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Import Tools')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + + <button + class="flex text-xs 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 dark:text-gray-200 transition" + on:click={async () => { + const _tools = await exportTools(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (_tools) { + let blob = new Blob([JSON.stringify(_tools)], { + type: 'application/json' + }); + saveAs(blob, `tools-export-${Date.now()}.json`); + } + }} + > + <div class=" self-center mr-2 font-medium line-clamp-1">{$i18n.t('Export Tools')}</div> + + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z" + clip-rule="evenodd" + /> + </svg> + </div> + </button> + </div> +</div> + +<div class=" my-16"> + <div class=" text-lg font-semibold mb-3 line-clamp-1"> + {$i18n.t('Made by OpenWebUI Community')} + </div> + + <a + class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" + href="https://openwebui.com/#open-webui-community" + target="_blank" + > + <div class=" self-center w-10 flex-shrink-0"> + <div + class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200" + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6"> + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + </div> + </div> + + <div class=" self-center"> + <div class=" font-semibold line-clamp-1">{$i18n.t('Discover a tool')}</div> + <div class=" text-sm line-clamp-1"> + {$i18n.t('Discover, download, and explore custom tools')} + </div> + </div> + </a> +</div> + +<DeleteConfirmDialog + bind:show={showDeleteConfirm} + title={$i18n.t('Delete tool?')} + on:confirm={() => { + deleteHandler(selectedTool); + }} +> + <div class=" text-sm text-gray-500"> + {$i18n.t('This will delete')} <span class=" font-semibold">{selectedTool.name}</span>. + </div> +</DeleteConfirmDialog> + +<ValvesModal bind:show={showValvesModal} type="tool" id={selectedTool?.id ?? null} /> +<ManifestModal bind:show={showManifestModal} manifest={selectedTool?.meta?.manifest ?? {}} /> + +<ConfirmDialog + bind:show={showConfirm} + on:confirm={() => { + const reader = new FileReader(); + reader.onload = async (event) => { + const _tools = JSON.parse(event.target.result); + console.log(_tools); + + for (const tool of _tools) { + const res = await createNewTool(localStorage.token, tool).catch((error) => { + toast.error(error); + return null; + }); + } + + toast.success($i18n.t('Tool imported successfully')); + tools.set(await getTools(localStorage.token)); + }; + + reader.readAsText(importFiles[0]); + }} +> + <div class="text-sm text-gray-500"> + <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> + <div>{$i18n.t('Please carefully review the following warnings:')}</div> + + <ul class=" mt-1 list-disc pl-4 text-xs"> + <li> + {$i18n.t('Tools have a function calling system that allows arbitrary code execution')}. + </li> + <li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li> + </ul> + </div> + + <div class="my-3"> + {$i18n.t( + 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.' + )} + </div> + </div> +</ConfirmDialog> diff --git a/src/lib/components/workspace/Tools/ToolMenu.svelte b/src/lib/components/workspace/Tools/ToolMenu.svelte new file mode 100644 index 0000000000000000000000000000000000000000..93a26b118df7a77cd11636ee0f048c604dfc662d --- /dev/null +++ b/src/lib/components/workspace/Tools/ToolMenu.svelte @@ -0,0 +1,117 @@ +<script lang="ts"> + import { DropdownMenu } from 'bits-ui'; + import { flyAndScale } from '$lib/utils/transitions'; + import { getContext } from 'svelte'; + + import Dropdown from '$lib/components/common/Dropdown.svelte'; + import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; + import Pencil from '$lib/components/icons/Pencil.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import Tags from '$lib/components/chat/Tags.svelte'; + import Share from '$lib/components/icons/Share.svelte'; + import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte'; + import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte'; + import ArrowDownTray from '$lib/components/icons/ArrowDownTray.svelte'; + + const i18n = getContext('i18n'); + + export let editHandler: Function; + export let shareHandler: Function; + export let cloneHandler: Function; + export let exportHandler: Function; + export let deleteHandler: Function; + export let onClose: Function; + + let show = false; +</script> + +<Dropdown + bind:show + on:change={(e) => { + if (e.detail === false) { + onClose(); + } + }} +> + <Tooltip content={$i18n.t('More')}> + <slot /> + </Tooltip> + + <div slot="content"> + <DropdownMenu.Content + class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow" + sideOffset={-2} + side="bottom" + align="start" + transition={flyAndScale} + > + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + editHandler(); + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125" + /> + </svg> + + <div class="flex items-center">{$i18n.t('Edit')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + shareHandler(); + }} + > + <Share /> + <div class="flex items-center">{$i18n.t('Share')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + cloneHandler(); + }} + > + <DocumentDuplicate /> + + <div class="flex items-center">{$i18n.t('Clone')}</div> + </DropdownMenu.Item> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + exportHandler(); + }} + > + <ArrowDownTray /> + + <div class="flex items-center">{$i18n.t('Export')}</div> + </DropdownMenu.Item> + + <hr class="border-gray-100 dark:border-gray-800 my-1" /> + + <DropdownMenu.Item + class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md" + on:click={() => { + deleteHandler(); + }} + > + <GarbageBin strokeWidth="2" /> + <div class="flex items-center">{$i18n.t('Delete')}</div> + </DropdownMenu.Item> + </DropdownMenu.Content> + </div> +</Dropdown> diff --git a/src/lib/components/workspace/Tools/ToolkitEditor.svelte b/src/lib/components/workspace/Tools/ToolkitEditor.svelte new file mode 100644 index 0000000000000000000000000000000000000000..718ddb39d46f135e2947925ec9cd4571e5e29324 --- /dev/null +++ b/src/lib/components/workspace/Tools/ToolkitEditor.svelte @@ -0,0 +1,285 @@ +<script> + import { getContext, createEventDispatcher, onMount } from 'svelte'; + + const i18n = getContext('i18n'); + + import CodeEditor from '$lib/components/common/CodeEditor.svelte'; + import { goto } from '$app/navigation'; + import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const dispatch = createEventDispatcher(); + + let formElement = null; + let loading = false; + let showConfirm = false; + + export let edit = false; + export let clone = false; + + export let id = ''; + export let name = ''; + export let meta = { + description: '' + }; + export let content = ''; + + $: if (name && !edit && !clone) { + id = name.replace(/\s+/g, '_').toLowerCase(); + } + + let codeEditor; + let boilerplate = `import os +import requests +from datetime import datetime + + +class Tools: + def __init__(self): + pass + + # Add your custom tools using pure Python code here, make sure to add type hints + # Use Sphinx-style docstrings to document your tools, they will be used for generating tools specifications + # Please refer to function_calling_filter_pipeline.py file from pipelines project for an example + + def get_user_name_and_email_and_id(self, __user__: dict = {}) -> str: + """ + Get the user name, Email and ID from the user object. + """ + + # Do not include :param for __user__ in the docstring as it should not be shown in the tool's specification + # The session user object will be passed as a parameter when the function is called + + print(__user__) + result = "" + + if "name" in __user__: + result += f"User: {__user__['name']}" + if "id" in __user__: + result += f" (ID: {__user__['id']})" + if "email" in __user__: + result += f" (Email: {__user__['email']})" + + if result == "": + result = "User: Unknown" + + return result + + def get_current_time(self) -> str: + """ + Get the current time in a more human-readable format. + :return: The current time. + """ + + now = datetime.now() + current_time = now.strftime("%I:%M:%S %p") # Using 12-hour format with AM/PM + current_date = now.strftime( + "%A, %B %d, %Y" + ) # Full weekday, month name, day, and year + + return f"Current Date and Time = {current_date}, {current_time}" + + def calculator(self, equation: str) -> str: + """ + Calculate the result of an equation. + :param equation: The equation to calculate. + """ + + # Avoid using eval in production code + # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html + try: + result = eval(equation) + return f"{equation} = {result}" + except Exception as e: + print(e) + return "Invalid equation" + + def get_current_weather(self, city: str) -> str: + """ + Get the current weather for a given city. + :param city: The name of the city to get the weather for. + :return: The current weather information or an error message. + """ + api_key = os.getenv("OPENWEATHER_API_KEY") + if not api_key: + return ( + "API key is not set in the environment variable 'OPENWEATHER_API_KEY'." + ) + + base_url = "http://api.openweathermap.org/data/2.5/weather" + params = { + "q": city, + "appid": api_key, + "units": "metric", # Optional: Use 'imperial' for Fahrenheit + } + + try: + response = requests.get(base_url, params=params) + response.raise_for_status() # Raise HTTPError for bad responses (4xx and 5xx) + data = response.json() + + if data.get("cod") != 200: + return f"Error fetching weather data: {data.get('message')}" + + weather_description = data["weather"][0]["description"] + temperature = data["main"]["temp"] + humidity = data["main"]["humidity"] + wind_speed = data["wind"]["speed"] + + return f"Weather in {city}: {temperature}°C" + except requests.RequestException as e: + return f"Error fetching weather data: {str(e)}" +`; + + const saveHandler = async () => { + loading = true; + dispatch('save', { + id, + name, + meta, + content + }); + }; + + const submitHandler = async () => { + if (codeEditor) { + const res = await codeEditor.formatPythonCodeHandler(); + + if (res) { + console.log('Code formatted successfully'); + saveHandler(); + } + } + }; +</script> + +<div class=" flex flex-col justify-between w-full overflow-y-auto h-full"> + <div class="mx-auto w-full md:px-0 h-full"> + <form + bind:this={formElement} + class=" flex flex-col max-h-[100dvh] h-full" + on:submit|preventDefault={() => { + if (edit) { + submitHandler(); + } else { + showConfirm = true; + } + }} + > + <div class="mb-2.5"> + <button + class="flex space-x-1" + on:click={() => { + goto('/workspace/tools'); + }} + type="button" + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + </div> + + <div class="flex flex-col flex-1 overflow-auto h-0 rounded-lg"> + <div class="w-full mb-2 flex flex-col gap-1.5"> + <div class="flex gap-2 w-full"> + <input + class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t('Toolkit Name (e.g. My ToolKit)')} + bind:value={name} + required + /> + + <input + class="w-full px-3 py-2 text-sm font-medium disabled:text-gray-300 dark:disabled:text-gray-700 bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t('Toolkit ID (e.g. my_toolkit)')} + bind:value={id} + required + disabled={edit} + /> + </div> + <input + class="w-full px-3 py-2 text-sm font-medium bg-gray-50 dark:bg-gray-850 dark:text-gray-200 rounded-lg outline-none" + type="text" + placeholder={$i18n.t( + 'Toolkit Description (e.g. A toolkit for performing various operations)' + )} + bind:value={meta.description} + required + /> + </div> + + <div class="mb-2 flex-1 overflow-auto h-0 rounded-lg"> + <CodeEditor + bind:value={content} + bind:this={codeEditor} + {boilerplate} + on:save={() => { + if (formElement) { + formElement.requestSubmit(); + } + }} + /> + </div> + + <div class="pb-3 flex justify-between"> + <div class="flex-1 pr-3"> + <div class="text-xs text-gray-500 line-clamp-2"> + <span class=" font-semibold dark:text-gray-200">{$i18n.t('Warning:')}</span> + {$i18n.t('Tools are a function calling system with arbitrary code execution')} <br />— + <span class=" font-medium dark:text-gray-400" + >{$i18n.t(`don't install random tools from sources you don't trust.`)}</span + > + </div> + </div> + + <button + class="px-3 py-1.5 text-sm font-medium bg-emerald-600 hover:bg-emerald-700 text-gray-50 transition rounded-lg" + type="submit" + > + {$i18n.t('Save')} + </button> + </div> + </div> + </form> + </div> +</div> + +<ConfirmDialog + bind:show={showConfirm} + on:confirm={() => { + submitHandler(); + }} +> + <div class="text-sm text-gray-500"> + <div class=" bg-yellow-500/20 text-yellow-700 dark:text-yellow-200 rounded-lg px-4 py-3"> + <div>{$i18n.t('Please carefully review the following warnings:')}</div> + + <ul class=" mt-1 list-disc pl-4 text-xs"> + <li> + {$i18n.t('Tools have a function calling system that allows arbitrary code execution.')} + </li> + <li>{$i18n.t('Do not install tools from sources you do not fully trust.')}</li> + </ul> + </div> + + <div class="my-3"> + {$i18n.t( + 'I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.' + )} + </div> + </div> +</ConfirmDialog> diff --git a/src/lib/components/workspace/common/ManifestModal.svelte b/src/lib/components/workspace/common/ManifestModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..0e646e4097f15dfe0a2051120533b44fbe2ccc08 --- /dev/null +++ b/src/lib/components/workspace/common/ManifestModal.svelte @@ -0,0 +1,104 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { createEventDispatcher } from 'svelte'; + import { onMount, getContext } from 'svelte'; + + import Modal from '../../common/Modal.svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let show = false; + export let manifest = {}; +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center">{$i18n.t('Show your support!')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + show = false; + }} + > + <div class="px-1 text-sm"> + <div class="my-2"> + {$i18n.t( + 'The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.' + )} + </div> + + <div class="my-2"> + {$i18n.t( + 'Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.' + )} + </div> + + <hr class="dark:border-gray-800 my-3" /> + <div class="my-2"> + {$i18n.t('Support this plugin:')} + <a + href={manifest.funding_url} + target="_blank" + class="underline text-blue-400 hover:text-blue-300">{manifest.funding_url}</a + > + </div> + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center" + type="submit" + > + {$i18n.t('Done')} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/components/workspace/common/ValvesModal.svelte b/src/lib/components/workspace/common/ValvesModal.svelte new file mode 100644 index 0000000000000000000000000000000000000000..1f23c510ec0305dfc2ba168ba9d30cc64cf94040 --- /dev/null +++ b/src/lib/components/workspace/common/ValvesModal.svelte @@ -0,0 +1,202 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { createEventDispatcher } from 'svelte'; + import { onMount, getContext } from 'svelte'; + import { addUser } from '$lib/apis/auths'; + + import Modal from '../../common/Modal.svelte'; + import { + getFunctionValvesById, + getFunctionValvesSpecById, + updateFunctionValvesById + } from '$lib/apis/functions'; + import { getToolValvesById, getToolValvesSpecById, updateToolValvesById } from '$lib/apis/tools'; + import Spinner from '../../common/Spinner.svelte'; + import Switch from '$lib/components/common/Switch.svelte'; + import Valves from '$lib/components/common/Valves.svelte'; + + const i18n = getContext('i18n'); + const dispatch = createEventDispatcher(); + + export let show = false; + + export let type = 'tool'; + export let id = null; + + let saving = false; + let loading = false; + + let valvesSpec = null; + let valves = {}; + + const submitHandler = async () => { + saving = true; + + if (valvesSpec) { + // Convert string to array + for (const property in valvesSpec.properties) { + if (valvesSpec.properties[property]?.type === 'array') { + valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim()); + } + } + + let res = null; + + if (type === 'tool') { + res = await updateToolValvesById(localStorage.token, id, valves).catch((error) => { + toast.error(error); + }); + } else if (type === 'function') { + res = await updateFunctionValvesById(localStorage.token, id, valves).catch((error) => { + toast.error(error); + }); + } + + if (res) { + toast.success('Valves updated successfully'); + dispatch('save'); + } + } + + saving = false; + }; + + const initHandler = async () => { + loading = true; + valves = {}; + valvesSpec = null; + + if (type === 'tool') { + valves = await getToolValvesById(localStorage.token, id); + valvesSpec = await getToolValvesSpecById(localStorage.token, id); + } else if (type === 'function') { + valves = await getFunctionValvesById(localStorage.token, id); + valvesSpec = await getFunctionValvesSpecById(localStorage.token, id); + } + + if (!valves) { + valves = {}; + } + + if (valvesSpec) { + // Convert array to string + for (const property in valvesSpec.properties) { + if (valvesSpec.properties[property]?.type === 'array') { + valves[property] = (valves[property] ?? []).join(','); + } + } + } + + loading = false; + }; + + $: if (show) { + initHandler(); + } +</script> + +<Modal size="sm" bind:show> + <div> + <div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2"> + <div class=" text-lg font-medium self-center">{$i18n.t('Valves')}</div> + <button + class="self-center" + on:click={() => { + show = false; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-5 h-5" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + + <div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200"> + <div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6"> + <form + class="flex flex-col w-full" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="px-1"> + {#if !loading} + <Valves {valvesSpec} bind:valves /> + {:else} + <Spinner className="size-5" /> + {/if} + </div> + + <div class="flex justify-end pt-3 text-sm font-medium"> + <button + class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {saving + ? ' cursor-not-allowed' + : ''}" + type="submit" + disabled={saving} + > + {$i18n.t('Save')} + + {#if saving} + <div class="ml-2 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> + </div> + </div> + </div> +</Modal> + +<style> + input::-webkit-outer-spin-button, + input::-webkit-inner-spin-button { + /* display: none; <- Crashes Chrome on hover */ + -webkit-appearance: none; + margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ + } + + .tabs::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .tabs { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } + + input[type='number'] { + -moz-appearance: textfield; /* Firefox */ + } +</style> diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..5fc1e7b1943d5ff0ff902aa7ee0fa1c633fef5da --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,99 @@ +import { browser, dev } from '$app/environment'; +// import { version } from '../../package.json'; + +export const APP_NAME = 'Open WebUI'; + +export const WEBUI_HOSTNAME = browser ? (dev ? `${location.hostname}:8080` : ``) : ''; +export const WEBUI_BASE_URL = browser ? (dev ? `http://${WEBUI_HOSTNAME}` : ``) : ``; +export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; + +export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`; +export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai`; +export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`; +export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`; +export const RAG_API_BASE_URL = `${WEBUI_BASE_URL}/rag/api/v1`; + +export const WEBUI_VERSION = APP_VERSION; +export const WEBUI_BUILD_HASH = APP_BUILD_HASH; +export const REQUIRED_OLLAMA_VERSION = '0.1.16'; + +export const SUPPORTED_FILE_TYPE = [ + 'application/epub+zip', + 'application/pdf', + 'text/plain', + 'text/csv', + 'text/xml', + 'text/html', + 'text/x-python', + 'text/css', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/octet-stream', + 'application/x-javascript', + 'text/markdown', + 'audio/mpeg', + 'audio/wav' +]; + +export const SUPPORTED_FILE_EXTENSIONS = [ + 'md', + 'rst', + 'go', + 'py', + 'java', + 'sh', + 'bat', + 'ps1', + 'cmd', + 'js', + 'ts', + 'css', + 'cpp', + 'hpp', + 'h', + 'c', + 'cs', + 'htm', + 'html', + 'sql', + 'log', + 'ini', + 'pl', + 'pm', + 'r', + 'dart', + 'dockerfile', + 'env', + 'php', + 'hs', + 'hsc', + 'lua', + 'nginxconf', + 'conf', + 'm', + 'mm', + 'plsql', + 'perl', + 'rb', + 'rs', + 'db2', + 'scala', + 'bash', + 'swift', + 'vue', + 'svelte', + 'doc', + 'docx', + 'pdf', + 'csv', + 'txt', + 'xls', + 'xlsx', + 'pptx', + 'ppt', + 'msg' +]; + +// Source: https://kit.svelte.dev/docs/modules#$env-static-public +// This feature, akin to $env/static/private, exclusively incorporates environment variables +// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). +// Consequently, these variables can be securely exposed to client-side code. diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..172c42f91bdc3fca84c460676147167448cfbe44 --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,79 @@ +import i18next from 'i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import type { i18n as i18nType } from 'i18next'; +import { writable } from 'svelte/store'; + +const createI18nStore = (i18n: i18nType) => { + const i18nWritable = writable(i18n); + + i18n.on('initialized', () => { + i18nWritable.set(i18n); + }); + i18n.on('loaded', () => { + i18nWritable.set(i18n); + }); + i18n.on('added', () => i18nWritable.set(i18n)); + i18n.on('languageChanged', () => { + i18nWritable.set(i18n); + }); + return i18nWritable; +}; + +const createIsLoadingStore = (i18n: i18nType) => { + const isLoading = writable(false); + + // if loaded resources are empty || {}, set loading to true + i18n.on('loaded', (resources) => { + // console.log('loaded:', resources); + Object.keys(resources).length !== 0 && isLoading.set(false); + }); + + // if resources failed loading, set loading to true + i18n.on('failedLoading', () => { + isLoading.set(true); + }); + + return isLoading; +}; + +export const initI18n = (defaultLocale: string | undefined) => { + let detectionOrder = defaultLocale + ? ['querystring', 'localStorage'] + : ['querystring', 'localStorage', 'navigator']; + let fallbackDefaultLocale = defaultLocale ? [defaultLocale] : ['en-US']; + + const loadResource = (language: string, namespace: string) => + import(`./locales/${language}/${namespace}.json`); + + i18next + .use(resourcesToBackend(loadResource)) + .use(LanguageDetector) + .init({ + debug: false, + detection: { + order: detectionOrder, + caches: ['localStorage'], + lookupQuerystring: 'lang', + lookupLocalStorage: 'locale' + }, + fallbackLng: { + default: fallbackDefaultLocale + }, + ns: 'translation', + returnEmptyString: false, + interpolation: { + escapeValue: false // not needed for svelte as it escapes by default + } + }); +}; + +const i18n = createI18nStore(i18next); +const isLoadingStore = createIsLoadingStore(i18next); + +export const getLanguages = async () => { + const languages = (await import(`./locales/languages.json`)).default; + return languages; +}; +export default i18n; +export const isLoading = isLoadingStore; diff --git a/src/lib/i18n/locales/ar-BH/translation.json b/src/lib/i18n/locales/ar-BH/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..2627a134c432ab5a37b5545a12631b716a7b4563 --- /dev/null +++ b/src/lib/i18n/locales/ar-BH/translation.json @@ -0,0 +1,718 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' أو '-1' لا توجد انتهاء", + "(Beta)": "(تجريبي)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "( `sh webui.sh --api`مثال)", + "(latest)": "(الأخير)", + "{{ models }}": "{{ نماذج }}", + "{{ owner }}: You cannot delete a base model": "{{ المالك }}: لا يمكنك حذف نموذج أساسي", + "{{modelName}} is thinking...": "{{modelName}} ...يفكر", + "{{user}}'s Chats": "دردشات {{user}}", + "{{webUIName}} Backend Required": "{{webUIName}} مطلوب", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "يتم استخدام نموذج المهمة عند تنفيذ مهام مثل إنشاء عناوين للدردشات واستعلامات بحث الويب", + "a user": "مستخدم", + "About": "عن", + "Account": "الحساب", + "Account Activation Pending": "", + "Accurate information": "معلومات دقيقة", + "Actions": "", + "Active Users": "", + "Add": "أضف", + "Add a model id": "إضافة معرف نموذج", + "Add a short description about what this model does": "أضف وصفا موجزا حول ما يفعله هذا النموذج", + "Add a short title for this prompt": "أضف عنوانًا قصيرًا لبداء المحادثة", + "Add a tag": "أضافة تاق", + "Add custom prompt": "أضافة مطالبة مخصصه", + "Add Docs": "إضافة المستندات", + "Add Files": "إضافة ملفات", + "Add Memory": "إضافة ذكرايات", + "Add message": "اضافة رسالة", + "Add Model": "اضافة موديل", + "Add Tag": "", + "Add Tags": "اضافة تاق", + "Add User": "اضافة مستخدم", + "Adjusting these settings will apply changes universally to all users.": "سيؤدي ضبط هذه الإعدادات إلى تطبيق التغييرات بشكل عام على كافة المستخدمين", + "admin": "المشرف", + "Admin": "", + "Admin Panel": "لوحة التحكم", + "Admin Settings": "اعدادات المشرف", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "التعليمات المتقدمة", + "Advanced Params": "المعلمات المتقدمة", + "all": "الكل", + "All Documents": "جميع الملفات", + "All Users": "جميع المستخدمين", + "Allow": "يسمح", + "Allow Chat Deletion": "يستطيع حذف المحادثات", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "الأحرف الأبجدية الرقمية والواصلات", + "Already have an account?": "هل تملك حساب ؟", + "an assistant": "مساعد", + "and": "و", + "and create a new shared link.": "و أنشئ رابط مشترك جديد.", + "API Base URL": "API الرابط الرئيسي", + "API Key": "API مفتاح", + "API Key created.": "API تم أنشاء المفتاح", + "API keys": "مفاتيح واجهة برمجة التطبيقات", + "April": "أبريل", + "Archive": "الأرشيف", + "Archive All Chats": "أرشفة جميع الدردشات", + "Archived Chats": "الأرشيف المحادثات", + "are allowed - Activate this command by typing": "مسموح - قم بتنشيط هذا الأمر عن طريق الكتابة", + "Are you sure?": "هل أنت متأكد ؟", + "Attach file": "أرفق ملف", + "Attention to detail": "انتبه للتفاصيل", + "Audio": "صوتي", + "Audio settings updated successfully": "", + "August": "أغسطس", + "Auto-playback response": "استجابة التشغيل التلقائي", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 الرابط الرئيسي", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 الرابط مطلوب", + "available!": "متاح", + "Back": "خلف", + "Bad Response": "استجابة خطاء", + "Banners": "لافتات", + "Base Model (From)": "النموذج الأساسي (من)", + "Batch Size (num_batch)": "", + "before": "قبل", + "Being lazy": "كون كسول", + "Brave Search API Key": "مفتاح واجهة برمجة تطبيقات البحث الشجاع", + "Bypass SSL verification for Websites": "تجاوز التحقق من SSL للموقع", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "اللغاء", + "Capabilities": "قدرات", + "Change Password": "تغير الباسورد", + "Chat": "المحادثة", + "Chat Background Image": "", + "Chat Bubble UI": "UI الدردشة", + "Chat Controls": "", + "Chat direction": "اتجاه المحادثة", + "Chat History": "تاريخ المحادثة", + "Chat History is off for this browser.": "سجل الدردشة معطل لهذا المتصفح", + "Chats": "المحادثات", + "Check Again": "تحقق مرة اخرى", + "Check for updates": "تحقق من التحديثات", + "Checking for updates...": "البحث عن تحديثات", + "Choose a model before saving...": "أختار موديل قبل الحفظ", + "Chunk Overlap": "Chunk تداخل", + "Chunk Params": "Chunk المتغيرات", + "Chunk Size": "Chunk حجم", + "Citation": "اقتباس", + "Clear memory": "", + "Click here for help.": "أضغط هنا للمساعدة", + "Click here to": "أضغط هنا الانتقال", + "Click here to download user import template file.": "", + "Click here to select": "أضغط هنا للاختيار", + "Click here to select a csv file.": "أضغط هنا للاختيار ملف csv", + "Click here to select a py file.": "", + "Click here to select documents.": "انقر هنا لاختيار المستندات", + "click here.": "أضغط هنا", + "Click on the user role button to change a user's role.": "أضغط على أسم الصلاحيات لتغيرها للمستخدم", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "استنساخ", + "Close": "أغلق", + "Code formatted successfully": "", + "Collection": "مجموعة", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI الرابط الافتراضي", + "ComfyUI Base URL is required.": "ComfyUI الرابط مطلوب", + "Command": "الأوامر", + "Concurrent Requests": "الطلبات المتزامنة", + "Confirm": "", + "Confirm Password": "تأكيد كلمة المرور", + "Confirm your action": "", + "Connections": "اتصالات", + "Contact Admin for WebUI Access": "", + "Content": "الاتصال", + "Content Extraction": "", + "Context Length": "طول السياق", + "Continue Response": "متابعة الرد", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "تم نسخ عنوان URL للدردشة المشتركة إلى الحافظة", + "Copy": "نسخ", + "Copy last code block": "انسخ كتلة التعليمات البرمجية الأخيرة", + "Copy last response": "انسخ الرد الأخير", + "Copy Link": "أنسخ الرابط", + "Copying to clipboard was successful!": "تم النسخ إلى الحافظة بنجاح", + "Create a model": "إنشاء نموذج", + "Create Account": "إنشاء حساب", + "Create new key": "عمل مفتاح جديد", + "Create new secret key": "عمل سر جديد", + "Created at": "أنشئت في", + "Created At": "أنشئت من", + "Created by": "", + "CSV Import": "", + "Current Model": "الموديل المختار", + "Current Password": "كلمة السر الحالية", + "Custom": "مخصص", + "Customize models for a specific purpose": "تخصيص النماذج لغرض معين", + "Dark": "مظلم", + "Dashboard": "", + "Database": "قاعدة البيانات", + "December": "ديسمبر", + "Default": "الإفتراضي", + "Default (Automatic1111)": "(Automatic1111) الإفتراضي", + "Default (SentenceTransformers)": "(SentenceTransformers) الإفتراضي", + "Default Model": "النموذج الافتراضي", + "Default model updated": "الإفتراضي تحديث الموديل", + "Default Prompt Suggestions": "الإفتراضي Prompt الاقتراحات", + "Default User Role": "الإفتراضي صلاحيات المستخدم", + "delete": "حذف", + "Delete": "حذف", + "Delete a model": "حذف الموديل", + "Delete All Chats": "حذف جميع الدردشات", + "Delete chat": "حذف المحادثه", + "Delete Chat": "حذف المحادثه.", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "أحذف هذا الرابط", + "Delete tool?": "", + "Delete User": "حذف المستخدم", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} حذف", + "Deleted {{name}}": "حذف {{name}}", + "Description": "وصف", + "Didn't fully follow instructions": "لم أتبع التعليمات بشكل كامل", + "Disabled": "", + "Discover a function": "", + "Discover a model": "اكتشف نموذجا", + "Discover a prompt": "اكتشاف موجه", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "اكتشاف وتنزيل واستكشاف المطالبات المخصصة", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "اكتشاف وتنزيل واستكشاف الإعدادات المسبقة للنموذج", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "اعرض اسم المستخدم بدلاً منك في الدردشة", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "المستند", + "Document Settings": "أعدادات المستند", + "Documentation": "", + "Documents": "مستندات", + "does not make any external connections, and your data stays securely on your locally hosted server.": "لا يجري أي اتصالات خارجية، وتظل بياناتك آمنة على الخادم المستضاف محليًا.", + "Don't Allow": "لا تسمح بذلك", + "Don't have an account?": "ليس لديك حساب؟", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "لا أحب النمط", + "Done": "", + "Download": "تحميل", + "Download canceled": "تم اللغاء التحميل", + "Download Database": "تحميل قاعدة البيانات", + "Drop any files here to add to the conversation": "أسقط أية ملفات هنا لإضافتها إلى المحادثة", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. الوحدات الزمنية الصالحة هي 's', 'm', 'h'.", + "Edit": "تعديل", + "Edit Doc": "تعديل الملف", + "Edit Memory": "", + "Edit User": "تعديل المستخدم", + "ElevenLabs": "", + "Email": "البريد", + "Embedding Batch Size": "", + "Embedding Model": "نموذج التضمين", + "Embedding Model Engine": "تضمين محرك النموذج", + "Embedding model set to \"{{embedding_model}}\"": "تم تعيين نموذج التضمين على \"{{embedding_model}}\"", + "Enable Chat History": "تمكين سجل الدردشة", + "Enable Community Sharing": "تمكين مشاركة المجتمع", + "Enable New Sign Ups": "تفعيل عمليات التسجيل الجديدة", + "Enable Web Search": "تمكين بحث الويب", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "تأكد من أن ملف CSV الخاص بك يتضمن 4 أعمدة بهذا الترتيب: Name, Email, Password, Role.", + "Enter {{role}} message here": "أدخل رسالة {{role}} هنا", + "Enter a detail about yourself for your LLMs to recall": "ادخل معلومات عنك تريد أن يتذكرها الموديل", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "أدخل مفتاح واجهة برمجة تطبيقات البحث الشجاع", + "Enter Chunk Overlap": "أدخل الChunk Overlap", + "Enter Chunk Size": "أدخل Chunk الحجم", + "Enter Github Raw URL": "أدخل عنوان URL ل Github Raw", + "Enter Google PSE API Key": "أدخل مفتاح واجهة برمجة تطبيقات PSE من Google", + "Enter Google PSE Engine Id": "أدخل معرف محرك PSE من Google", + "Enter Image Size (e.g. 512x512)": "(e.g. 512x512) أدخل حجم الصورة ", + "Enter language codes": "أدخل كود اللغة", + "Enter model tag (e.g. {{modelTag}})": "(e.g. {{modelTag}}) أدخل الموديل تاق", + "Enter Number of Steps (e.g. 50)": "(e.g. 50) أدخل عدد الخطوات", + "Enter Score": "أدخل النتيجة", + "Enter Searxng Query URL": "أدخل عنوان URL لاستعلام Searxng", + "Enter Serper API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "أدخل مفتاح واجهة برمجة تطبيقات Serpstack", + "Enter stop sequence": "أدخل تسلسل التوقف", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "أدخل Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "الرابط (e.g. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "URL (e.g. http://localhost:11434)", + "Enter Your Email": "أدخل البريد الاكتروني", + "Enter Your Full Name": "أدخل الاسم كامل", + "Enter your message": "", + "Enter Your Password": "ادخل كلمة المرور", + "Enter Your Role": "أدخل الصلاحيات", + "Error": "خطأ", + "Experimental": "تجريبي", + "Export": "تصدير", + "Export All Chats (All Users)": "تصدير جميع الدردشات (جميع المستخدمين)", + "Export chat (.json)": "", + "Export Chats": "تصدير جميع الدردشات", + "Export Documents Mapping": "تصدير وثائق الخرائط", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "نماذج التصدير", + "Export Prompts": "مطالبات التصدير", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "فشل في إنشاء مفتاح API.", + "Failed to read clipboard contents": "فشل في قراءة محتويات الحافظة", + "Failed to update settings": "", + "February": "فبراير", + "Feel free to add specific details": "لا تتردد في إضافة تفاصيل محددة", + "File": "", + "File Mode": "وضع الملف", + "File not found.": "لم يتم العثور على الملف.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "تم اكتشاف انتحال بصمة الإصبع: غير قادر على استخدام الأحرف الأولى كصورة رمزية. الافتراضي لصورة الملف الشخصي الافتراضية.", + "Fluidly stream large external response chunks": "دفق قطع الاستجابة الخارجية الكبيرة بسلاسة", + "Focus chat input": "التركيز على إدخال الدردشة", + "Followed instructions perfectly": "اتبعت التعليمات على أكمل وجه", + "Form": "", + "Format your variables using square brackets like this:": "قم بتنسيق المتغيرات الخاصة بك باستخدام الأقواس المربعة مثل هذا:", + "Frequency Penalty": "عقوبة التردد", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "عام", + "General Settings": "الاعدادات العامة", + "Generate Image": "", + "Generating search query": "إنشاء استعلام بحث", + "Generation Info": "معلومات الجيل", + "Get up and running with": "", + "Global": "", + "Good Response": "استجابة جيدة", + "Google PSE API Key": "مفتاح واجهة برمجة تطبيقات PSE من Google", + "Google PSE Engine Id": "معرف محرك PSE من Google", + "h:mm a": "الساعة:الدقائق صباحا/مساء", + "has no conversations.": "ليس لديه محادثات.", + "Hello, {{name}}": " {{name}} مرحبا", + "Help": "مساعدة", + "Hide": "أخفاء", + "Hide Model": "", + "How can I help you today?": "كيف استطيع مساعدتك اليوم؟", + "Hybrid Search": "البحث الهجين", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "توليد الصور (تجريبي)", + "Image Generation Engine": "محرك توليد الصور", + "Image Settings": "إعدادات الصورة", + "Images": "الصور", + "Import Chats": "استيراد الدردشات", + "Import Documents Mapping": "استيراد خرائط المستندات", + "Import Functions": "", + "Import Models": "استيراد النماذج", + "Import Prompts": "مطالبات الاستيراد", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "قم بتضمين علامة `-api` عند تشغيل Stable-diffusion-webui", + "Info": "معلومات", + "Input commands": "إدخال الأوامر", + "Install from Github URL": "التثبيت من عنوان URL لجيثب", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "واجهه المستخدم", + "Invalid Tag": "تاق غير صالحة", + "January": "يناير", + "join our Discord for help.": "انضم إلى Discord للحصول على المساعدة.", + "JSON": "JSON", + "JSON Preview": "معاينة JSON", + "July": "يوليو", + "June": "يونيو", + "JWT Expiration": "JWT تجريبي", + "JWT Token": "JWT Token", + "Keep Alive": "Keep Alive", + "Keyboard shortcuts": "اختصارات لوحة المفاتيح", + "Knowledge": "", + "Language": "اللغة", + "large language models, locally.": "", + "Last Active": "آخر نشاط", + "Last Modified": "", + "Light": "فاتح", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "يمكن أن تصدر بعض الأخطاء. لذلك يجب التحقق من المعلومات المهمة", + "Local Models": "", + "LTR": "من جهة اليسار إلى اليمين", + "Made by OpenWebUI Community": "OpenWebUI تم إنشاؤه بواسطة مجتمع ", + "Make sure to enclose them with": "تأكد من إرفاقها", + "Manage": "", + "Manage Models": "إدارة النماذج", + "Manage Ollama Models": "Ollama إدارة موديلات ", + "Manage Pipelines": "إدارة خطوط الأنابيب", + "Manage Valves": "", + "March": "مارس", + "Max Tokens (num_predict)": "ماكس توكنز (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "يمكن تنزيل 3 نماذج كحد أقصى في وقت واحد. الرجاء معاودة المحاولة في وقت لاحق.", + "May": "مايو", + "Memories accessible by LLMs will be shown here.": "سيتم عرض الذكريات التي يمكن الوصول إليها بواسطة LLMs هنا.", + "Memory": "الذاكرة", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "لن تتم مشاركة الرسائل التي ترسلها بعد إنشاء الرابط الخاص بك. سيتمكن المستخدمون الذين لديهم عنوان URL من عرض الدردشة المشتركة", + "Minimum Score": "الحد الأدنى من النقاط", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "تم تحميل النموذج '{{modelName}}' بنجاح", + "Model '{{modelTag}}' is already in queue for downloading.": "النموذج '{{modelTag}}' موجود بالفعل في قائمة الانتظار للتحميل", + "Model {{modelId}} not found": "لم يتم العثور على النموذج {{modelId}}.", + "Model {{modelName}} is not vision capable": "نموذج {{modelName}} غير قادر على الرؤية", + "Model {{name}} is now {{status}}": "نموذج {{name}} هو الآن {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "تم اكتشاف مسار نظام الملفات النموذجي. الاسم المختصر للنموذج مطلوب للتحديث، ولا يمكن الاستمرار.", + "Model ID": "رقم الموديل", + "Model not selected": "لم تختار موديل", + "Model Params": "معلمات النموذج", + "Model updated successfully": "", + "Model Whitelisting": "القائمة البيضاء للموديل", + "Model(s) Whitelisted": "القائمة البيضاء الموديل", + "Modelfile Content": "محتوى الملف النموذجي", + "Models": "الموديلات", + "More": "المزيد", + "Name": "الأسم", + "Name Tag": "أسم التاق", + "Name your model": "قم بتسمية النموذج الخاص بك", + "New Chat": "دردشة جديدة", + "New Password": "كلمة المرور الجديدة", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "لا توجد نتايج", + "No search query generated": "لم يتم إنشاء استعلام بحث", + "No source available": "لا يوجد مصدر متاح", + "No valves to update": "", + "None": "اي", + "Not factually correct": "ليس صحيحا من حيث الواقع", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ملاحظة: إذا قمت بتعيين الحد الأدنى من النقاط، فلن يؤدي البحث إلا إلى إرجاع المستندات التي لها نقاط أكبر من أو تساوي الحد الأدنى من النقاط.", + "Notifications": "إشعارات", + "November": "نوفمبر", + "num_thread (Ollama)": "num_thread (أولاما)", + "OAuth ID": "", + "October": "اكتوبر", + "Off": "أغلاق", + "Okay, Let's Go!": "حسنا دعنا نذهب!", + "OLED Dark": "OLED داكن", + "Ollama": "Ollama", + "Ollama API": "أولاما API", + "Ollama API disabled": "أولاما API معطلة", + "Ollama API is disabled": "", + "Ollama Version": "Ollama الاصدار", + "On": "تشغيل", + "Only": "فقط", + "Only alphanumeric characters and hyphens are allowed in the command string.": "يُسمح فقط بالأحرف الأبجدية الرقمية والواصلات في سلسلة الأمر.", + "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.": "خطاء! تمسك بقوة! ملفاتك لا تزال في فرن المعالجة. نحن نطبخهم إلى حد الكمال. يرجى التحلي بالصبر وسنخبرك عندما يصبحون جاهزين.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "خطاء! يبدو أن عنوان URL غير صالح. يرجى التحقق مرة أخرى والمحاولة مرة أخرى.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "خطاء! أنت تستخدم طريقة غير مدعومة (الواجهة الأمامية فقط). يرجى تقديم واجهة WebUI من الواجهة الخلفية.", + "Open AI (Dall-E)": "AI (Dall-E) فتح", + "Open new chat": "فتح محادثة جديده", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API إعدادات", + "OpenAI API Key is required.": "OpenAI API.مطلوب مفتاح ", + "OpenAI URL/Key required.": "URL/مفتاح OpenAI.مطلوب عنوان ", + "or": "أو", + "Other": "آخر", + "Password": "الباسورد", + "PDF document (.pdf)": "PDF ملف (.pdf)", + "PDF Extract Images (OCR)": "PDF أستخرج الصور (OCR)", + "pending": "قيد الانتظار", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "{{error}} تم رفض الإذن عند الوصول إلى الميكروفون ", + "Personalization": "التخصيص", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "خطوط الانابيب", + "Pipelines Not Detected": "", + "Pipelines Valves": "صمامات خطوط الأنابيب", + "Plain text (.txt)": "نص عادي (.txt)", + "Playground": "مكان التجربة", + "Please carefully review the following warnings:": "", + "Positive attitude": "موقف ايجابي", + "Previous 30 days": "أخر 30 يوم", + "Previous 7 days": "أخر 7 أيام", + "Profile Image": "صورة الملف الشخصي", + "Prompt": "مطالبة", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "موجه (على سبيل المثال: أخبرني بحقيقة ممتعة عن الإمبراطورية الرومانية)", + "Prompt Content": "محتوى عاجل", + "Prompt suggestions": "اقتراحات سريعة", + "Prompts": "مطالبات", + "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com \"{{searchValue}}\" أسحب من ", + "Pull a model from Ollama.com": "Ollama.com سحب الموديل من ", + "Query Params": "Query Params", + "RAG Template": "RAG تنمبلت", + "Read Aloud": "أقراء لي", + "Record voice": "سجل صوت", + "Redirecting you to OpenWebUI Community": "OpenWebUI إعادة توجيهك إلى مجتمع ", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "رفض عندما لا ينبغي أن يكون", + "Regenerate": "تجديد", + "Release Notes": "ملاحظات الإصدار", + "Remove": "إزالة", + "Remove Model": "حذف الموديل", + "Rename": "إعادة تسمية", + "Repeat Last N": "N كرر آخر", + "Request Mode": "وضع الطلب", + "Reranking Model": "إعادة تقييم النموذج", + "Reranking model disabled": "تم تعطيل نموذج إعادة الترتيب", + "Reranking model set to \"{{reranking_model}}\"": "تم ضبط نموذج إعادة الترتيب على \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "إعادة تعيين تخزين المتجهات", + "Response AutoCopy to Clipboard": "النسخ التلقائي للاستجابة إلى الحافظة", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "منصب", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "من اليمين إلى اليسار", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "حفظ", + "Save & Create": "حفظ وإنشاء", + "Save & Update": "حفظ وتحديث", + "Save Tag": "", + "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": "لم يعد حفظ سجلات الدردشة مباشرة في مساحة تخزين متصفحك مدعومًا. يرجى تخصيص بعض الوقت لتنزيل وحذف سجلات الدردشة الخاصة بك عن طريق النقر على الزر أدناه. لا تقلق، يمكنك بسهولة إعادة استيراد سجلات الدردشة الخاصة بك إلى الواجهة الخلفية من خلاله", + "Scan": "مسح", + "Scan complete!": "تم المسح", + "Scan for documents from {{path}}": "{{path}} مسح على الملفات من", + "Search": "البحث", + "Search a model": "البحث عن موديل", + "Search Chats": "البحث في الدردشات", + "Search Documents": "البحث المستندات", + "Search Functions": "", + "Search Models": "نماذج البحث", + "Search Prompts": "أبحث حث", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "عدد نتائج البحث", + "Search Tools": "", + "Searched {{count}} sites_zero": "تم البحث في {{count}} sites_zero", + "Searched {{count}} sites_one": "تم البحث في {{count}} sites_one", + "Searched {{count}} sites_two": "تم البحث في {{count}} sites_two", + "Searched {{count}} sites_few": "تم البحث في {{count}} sites_few", + "Searched {{count}} sites_many": "تم البحث في {{count}} sites_many", + "Searched {{count}} sites_other": "تم البحث في {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "عنوان URL لاستعلام Searxng", + "See readme.md for instructions": "readme.md للحصول على التعليمات", + "See what's new": "ما الجديد", + "Seed": "Seed", + "Select a base model": "حدد نموذجا أساسيا", + "Select a engine": "", + "Select a function": "", + "Select a mode": "أختار موديل", + "Select a model": "أختار الموديل", + "Select a pipeline": "حدد مسارا", + "Select a pipeline url": "حدد عنوان URL لخط الأنابيب", + "Select a tool": "", + "Select an Ollama instance": "أختار سيرفر ", + "Select Documents": "", + "Select model": " أختار موديل", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "النموذج (النماذج) المحددة لا تدعم مدخلات الصور", + "Send": "تم", + "Send a Message": "يُرجى إدخال طلبك هنا", + "Send message": "يُرجى إدخال طلبك هنا.", + "September": "سبتمبر", + "Serper API Key": "مفتاح واجهة برمجة تطبيقات سيربر", + "Serply API Key": "", + "Serpstack API Key": "مفتاح واجهة برمجة تطبيقات Serpstack", + "Server connection verified": "تم التحقق من اتصال الخادم", + "Set as default": "الافتراضي", + "Set Default Model": "تفعيد الموديل الافتراضي", + "Set embedding model (e.g. {{model}})": "ضبط نموذج المتجهات (على سبيل المثال: {{model}})", + "Set Image Size": "حجم الصورة", + "Set reranking model (e.g. {{model}})": "ضبط نموذج إعادة الترتيب (على سبيل المثال: {{model}})", + "Set Steps": "ضبط الخطوات", + "Set Task Model": "تعيين نموذج المهمة", + "Set Voice": "ضبط الصوت", + "Settings": "الاعدادات", + "Settings saved successfully!": "تم حفظ الاعدادات بنجاح", + "Settings updated successfully": "", + "Share": "كشاركة", + "Share Chat": "مشاركة الدردشة", + "Share to OpenWebUI Community": "OpenWebUI شارك في مجتمع", + "short-summary": "ملخص قصير", + "Show": "عرض", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "إظهار الاختصارات", + "Show your support!": "", + "Showcased creativity": "أظهر الإبداع", + "Sign in": "تسجيل الدخول", + "Sign Out": "تسجيل الخروج", + "Sign up": "تسجيل", + "Signing in": "جاري الدخول", + "Source": "المصدر", + "Speech recognition error: {{error}}": "{{error}} خطأ في التعرف على الكلام", + "Speech-to-Text Engine": "محرك تحويل الكلام إلى نص", + "Stop Sequence": "وقف التسلسل", + "STT Model": "", + "STT Settings": "STT اعدادات", + "Submit": "إرسال", + "Subtitle (e.g. about the Roman Empire)": "(e.g. about the Roman Empire) الترجمة", + "Success": "نجاح", + "Successfully updated.": "تم التحديث بنجاح", + "Suggested": "مقترحات", + "Support": "", + "Support this plugin:": "", + "System": "النظام", + "System Prompt": "محادثة النظام", + "Tags": "الوسوم", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "أخبرنا المزيد:", + "Temperature": "درجة حرارة", + "Template": "نموذج", + "Text Completion": "اكتمال النص", + "Text-to-Speech Engine": "محرك تحويل النص إلى كلام", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "شكرا لملاحظاتك!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "يجب أن تكون النتيجة قيمة تتراوح بين 0.0 (0%) و1.0 (100%).", + "Theme": "الثيم", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "وهذا يضمن حفظ محادثاتك القيمة بشكل آمن في قاعدة بياناتك الخلفية. شكرًا لك!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "لا تتم مزامنة هذا الإعداد عبر المتصفحات أو الأجهزة.", + "This will delete": "", + "Thorough explanation": "شرح شامل", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ملاحضة: قم بتحديث عدة فتحات متغيرة على التوالي عن طريق الضغط على مفتاح tab في مدخلات الدردشة بعد كل استبدال.", + "Title": "العنوان", + "Title (e.g. Tell me a fun fact)": "(e.g. Tell me a fun fact) العناون", + "Title Auto-Generation": "توليد تلقائي للعنوان", + "Title cannot be an empty string.": "العنوان مطلوب", + "Title Generation Prompt": "موجه إنشاء العنوان", + "to": "الى", + "To access the available model names for downloading,": "للوصول إلى أسماء الموديلات المتاحة للتنزيل،", + "To access the GGUF models available for downloading,": "للوصول إلى الموديلات GGUF المتاحة للتنزيل،", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "الى كتابة المحادثه", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "اليوم", + "Toggle settings": "فتح وأغلاق الاعدادات", + "Toggle sidebar": "فتح وأغلاق الشريط الجانبي", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "هل تواجه مشكلة في الوصول", + "TTS Model": "", + "TTS Settings": "TTS اعدادات", + "TTS Voice": "", + "Type": "نوع", + "Type Hugging Face Resolve (Download) URL": "اكتب عنوان URL لحل مشكلة الوجه (تنزيل).", + "Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}خطاء أوه! حدثت مشكلة في الاتصال بـ ", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "تحديث ونسخ الرابط", + "Update password": "تحديث كلمة المرور", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "GGUF رفع موديل نوع", + "Upload Files": "تحميل الملفات", + "Upload Pipeline": "", + "Upload Progress": "جاري التحميل", + "URL Mode": "رابط الموديل", + "Use '#' in the prompt input to load and select your documents.": "أستخدم '#' في المحادثة لربطهامن المستندات", + "Use Gravatar": "Gravatar أستخدم", + "Use Initials": "Initials أستخدم", + "use_mlock (Ollama)": "use_mlock (أولاما)", + "use_mmap (Ollama)": "use_mmap (أولاما)", + "user": "مستخدم", + "User location successfully retrieved.": "", + "User Permissions": "صلاحيات المستخدم", + "Users": "المستخدمين", + "Utilize": "يستخدم", + "Valid time units:": "وحدات زمنية صالحة:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "المتغير", + "variable to have them replaced with clipboard content.": "متغير لاستبدالها بمحتوى الحافظة.", + "Version": "إصدار", + "Voice": "", + "Warning": "تحذير", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "تحذير: إذا قمت بتحديث أو تغيير نموذج التضمين الخاص بك، فستحتاج إلى إعادة استيراد كافة المستندات.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Web تحميل اعدادات", + "Web Params": "Web تحميل اعدادات", + "Web Search": "بحث الويب", + "Web Search Engine": "محرك بحث الويب", + "Webhook URL": "Webhook الرابط", + "WebUI Settings": "WebUI اعدادات", + "WebUI will make requests to": "سوف يقوم WebUI بتقديم طلبات ل", + "What’s New in": "ما هو الجديد", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "عند إيقاف تشغيل السجل، لن تظهر الدردشات الجديدة على هذا المتصفح في سجلك على أي من أجهزتك.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "مساحة العمل", + "Write a prompt suggestion (e.g. Who are you?)": "اكتب اقتراحًا سريعًا (على سبيل المثال، من أنت؟)", + "Write a summary in 50 words that summarizes [topic or keyword].": "اكتب ملخصًا في 50 كلمة يلخص [الموضوع أو الكلمة الرئيسية]", + "Yesterday": "أمس", + "You": "انت", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "لا يمكنك استنساخ نموذج أساسي", + "You have no archived conversations.": "لا تملك محادثات محفوظه", + "You have shared this chat": "تم مشاركة هذه المحادثة", + "You're a helpful assistant.": "مساعدك المفيد هنا", + "You're now logged in.": "لقد قمت الآن بتسجيل الدخول.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube تحميل اعدادات" +} diff --git a/src/lib/i18n/locales/bg-BG/translation.json b/src/lib/i18n/locales/bg-BG/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..917c05bb6853bf5df77e03b78ec29bb7e80c8007 --- /dev/null +++ b/src/lib/i18n/locales/bg-BG/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' или '-1' за неограничен срок.", + "(Beta)": "(Бета)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)", + "(latest)": "(последна)", + "{{ models }}": "{{ модели }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Не можете да изтриете базов модел", + "{{modelName}} is thinking...": "{{modelName}} мисли ...", + "{{user}}'s Chats": "{{user}}'s чатове", + "{{webUIName}} Backend Required": "{{webUIName}} Изисква се Бекенд", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Моделът на задачите се използва при изпълнение на задачи като генериране на заглавия за чатове и заявки за търсене в мрежата", + "a user": "потребител", + "About": "Относно", + "Account": "Акаунт", + "Account Activation Pending": "", + "Accurate information": "Точни информация", + "Actions": "", + "Active Users": "", + "Add": "Добавяне", + "Add a model id": "Добавяне на ИД на модел", + "Add a short description about what this model does": "Добавете кратко описание за това какво прави този модел", + "Add a short title for this prompt": "Добавяне на кратко заглавие за този промпт", + "Add a tag": "Добавяне на таг", + "Add custom prompt": "Добавяне на собствен промпт", + "Add Docs": "Добавяне на Документи", + "Add Files": "Добавяне на Файлове", + "Add Memory": "Добавяне на Памет", + "Add message": "Добавяне на съобщение", + "Add Model": "Добавяне на Модел", + "Add Tag": "", + "Add Tags": "добавяне на тагове", + "Add User": "Добавяне на потребител", + "Adjusting these settings will apply changes universally to all users.": "При промяна на тези настройки промените се прилагат за всички потребители.", + "admin": "админ", + "Admin": "", + "Admin Panel": "Панел на Администратор", + "Admin Settings": "Настройки на Администратор", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Разширени Параметри", + "Advanced Params": "Разширени параметри", + "all": "всички", + "All Documents": "Всички Документи", + "All Users": "Всички Потребители", + "Allow": "Позволи", + "Allow Chat Deletion": "Позволи Изтриване на Чат", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "алфанумерични знаци и тире", + "Already have an account?": "Вече имате акаунт? ", + "an assistant": "асистент", + "and": "и", + "and create a new shared link.": "и създай нов общ линк.", + "API Base URL": "API Базов URL", + "API Key": "API Ключ", + "API Key created.": "API Ключ създаден.", + "API keys": "API Ключове", + "April": "Април", + "Archive": "Архивирани Чатове", + "Archive All Chats": "Архив Всички чатове", + "Archived Chats": "Архивирани Чатове", + "are allowed - Activate this command by typing": "са разрешени - Активирайте тази команда чрез въвеждане", + "Are you sure?": "Сигурни ли сте?", + "Attach file": "Прикачване на файл", + "Attention to detail": "Внимание към детайлите", + "Audio": "Аудио", + "Audio settings updated successfully": "", + "August": "Август", + "Auto-playback response": "Аувтоматично възпроизвеждане на Отговора", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Базов URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Базов URL е задължителен.", + "available!": "наличен!", + "Back": "Назад", + "Bad Response": "Невалиден отговор от API", + "Banners": "Банери", + "Base Model (From)": "Базов модел (от)", + "Batch Size (num_batch)": "", + "before": "преди", + "Being lazy": "Да бъдеш мързелив", + "Brave Search API Key": "Смел ключ за API за търсене", + "Bypass SSL verification for Websites": "Изключване на SSL проверката за сайтове", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Отказ", + "Capabilities": "Възможности", + "Change Password": "Промяна на Парола", + "Chat": "Чат", + "Chat Background Image": "", + "Chat Bubble UI": "UI за чат бублон", + "Chat Controls": "", + "Chat direction": "Направление на чата", + "Chat History": "Чат История", + "Chat History is off for this browser.": "Чат История е изключен за този браузър.", + "Chats": "Чатове", + "Check Again": "Проверете Още Веднъж", + "Check for updates": "Проверка за актуализации", + "Checking for updates...": "Проверка за актуализации...", + "Choose a model before saving...": "Изберете модел преди запазване...", + "Chunk Overlap": "Chunk Overlap", + "Chunk Params": "Chunk Params", + "Chunk Size": "Chunk Size", + "Citation": "Цитат", + "Clear memory": "", + "Click here for help.": "Натиснете тук за помощ.", + "Click here to": "Натиснете тук за", + "Click here to download user import template file.": "", + "Click here to select": "Натиснете тук, за да изберете", + "Click here to select a csv file.": "Натиснете тук, за да изберете csv файл.", + "Click here to select a py file.": "", + "Click here to select documents.": "Натиснете тук, за да изберете документи.", + "click here.": "натиснете тук.", + "Click on the user role button to change a user's role.": "Натиснете върху бутона за промяна на ролята на потребителя.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Клонинг", + "Close": "Затвори", + "Code formatted successfully": "", + "Collection": "Колекция", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "ComfyUI Base URL е задължително.", + "Command": "Команда", + "Concurrent Requests": "Едновременни искания", + "Confirm": "", + "Confirm Password": "Потвърди Парола", + "Confirm your action": "", + "Connections": "Връзки", + "Contact Admin for WebUI Access": "", + "Content": "Съдържание", + "Content Extraction": "", + "Context Length": "Дължина на Контекста", + "Continue Response": "Продължи отговора", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Копирана е връзката за чат!", + "Copy": "Копирай", + "Copy last code block": "Копиране на последен код блок", + "Copy last response": "Копиране на последен отговор", + "Copy Link": "Копиране на връзка", + "Copying to clipboard was successful!": "Копирането в клипборда беше успешно!", + "Create a model": "Създаване на модел", + "Create Account": "Създаване на Акаунт", + "Create new key": "Създаване на нов ключ", + "Create new secret key": "Създаване на нов секретен ключ", + "Created at": "Създадено на", + "Created At": "Създадено на", + "Created by": "", + "CSV Import": "", + "Current Model": "Текущ модел", + "Current Password": "Текуща Парола", + "Custom": "Персонализиран", + "Customize models for a specific purpose": "Персонализиране на модели за конкретна цел", + "Dark": "Тъмен", + "Dashboard": "", + "Database": "База данни", + "December": "Декември", + "Default": "По подразбиране", + "Default (Automatic1111)": "По подразбиране (Automatic1111)", + "Default (SentenceTransformers)": "По подразбиране (SentenceTransformers)", + "Default Model": "Модел по подразбиране", + "Default model updated": "Моделът по подразбиране е обновен", + "Default Prompt Suggestions": "Промпт Предложения по подразбиране", + "Default User Role": "Роля на потребителя по подразбиране", + "delete": "изтриване", + "Delete": "Изтриване", + "Delete a model": "Изтриване на модел", + "Delete All Chats": "Изтриване на всички чатове", + "Delete chat": "Изтриване на чат", + "Delete Chat": "Изтриване на Чат", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "Изтриване на този линк", + "Delete tool?": "", + "Delete User": "Изтриване на потребител", + "Deleted {{deleteModelTag}}": "Изтрито {{deleteModelTag}}", + "Deleted {{name}}": "Изтрито {{име}}", + "Description": "Описание", + "Didn't fully follow instructions": "Не следва инструкциите", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Открийте модел", + "Discover a prompt": "Откриване на промпт", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Откриване, сваляне и преглед на персонализирани промптове", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Откриване, сваляне и преглед на пресетове на модели", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Показване на потребителското име вместо Вие в чата", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Документ", + "Document Settings": "Документ Настройки", + "Documentation": "", + "Documents": "Документи", + "does not make any external connections, and your data stays securely on your locally hosted server.": "няма външни връзки, и вашите данни остават сигурни на локално назначен сървър.", + "Don't Allow": "Не Позволявай", + "Don't have an account?": "Нямате акаунт?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Не харесваш стила?", + "Done": "", + "Download": "Изтегляне отменено", + "Download canceled": "Изтегляне отменено", + "Download Database": "Сваляне на база данни", + "Drop any files here to add to the conversation": "Пускане на файлове тук, за да ги добавите в чата", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр. '30с','10м'. Валидни единици са 'с', 'м', 'ч'.", + "Edit": "Редактиране", + "Edit Doc": "Редактиране на документ", + "Edit Memory": "", + "Edit User": "Редактиране на потребител", + "ElevenLabs": "", + "Email": "Имейл", + "Embedding Batch Size": "", + "Embedding Model": "Модел за вграждане", + "Embedding Model Engine": "Модел за вграждане", + "Embedding model set to \"{{embedding_model}}\"": "Модел за вграждане е настроен на \"{{embedding_model}}\"", + "Enable Chat History": "Вклюване на Чат История", + "Enable Community Sharing": "Разрешаване на споделяне в общност", + "Enable New Sign Ups": "Вклюване на Нови Потребители", + "Enable Web Search": "Разрешаване на търсене в уеб", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Уверете се, че вашият CSV файл включва 4 колони в следния ред: Име, Имейл, Парола, Роля.", + "Enter {{role}} message here": "Въведете съобщение за {{role}} тук", + "Enter a detail about yourself for your LLMs to recall": "Въведете подробности за себе си, за да се herinnerат вашите LLMs", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Въведете Brave Search API ключ", + "Enter Chunk Overlap": "Въведете Chunk Overlap", + "Enter Chunk Size": "Въведете Chunk Size", + "Enter Github Raw URL": "Въведете URL адреса на Github Raw", + "Enter Google PSE API Key": "Въведете Google PSE API ключ", + "Enter Google PSE Engine Id": "Въведете идентификатор на двигателя на Google PSE", + "Enter Image Size (e.g. 512x512)": "Въведете размер на изображението (напр. 512x512)", + "Enter language codes": "Въведете кодове на езика", + "Enter model tag (e.g. {{modelTag}})": "Въведете таг на модел (напр. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Въведете брой стъпки (напр. 50)", + "Enter Score": "Въведете оценка", + "Enter Searxng Query URL": "Въведете URL адреса на заявката на Searxng", + "Enter Serper API Key": "Въведете Serper API ключ", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Въведете Serpstack API ключ", + "Enter stop sequence": "Въведете стоп последователност", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Въведете Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Въведете URL (напр. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Въведете URL (напр. http://localhost:11434)", + "Enter Your Email": "Въведете имейл", + "Enter Your Full Name": "Въведете вашето пълно име", + "Enter your message": "", + "Enter Your Password": "Въведете вашата парола", + "Enter Your Role": "Въведете вашата роля", + "Error": "Грешка", + "Experimental": "Експериментално", + "Export": "Износ", + "Export All Chats (All Users)": "Експортване на всички чатове (За всички потребители)", + "Export chat (.json)": "", + "Export Chats": "Експортване на чатове", + "Export Documents Mapping": "Експортване на документен мапинг", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Експортиране на модели", + "Export Prompts": "Експортване на промптове", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Неуспешно създаване на API ключ.", + "Failed to read clipboard contents": "Грешка при четене на съдържанието от клипборда", + "Failed to update settings": "", + "February": "Февруари", + "Feel free to add specific details": "Feel free to add specific details", + "File": "", + "File Mode": "Файл Мод", + "File not found.": "Файл не е намерен.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Потвърждаване на отпечатък: Не може да се използва инициализационна буква като аватар. Потребителят се връща към стандартна аватарка.", + "Fluidly stream large external response chunks": "Плавно предаване на големи части от външен отговор", + "Focus chat input": "Фокусиране на чат вход", + "Followed instructions perfectly": "Следвайте инструкциите перфектно", + "Form": "", + "Format your variables using square brackets like this:": "Форматирайте вашите променливи, като използвате квадратни скоби, както следва:", + "Frequency Penalty": "Наказание за честота", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Основни", + "General Settings": "Основни Настройки", + "Generate Image": "", + "Generating search query": "Генериране на заявка за търсене", + "Generation Info": "Информация за Генерация", + "Get up and running with": "", + "Global": "", + "Good Response": "Добра отговор", + "Google PSE API Key": "Google PSE API ключ", + "Google PSE Engine Id": "Идентификатор на двигателя на Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "няма разговори.", + "Hello, {{name}}": "Здравей, {{name}}", + "Help": "Помощ", + "Hide": "Скрий", + "Hide Model": "", + "How can I help you today?": "Как мога да ви помогна днес?", + "Hybrid Search": "Hybrid Search", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Генерация на изображения (Експериментално)", + "Image Generation Engine": "Двигател за генериране на изображения", + "Image Settings": "Настройки на изображения", + "Images": "Изображения", + "Import Chats": "Импортване на чатове", + "Import Documents Mapping": "Импортване на документен мапинг", + "Import Functions": "", + "Import Models": "Импортиране на модели", + "Import Prompts": "Импортване на промптове", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Включете флага `--api`, когато стартирате stable-diffusion-webui", + "Info": "Информация", + "Input commands": "Въведете команди", + "Install from Github URL": "Инсталиране от URL адреса на Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Интерфейс", + "Invalid Tag": "Невалиден тег", + "January": "Януари", + "join our Discord for help.": "свържете се с нашия Discord за помощ.", + "JSON": "JSON", + "JSON Preview": "JSON Преглед", + "July": "Июл", + "June": "Июн", + "JWT Expiration": "JWT Expiration", + "JWT Token": "JWT Token", + "Keep Alive": "Keep Alive", + "Keyboard shortcuts": "Клавиши за бърз достъп", + "Knowledge": "", + "Language": "Език", + "large language models, locally.": "", + "Last Active": "Последни активни", + "Last Modified": "", + "Light": "Светъл", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMs могат да правят грешки. Проверете важните данни.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Направено от OpenWebUI общността", + "Make sure to enclose them with": "Уверете се, че са заключени с", + "Manage": "", + "Manage Models": "Управление на Моделите", + "Manage Ollama Models": "Управление на Ollama Моделите", + "Manage Pipelines": "Управление на тръбопроводи", + "Manage Valves": "", + "March": "Март", + "Max Tokens (num_predict)": "Макс токени (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 модели могат да бъдат сваляни едновременно. Моля, опитайте отново по-късно.", + "May": "Май", + "Memories accessible by LLMs will be shown here.": "Мемории достъпни от LLMs ще бъдат показани тук.", + "Memory": "Мемория", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Съобщенията, които изпращате след създаването на връзката, няма да бъдат споделяни. Потребителите с URL адреса ще могат да видят споделения чат.", + "Minimum Score": "Минимална оценка", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Моделът '{{modelName}}' беше успешно свален.", + "Model '{{modelTag}}' is already in queue for downloading.": "Моделът '{{modelTag}}' е вече в очакване за сваляне.", + "Model {{modelId}} not found": "Моделът {{modelId}} не е намерен", + "Model {{modelName}} is not vision capable": "Моделът {{modelName}} не може да се вижда", + "Model {{name}} is now {{status}}": "Моделът {{name}} сега е {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Открит е път до файловата система на модела. За актуализацията се изисква съкратено име на модела, не може да продължи.", + "Model ID": "ИД на модел", + "Model not selected": "Не е избран модел", + "Model Params": "Модел Params", + "Model updated successfully": "", + "Model Whitelisting": "Модел Whitelisting", + "Model(s) Whitelisted": "Модели Whitelisted", + "Modelfile Content": "Съдържание на модфайл", + "Models": "Модели", + "More": "Повече", + "Name": "Име", + "Name Tag": "Име Таг", + "Name your model": "Дайте име на вашия модел", + "New Chat": "Нов чат", + "New Password": "Нова парола", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Няма намерени резултати", + "No search query generated": "Не е генерирана заявка за търсене", + "No source available": "Няма наличен източник", + "No valves to update": "", + "None": "Никой", + "Not factually correct": "Не е фактологически правилно", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Забележка: Ако зададете минимален резултат, търсенето ще върне само документи с резултат, по-голям или равен на минималния резултат.", + "Notifications": "Десктоп Известия", + "November": "Ноември", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Октомври", + "Off": "Изкл.", + "Okay, Let's Go!": "ОК, Нека започваме!", + "OLED Dark": "OLED тъмно", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API деактивиран", + "Ollama API is disabled": "", + "Ollama Version": "Ollama Версия", + "On": "Вкл.", + "Only": "Само", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерични знаци и тире са разрешени в командния низ.", + "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.": "Упс! Задръжте! Файловете ви все още са в пещта за обработка. Готвим ги до съвършенство. Моля, бъдете търпеливи и ще ви уведомим, когато са готови.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изглежда URL адресът е невалиден. Моля, проверете отново и опитайте пак.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Използвате неподдържан метод (само фронтенд). Моля, сервирайте WebUI от бекенда.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Отвори нов чат", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API Config", + "OpenAI API Key is required.": "OpenAI API ключ е задължителен.", + "OpenAI URL/Key required.": "OpenAI URL/Key е задължителен.", + "or": "или", + "Other": "Other", + "Password": "Парола", + "PDF document (.pdf)": "PDF документ (.pdf)", + "PDF Extract Images (OCR)": "PDF Extract Images (OCR)", + "pending": "в очакване", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}", + "Personalization": "Персонализация", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Тръбопроводи", + "Pipelines Not Detected": "", + "Pipelines Valves": "Тръбопроводи Вентили", + "Plain text (.txt)": "Plain text (.txt)", + "Playground": "Плейграунд", + "Please carefully review the following warnings:": "", + "Positive attitude": "Позитивна ативност", + "Previous 30 days": "Предыдущите 30 дни", + "Previous 7 days": "Предыдущите 7 дни", + "Profile Image": "Профилна снимка", + "Prompt": "Промпт", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Промпт (напр. Обмисли ме забавна факт за Римската империя)", + "Prompt Content": "Съдържание на промпта", + "Prompt suggestions": "Промпт предложения", + "Prompts": "Промптове", + "Pull \"{{searchValue}}\" from Ollama.com": "Извади \"{{searchValue}}\" от Ollama.com", + "Pull a model from Ollama.com": "Издърпайте модел от Ollama.com", + "Query Params": "Query Параметри", + "RAG Template": "RAG Шаблон", + "Read Aloud": "Прочети на Голос", + "Record voice": "Записване на глас", + "Redirecting you to OpenWebUI Community": "Пренасочване към OpenWebUI общността", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Отказано, когато не трябва да бъде", + "Regenerate": "Регенериране", + "Release Notes": "Бележки по изданието", + "Remove": "Изтриване", + "Remove Model": "Изтриване на модела", + "Rename": "Преименуване", + "Repeat Last N": "Repeat Last N", + "Request Mode": "Request Mode", + "Reranking Model": "Reranking Model", + "Reranking model disabled": "Reranking model disabled", + "Reranking model set to \"{{reranking_model}}\"": "Reranking model set to \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Ресет Vector Storage", + "Response AutoCopy to Clipboard": "Аувтоматично копиране на отговор в клипборда", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Роля", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Запис", + "Save & Create": "Запис & Създаване", + "Save & Update": "Запис & Актуализиране", + "Save Tag": "", + "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": "Запазването на чат логове директно в хранилището на вашия браузър вече не се поддържа. Моля, отделете малко време, за да изтеглите и изтриете чат логовете си, като щракнете върху бутона по-долу. Не се притеснявайте, можете лесно да импортирате отново чат логовете си в бекенда чрез", + "Scan": "Сканиране", + "Scan complete!": "Сканиране завършено!", + "Scan for documents from {{path}}": "Сканиране за документи в {{path}}", + "Search": "Търси", + "Search a model": "Търси модел", + "Search Chats": "Търсене на чатове", + "Search Documents": "Търси Документи", + "Search Functions": "", + "Search Models": "Търсене на модели", + "Search Prompts": "Търси Промптове", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Брой резултати от търсенето", + "Search Tools": "", + "Searched {{count}} sites_one": "Търси се в {{count}} sites_one", + "Searched {{count}} sites_other": "Търси се в {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "URL адрес на заявка на Searxng", + "See readme.md for instructions": "Виж readme.md за инструкции", + "See what's new": "Виж какво е новото", + "Seed": "Seed", + "Select a base model": "Изберете базов модел", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Изберете режим", + "Select a model": "Изберете модел", + "Select a pipeline": "Изберете тръбопровод", + "Select a pipeline url": "Избор на URL адрес на канал", + "Select a tool": "", + "Select an Ollama instance": "Изберете Ollama инстанция", + "Select Documents": "", + "Select model": "Изберете модел", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Избраният(те) модел(и) не поддържа въвеждане на изображения", + "Send": "Изпрати", + "Send a Message": "Изпращане на Съобщение", + "Send message": "Изпращане на съобщение", + "September": "Септември", + "Serper API Key": "Serper API ключ", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API ключ", + "Server connection verified": "Server connection verified", + "Set as default": "Задай по подразбиране", + "Set Default Model": "Задай Модел По Подразбиране", + "Set embedding model (e.g. {{model}})": "Задай embedding model (e.g. {{model}})", + "Set Image Size": "Задай Размер на Изображението", + "Set reranking model (e.g. {{model}})": "Задай reranking model (e.g. {{model}})", + "Set Steps": "Задай Стъпки", + "Set Task Model": "Задаване на модел на задача", + "Set Voice": "Задай Глас", + "Settings": "Настройки", + "Settings saved successfully!": "Настройките са запазени успешно!", + "Settings updated successfully": "", + "Share": "Подели", + "Share Chat": "Подели Чат", + "Share to OpenWebUI Community": "Споделите с OpenWebUI Общността", + "short-summary": "short-summary", + "Show": "Покажи", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Покажи", + "Show your support!": "", + "Showcased creativity": "Показана креативност", + "Sign in": "Вписване", + "Sign Out": "Изход", + "Sign up": "Регистрация", + "Signing in": "Вписване", + "Source": "Източник", + "Speech recognition error: {{error}}": "Speech recognition error: {{error}}", + "Speech-to-Text Engine": "Speech-to-Text Engine", + "Stop Sequence": "Stop Sequence", + "STT Model": "", + "STT Settings": "STT Настройки", + "Submit": "Изпращане", + "Subtitle (e.g. about the Roman Empire)": "Подтитул (напр. за Римска империя)", + "Success": "Успех", + "Successfully updated.": "Успешно обновено.", + "Suggested": "Препоръчано", + "Support": "", + "Support this plugin:": "", + "System": "Система", + "System Prompt": "Системен Промпт", + "Tags": "Тагове", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Повече информация:", + "Temperature": "Температура", + "Template": "Шаблон", + "Text Completion": "Text Completion", + "Text-to-Speech Engine": "Text-to-Speech Engine", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Благодарим ви за вашия отзив!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "The score should be a value between 0.0 (0%) and 1.0 (100%).", + "Theme": "Тема", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Това гарантира, че ценните ви разговори се запазват сигурно във вашата бекенд база данни. Благодарим ви!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Тази настройка не се синхронизира между браузъри или устройства.", + "This will delete": "", + "Thorough explanation": "Това е подробно описание.", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Съвет: Актуализирайте няколко слота за променливи последователно, като натискате клавиша Tab в чат входа след всяка подмяна.", + "Title": "Заглавие", + "Title (e.g. Tell me a fun fact)": "Заглавие (напр. Моля, кажете ми нещо забавно)", + "Title Auto-Generation": "Автоматично Генериране на Заглавие", + "Title cannot be an empty string.": "Заглавието не може да бъде празно.", + "Title Generation Prompt": "Промпт за Генериране на Заглавие", + "to": "в", + "To access the available model names for downloading,": "За да получите достъп до наличните имена на модели за изтегляне,", + "To access the GGUF models available for downloading,": "За да получите достъп до GGUF моделите, налични за изтегляне,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "към чат входа.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "днес", + "Toggle settings": "Toggle settings", + "Toggle sidebar": "Toggle sidebar", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Проблеми с достъпът до Ollama?", + "TTS Model": "", + "TTS Settings": "TTS Настройки", + "TTS Voice": "", + "Type": "Вид", + "Type Hugging Face Resolve (Download) URL": "Въведете Hugging Face Resolve (Download) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "О, не! Възникна проблем при свързването с {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Обнови и копирай връзка", + "Update password": "Обновяване на парола", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Качване на GGUF модел", + "Upload Files": "Качване на файлове", + "Upload Pipeline": "", + "Upload Progress": "Прогрес на качването", + "URL Mode": "URL Mode", + "Use '#' in the prompt input to load and select your documents.": "Използвайте '#' във промпта за да заредите и изберете вашите документи.", + "Use Gravatar": "Използвайте Gravatar", + "Use Initials": "Използвайте Инициали", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "потребител", + "User location successfully retrieved.": "", + "User Permissions": "Права на потребителя", + "Users": "Потребители", + "Utilize": "Използване", + "Valid time units:": "Валидни единици за време:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "променлива", + "variable to have them replaced with clipboard content.": "променливи да се заменят съдържанието от клипборд.", + "Version": "Версия", + "Voice": "", + "Warning": "Предупреждение", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Предупреждение: Ако актуализирате или промените вашия модел за вграждане, трябва да повторите импортирането на всички документи.", + "Web": "Уеб", + "Web API": "", + "Web Loader Settings": "Настройки за зареждане на уеб", + "Web Params": "Параметри за уеб", + "Web Search": "Търсене в уеб", + "Web Search Engine": "Уеб търсачка", + "Webhook URL": "Уебхук URL", + "WebUI Settings": "WebUI Настройки", + "WebUI will make requests to": "WebUI ще направи заявки към", + "What’s New in": "Какво е новото в", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когато историята е изключена, нови чатове в този браузър ще не се показват в историята на никои от вашия профил.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Работно пространство", + "Write a prompt suggestion (e.g. Who are you?)": "Напиши предложение за промпт (напр. Кой сте вие?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Напиши описание в 50 знака, което описва [тема или ключова дума].", + "Yesterday": "вчера", + "You": "вие", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Не можете да клонирате базов модел", + "You have no archived conversations.": "Нямате архивирани разговори.", + "You have shared this chat": "Вие сте споделели този чат", + "You're a helpful assistant.": "Вие сте полезен асистент.", + "You're now logged in.": "Сега, вие влязохте в системата.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube Loader Settings" +} diff --git a/src/lib/i18n/locales/bn-BD/translation.json b/src/lib/i18n/locales/bn-BD/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..8b645845125fd9d27523158414529ca8ac190ff8 --- /dev/null +++ b/src/lib/i18n/locales/bn-BD/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' অথবা অনির্দিষ্টকাল মেয়াদের জন্য '-1' ", + "(Beta)": "(পরিক্ষামূলক)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(যেমন `sh webui.sh --api`)", + "(latest)": "(সর্বশেষ)", + "{{ models }}": "{{ মডেল}}", + "{{ owner }}: You cannot delete a base model": "{{ owner}}: আপনি একটি বেস মডেল মুছতে পারবেন না", + "{{modelName}} is thinking...": "{{modelName}} চিন্তা করছে...", + "{{user}}'s Chats": "{{user}}র চ্যাটস", + "{{webUIName}} Backend Required": "{{webUIName}} ব্যাকএন্ড আবশ্যক", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "চ্যাট এবং ওয়েব অনুসন্ধান প্রশ্নের জন্য শিরোনাম তৈরি করার মতো কাজগুলি সম্পাদন করার সময় একটি টাস্ক মডেল ব্যবহার করা হয়", + "a user": "একজন ব্যাবহারকারী", + "About": "সম্পর্কে", + "Account": "একাউন্ট", + "Account Activation Pending": "", + "Accurate information": "সঠিক তথ্য", + "Actions": "", + "Active Users": "", + "Add": "যোগ করুন", + "Add a model id": "একটি মডেল ID যোগ করুন", + "Add a short description about what this model does": "এই মডেলটি কী করে সে সম্পর্কে একটি সংক্ষিপ্ত বিবরণ যুক্ত করুন", + "Add a short title for this prompt": "এই প্রম্পটের জন্য একটি সংক্ষিপ্ত টাইটেল যোগ করুন", + "Add a tag": "একটি ট্যাগ যোগ করুন", + "Add custom prompt": "একটি কাস্টম প্রম্পট যোগ করুন", + "Add Docs": "ডকুমেন্ট যোগ করুন", + "Add Files": "ফাইল যোগ করুন", + "Add Memory": "মেমোরি যোগ করুন", + "Add message": "মেসেজ যোগ করুন", + "Add Model": "মডেল যোগ করুন", + "Add Tag": "", + "Add Tags": "ট্যাগ যোগ করুন", + "Add User": "ইউজার যোগ করুন", + "Adjusting these settings will apply changes universally to all users.": "এই সেটিংগুলো পরিবর্তন করলে তা সব ইউজারের উপরেই প্রয়োগ করা হবে", + "admin": "এডমিন", + "Admin": "", + "Admin Panel": "এডমিন প্যানেল", + "Admin Settings": "এডমিন সেটিংস", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "এডভান্সড প্যারামিটার্স", + "Advanced Params": "অ্যাডভান্সড প্যারাম", + "all": "সব", + "All Documents": "সব ডকুমেন্ট", + "All Users": "সব ইউজার", + "Allow": "অনুমোদন", + "Allow Chat Deletion": "চ্যাট ডিলিট করতে দিন", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন", + "Already have an account?": "আগে থেকেই একাউন্ট আছে?", + "an assistant": "একটা এসিস্ট্যান্ট", + "and": "এবং", + "and create a new shared link.": "এবং একটি নতুন শেয়ারে লিংক তৈরি করুন.", + "API Base URL": "এপিআই বেজ ইউআরএল", + "API Key": "এপিআই কোড", + "API Key created.": "একটি এপিআই কোড তৈরি করা হয়েছে.", + "API keys": "এপিআই কোডস", + "April": "আপ্রিল", + "Archive": "আর্কাইভ", + "Archive All Chats": "আর্কাইভ করুন সকল চ্যাট", + "Archived Chats": "চ্যাট ইতিহাস সংরক্ষণাগার", + "are allowed - Activate this command by typing": "অনুমোদিত - কমান্ডটি চালু করার জন্য লিখুন", + "Are you sure?": "আপনি নিশ্চিত?", + "Attach file": "ফাইল যুক্ত করুন", + "Attention to detail": "বিস্তারিত বিশেষতা", + "Audio": "অডিও", + "Audio settings updated successfully": "", + "August": "আগস্ট", + "Auto-playback response": "রেসপন্স অটো-প্লেব্যাক", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 বেজ ইউআরএল", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 বেজ ইউআরএল আবশ্যক", + "available!": "উপলব্ধ!", + "Back": "পেছনে", + "Bad Response": "খারাপ প্রতিক্রিয়া", + "Banners": "ব্যানার", + "Base Model (From)": "বেস মডেল (থেকে)", + "Batch Size (num_batch)": "", + "before": "পূর্ববর্তী", + "Being lazy": "অলস হওয়া", + "Brave Search API Key": "সাহসী অনুসন্ধান API কী", + "Bypass SSL verification for Websites": "ওয়েবসাইটের জন্য SSL যাচাই বাতিল করুন", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "বাতিল", + "Capabilities": "সক্ষমতা", + "Change Password": "পাসওয়ার্ড পরিবর্তন করুন", + "Chat": "চ্যাট", + "Chat Background Image": "", + "Chat Bubble UI": "চ্যাট বাবল UI", + "Chat Controls": "", + "Chat direction": "চ্যাট দিকনির্দেশ", + "Chat History": "চ্যাট হিস্টোরি", + "Chat History is off for this browser.": "এই ব্রাউজারের জন্য চ্যাট হিস্টোরি বন্ধ আছে", + "Chats": "চ্যাটসমূহ", + "Check Again": "আবার চেক করুন", + "Check for updates": "নতুন আপডেট আছে কিনা চেক করুন", + "Checking for updates...": "নতুন আপডেট আছে কিনা চেক করা হচ্ছে...", + "Choose a model before saving...": "সেভ করার আগে একটি মডেল নির্বাচন করুন", + "Chunk Overlap": "চাঙ্ক ওভারল্যাপ", + "Chunk Params": "চাঙ্ক প্যারামিটার্স", + "Chunk Size": "চাঙ্ক সাইজ", + "Citation": "উদ্ধৃতি", + "Clear memory": "", + "Click here for help.": "সাহায্যের জন্য এখানে ক্লিক করুন", + "Click here to": "এখানে ক্লিক করুন", + "Click here to download user import template file.": "", + "Click here to select": "নির্বাচন করার জন্য এখানে ক্লিক করুন", + "Click here to select a csv file.": "একটি csv ফাইল নির্বাচন করার জন্য এখানে ক্লিক করুন", + "Click here to select a py file.": "", + "Click here to select documents.": "ডকুমেন্টগুলো নির্বাচন করার জন্য এখানে ক্লিক করুন", + "click here.": "এখানে ক্লিক করুন", + "Click on the user role button to change a user's role.": "ইউজারের পদবি পরিবর্তন করার জন্য ইউজারের পদবি বাটনে ক্লিক করুন", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "ক্লোন", + "Close": "বন্ধ", + "Code formatted successfully": "", + "Collection": "সংগ্রহ", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "ComfyUI Base URL আবশ্যক।", + "Command": "কমান্ড", + "Concurrent Requests": "সমকালীন অনুরোধ", + "Confirm": "", + "Confirm Password": "পাসওয়ার্ড নিশ্চিত করুন", + "Confirm your action": "", + "Connections": "কানেকশনগুলো", + "Contact Admin for WebUI Access": "", + "Content": "বিষয়বস্তু", + "Content Extraction": "", + "Context Length": "কনটেক্সটের দৈর্ঘ্য", + "Continue Response": "যাচাই করুন", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "শেয়ারকৃত কথা-ব্যবহারের URL ক্লিপবোর্ডে কপি করা হয়েছে!", + "Copy": "অনুলিপি", + "Copy last code block": "সর্বশেষ কোড ব্লক কপি করুন", + "Copy last response": "সর্বশেষ রেসপন্স কপি করুন", + "Copy Link": "লিংক কপি করুন", + "Copying to clipboard was successful!": "ক্লিপবোর্ডে কপি করা সফল হয়েছে", + "Create a model": "একটি মডেল তৈরি করুন", + "Create Account": "একাউন্ট তৈরি করুন", + "Create new key": "একটি নতুন কী তৈরি করুন", + "Create new secret key": "একটি নতুন সিক্রেট কী তৈরি করুন", + "Created at": "নির্মানকাল", + "Created At": "নির্মানকাল", + "Created by": "", + "CSV Import": "", + "Current Model": "বর্তমান মডেল", + "Current Password": "বর্তমান পাসওয়ার্ড", + "Custom": "কাস্টম", + "Customize models for a specific purpose": "একটি নির্দিষ্ট উদ্দেশ্যে মডেল কাস্টমাইজ করুন", + "Dark": "ডার্ক", + "Dashboard": "", + "Database": "ডেটাবেজ", + "December": "ডেসেম্বর", + "Default": "ডিফল্ট", + "Default (Automatic1111)": "ডিফল্ট (Automatic1111)", + "Default (SentenceTransformers)": "ডিফল্ট (SentenceTransformers)", + "Default Model": "ডিফল্ট মডেল", + "Default model updated": "ডিফল্ট মডেল আপডেট হয়েছে", + "Default Prompt Suggestions": "ডিফল্ট প্রম্পট সাজেশন", + "Default User Role": "ইউজারের ডিফল্ট পদবি", + "delete": "মুছে ফেলুন", + "Delete": "মুছে ফেলুন", + "Delete a model": "একটি মডেল মুছে ফেলুন", + "Delete All Chats": "সব চ্যাট মুছে ফেলুন", + "Delete chat": "চ্যাট মুছে ফেলুন", + "Delete Chat": "চ্যাট মুছে ফেলুন", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "এই লিংক মুছে ফেলুন", + "Delete tool?": "", + "Delete User": "ইউজার মুছে ফেলুন", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} মুছে ফেলা হয়েছে", + "Deleted {{name}}": "{{name}} মোছা হয়েছে", + "Description": "বিবরণ", + "Didn't fully follow instructions": "ইনস্ট্রাকশন সম্পূর্ণ অনুসরণ করা হয়নি", + "Disabled": "", + "Discover a function": "", + "Discover a model": "একটি মডেল আবিষ্কার করুন", + "Discover a prompt": "একটি প্রম্পট খুঁজে বের করুন", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "কাস্টম প্রম্পটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "মডেল প্রিসেটগুলো আবিস্কার, ডাউনলোড এবং এক্সপ্লোর করুন", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "চ্যাটে 'আপনি'-র পরবর্তে ইউজারনেম দেখান", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "ডকুমেন্ট", + "Document Settings": "ডকুমেন্ট সেটিংসমূহ", + "Documentation": "", + "Documents": "ডকুমেন্টসমূহ", + "does not make any external connections, and your data stays securely on your locally hosted server.": "কোন এক্সটার্নাল কানেকশন তৈরি করে না, এবং আপনার ডেটা আর লোকালি হোস্টেড সার্ভারেই নিরাপদে থাকে।", + "Don't Allow": "অনুমোদন দেবেন না", + "Don't have an account?": "একাউন্ট নেই?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "স্টাইল পছন্দ করেন না", + "Done": "", + "Download": "ডাউনলোড", + "Download canceled": "ডাউনলোড বাতিল করা হয়েছে", + "Download Database": "ডেটাবেজ ডাউনলোড করুন", + "Drop any files here to add to the conversation": "আলোচনায় যুক্ত করার জন্য যে কোন ফাইল এখানে ড্রপ করুন", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "যেমন '30s','10m'. সময়ের অনুমোদিত অনুমোদিত এককগুলি হচ্ছে 's', 'm', 'h'.", + "Edit": "এডিট করুন", + "Edit Doc": "ডকুমেন্ট এডিট করুন", + "Edit Memory": "", + "Edit User": "ইউজার এডিট করুন", + "ElevenLabs": "", + "Email": "ইমেইল", + "Embedding Batch Size": "", + "Embedding Model": "ইমেজ ইমেবডিং মডেল", + "Embedding Model Engine": "ইমেজ ইমেবডিং মডেল ইঞ্জিন", + "Embedding model set to \"{{embedding_model}}\"": "ইমেজ ইমেবডিং মডেল সেট করা হয়েছে - \"{{embedding_model}}\"", + "Enable Chat History": "চ্যাট হিস্টোরি চালু করুন", + "Enable Community Sharing": "সম্প্রদায় শেয়ারকরণ সক্ষম করুন", + "Enable New Sign Ups": "নতুন সাইনআপ চালু করুন", + "Enable Web Search": "ওয়েব অনুসন্ধান সক্ষম করুন", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "আপনার সিএসভি ফাইলটিতে এই ক্রমে 4 টি কলাম অন্তর্ভুক্ত রয়েছে তা নিশ্চিত করুন: নাম, ইমেল, পাসওয়ার্ড, ভূমিকা।.", + "Enter {{role}} message here": "{{role}} মেসেজ এখানে লিখুন", + "Enter a detail about yourself for your LLMs to recall": "আপনার এলএলএমগুলি স্মরণ করার জন্য নিজের সম্পর্কে একটি বিশদ লিখুন", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "সাহসী অনুসন্ধান API কী লিখুন", + "Enter Chunk Overlap": "চাঙ্ক ওভারল্যাপ লিখুন", + "Enter Chunk Size": "চাংক সাইজ লিখুন", + "Enter Github Raw URL": "গিটহাব কাঁচা URL লিখুন", + "Enter Google PSE API Key": "গুগল পিএসই এপিআই কী লিখুন", + "Enter Google PSE Engine Id": "গুগল পিএসই ইঞ্জিন আইডি লিখুন", + "Enter Image Size (e.g. 512x512)": "ছবির মাপ লিখুন (যেমন 512x512)", + "Enter language codes": "ল্যাঙ্গুয়েজ কোড লিখুন", + "Enter model tag (e.g. {{modelTag}})": "মডেল ট্যাগ লিখুন (e.g. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "ধাপের সংখ্যা দিন (যেমন: 50)", + "Enter Score": "স্কোর দিন", + "Enter Searxng Query URL": "Searxng ক্যোয়ারী URL লিখুন", + "Enter Serper API Key": "Serper API কী লিখুন", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Serpstack API কী লিখুন", + "Enter stop sequence": "স্টপ সিকোয়েন্স লিখুন", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Top K লিখুন", + "Enter URL (e.g. http://127.0.0.1:7860/)": "ইউআরএল দিন (যেমন http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "ইউআরএল দিন (যেমন http://localhost:11434)", + "Enter Your Email": "আপনার ইমেইল লিখুন", + "Enter Your Full Name": "আপনার পূর্ণ নাম লিখুন", + "Enter your message": "", + "Enter Your Password": "আপনার পাসওয়ার্ড লিখুন", + "Enter Your Role": "আপনার রোল লিখুন", + "Error": "ত্রুটি", + "Experimental": "পরিক্ষামূলক", + "Export": "রপ্তানি", + "Export All Chats (All Users)": "সব চ্যাট এক্সপোর্ট করুন (সব ইউজারের)", + "Export chat (.json)": "", + "Export Chats": "চ্যাটগুলো এক্সপোর্ট করুন", + "Export Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং এক্সপোর্ট করুন", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "রপ্তানি মডেল", + "Export Prompts": "প্রম্পটগুলো একপোর্ট করুন", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "API Key তৈরি করা যায়নি।", + "Failed to read clipboard contents": "ক্লিপবোর্ডের বিষয়বস্তু পড়া সম্ভব হয়নি", + "Failed to update settings": "", + "February": "ফেব্রুয়ারি", + "Feel free to add specific details": "নির্দিষ্ট বিবরণ যোগ করতে বিনা দ্বিধায়", + "File": "", + "File Mode": "ফাইল মোড", + "File not found.": "ফাইল পাওয়া যায়নি", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ফিঙ্গারপ্রিন্ট স্পুফিং ধরা পড়েছে: অ্যাভাটার হিসেবে নামের আদ্যক্ষর ব্যবহার করা যাচ্ছে না। ডিফল্ট প্রোফাইল পিকচারে ফিরিয়ে নেয়া হচ্ছে।", + "Fluidly stream large external response chunks": "বড় এক্সটার্নাল রেসপন্স চাঙ্কগুলো মসৃণভাবে প্রবাহিত করুন", + "Focus chat input": "চ্যাট ইনপুট ফোকাস করুন", + "Followed instructions perfectly": "নির্দেশাবলী নিখুঁতভাবে অনুসরণ করা হয়েছে", + "Form": "", + "Format your variables using square brackets like this:": "আপনার ভেরিয়বলগুলো এভাবে স্কয়ার ব্রাকেটের মাধ্যমে সাজান", + "Frequency Penalty": "ফ্রিকোয়েন্সি পেনাল্টি", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "সাধারণ", + "General Settings": "সাধারণ সেটিংসমূহ", + "Generate Image": "", + "Generating search query": "অনুসন্ধান ক্যোয়ারী তৈরি করা হচ্ছে", + "Generation Info": "জেনারেশন ইনফো", + "Get up and running with": "", + "Global": "", + "Good Response": "ভালো সাড়া", + "Google PSE API Key": "গুগল পিএসই এপিআই কী", + "Google PSE Engine Id": "গুগল পিএসই ইঞ্জিন আইডি", + "h:mm a": "h:mm a", + "has no conversations.": "কোন কনভার্সেশন আছে না।", + "Hello, {{name}}": "হ্যালো, {{name}}", + "Help": "সহায়তা", + "Hide": "লুকান", + "Hide Model": "", + "How can I help you today?": "আপনাকে আজ কিভাবে সাহায্য করতে পারি?", + "Hybrid Search": "হাইব্রিড অনুসন্ধান", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "ইমেজ জেনারেশন (পরিক্ষামূলক)", + "Image Generation Engine": "ইমেজ জেনারেশন ইঞ্জিন", + "Image Settings": "ছবির সেটিংসমূহ", + "Images": "ছবিসমূহ", + "Import Chats": "চ্যাটগুলি ইমপোর্ট করুন", + "Import Documents Mapping": "ডকুমেন্টসমূহ ম্যাপিং ইমপোর্ট করুন", + "Import Functions": "", + "Import Models": "মডেল আমদানি করুন", + "Import Prompts": "প্রম্পটগুলো ইমপোর্ট করুন", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui চালু করার সময় `--api` ফ্ল্যাগ সংযুক্ত করুন", + "Info": "তথ্য", + "Input commands": "ইনপুট কমান্ডস", + "Install from Github URL": "Github URL থেকে ইনস্টল করুন", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "ইন্টারফেস", + "Invalid Tag": "অবৈধ ট্যাগ", + "January": "জানুয়ারী", + "join our Discord for help.": "সাহায্যের জন্য আমাদের Discord-এ যুক্ত হোন", + "JSON": "JSON", + "JSON Preview": "JSON প্রিভিউ", + "July": "জুলাই", + "June": "জুন", + "JWT Expiration": "JWT-র মেয়াদ", + "JWT Token": "JWT টোকেন", + "Keep Alive": "সচল রাখুন", + "Keyboard shortcuts": "কিবোর্ড শর্টকাটসমূহ", + "Knowledge": "", + "Language": "ভাষা", + "large language models, locally.": "", + "Last Active": "সর্বশেষ সক্রিয়", + "Last Modified": "", + "Light": "লাইট", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLM ভুল করতে পারে। গুরুত্বপূর্ণ তথ্য যাচাই করে নিন।", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "OpenWebUI কমিউনিটিকর্তৃক নির্মিত", + "Make sure to enclose them with": "এটা দিয়ে বন্ধনী দিতে ভুলবেন না", + "Manage": "", + "Manage Models": "মডেলসমূহ ব্যবস্থাপনা করুন", + "Manage Ollama Models": "Ollama মডেলসূহ ব্যবস্থাপনা করুন", + "Manage Pipelines": "পাইপলাইন পরিচালনা করুন", + "Manage Valves": "", + "March": "মার্চ", + "Max Tokens (num_predict)": "সর্বোচ্চ টোকেন (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "একসঙ্গে সর্বোচ্চ তিনটি মডেল ডাউনলোড করা যায়। দয়া করে পরে আবার চেষ্টা করুন।", + "May": "মে", + "Memories accessible by LLMs will be shown here.": "LLMs দ্বারা অ্যাক্সেসযোগ্য মেমোরিগুলি এখানে দেখানো হবে।", + "Memory": "মেমোরি", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "আপনার লিঙ্ক তৈরি করার পরে আপনার পাঠানো বার্তাগুলি শেয়ার করা হবে না। ইউআরএল ব্যবহারকারীরা শেয়ার করা চ্যাট দেখতে পারবেন।", + "Minimum Score": "Minimum Score", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' মডেল সফলভাবে ডাউনলোড হয়েছে।", + "Model '{{modelTag}}' is already in queue for downloading.": "{{modelTag}} ডাউনলোডের জন্য আগে থেকেই অপেক্ষমান আছে।", + "Model {{modelId}} not found": "{{modelId}} মডেল পাওয়া যায়নি", + "Model {{modelName}} is not vision capable": "মডেল {{modelName}} দৃষ্টি সক্ষম নয়", + "Model {{name}} is now {{status}}": "মডেল {{name}} এখন {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "মডেল ফাইলসিস্টেম পাথ পাওয়া গেছে। আপডেটের জন্য মডেলের শর্টনেম আবশ্যক, এগিয়ে যাওয়া যাচ্ছে না।", + "Model ID": "মডেল ID", + "Model not selected": "মডেল নির্বাচন করা হয়নি", + "Model Params": "মডেল প্যারাম", + "Model updated successfully": "", + "Model Whitelisting": "মডেল হোয়াইটলিস্টিং", + "Model(s) Whitelisted": "হোয়াইটলিস্টেড মডেল(সমূহ)", + "Modelfile Content": "মডেলফাইল কনটেন্ট", + "Models": "মডেলসমূহ", + "More": "আরো", + "Name": "নাম", + "Name Tag": "নামের ট্যাগ", + "Name your model": "আপনার মডেলের নাম দিন", + "New Chat": "নতুন চ্যাট", + "New Password": "নতুন পাসওয়ার্ড", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "কোন ফলাফল পাওয়া যায়নি", + "No search query generated": "কোনও অনুসন্ধান ক্যোয়ারী উত্পন্ন হয়নি", + "No source available": "কোন উৎস পাওয়া যায়নি", + "No valves to update": "", + "None": "কোনোটিই নয়", + "Not factually correct": "তথ্যগত দিক থেকে সঠিক নয়", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "দ্রষ্টব্য: আপনি যদি ন্যূনতম স্কোর সেট করেন তবে অনুসন্ধানটি কেবলমাত্র ন্যূনতম স্কোরের চেয়ে বেশি বা সমান স্কোর সহ নথিগুলি ফেরত দেবে।", + "Notifications": "নোটিফিকেশনসমূহ", + "November": "নভেম্বর", + "num_thread (Ollama)": "num_thread (ওলামা)", + "OAuth ID": "", + "October": "অক্টোবর", + "Off": "বন্ধ", + "Okay, Let's Go!": "ঠিক আছে, চলুন যাই!", + "OLED Dark": "OLED ডার্ক", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API নিষ্ক্রিয় করা হয়েছে", + "Ollama API is disabled": "", + "Ollama Version": "Ollama ভার্সন", + "On": "চালু", + "Only": "শুধুমাত্র", + "Only alphanumeric characters and hyphens are allowed in the command string.": "কমান্ড স্ট্রিং-এ শুধুমাত্র ইংরেজি অক্ষর, সংখ্যা এবং হাইফেন ব্যবহার করা যাবে।", + "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.": "আহা! আরেকটু ধৈর্য্য ধরুন! আপনার ফাইলগুলো এখনো প্রোসেস চলছে, আমরা ওগুলোকে সেরা প্রক্রিয়াজাত করছি। তৈরি হয়ে গেলে আপনাকে জানিয়ে দেয়া হবে।", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "ওহ, মনে হচ্ছে ইউআরএলটা ইনভ্যালিড। দয়া করে আর চেক করে চেষ্টা করুন।", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "আপনি একটা আনসাপোর্টেড পদ্ধতি (শুধু ফ্রন্টএন্ড) ব্যবহার করছেন। দয়া করে WebUI ব্যাকএন্ড থেকে চালনা করুন।", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "নতুন চ্যাট খুলুন", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI এপিআই", + "OpenAI API Config": "OpenAI এপিআই কনফিগ", + "OpenAI API Key is required.": "OpenAI API কোড আবশ্যক", + "OpenAI URL/Key required.": "OpenAI URL/Key আবশ্যক", + "or": "অথবা", + "Other": "অন্যান্য", + "Password": "পাসওয়ার্ড", + "PDF document (.pdf)": "PDF ডকুমেন্ট (.pdf)", + "PDF Extract Images (OCR)": "পিডিএফ এর ছবি থেকে লেখা বের করুন (OCR)", + "pending": "অপেক্ষমান", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "মাইক্রোফোন ব্যবহারের অনুমতি পাওয়া যায়নি: {{error}}", + "Personalization": "ডিজিটাল বাংলা", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "পাইপলাইন", + "Pipelines Not Detected": "", + "Pipelines Valves": "পাইপলাইন ভালভ", + "Plain text (.txt)": "প্লায়েন টেক্সট (.txt)", + "Playground": "খেলাঘর", + "Please carefully review the following warnings:": "", + "Positive attitude": "পজিটিভ আক্রমণ", + "Previous 30 days": "পূর্ব ৩০ দিন", + "Previous 7 days": "পূর্ব ৭ দিন", + "Profile Image": "প্রোফাইল ইমেজ", + "Prompt": "প্রম্প্ট", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "প্রম্প্ট (উদাহরণস্বরূপ, আমি রোমান ইমপার্টের সম্পর্কে একটি উপস্থিতি জানতে বল)", + "Prompt Content": "প্রম্পট কন্টেন্ট", + "Prompt suggestions": "প্রম্পট সাজেশনসমূহ", + "Prompts": "প্রম্পটসমূহ", + "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com থেকে \"{{searchValue}}\" টানুন", + "Pull a model from Ollama.com": "Ollama.com থেকে একটি টেনে আনুন আনুন", + "Query Params": "Query প্যারামিটারসমূহ", + "RAG Template": "RAG টেম্পলেট", + "Read Aloud": "পড়াশোনা করুন", + "Record voice": "ভয়েস রেকর্ড করুন", + "Redirecting you to OpenWebUI Community": "আপনাকে OpenWebUI কমিউনিটিতে পাঠানো হচ্ছে", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "যদি উপযুক্ত নয়, তবে রেজিগেনেট করা হচ্ছে", + "Regenerate": "রেজিগেনেট করুন", + "Release Notes": "রিলিজ নোটসমূহ", + "Remove": "রিমুভ করুন", + "Remove Model": "মডেল রিমুভ করুন", + "Rename": "রেনেম", + "Repeat Last N": "রিপিট Last N", + "Request Mode": "রিকোয়েস্ট মোড", + "Reranking Model": "রির্যাক্টিং মডেল", + "Reranking model disabled": "রির্যাক্টিং মডেল নিষ্ক্রিয় করা", + "Reranking model set to \"{{reranking_model}}\"": "রির ্যাঙ্কিং মডেল \"{{reranking_model}}\" -এ সেট করা আছে", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "ভেক্টর স্টোরেজ রিসেট করুন", + "Response AutoCopy to Clipboard": "রেসপন্সগুলো স্বয়ংক্রিভাবে ক্লিপবোর্ডে কপি হবে", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "পদবি", + "Rosé Pine": "রোজ পাইন", + "Rosé Pine Dawn": "ভোরের রোজ পাইন", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "সংরক্ষণ", + "Save & Create": "সংরক্ষণ এবং তৈরি করুন", + "Save & Update": "সংরক্ষণ এবং আপডেট করুন", + "Save Tag": "", + "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": "মাধ্যমে", + "Scan": "স্ক্যান", + "Scan complete!": "স্ক্যান সম্পন্ন হয়েছে!", + "Scan for documents from {{path}}": "ডকুমেন্টসমূহের জন্য {{path}} স্ক্যান করুন", + "Search": "অনুসন্ধান", + "Search a model": "মডেল অনুসন্ধান করুন", + "Search Chats": "চ্যাট অনুসন্ধান করুন", + "Search Documents": "ডকুমেন্টসমূহ অনুসন্ধান করুন", + "Search Functions": "", + "Search Models": "অনুসন্ধান মডেল", + "Search Prompts": "প্রম্পটসমূহ অনুসন্ধান করুন", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "অনুসন্ধানের ফলাফল গণনা", + "Search Tools": "", + "Searched {{count}} sites_one": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_one", + "Searched {{count}} sites_other": "{{কাউন্ট}} অনুসন্ধান করা হয়েছে sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng ক্যোয়ারী URL", + "See readme.md for instructions": "নির্দেশিকার জন্য readme.md দেখুন", + "See what's new": "নতুন কী আছে দেখুন", + "Seed": "সীড", + "Select a base model": "একটি বেস মডেল নির্বাচন করুন", + "Select a engine": "", + "Select a function": "", + "Select a mode": "একটি মডেল নির্বাচন করুন", + "Select a model": "একটি মডেল নির্বাচন করুন", + "Select a pipeline": "একটি পাইপলাইন নির্বাচন করুন", + "Select a pipeline url": "একটি পাইপলাইন URL নির্বাচন করুন", + "Select a tool": "", + "Select an Ollama instance": "একটি Ollama ইন্সট্যান্স নির্বাচন করুন", + "Select Documents": "", + "Select model": "মডেল নির্বাচন করুন", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "নির্বাচিত মডেল(গুলি) চিত্র ইনপুট সমর্থন করে না", + "Send": "পাঠান", + "Send a Message": "একটি মেসেজ পাঠান", + "Send message": "মেসেজ পাঠান", + "September": "সেপ্টেম্বর", + "Serper API Key": "Serper API Key", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API Key", + "Server connection verified": "সার্ভার কানেকশন যাচাই করা হয়েছে", + "Set as default": "ডিফল্ট হিসেবে নির্ধারণ করুন", + "Set Default Model": "ডিফল্ট মডেল নির্ধারণ করুন", + "Set embedding model (e.g. {{model}})": "ইমেম্বিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})", + "Set Image Size": "ছবির সাইজ নির্ধারণ করুন", + "Set reranking model (e.g. {{model}})": "রি-র্যাংকিং মডেল নির্ধারণ করুন (উদাহরণ {{model}})", + "Set Steps": "পরবর্তী ধাপসমূহ", + "Set Task Model": "টাস্ক মডেল সেট করুন", + "Set Voice": "কন্ঠস্বর নির্ধারণ করুন", + "Settings": "সেটিংসমূহ", + "Settings saved successfully!": "সেটিংগুলো সফলভাবে সংরক্ষিত হয়েছে", + "Settings updated successfully": "", + "Share": "শেয়ার করুন", + "Share Chat": "চ্যাট শেয়ার করুন", + "Share to OpenWebUI Community": "OpenWebUI কমিউনিটিতে শেয়ার করুন", + "short-summary": "সংক্ষিপ্ত বিবরণ", + "Show": "দেখান", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "শর্টকাটগুলো দেখান", + "Show your support!": "", + "Showcased creativity": "সৃজনশীলতা প্রদর্শন", + "Sign in": "সাইন ইন", + "Sign Out": "সাইন আউট", + "Sign up": "সাইন আপ", + "Signing in": "সাইন ইন", + "Source": "উৎস", + "Speech recognition error: {{error}}": "স্পিচ রিকগনিশনে সমস্যা: {{error}}", + "Speech-to-Text Engine": "স্পিচ-টু-টেক্সট ইঞ্জিন", + "Stop Sequence": "সিকোয়েন্স থামান", + "STT Model": "", + "STT Settings": "STT সেটিংস", + "Submit": "সাবমিট", + "Subtitle (e.g. about the Roman Empire)": "সাবটাইটল (রোমান ইম্পার্টের সম্পর্কে)", + "Success": "সফল", + "Successfully updated.": "সফলভাবে আপডেট হয়েছে", + "Suggested": "প্রস্তাবিত", + "Support": "", + "Support this plugin:": "", + "System": "সিস্টেম", + "System Prompt": "সিস্টেম প্রম্পট", + "Tags": "ট্যাগসমূহ", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "আরও বলুন:", + "Temperature": "তাপমাত্রা", + "Template": "টেম্পলেট", + "Text Completion": "লেখা সম্পন্নকরণ", + "Text-to-Speech Engine": "টেক্সট-টু-স্পিচ ইঞ্জিন", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "আপনার মতামত ধন্যবাদ!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "স্কোর একটি 0.0 (0%) এবং 1.0 (100%) এর মধ্যে একটি মান হওয়া উচিত।", + "Theme": "থিম", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "এটা নিশ্চিত করে যে, আপনার গুরুত্বপূর্ণ আলোচনা নিরাপদে আপনার ব্যাকএন্ড ডেটাবেজে সংরক্ষিত আছে। ধন্যবাদ!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "এই সেটিং অন্যন্য ব্রাউজার বা ডিভাইসের সাথে সিঙ্ক্রোনাইজ নয় না।", + "This will delete": "", + "Thorough explanation": "পুঙ্খানুপুঙ্খ ব্যাখ্যা", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "পরামর্শ: একাধিক ভেরিয়েবল স্লট একের পর এক রিপ্লেস করার জন্য চ্যাট ইনপুটে কিবোর্ডের Tab বাটন ব্যবহার করুন।", + "Title": "শিরোনাম", + "Title (e.g. Tell me a fun fact)": "শিরোনাম (একটি উপস্থিতি বিবরণ জানান)", + "Title Auto-Generation": "স্বয়ংক্রিয় শিরোনামগঠন", + "Title cannot be an empty string.": "শিরোনাম অবশ্যই একটি পাশাপাশি শব্দ হতে হবে।", + "Title Generation Prompt": "শিরোনামগঠন প্রম্পট", + "to": "প্রতি", + "To access the available model names for downloading,": "ডাউনলোডের জন্য এভেইলএবল মডেলের নামগুলো এক্সেস করতে,", + "To access the GGUF models available for downloading,": "ডাউলোডের জন্য এভেইলএবল GGUF মডেলগুলো এক্সেস করতে,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "চ্যাট ইনপুটে", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "আজ", + "Toggle settings": "সেটিংস টোগল", + "Toggle sidebar": "সাইডবার টোগল", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Ollama এক্সেস করতে সমস্যা হচ্ছে?", + "TTS Model": "", + "TTS Settings": "TTS সেটিংসমূহ", + "TTS Voice": "", + "Type": "টাইপ", + "Type Hugging Face Resolve (Download) URL": "Hugging Face থেকে ডাউনলোড করার ইউআরএল টাইপ করুন", + "Uh-oh! There was an issue connecting to {{provider}}.": "ওহ-হো! {{provider}} এর সাথে কানেকশনে সমস্যা হয়েছে।", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "আপডেট এবং লিংক কপি করুন", + "Update password": "পাসওয়ার্ড আপডেট করুন", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "একটি GGUF মডেল আপলোড করুন", + "Upload Files": "ফাইল আপলোড করুন", + "Upload Pipeline": "", + "Upload Progress": "আপলোড হচ্ছে", + "URL Mode": "ইউআরএল মোড", + "Use '#' in the prompt input to load and select your documents.": "আপনার ডকুমেন্টসমূহ নির্বাচন করার জন্য আপনার প্রম্পট ইনপুটে '# ব্যবহার করুন।", + "Use Gravatar": "Gravatar ব্যবহার করুন", + "Use Initials": "নামের আদ্যক্ষর ব্যবহার করুন", + "use_mlock (Ollama)": "use_mlock (ওলামা)", + "use_mmap (Ollama)": "use_mmap (ওলামা)", + "user": "ব্যবহারকারী", + "User location successfully retrieved.": "", + "User Permissions": "ইউজার পারমিশনসমূহ", + "Users": "ব্যাবহারকারীগণ", + "Utilize": "ইউটিলাইজ", + "Valid time units:": "সময়ের গ্রহণযোগ্য এককসমূহ:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "ভেরিয়েবল", + "variable to have them replaced with clipboard content.": "ক্লিপবোর্ডের কন্টেন্ট দিয়ে যেই ভেরিয়েবল রিপ্লেস করা যাবে।", + "Version": "ভার্সন", + "Voice": "", + "Warning": "সতর্কীকরণ", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "সতর্কীকরণ: আপনি যদি আপনার এম্বেডিং মডেল আপডেট বা পরিবর্তন করেন, তাহলে আপনাকে সমস্ত নথি পুনরায় আমদানি করতে হবে।.", + "Web": "ওয়েব", + "Web API": "", + "Web Loader Settings": "ওয়েব লোডার সেটিংস", + "Web Params": "ওয়েব প্যারামিটারসমূহ", + "Web Search": "ওয়েব অনুসন্ধান", + "Web Search Engine": "ওয়েব সার্চ ইঞ্জিন", + "Webhook URL": "ওয়েবহুক URL", + "WebUI Settings": "WebUI সেটিংসমূহ", + "WebUI will make requests to": "WebUI যেখানে রিকোয়েস্ট পাঠাবে", + "What’s New in": "এতে নতুন কী", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "যদি হিস্টোরি বন্ধ থাকে তাহলে এই ব্রাউজারের নতুন চ্যাটগুলো আপনার কোন ডিভাইসের হিস্টোরিতেই দেখা যাবে না।", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "ওয়ার্কস্পেস", + "Write a prompt suggestion (e.g. Who are you?)": "একটি প্রম্পট সাজেশন লিখুন (যেমন Who are you?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "৫০ শব্দের মধ্যে [topic or keyword] এর একটি সারসংক্ষেপ লিখুন।", + "Yesterday": "আগামী", + "You": "আপনি", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "আপনি একটি বেস মডেল ক্লোন করতে পারবেন না", + "You have no archived conversations.": "আপনার কোনও আর্কাইভ করা কথোপকথন নেই।", + "You have shared this chat": "আপনি এই চ্যাটটি শেয়ার করেছেন", + "You're a helpful assistant.": "আপনি একজন উপকারী এসিস্ট্যান্ট", + "You're now logged in.": "আপনি এখন লগইন করা অবস্থায় আছেন", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "YouTube লোডার সেটিংস" +} diff --git a/src/lib/i18n/locales/ca-ES/translation.json b/src/lib/i18n/locales/ca-ES/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..f1835d2d8c40c9c0ebfe2f682de5826312aaf65f --- /dev/null +++ b/src/lib/i18n/locales/ca-ES/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' perquè no caduqui mai.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(p. ex. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(p. ex. `sh webui.sh --api`)", + "(latest)": "(últim)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: No es pot eliminar un model base", + "{{modelName}} is thinking...": "{{modelName}} està pensant...", + "{{user}}'s Chats": "Els xats de {{user}}", + "{{webUIName}} Backend Required": "El Backend de {{webUIName}} és necessari", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un model de tasca s'utilitza quan es realitzen tasques com ara generar títols per a xats i consultes de cerca per a la web", + "a user": "un usuari", + "About": "Sobre", + "Account": "Compte", + "Account Activation Pending": "Activació del compte pendent", + "Accurate information": "Informació precisa", + "Actions": "", + "Active Users": "Usuaris actius", + "Add": "Afegir", + "Add a model id": "Afegeix un identificador de model", + "Add a short description about what this model does": "Afegeix una breu descripció sobre què fa aquest model", + "Add a short title for this prompt": "Afegeix un títol curt per a aquesta indicació", + "Add a tag": "Afegeix una etiqueta", + "Add custom prompt": "Afegir una indicació personalitzada", + "Add Docs": "Afegir documents", + "Add Files": "Afegir arxius", + "Add Memory": "Afegir memòria", + "Add message": "Afegir un missatge", + "Add Model": "Afegir un model", + "Add Tag": "", + "Add Tags": "Afegir etiquetes", + "Add User": "Afegir un usuari", + "Adjusting these settings will apply changes universally to all users.": "Si ajustes aquesta preferència, els canvis s'aplicaran de manera universal a tots els usuaris.", + "admin": "administrador", + "Admin": "Administrador", + "Admin Panel": "Panell d'administració", + "Admin Settings": "Preferències d'administració", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Els administradors tenen accés a totes les eines en tot moment; els usuaris necessiten eines assignades per model a l'espai de treball.", + "Advanced Parameters": "Paràmetres avançats", + "Advanced Params": "Paràmetres avançats", + "all": "tots", + "All Documents": "Tots els documents", + "All Users": "Tots els usuaris", + "Allow": "Permetre", + "Allow Chat Deletion": "Permetre la supressió del xat", + "Allow non-local voices": "Permetre veus no locals", + "Allow User Location": "Permetre la ubicació de l'usuari", + "Allow Voice Interruption in Call": "Permetre la interrupció de la veu en una trucada", + "alphanumeric characters and hyphens": "caràcters alfanumèrics i guions", + "Already have an account?": "Ja tens un compte?", + "an assistant": "un assistent", + "and": "i", + "and create a new shared link.": "i crear un nou enllaç compartit.", + "API Base URL": "URL Base de l'API", + "API Key": "clau API", + "API Key created.": "clau API creada.", + "API keys": "Claus de l'API", + "April": "Abril", + "Archive": "Arxiu", + "Archive All Chats": "Arxiva tots els xats", + "Archived Chats": "Xats arxivats", + "are allowed - Activate this command by typing": "estan permesos - Activa aquesta comanda escrivint", + "Are you sure?": "Estàs segur?", + "Attach file": "Adjuntar arxiu", + "Attention to detail": "Atenció al detall", + "Audio": "Àudio", + "Audio settings updated successfully": "Les preferències d'àudio s'han actualitzat correctament", + "August": "Agost", + "Auto-playback response": "Reproduir la resposta automàticament", + "AUTOMATIC1111 Api Auth String": "Cadena d'autenticació de l'API d'AUTOMATIC1111", + "AUTOMATIC1111 Base URL": "URL Base d'AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "Es requereix l'URL Base d'AUTOMATIC1111.", + "available!": "disponible!", + "Back": "Enrere", + "Bad Response": "Resposta errònia", + "Banners": "Banners", + "Base Model (From)": "Model base (des de)", + "Batch Size (num_batch)": "Mida del lot (num_batch)", + "before": "abans", + "Being lazy": "Essent mandrós", + "Brave Search API Key": "Clau API de Brave Search", + "Bypass SSL verification for Websites": "Desactivar la verificació SSL per a l'accés a Internet", + "Call": "Trucada", + "Call feature is not supported when using Web STT engine": "La funció de trucada no s'admet quan s'utilitza el motor Web STT", + "Camera": "Càmera", + "Cancel": "Cancel·lar", + "Capabilities": "Capacitats", + "Change Password": "Canviar la contrasenya", + "Chat": "Xat", + "Chat Background Image": "Imatge de fons del xat", + "Chat Bubble UI": "Chat Bubble UI", + "Chat Controls": "Controls de xat", + "Chat direction": "Direcció del xat", + "Chat History": "Històric del xat", + "Chat History is off for this browser.": "L'historic del xat està desactivat per a aquest navegador.", + "Chats": "Xats", + "Check Again": "Comprovar-ho de nou", + "Check for updates": "Comprovar si hi ha actualitzacions", + "Checking for updates...": "Comprovant actualitzacions...", + "Choose a model before saving...": "Triar un model abans de desar...", + "Chunk Overlap": "Solapament de blocs", + "Chunk Params": "Paràmetres dels blocs", + "Chunk Size": "Mida del bloc", + "Citation": "Cita", + "Clear memory": "Esborrar la memòria", + "Click here for help.": "Clica aquí per obtenir ajuda.", + "Click here to": "Clic aquí per", + "Click here to download user import template file.": "Fes clic aquí per descarregar l'arxiu de plantilla d'importació d'usuaris", + "Click here to select": "Clica aquí per seleccionar", + "Click here to select a csv file.": "Clica aquí per seleccionar un fitxer csv.", + "Click here to select a py file.": "Clica aquí per seleccionar un fitxer py.", + "Click here to select documents.": "Clica aquí per seleccionar documents.", + "click here.": "clica aquí.", + "Click on the user role button to change a user's role.": "Clica sobre el botó de rol d'usuari per canviar el rol d'un usuari.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permís d'escriptura al porta-retalls denegat. Comprova els ajustos de navegador per donar l'accés necessari.", + "Clone": "Clonar", + "Close": "Tancar", + "Code formatted successfully": "Codi formatat correctament", + "Collection": "Col·lecció", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL base de ComfyUI", + "ComfyUI Base URL is required.": "L'URL base de ComfyUI és obligatòria.", + "Command": "Comanda", + "Concurrent Requests": "Peticions simultànies", + "Confirm": "Confirmar", + "Confirm Password": "Confirmar la contrasenya", + "Confirm your action": "Confirma la teva acció", + "Connections": "Connexions", + "Contact Admin for WebUI Access": "Posat en contacte amb l'administrador per accedir a WebUI", + "Content": "Contingut", + "Content Extraction": "Extracció de contingut", + "Context Length": "Mida del context", + "Continue Response": "Continuar la resposta", + "Continue with {{provider}}": "Continuar amb {{provider}}", + "Controls": "Controls", + "Copied shared chat URL to clipboard!": "S'ha copiat l'URL compartida al porta-retalls!", + "Copy": "Copiar", + "Copy last code block": "Copiar l'últim bloc de codi", + "Copy last response": "Copiar l'última resposta", + "Copy Link": "Copiar l'enllaç", + "Copying to clipboard was successful!": "La còpia al porta-retalls s'ha realitzat correctament", + "Create a model": "Crear un model", + "Create Account": "Crear un compte", + "Create new key": "Crear una nova clau", + "Create new secret key": "Crear una nova clau secreta", + "Created at": "Creat el", + "Created At": "Creat el", + "Created by": "Creat per", + "CSV Import": "Importar CSV", + "Current Model": "Model actual", + "Current Password": "Contrasenya actual", + "Custom": "Personalitzat", + "Customize models for a specific purpose": "Personalitzar models per a un propòsit específic", + "Dark": "Fosc", + "Dashboard": "Tauler", + "Database": "Base de dades", + "December": "Desembre", + "Default": "Per defecte", + "Default (Automatic1111)": "Per defecte (Automatic1111)", + "Default (SentenceTransformers)": "Per defecte (SentenceTransformers)", + "Default Model": "Model per defecte", + "Default model updated": "Model per defecte actualitzat", + "Default Prompt Suggestions": "Suggeriments d'indicació per defecte", + "Default User Role": "Rol d'usuari per defecte", + "delete": "eliminar", + "Delete": "Eliminar", + "Delete a model": "Eliminar un model", + "Delete All Chats": "Eliminar tots els xats", + "Delete chat": "Eliminar xat", + "Delete Chat": "Eliminar xat", + "Delete chat?": "Eliminar el xat?", + "Delete Doc": "", + "Delete function?": "Eliminar funció?", + "Delete prompt?": "Eliminar indicació?", + "delete this link": "Eliminar aquest enllaç", + "Delete tool?": "Eliminar eina?", + "Delete User": "Eliminar usuari", + "Deleted {{deleteModelTag}}": "S'ha eliminat {{deleteModelTag}}", + "Deleted {{name}}": "S'ha eliminat {{name}}", + "Description": "Descripció", + "Didn't fully follow instructions": "No s'han seguit les instruccions completament", + "Disabled": "Deshabilitat", + "Discover a function": "Descobrir una funció", + "Discover a model": "Descobrir un model", + "Discover a prompt": "Descobrir una indicació", + "Discover a tool": "Descobrir una eina", + "Discover, download, and explore custom functions": "Descobrir, descarregar i explorar funcions personalitzades", + "Discover, download, and explore custom prompts": "Descobrir, descarregar i explorar indicacions personalitzades", + "Discover, download, and explore custom tools": "Descobrir, descarregar i explorar eines personalitzades", + "Discover, download, and explore model presets": "Descobrir, descarregar i explorar models preconfigurats", + "Dismissible": "Descartable", + "Display Emoji in Call": "Mostrar emojis a la trucada", + "Display the username instead of You in the Chat": "Mostrar el nom d'usuari en lloc de 'Tu' al xat", + "Do not install functions from sources you do not fully trust.": "No instal·lis funcions de fonts en què no confiïs plenament.", + "Do not install tools from sources you do not fully trust.": "No instal·lis eines de fonts en què no confiïs plenament.", + "Document": "Document", + "Document Settings": "Preferències de documents", + "Documentation": "Documentació", + "Documents": "Documents", + "does not make any external connections, and your data stays securely on your locally hosted server.": "no realitza connexions externes, i les teves dades romanen segures al teu servidor allotjat localment.", + "Don't Allow": "No permetre", + "Don't have an account?": "No tens un compte?", + "don't install random functions from sources you don't trust.": "no instal·lis funcions aleatòries de fonts en què no confiïs.", + "don't install random tools from sources you don't trust.": "no instal·lis eines aleatòries de fonts en què no confiïs.", + "Don't like the style": "No t'agrada l'estil?", + "Done": "Fet", + "Download": "Descarregar", + "Download canceled": "Descàrrega cancel·lada", + "Download Database": "Descarregar la base de dades", + "Drop any files here to add to the conversation": "Deixa qualsevol arxiu aquí per afegir-lo a la conversa", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ex. '30s','10m'. Les unitats de temps vàlides són 's', 'm', 'h'.", + "Edit": "Editar", + "Edit Doc": "Editar el document", + "Edit Memory": "Editar la memòria", + "Edit User": "Editar l'usuari", + "ElevenLabs": "", + "Email": "Correu electrònic", + "Embedding Batch Size": "Mida del lot d'incrustació", + "Embedding Model": "Model d'incrustació", + "Embedding Model Engine": "Motor de model d'incrustació", + "Embedding model set to \"{{embedding_model}}\"": "Model d'incrustació configurat a \"{{embedding_model}}\"", + "Enable Chat History": "Activar l'historial de xats", + "Enable Community Sharing": "Activar l'ús compartit amb la comunitat", + "Enable New Sign Ups": "Permetre nous registres", + "Enable Web Search": "Activar la cerca web", + "Enabled": "Habilitat", + "Engine": "Motor", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assegura't que els teus fitxers CSV inclouen 4 columnes en aquest ordre: Nom, Correu electrònic, Contrasenya, Rol.", + "Enter {{role}} message here": "Introdueix aquí el missatge de {{role}}", + "Enter a detail about yourself for your LLMs to recall": "Introdueix un detall sobre tu què els teus models de llenguatge puguin recordar", + "Enter api auth string (e.g. username:password)": "Entra la cadena d'autenticació api (p. ex. nom d'usuari:contrasenya)", + "Enter Brave Search API Key": "Introdueix la clau API de Brave Search", + "Enter Chunk Overlap": "Introdueix la mida de solapament de blocs", + "Enter Chunk Size": "Introdueix la mida del bloc", + "Enter Github Raw URL": "Introdueix l'URL en brut de Github", + "Enter Google PSE API Key": "Introdueix la clau API de Google PSE", + "Enter Google PSE Engine Id": "Introdueix l'identificador del motor PSE de Google", + "Enter Image Size (e.g. 512x512)": "Introdueix la mida de la imatge (p. ex. 512x512)", + "Enter language codes": "Introdueix els codis de llenguatge", + "Enter model tag (e.g. {{modelTag}})": "Introdueix l'etiqueta del model (p. ex. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Introdueix el nombre de passos (p. ex. 50)", + "Enter Score": "Introdueix la puntuació", + "Enter Searxng Query URL": "Introdueix l'URL de consulta de Searxng", + "Enter Serper API Key": "Introdueix la clau API Serper", + "Enter Serply API Key": "Introdueix la clau API Serply", + "Enter Serpstack API Key": "Introdueix la clau API Serpstack", + "Enter stop sequence": "Introdueix la seqüència de parada", + "Enter system prompt": "Introdueix la indicació de sistema", + "Enter Tavily API Key": "Introdueix la clau API de Tavily", + "Enter Tika Server URL": "Introdueix l'URL del servidor Tika", + "Enter Top K": "Introdueix Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Introdueix l'URL (p. ex. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Introdueix l'URL (p. ex. http://localhost:11434)", + "Enter Your Email": "Introdueix el teu correu electrònic", + "Enter Your Full Name": "Introdueix el teu nom complet", + "Enter your message": "Introdueix el teu missatge", + "Enter Your Password": "Introdueix la teva contrasenya", + "Enter Your Role": "Introdueix el teu rol", + "Error": "Error", + "Experimental": "Experimental", + "Export": "Exportar", + "Export All Chats (All Users)": "Exportar tots els xats (Tots els usuaris)", + "Export chat (.json)": "Exportar el xat (.json)", + "Export Chats": "Exportar els xats", + "Export Documents Mapping": "Exportar el mapatge de documents", + "Export Functions": "Exportar funcions", + "Export LiteLLM config.yaml": "Exportar la configuració LiteLLM config.yaml", + "Export Models": "Exportar els models", + "Export Prompts": "Exportar les indicacions", + "Export Tools": "Exportar les eines", + "External Models": "Models externs", + "Failed to create API Key.": "No s'ha pogut crear la clau API.", + "Failed to read clipboard contents": "No s'ha pogut llegir el contingut del porta-retalls", + "Failed to update settings": "No s'han pogut actualitzar les preferències", + "February": "Febrer", + "Feel free to add specific details": "Sent-te lliure d'afegir detalls específics", + "File": "Arxiu", + "File Mode": "Mode d'arxiu", + "File not found.": "No s'ha trobat l'arxiu.", + "Files": "", + "Filter is now globally disabled": "El filtre ha estat desactivat globalment", + "Filter is now globally enabled": "El filtre ha estat activat globalment", + "Filters": "Filtres", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "S'ha detectat la suplantació d'identitat de l'empremta digital: no es poden utilitzar les inicials com a avatar. S'estableix la imatge de perfil predeterminada.", + "Fluidly stream large external response chunks": "Transmetre amb fluïdesa grans trossos de resposta externa", + "Focus chat input": "Estableix el focus a l'entrada del xat", + "Followed instructions perfectly": "S'han seguit les instruccions perfectament", + "Form": "Formulari", + "Format your variables using square brackets like this:": "Formata les teves variables utilitzant claudàtors així:", + "Frequency Penalty": "Penalització per freqüència", + "Function created successfully": "La funció s'ha creat correctament", + "Function deleted successfully": "La funció s'ha eliminat correctament", + "Function Description (e.g. A filter to remove profanity from text)": "Descripció de la funció (per exemple, un filtre per eliminar paraules malsonants del text)", + "Function ID (e.g. my_filter)": "ID de la funció (per exemple, el_meu_filtre)", + "Function is now globally disabled": "La funció ha estat desactivada globalment", + "Function is now globally enabled": "La funció ha estat activada globalment", + "Function Name (e.g. My Filter)": "Nom de la funció (per exemple, El Meu Filtre)", + "Function updated successfully": "La funció s'ha actualitzat correctament", + "Functions": "Funcions", + "Functions allow arbitrary code execution": "Les funcions permeten l'execució de codi arbitrari", + "Functions allow arbitrary code execution.": "Les funcions permeten l'execució de codi arbitrari.", + "Functions imported successfully": "Les funcions s'han importat correctament", + "General": "General", + "General Settings": "Preferències generals", + "Generate Image": "Generar imatge", + "Generating search query": "Generant consulta", + "Generation Info": "Informació sobre la generació", + "Get up and running with": "Posa't en marxa amb", + "Global": "Global", + "Good Response": "Bona resposta", + "Google PSE API Key": "Clau API PSE de Google", + "Google PSE Engine Id": "Identificador del motor PSE de Google", + "h:mm a": "h:mm a", + "has no conversations.": "no té converses.", + "Hello, {{name}}": "Hola, {{name}}", + "Help": "Ajuda", + "Hide": "Amaga", + "Hide Model": "Amagar el model", + "How can I help you today?": "Com et puc ajudar avui?", + "Hybrid Search": "Cerca híbrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Afirmo que he llegit i entenc les implicacions de la meva acció. Soc conscient dels riscos associats a l'execució de codi arbitrari i he verificat la fiabilitat de la font.", + "Image Generation (Experimental)": "Generació d'imatges (Experimental)", + "Image Generation Engine": "Motor de generació d'imatges", + "Image Settings": "Preferències d'imatges", + "Images": "Imatges", + "Import Chats": "Importar xats", + "Import Documents Mapping": "Importar el mapatge de documents", + "Import Functions": "Importar funcions", + "Import Models": "Importar models", + "Import Prompts": "Importar indicacions", + "Import Tools": "Importar eines", + "Include `--api-auth` flag when running stable-diffusion-webui": "Inclou `--api-auth` quan executis stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Inclou `--api` quan executis stable-diffusion-webui", + "Info": "Informació", + "Input commands": "Entra comandes", + "Install from Github URL": "Instal·lar des de l'URL de Github", + "Instant Auto-Send After Voice Transcription": "Enviament automàtic després de la transcripció de veu", + "Interface": "Interfície", + "Invalid Tag": "Etiqueta no vàlida", + "January": "Gener", + "join our Discord for help.": "uneix-te al nostre Discord per obtenir ajuda.", + "JSON": "JSON", + "JSON Preview": "Vista prèvia del document JSON", + "July": "Juliol", + "June": "Juny", + "JWT Expiration": "Caducitat del JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Manté actiu", + "Keyboard shortcuts": "Dreceres de teclat", + "Knowledge": "Coneixement", + "Language": "Idioma", + "large language models, locally.": "models de llenguatge extensos, localment", + "Last Active": "Activitat recent", + "Last Modified": "Modificació", + "Light": "Clar", + "Listening...": "Escoltant...", + "LLMs can make mistakes. Verify important information.": "Els models de llenguatge poden cometre errors. Verifica la informació important.", + "Local Models": "Models locals", + "LTR": "LTR", + "Made by OpenWebUI Community": "Creat per la Comunitat OpenWebUI", + "Make sure to enclose them with": "Assegura't d'envoltar-los amb", + "Manage": "Gestionar", + "Manage Models": "Gestionar els models", + "Manage Ollama Models": "Gestionar els models Ollama", + "Manage Pipelines": "Gestionar les Pipelines", + "Manage Valves": "Gestionar les Valves", + "March": "Març", + "Max Tokens (num_predict)": "Nombre màxim de Tokens (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es poden descarregar un màxim de 3 models simultàniament. Si us plau, prova-ho més tard.", + "May": "Maig", + "Memories accessible by LLMs will be shown here.": "Les memòries accessibles pels models de llenguatge es mostraran aquí.", + "Memory": "Memòria", + "Memory added successfully": "Memòria afegida correctament", + "Memory cleared successfully": "Memòria eliminada correctament", + "Memory deleted successfully": "Memòria eliminada correctament", + "Memory updated successfully": "Memòria actualitzada correctament", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Els missatges enviats després de crear el teu enllaç no es compartiran. Els usuaris amb l'URL podran veure el xat compartit.", + "Minimum Score": "Puntuació mínima", + "Mirostat": "Mirostat", + "Mirostat Eta": "Eta de Mirostat", + "Mirostat Tau": "Tau de Mirostat", + "MMMM DD, YYYY": "DD de MMMM, YYYY", + "MMMM DD, YYYY HH:mm": "DD de MMMM, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "DD de MMMM, YYYY HH:mm:ss, A", + "Model '{{modelName}}' has been successfully downloaded.": "El model '{{modelName}}' s'ha descarregat correctament.", + "Model '{{modelTag}}' is already in queue for downloading.": "El model '{{modelTag}}' ja està en cua per ser descarregat.", + "Model {{modelId}} not found": "No s'ha trobat el model {{modelId}}", + "Model {{modelName}} is not vision capable": "El model {{modelName}} no és capaç de visió", + "Model {{name}} is now {{status}}": "El model {{name}} ara és {{status}}", + "Model created successfully!": "Model creat correctament", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "S'ha detectat el camí del sistema de fitxers del model. És necessari un nom curt del model per actualitzar, no es pot continuar.", + "Model ID": "Identificador del model", + "Model not selected": "Model no seleccionat", + "Model Params": "Paràmetres del model", + "Model updated successfully": "Model actualitzat correctament", + "Model Whitelisting": "Llista blanca de models", + "Model(s) Whitelisted": "Model(s) a la llista blanca", + "Modelfile Content": "Contingut del Modelfile", + "Models": "Models", + "More": "Més", + "Name": "Nom", + "Name Tag": "Etiqueta de nom", + "Name your model": "Posa un nom al teu model", + "New Chat": "Nou xat", + "New Password": "Nova contrasenya", + "No content to speak": "No hi ha contingut per parlar", + "No documents found": "No s'han trobat documents", + "No file selected": "No s'ha escollit cap fitxer", + "No results found": "No s'han trobat resultats", + "No search query generated": "No s'ha generat cap consulta", + "No source available": "Sense font disponible", + "No valves to update": "No hi ha cap Valve per actualitzar", + "None": "Cap", + "Not factually correct": "No és clarament correcte", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si s'estableix una puntuació mínima, la cerca només retornarà documents amb una puntuació major o igual a la puntuació mínima.", + "Notifications": "Notificacions", + "November": "Novembre", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "ID OAuth", + "October": "Octubre", + "Off": "Desactivat", + "Okay, Let's Go!": "D'acord, som-hi!", + "OLED Dark": "OLED Fosc", + "Ollama": "Ollama", + "Ollama API": "API d'Ollama", + "Ollama API disabled": "API d'Ollama desactivada", + "Ollama API is disabled": "L'API d'Ollama està desactivada", + "Ollama Version": "Versió d'Ollama", + "On": "Activat", + "Only": "Només", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Només es permeten caràcters alfanumèrics i guions en la comanda.", + "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.": "Ep! Un moment! Els teus fitxers encara s'estan processant. Els estem cuinant a la perfecció. Si us plau, tingues paciència i t'avisarem quan estiguin preparats.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ui! Sembla que l'URL no és vàlida. Si us plau, revisa-la i torna-ho a provar.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Ui! Hi ha hagut un error en la resposta anterior. Torna a provar-ho o contacta amb un administrador", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ui! Estàs utilitzant un mètode no suportat (només frontend). Si us plau, serveix la WebUI des del backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Obre un xat nou", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La versió d'Open WebUI (v{{OPEN_WEBUI_VERSION}}) és inferior a la versió requerida (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "API d'OpenAI", + "OpenAI API Config": "Configuració de l'API d'OpenAI", + "OpenAI API Key is required.": "Es requereix la clau API d'OpenAI.", + "OpenAI URL/Key required.": "URL/Clau d'OpenAI requerides.", + "or": "o", + "Other": "Altres", + "Password": "Contrasenya", + "PDF document (.pdf)": "Document PDF (.pdf)", + "PDF Extract Images (OCR)": "Extreu imatges del PDF (OCR)", + "pending": "pendent", + "Permission denied when accessing media devices": "Permís denegat en accedir a dispositius multimèdia", + "Permission denied when accessing microphone": "Permís denegat en accedir al micròfon", + "Permission denied when accessing microphone: {{error}}": "Permís denegat en accedir al micròfon: {{error}}", + "Personalization": "Personalització", + "Pin": "Fixar", + "Pinned": "Fixat", + "Pipeline deleted successfully": "Pipeline eliminada correctament", + "Pipeline downloaded successfully": "Pipeline descarregada correctament", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "No s'ha detectat Pipelines", + "Pipelines Valves": "Vàlvules de les Pipelines", + "Plain text (.txt)": "Text pla (.txt)", + "Playground": "Zona de jocs", + "Please carefully review the following warnings:": "Si us plau, revisa els següents avisos amb cura:", + "Positive attitude": "Actitud positiva", + "Previous 30 days": "30 dies anteriors", + "Previous 7 days": "7 dies anteriors", + "Profile Image": "Imatge de perfil", + "Prompt": "Indicació", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Indicació (p.ex. Digues-me quelcom divertit sobre l'Imperi Romà)", + "Prompt Content": "Contingut de la indicació", + "Prompt suggestions": "Suggeriments d'indicacions", + "Prompts": "Indicacions", + "Pull \"{{searchValue}}\" from Ollama.com": "Obtenir \"{{searchValue}}\" de Ollama.com", + "Pull a model from Ollama.com": "Obtenir un model d'Ollama.com", + "Query Params": "Paràmetres de consulta", + "RAG Template": "Plantilla RAG", + "Read Aloud": "Llegir en veu alta", + "Record voice": "Enregistrar la veu", + "Redirecting you to OpenWebUI Community": "Redirigint-te a la comunitat OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Fes referència a tu mateix com a \"Usuari\" (p. ex., \"L'usuari està aprenent espanyol\")", + "Refused when it shouldn't have": "Refusat quan no hauria d'haver estat", + "Regenerate": "Regenerar", + "Release Notes": "Notes de la versió", + "Remove": "Eliminar", + "Remove Model": "Eliminar el model", + "Rename": "Canviar el nom", + "Repeat Last N": "Repeteix els darrers N", + "Request Mode": "Mode de sol·licitud", + "Reranking Model": "Model de reavaluació", + "Reranking model disabled": "Model de reavaluació desactivat", + "Reranking model set to \"{{reranking_model}}\"": "Model de reavaluació establert a \"{{reranking_model}}\"", + "Reset": "Restableix", + "Reset Upload Directory": "Restableix el directori de pujades", + "Reset Vector Storage": "Restableix l'emmagatzematge de vectors", + "Response AutoCopy to Clipboard": "Copiar la resposta automàticament al porta-retalls", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de resposta no es poden activar perquè els permisos del lloc web han estat rebutjats. Comprova les preferències del navegador per donar l'accés necessari.", + "Role": "Rol", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Albada Rosé Pine", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "Executa Llama 2, Code Llama, i altres models. Personalitza i crea els teus propis models.", + "Running": "S'està executant", + "Save": "Desar", + "Save & Create": "Desar i crear", + "Save & Update": "Desar i actualitzar", + "Save Tag": "", + "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": "Desar els registres de xat directament a l'emmagatzematge del teu navegador ja no està suportat. Si us plau, descarregr i elimina els registres de xat fent clic al botó de sota. No et preocupis, pots tornar a importar fàcilment els teus registres de xat al backend a través de", + "Scan": "Escanejar", + "Scan complete!": "Escaneigr completat!", + "Scan for documents from {{path}}": "Escanejar documents des de {{path}}", + "Search": "Cercar", + "Search a model": "Cercar un model", + "Search Chats": "Cercar xats", + "Search Documents": "Cercar documents", + "Search Functions": "Cercar funcions", + "Search Models": "Cercar models", + "Search Prompts": "Cercar indicacions", + "Search Query Generation Prompt": "Indicació de cerca de generació de consultes", + "Search Query Generation Prompt Length Threshold": "Mida màxima de la indicació de cerca de generació de consultes", + "Search Result Count": "Recompte de resultats de cerca", + "Search Tools": "Cercar eines", + "Searched {{count}} sites_one": "S'ha cercat {{count}} una pàgina", + "Searched {{count}} sites_many": "S'han cercat {{count}} pàgines", + "Searched {{count}} sites_other": "S'han cercat {{count}} pàgines", + "Searching \"{{searchQuery}}\"": "Cercant \"{{searchQuery}}\"", + "Searxng Query URL": "URL de consulta de Searxng", + "See readme.md for instructions": "Consulta l'arxiu readme.md per obtenir instruccions", + "See what's new": "Veure què hi ha de nou", + "Seed": "Llavor", + "Select a base model": "Seleccionar un model base", + "Select a engine": "Seleccionar un motor", + "Select a function": "Seleccionar una funció", + "Select a mode": "Seleccionar un mode", + "Select a model": "Seleccionar un model", + "Select a pipeline": "Seleccionar una Pipeline", + "Select a pipeline url": "Seleccionar l'URL d'una Pipeline", + "Select a tool": "Seleccionar una eina", + "Select an Ollama instance": "Seleccionar una instància d'Ollama", + "Select Documents": "Seleccionar documents", + "Select model": "Seleccionar un model", + "Select only one model to call": "Seleccionar només un model per trucar", + "Selected model(s) do not support image inputs": "El(s) model(s) seleccionats no admeten l'entrada d'imatges", + "Send": "Enviar", + "Send a Message": "Enviar un missatge", + "Send message": "Enviar missatge", + "September": "Setembre", + "Serper API Key": "Clau API de Serper", + "Serply API Key": "Clau API de Serply", + "Serpstack API Key": "Clau API de Serpstack", + "Server connection verified": "Connexió al servidor verificada", + "Set as default": "Establir com a predeterminat", + "Set Default Model": "Establir el model predeterminat", + "Set embedding model (e.g. {{model}})": "Establir el model d'incrustació (p.ex. {{model}})", + "Set Image Size": "Establir la mida de la image", + "Set reranking model (e.g. {{model}})": "Establir el model de reavaluació (p.ex. {{model}})", + "Set Steps": "Establir el nombre de passos", + "Set Task Model": "Establir el model de tasca", + "Set Voice": "Establir la veu", + "Settings": "Preferències", + "Settings saved successfully!": "Les preferències s'han desat correctament", + "Settings updated successfully": "Les preferències s'han actualitzat correctament", + "Share": "Compartir", + "Share Chat": "Compartir el xat", + "Share to OpenWebUI Community": "Compartir amb la comunitat OpenWebUI", + "short-summary": "resum breu", + "Show": "Mostrar", + "Show Admin Details in Account Pending Overlay": "Mostrar els detalls de l'administrador a la superposició del compte pendent", + "Show Model": "Mostrar el model", + "Show shortcuts": "Mostrar dreceres", + "Show your support!": "Mostra el teu suport!", + "Showcased creativity": "Creativitat mostrada", + "Sign in": "Iniciar sessió", + "Sign Out": "Tancar sessió", + "Sign up": "Registrar-se", + "Signing in": "Iniciant sessió", + "Source": "Font", + "Speech recognition error: {{error}}": "Error de reconeixement de veu: {{error}}", + "Speech-to-Text Engine": "Motor de veu a text", + "Stop Sequence": "Atura la seqüència", + "STT Model": "Model SST", + "STT Settings": "Preferències de STT", + "Submit": "Enviar", + "Subtitle (e.g. about the Roman Empire)": "Subtítol (per exemple, sobre l'Imperi Romà)", + "Success": "Èxit", + "Successfully updated.": "Actualitzat correctament.", + "Suggested": "Suggerit", + "Support": "Dona suport", + "Support this plugin:": "Dona suport a aquest complement:", + "System": "Sistema", + "System Prompt": "Indicació del Sistema", + "Tags": "Etiquetes", + "Tap to interrupt": "Prem per interrompre", + "Tavily API Key": "Clau API de Tavily", + "Tell us more:": "Dona'ns més informació:", + "Temperature": "Temperatura", + "Template": "Plantilla", + "Text Completion": "Completament de text", + "Text-to-Speech Engine": "Motor de text a veu", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Gràcies pel teu comentari!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Els desenvolupadors d'aquest complement són voluntaris apassionats de la comunitat. Si trobeu útil aquest complement, considereu contribuir al seu desenvolupament.", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "El valor de puntuació hauria de ser entre 0.0 (0%) i 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "Pensant...", + "This action cannot be undone. Do you wish to continue?": "Aquesta acció no es pot desfer. Vols continuar?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Això assegura que les teves converses valuoses queden desades de manera segura a la teva base de dades. Gràcies!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Aquesta és una funció experimental, és possible que no funcioni com s'espera i està subjecta a canvis en qualsevol moment.", + "This setting does not sync across browsers or devices.": "Aquesta preferència no es sincronitza entre navegadors ni dispositius.", + "This will delete": "Això eliminarà", + "Thorough explanation": "Explicació en detall", + "Tika": "Tika", + "Tika Server URL required.": "La URL del servidor Tika és obligatòria.", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consell: Actualitza les diverses variables consecutivament prement la tecla de tabulació en l'entrada del xat després de cada reemplaçament.", + "Title": "Títol", + "Title (e.g. Tell me a fun fact)": "Títol (p. ex. Digues-me quelcom divertit)", + "Title Auto-Generation": "Generació automàtica de títol", + "Title cannot be an empty string.": "El títol no pot ser una cadena buida.", + "Title Generation Prompt": "Indicació de generació de títol", + "to": "a", + "To access the available model names for downloading,": "Per accedir als noms dels models disponibles per descarregar,", + "To access the GGUF models available for downloading,": "Per accedir als models GGUF disponibles per descarregar,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Per accedir a la WebUI, poseu-vos en contacte amb l'administrador. Els administradors poden gestionar els estats dels usuaris des del tauler d'administració.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Per afegir documents aquí, puja-ls primer a l'espai de treball \"Documents\".", + "to chat input.": "a l'entrada del xat.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Per seleccionar filtres aquí, afegeix-los primer a l'espai de treball \"Funcions\".", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Per seleccionar kits d'eines aquí, afegeix-los primer a l'espai de treball \"Eines\".", + "Today": "Avui", + "Toggle settings": "Alterna preferències", + "Toggle sidebar": "Alterna la barra lateral", + "Tokens To Keep On Context Refresh (num_keep)": "Tokens a mantenir en l'actualització del context (num_keep)", + "Tool created successfully": "Eina creada correctament", + "Tool deleted successfully": "Eina eliminada correctament", + "Tool imported successfully": "Eina importada correctament", + "Tool updated successfully": "Eina actualitzada correctament", + "Toolkit Description (e.g. A toolkit for performing various operations)": "Descripció del kit d'eines (p. ex. Un kit d'eines per fer diverses operacions)", + "Toolkit ID (e.g. my_toolkit)": "ID del kit d'eines (p. ex. el_meu_kit)", + "Toolkit Name (e.g. My ToolKit)": "Nom del kit d'eines (p. ex. El meu kit)", + "Tools": "Eines", + "Tools are a function calling system with arbitrary code execution": "Les eines són un sistema de crida a funcions amb execució de codi arbitrari", + "Tools have a function calling system that allows arbitrary code execution": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari", + "Tools have a function calling system that allows arbitrary code execution.": "Les eines disposen d'un sistema de crida a funcions que permet execució de codi arbitrari.", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemes en accedir a Ollama?", + "TTS Model": "Model TTS", + "TTS Settings": "Preferències de TTS", + "TTS Voice": "Veu TTS", + "Type": "Tipus", + "Type Hugging Face Resolve (Download) URL": "Escriu l'URL de Resolució (Descàrrega) de Hugging Face", + "Uh-oh! There was an issue connecting to {{provider}}.": "Oh! Hi ha hagut un problema connectant a {{provider}}.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Tipus de fitxer desconegut '{{file_type}}'. Continuant amb la càrrega del fitxer.", + "Unpin": "Alliberar", + "Update": "Actualitzar", + "Update and Copy Link": "Actualitzar i copiar l'enllaç", + "Update password": "Actualitzar la contrasenya", + "Updated at": "Actualitzat", + "Upload": "Pujar", + "Upload a GGUF model": "Pujar un model GGUF", + "Upload Files": "Pujar fitxers", + "Upload Pipeline": "Pujar una Pipeline", + "Upload Progress": "Progrés de càrrega", + "URL Mode": "Mode URL", + "Use '#' in the prompt input to load and select your documents.": "Utilitza '#' a l'entrada de la indicació per carregar i seleccionar els teus documents.", + "Use Gravatar": "Utilitzar Gravatar", + "Use Initials": "Utilitzar inicials", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "usuari", + "User location successfully retrieved.": "Ubicació de l'usuari obtinguda correctament", + "User Permissions": "Permisos d'usuari", + "Users": "Usuaris", + "Utilize": "Utilitzar", + "Valid time units:": "Unitats de temps vàlides:", + "Valves": "Valves", + "Valves updated": "Valves actualitzat", + "Valves updated successfully": "Valves actualitat correctament", + "variable": "variable", + "variable to have them replaced with clipboard content.": "variable per tenir-les reemplaçades amb el contingut del porta-retalls.", + "Version": "Versió", + "Voice": "Veu", + "Warning": "Avís", + "Warning:": "Avís:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avís: Si s'actualitza o es canvia el model d'incrustació, s'hauran de tornar a importar tots els documents.", + "Web": "Web", + "Web API": "Web API", + "Web Loader Settings": "Preferències del carregador web", + "Web Params": "Paràmetres web", + "Web Search": "Cerca la web", + "Web Search Engine": "Motor de cerca de la web", + "Webhook URL": "URL del webhook", + "WebUI Settings": "Preferències de WebUI", + "WebUI will make requests to": "WebUI farà peticions a", + "What’s New in": "Què hi ha de nou a", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quan l'historial està desactivat, els nous xats en aquest navegador no apareixeran en el teu historial en cap dels teus dispositius.", + "Whisper (Local)": "Whisper (local)", + "Widescreen Mode": "Mode de pantalla ampla", + "Workspace": "Espai de treball", + "Write a prompt suggestion (e.g. Who are you?)": "Escriu una suggerència d'indicació (p. ex. Qui ets?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Escriu un resum en 50 paraules que resumeixi [tema o paraula clau].", + "Yesterday": "Ahir", + "You": "Tu", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Pots personalitzar les teves interaccions amb els models de llenguatge afegint memòries mitjançant el botó 'Gestiona' que hi ha a continuació, fent-les més útils i adaptades a tu.", + "You cannot clone a base model": "No es pot clonar un model base", + "You have no archived conversations.": "No tens converses arxivades.", + "You have shared this chat": "Has compartit aquest xat", + "You're a helpful assistant.": "Ets un assistent útil.", + "You're now logged in.": "Ara estàs connectat.", + "Your account status is currently pending activation.": "El compte està actualment pendent d'activació", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Tota la teva contribució anirà directament al desenvolupador del complement; Open WebUI no se'n queda cap percentatge. Tanmateix, la plataforma de finançament escollida pot tenir les seves pròpies comissions.", + "Youtube": "Youtube", + "Youtube Loader Settings": "Preferències del carregador de Youtube" +} diff --git a/src/lib/i18n/locales/ceb-PH/translation.json b/src/lib/i18n/locales/ceb-PH/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..1dad6dd0eb0355bcf80919a0744abe23d995aea9 --- /dev/null +++ b/src/lib/i18n/locales/ceb-PH/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' para walay expiration.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(pananglitan `sh webui.sh --api`)", + "(latest)": "", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "{{modelName}} hunahunaa...", + "{{user}}'s Chats": "", + "{{webUIName}} Backend Required": "Backend {{webUIName}} gikinahanglan", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "usa ka user", + "About": "Mahitungod sa", + "Account": "Account", + "Account Activation Pending": "", + "Accurate information": "", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "Pagdugang og usa ka mubo nga titulo alang niini nga prompt", + "Add a tag": "Pagdugang og tag", + "Add custom prompt": "Pagdugang og custom prompt", + "Add Docs": "Pagdugang og mga dokumento", + "Add Files": "Idugang ang mga file", + "Add Memory": "", + "Add message": "Pagdugang og mensahe", + "Add Model": "", + "Add Tag": "", + "Add Tags": "idugang ang mga tag", + "Add User": "", + "Adjusting these settings will apply changes universally to all users.": "Ang pag-adjust niini nga mga setting magamit ang mga pagbag-o sa tanan nga tiggamit.", + "admin": "Administrator", + "Admin": "", + "Admin Panel": "Admin Panel", + "Admin Settings": "Mga setting sa administratibo", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "advanced settings", + "Advanced Params": "", + "all": "tanan", + "All Documents": "", + "All Users": "Ang tanan nga mga tiggamit", + "Allow": "Sa pagtugot", + "Allow Chat Deletion": "Tugoti nga mapapas ang mga chat", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "alphanumeric nga mga karakter ug hyphen", + "Already have an account?": "Naa na kay account ?", + "an assistant": "usa ka katabang", + "and": "Ug", + "and create a new shared link.": "", + "API Base URL": "API Base URL", + "API Key": "yawe sa API", + "API Key created.": "", + "API keys": "", + "April": "", + "Archive": "", + "Archive All Chats": "", + "Archived Chats": "pagrekord sa chat", + "are allowed - Activate this command by typing": "gitugotan - I-enable kini nga sugo pinaagi sa pag-type", + "Are you sure?": "Sigurado ka ?", + "Attach file": "Ilakip ang usa ka file", + "Attention to detail": "Pagtagad sa mga detalye", + "Audio": "Audio", + "Audio settings updated successfully": "", + "August": "", + "Auto-playback response": "Autoplay nga tubag", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Base URL AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "Ang AUTOMATIC1111 base URL gikinahanglan.", + "available!": "magamit!", + "Back": "Balik", + "Bad Response": "", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "", + "Being lazy": "", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Pagkanselar", + "Capabilities": "", + "Change Password": "Usba ang password", + "Chat": "Panaghisgot", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "Kasaysayan sa chat", + "Chat History is off for this browser.": "Ang kasaysayan sa chat gi-disable alang niini nga browser.", + "Chats": "Mga panaghisgot", + "Check Again": "Susiha pag-usab", + "Check for updates": "Susiha ang mga update", + "Checking for updates...": "Pagsusi alang sa mga update...", + "Choose a model before saving...": "Pagpili og template sa dili pa i-save...", + "Chunk Overlap": "Block overlap", + "Chunk Params": "Mga Setting sa Block", + "Chunk Size": "Gidak-on sa block", + "Citation": "Mga kinutlo", + "Clear memory": "", + "Click here for help.": "I-klik dinhi alang sa tabang.", + "Click here to": "", + "Click here to download user import template file.": "", + "Click here to select": "I-klik dinhi aron makapili", + "Click here to select a csv file.": "", + "Click here to select a py file.": "", + "Click here to select documents.": "Pag-klik dinhi aron mapili ang mga dokumento.", + "click here.": "I-klik dinhi.", + "Click on the user role button to change a user's role.": "I-klik ang User Role button aron usbon ang role sa user.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "Suod nga", + "Code formatted successfully": "", + "Collection": "Koleksyon", + "ComfyUI": "", + "ComfyUI Base URL": "", + "ComfyUI Base URL is required.": "", + "Command": "Pag-order", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "Kumpirma ang password", + "Confirm your action": "", + "Connections": "Mga koneksyon", + "Contact Admin for WebUI Access": "", + "Content": "Kontento", + "Content Extraction": "", + "Context Length": "Ang gitas-on sa konteksto", + "Continue Response": "", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "", + "Copy": "", + "Copy last code block": "Kopyaha ang katapusang bloke sa code", + "Copy last response": "Kopyaha ang kataposang tubag", + "Copy Link": "", + "Copying to clipboard was successful!": "Ang pagkopya sa clipboard malampuson!", + "Create a model": "", + "Create Account": "Paghimo og account", + "Create new key": "", + "Create new secret key": "", + "Created at": "Gihimo ang", + "Created At": "", + "Created by": "", + "CSV Import": "", + "Current Model": "Kasamtangang modelo", + "Current Password": "Kasamtangang Password", + "Custom": "Custom", + "Customize models for a specific purpose": "", + "Dark": "Ngitngit", + "Dashboard": "", + "Database": "Database", + "December": "", + "Default": "Pinaagi sa default", + "Default (Automatic1111)": "Default (Awtomatiko1111)", + "Default (SentenceTransformers)": "", + "Default Model": "", + "Default model updated": "Gi-update nga default template", + "Default Prompt Suggestions": "Default nga prompt nga mga sugyot", + "Default User Role": "Default nga Papel sa Gumagamit", + "delete": "DELETE", + "Delete": "", + "Delete a model": "Pagtangtang sa usa ka template", + "Delete All Chats": "", + "Delete chat": "Pagtangtang sa panaghisgot", + "Delete Chat": "", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "", + "Delete tool?": "", + "Delete User": "", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} gipapas", + "Deleted {{name}}": "", + "Description": "Deskripsyon", + "Didn't fully follow instructions": "", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "Pagkaplag usa ka prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Pagdiskubre, pag-download ug pagsuhid sa mga naandan nga pag-aghat", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Pagdiskobre, pag-download, ug pagsuhid sa mga preset sa template", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Ipakita ang username imbes nga 'Ikaw' sa Panaghisgutan", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokumento", + "Document Settings": "Mga Setting sa Dokumento", + "Documentation": "", + "Documents": "Mga dokumento", + "does not make any external connections, and your data stays securely on your locally hosted server.": "wala maghimo ug eksternal nga koneksyon, ug ang imong data nagpabiling luwas sa imong lokal nga host server.", + "Don't Allow": "Dili tugotan", + "Don't have an account?": "Wala kay account ?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "", + "Done": "", + "Download": "", + "Download canceled": "", + "Download Database": "I-download ang database", + "Drop any files here to add to the conversation": "Ihulog ang bisan unsang file dinhi aron idugang kini sa panag-istoryahanay", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p. ", + "Edit": "", + "Edit Doc": "I-edit ang dokumento", + "Edit Memory": "", + "Edit User": "I-edit ang tiggamit", + "ElevenLabs": "", + "Email": "E-mail", + "Embedding Batch Size": "", + "Embedding Model": "", + "Embedding Model Engine": "", + "Embedding model set to \"{{embedding_model}}\"": "", + "Enable Chat History": "I-enable ang kasaysayan sa chat", + "Enable Community Sharing": "", + "Enable New Sign Ups": "I-enable ang bag-ong mga rehistro", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "", + "Enter {{role}} message here": "Pagsulod sa mensahe {{role}} dinhi", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "Pagsulod sa block overlap", + "Enter Chunk Size": "Isulod ang block size", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "Pagsulod sa gidak-on sa hulagway (pananglitan 512x512)", + "Enter language codes": "", + "Enter model tag (e.g. {{modelTag}})": "Pagsulod sa template tag (e.g. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Pagsulod sa gidaghanon sa mga lakang (e.g. 50)", + "Enter Score": "", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "Pagsulod sa katapusan nga han-ay", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Pagsulod sa Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Pagsulod sa URL (e.g. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "", + "Enter Your Email": "Pagsulod sa imong e-mail address", + "Enter Your Full Name": "Ibutang ang imong tibuok nga ngalan", + "Enter your message": "", + "Enter Your Password": "Ibutang ang imong password", + "Enter Your Role": "", + "Error": "", + "Experimental": "Eksperimento", + "Export": "", + "Export All Chats (All Users)": "I-export ang tanan nga mga chat (Tanan nga tiggamit)", + "Export chat (.json)": "", + "Export Chats": "I-export ang mga chat", + "Export Documents Mapping": "I-export ang pagmapa sa dokumento", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "Export prompts", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "", + "Failed to read clipboard contents": "Napakyas sa pagbasa sa sulod sa clipboard", + "Failed to update settings": "", + "February": "", + "Feel free to add specific details": "", + "File": "", + "File Mode": "File mode", + "File not found.": "Wala makit-an ang file.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", + "Fluidly stream large external response chunks": "Hapsay nga paghatud sa daghang mga tipik sa eksternal nga mga tubag", + "Focus chat input": "Pag-focus sa entry sa diskusyon", + "Followed instructions perfectly": "", + "Form": "", + "Format your variables using square brackets like this:": "I-format ang imong mga variable gamit ang square brackets sama niini:", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Heneral", + "General Settings": "kinatibuk-ang mga setting", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "", + "Get up and running with": "", + "Global": "", + "Good Response": "", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "", + "Hello, {{name}}": "Maayong buntag, {{name}}", + "Help": "", + "Hide": "Tagoa", + "Hide Model": "", + "How can I help you today?": "Unsaon nako pagtabang kanimo karon?", + "Hybrid Search": "", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Pagmugna og hulagway (Eksperimento)", + "Image Generation Engine": "Makina sa paghimo og imahe", + "Image Settings": "Mga Setting sa Imahen", + "Images": "Mga hulagway", + "Import Chats": "Import nga mga chat", + "Import Documents Mapping": "Import nga pagmapa sa dokumento", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "Import prompt", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Iapil ang `--api` nga bandila kung nagdagan nga stable-diffusion-webui", + "Info": "", + "Input commands": "Pagsulod sa input commands", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Interface", + "Invalid Tag": "", + "January": "", + "join our Discord for help.": "Apil sa among Discord alang sa tabang.", + "JSON": "JSON", + "JSON Preview": "", + "July": "", + "June": "", + "JWT Expiration": "Pag-expire sa JWT", + "JWT Token": "JWT token", + "Keep Alive": "Padayon nga aktibo", + "Keyboard shortcuts": "Mga shortcut sa keyboard", + "Knowledge": "", + "Language": "Pinulongan", + "large language models, locally.": "", + "Last Active": "", + "Last Modified": "", + "Light": "Kahayag", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "Ang mga LLM mahimong masayop. ", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "Gihimo sa komunidad sa OpenWebUI", + "Make sure to enclose them with": "Siguruha nga palibutan sila", + "Manage": "", + "Manage Models": "Pagdumala sa mga templates", + "Manage Ollama Models": "Pagdumala sa mga modelo sa Ollama", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Ang labing taas nga 3 nga mga disenyo mahimong ma-download nga dungan. ", + "May": "", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Ang modelo'{{modelName}}' malampuson nga na-download.", + "Model '{{modelTag}}' is already in queue for downloading.": "Ang modelo'{{modelTag}}' naa na sa pila para ma-download.", + "Model {{modelId}} not found": "Modelo {{modelId}} wala makit-an", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "", + "Model ID": "", + "Model not selected": "Wala gipili ang modelo", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "Whitelist sa modelo", + "Model(s) Whitelisted": "Gi-whitelist nga (mga) modelo", + "Modelfile Content": "Mga sulod sa template file", + "Models": "Mga modelo", + "More": "", + "Name": "Ngalan", + "Name Tag": "Tag sa ngalan", + "Name your model": "", + "New Chat": "Bag-ong diskusyon", + "New Password": "Bag-ong Password", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "", + "No search query generated": "", + "No source available": "Walay tinubdan nga anaa", + "No valves to update": "", + "None": "", + "Not factually correct": "", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "", + "Notifications": "Mga pahibalo sa desktop", + "November": "", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "", + "Off": "Napuo", + "Okay, Let's Go!": "Okay, lakaw na!", + "OLED Dark": "", + "Ollama": "", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "Ollama nga bersyon", + "On": "Gipaandar", + "Only": "Lamang", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Ang alphanumeric nga mga karakter ug hyphen lang ang gitugotan sa command string.", + "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.": "Oops! ", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! ", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! ", + "Open AI (Dall-E)": "Buksan ang AI (Dall-E)", + "Open new chat": "Ablihi ang bag-ong diskusyon", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "", + "OpenAI API Key is required.": "Ang yawe sa OpenAI API gikinahanglan.", + "OpenAI URL/Key required.": "", + "or": "O", + "Other": "", + "Password": "Password", + "PDF document (.pdf)": "", + "PDF Extract Images (OCR)": "PDF Image Extraction (OCR)", + "pending": "gipugngan", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Gidili ang pagtugot sa dihang nag-access sa mikropono: {{error}}", + "Personalization": "", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "", + "Playground": "Dulaanan", + "Please carefully review the following warnings:": "", + "Positive attitude": "", + "Previous 30 days": "", + "Previous 7 days": "", + "Profile Image": "", + "Prompt": "", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "", + "Prompt Content": "Ang sulod sa prompt", + "Prompt suggestions": "Maabtik nga mga Sugyot", + "Prompts": "Mga aghat", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "Pagkuha ug template gikan sa Ollama.com", + "Query Params": "Mga parameter sa pangutana", + "RAG Template": "RAG nga modelo", + "Read Aloud": "", + "Record voice": "Irekord ang tingog", + "Redirecting you to OpenWebUI Community": "Gi-redirect ka sa komunidad sa OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "", + "Regenerate": "", + "Release Notes": "Release Notes", + "Remove": "", + "Remove Model": "", + "Rename": "", + "Repeat Last N": "Balika ang katapusang N", + "Request Mode": "Query mode", + "Reranking Model": "", + "Reranking model disabled": "", + "Reranking model set to \"{{reranking_model}}\"": "", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "I-reset ang pagtipig sa vector", + "Response AutoCopy to Clipboard": "Awtomatikong kopya sa tubag sa clipboard", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Papel", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Aube Pine Rosé", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Tipigi", + "Save & Create": "I-save ug Paghimo", + "Save & Update": "I-save ug I-update", + "Save Tag": "", + "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": "Ang pag-save sa mga chat log direkta sa imong browser storage dili na suportado. ", + "Scan": "Aron ma-scan", + "Scan complete!": "Nakompleto ang pag-scan!", + "Scan for documents from {{path}}": "I-scan ang mga dokumento gikan sa {{path}}", + "Search": "Pagpanukiduki", + "Search a model": "", + "Search Chats": "", + "Search Documents": "Pangitaa ang mga dokumento", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "Pangitaa ang mga prompt", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "Tan-awa ang readme.md alang sa mga panudlo", + "See what's new": "Tan-awa unsay bag-o", + "Seed": "Binhi", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Pagpili og mode", + "Select a model": "Pagpili og modelo", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "Pagpili usa ka pananglitan sa Ollama", + "Select Documents": "", + "Select model": "Pagpili og modelo", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "Magpadala ug mensahe", + "Send message": "Magpadala ug mensahe", + "September": "", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "Gipamatud-an nga koneksyon sa server", + "Set as default": "Define pinaagi sa default", + "Set Default Model": "Ibutang ang default template", + "Set embedding model (e.g. {{model}})": "", + "Set Image Size": "Ibutang ang gidak-on sa hulagway", + "Set reranking model (e.g. {{model}})": "", + "Set Steps": "Ipasabot ang mga lakang", + "Set Task Model": "", + "Set Voice": "Ibutang ang tingog", + "Settings": "Mga setting", + "Settings saved successfully!": "Malampuson nga na-save ang mga setting!", + "Settings updated successfully": "", + "Share": "", + "Share Chat": "", + "Share to OpenWebUI Community": "Ipakigbahin sa komunidad sa OpenWebUI", + "short-summary": "mubo nga summary", + "Show": "Pagpakita", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Ipakita ang mga shortcut", + "Show your support!": "", + "Showcased creativity": "", + "Sign in": "Para maka log in", + "Sign Out": "Pag-sign out", + "Sign up": "Pagrehistro", + "Signing in": "", + "Source": "Tinubdan", + "Speech recognition error: {{error}}": "Sayop sa pag-ila sa tingog: {{error}}", + "Speech-to-Text Engine": "Engine sa pag-ila sa tingog", + "Stop Sequence": "Pagkasunod-sunod sa pagsira", + "STT Model": "", + "STT Settings": "Mga setting sa STT", + "Submit": "Isumite", + "Subtitle (e.g. about the Roman Empire)": "", + "Success": "Kalampusan", + "Successfully updated.": "Malampuson nga na-update.", + "Suggested": "", + "Support": "", + "Support this plugin:": "", + "System": "Sistema", + "System Prompt": "Madasig nga Sistema", + "Tags": "Mga tag", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "", + "Temperature": "Temperatura", + "Template": "Modelo", + "Text Completion": "Pagkompleto sa teksto", + "Text-to-Speech Engine": "Text-to-speech nga makina", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "", + "Theme": "Tema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Kini nagsiguro nga ang imong bililhon nga mga panag-istoryahanay luwas nga natipig sa imong backend database. ", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Kini nga setting wala mag-sync tali sa mga browser o device.", + "This will delete": "", + "Thorough explanation": "", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Sugyot: Pag-update sa daghang variable nga lokasyon nga sunud-sunod pinaagi sa pagpindot sa tab key sa chat entry pagkahuman sa matag puli.", + "Title": "Titulo", + "Title (e.g. Tell me a fun fact)": "", + "Title Auto-Generation": "Awtomatikong paghimo sa titulo", + "Title cannot be an empty string.": "", + "Title Generation Prompt": "Madasig nga henerasyon sa titulo", + "to": "adunay", + "To access the available model names for downloading,": "Aron ma-access ang mga ngalan sa modelo nga ma-download,", + "To access the GGUF models available for downloading,": "Aron ma-access ang mga modelo sa GGUF nga magamit alang sa pag-download,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "sa entrada sa iring.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "", + "Toggle settings": "I-toggle ang mga setting", + "Toggle sidebar": "I-toggle ang sidebar", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Ibabaw nga P", + "Trouble accessing Ollama?": "Adunay mga problema sa pag-access sa Ollama?", + "TTS Model": "", + "TTS Settings": "Mga Setting sa TTS", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "Pagsulod sa resolusyon (pag-download) URL Hugging Face", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "", + "Update password": "I-update ang password", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Pag-upload ug modelo sa GGUF", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "Pag-uswag sa Pag-upload", + "URL Mode": "URL mode", + "Use '#' in the prompt input to load and select your documents.": "Gamita ang '#' sa dali nga pagsulod aron makarga ug mapili ang imong mga dokumento.", + "Use Gravatar": "Paggamit sa Gravatar", + "Use Initials": "", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "tiggamit", + "User location successfully retrieved.": "", + "User Permissions": "Mga permiso sa tiggamit", + "Users": "Mga tiggamit", + "Utilize": "Sa paggamit", + "Valid time units:": "Balido nga mga yunit sa oras:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variable", + "variable to have them replaced with clipboard content.": "variable aron pulihan kini sa mga sulud sa clipboard.", + "Version": "Bersyon", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "", + "Web Params": "", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "", + "WebUI Settings": "Mga Setting sa WebUI", + "WebUI will make requests to": "Ang WebUI maghimo mga hangyo sa", + "What’s New in": "Unsay bag-o sa", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kung ang kasaysayan gipalong, ang mga bag-ong chat sa kini nga browser dili makita sa imong kasaysayan sa bisan unsang mga aparato.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "Pagsulat og gisugyot nga prompt (eg. Kinsa ka?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Pagsulat og 50 ka pulong nga summary nga nagsumaryo [topic o keyword].", + "Yesterday": "", + "You": "", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "", + "You have shared this chat": "", + "You're a helpful assistant.": "Usa ka ka mapuslanon nga katabang", + "You're now logged in.": "Konektado ka na karon.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "", + "Youtube Loader Settings": "" +} diff --git a/src/lib/i18n/locales/de-DE/translation.json b/src/lib/i18n/locales/de-DE/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..dc19902e66ef3e59734c45e08a965b827a3c43b3 --- /dev/null +++ b/src/lib/i18n/locales/de-DE/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' oder '-1' für keine Ablaufzeit.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(z. B. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(z. B. `sh webui.sh --api`)", + "(latest)": "(neueste)", + "{{ models }}": "{{ Modelle }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Sie können ein Basismodell nicht löschen", + "{{modelName}} is thinking...": "{{modelName}} denkt nach...", + "{{user}}'s Chats": "{{user}}s Unterhaltungen", + "{{webUIName}} Backend Required": "{{webUIName}}-Backend erforderlich", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Aufgabenmodelle können Unterhaltungstitel oder Websuchanfragen generieren.", + "a user": "ein Benutzer", + "About": "Über", + "Account": "Konto", + "Account Activation Pending": "Kontoaktivierung ausstehend", + "Accurate information": "Präzise Information(en)", + "Actions": "", + "Active Users": "Aktive Benutzer", + "Add": "Hinzufügen", + "Add a model id": "Modell-ID hinzufügen", + "Add a short description about what this model does": "Fügen Sie eine kurze Beschreibung über dieses Modell hinzu", + "Add a short title for this prompt": "Fügen Sie einen kurzen Titel für diesen Prompt hinzu", + "Add a tag": "Tag hinzufügen", + "Add custom prompt": "Benutzerdefinierten Prompt hinzufügen", + "Add Docs": "Dokumente hinzufügen", + "Add Files": "Dateien hinzufügen", + "Add Memory": "Erinnerung hinzufügen", + "Add message": "Nachricht hinzufügen", + "Add Model": "Modell hinzufügen", + "Add Tag": "Tag hinzufügen", + "Add Tags": "Tags hinzufügen", + "Add User": "Benutzer hinzufügen", + "Adjusting these settings will apply changes universally to all users.": "Das Anpassen dieser Einstellungen wird Änderungen universell auf alle Benutzer anwenden.", + "admin": "Administrator", + "Admin": "Administrator", + "Admin Panel": "Administrationsbereich", + "Admin Settings": "Administrationsbereich", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Administratoren haben jederzeit Zugriff auf alle Werkzeuge. Benutzer können im Arbeitsbereich zugewiesen.", + "Advanced Parameters": "Erweiterte Parameter", + "Advanced Params": "Erweiterte Parameter", + "all": "Alle", + "All Documents": "Alle Dokumente", + "All Users": "Alle Benutzer", + "Allow": "Erlauben", + "Allow Chat Deletion": "Unterhaltungen löschen erlauben", + "Allow non-local voices": "Nicht-lokale Stimmen erlauben", + "Allow User Location": "Standort freigeben", + "Allow Voice Interruption in Call": "Unterbrechung durch Stimme im Anruf zulassen", + "alphanumeric characters and hyphens": "alphanumerische Zeichen und Bindestriche", + "Already have an account?": "Haben Sie bereits einen Account?", + "an assistant": "ein Assistent", + "and": "und", + "and create a new shared link.": "und erstellen Sie einen neuen freigegebenen Link.", + "API Base URL": "API-Basis-URL", + "API Key": "API-Schlüssel", + "API Key created.": "API-Schlüssel erstellt.", + "API keys": "API-Schlüssel", + "April": "April", + "Archive": "Archivieren", + "Archive All Chats": "Alle Unterhaltungen archivieren", + "Archived Chats": "Archivierte Unterhaltungen", + "are allowed - Activate this command by typing": "sind erlaubt — Aktivieren Sie diesen Befehl durch Eingabe von", + "Are you sure?": "Sind Sie sicher?", + "Attach file": "Datei anhängen", + "Attention to detail": "Aufmerksamkeit für Details", + "Audio": "Audio", + "Audio settings updated successfully": "Audioeinstellungen erfolgreich aktualisiert", + "August": "August", + "Auto-playback response": "Antwort automatisch abspielen", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111-API-Authentifizierungszeichenfolge", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111-Basis-URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111-Basis-URL ist erforderlich.", + "available!": "Verfügbar!", + "Back": "Zurück", + "Bad Response": "Schlechte Antwort", + "Banners": "Banner", + "Base Model (From)": "Basismodell (From)", + "Batch Size (num_batch)": "Stapelgröße (num_batch)", + "before": "bereits geteilt", + "Being lazy": "Faulheit", + "Brave Search API Key": "Brave Search API-Schlüssel", + "Bypass SSL verification for Websites": "SSL-Überprüfung für Webseiten umgehen", + "Call": "Anrufen", + "Call feature is not supported when using Web STT engine": "Die Anruffunktion wird nicht unterstützt, wenn die Web-STT-Engine verwendet wird.", + "Camera": "Kamera", + "Cancel": "Abbrechen", + "Capabilities": "Fähigkeiten", + "Change Password": "Passwort ändern", + "Chat": "Gespräch", + "Chat Background Image": "Hintergrundbild des Unterhaltungsfensters", + "Chat Bubble UI": "Chat Bubble UI", + "Chat Controls": "", + "Chat direction": "Textrichtung", + "Chat History": "Unterhaltungsverlauf", + "Chat History is off for this browser.": "Unterhaltungsverlauf ist in diesem Browser deaktiviert.", + "Chats": "Unterhaltungen", + "Check Again": "Erneut überprüfen", + "Check for updates": "Nach Updates suchen", + "Checking for updates...": "Sucht nach Updates...", + "Choose a model before saving...": "Wählen Sie ein Modell, bevor Sie speichern...", + "Chunk Overlap": "Blocküberlappung", + "Chunk Params": "Blockparameter", + "Chunk Size": "Blockgröße", + "Citation": "Zitate", + "Clear memory": "Alle Erinnerungen entfernen", + "Click here for help.": "Klicken Sie hier für Hilfe.", + "Click here to": "Klicke Sie hier, um", + "Click here to download user import template file.": "Klicken Sie hier, um die Vorlage für den Benutzerimport herunterzuladen.", + "Click here to select": "Klicke Sie zum Auswählen hier", + "Click here to select a csv file.": "Klicken Sie zum Auswählen einer CSV-Datei hier.", + "Click here to select a py file.": "Klicken Sie zum Auswählen einer py-Datei hier.", + "Click here to select documents.": "Klicken Sie zum Auswählen von Dokumenten hier", + "click here.": "hier klicken.", + "Click on the user role button to change a user's role.": "Klicken Sie auf die Benutzerrolle, um sie zu ändern.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Schreibberechtigung für die Zwischenablage verweigert. Bitte überprüfen Sie Ihre Browsereinstellungen, um den erforderlichen Zugriff zu erlauben.", + "Clone": "Klonen", + "Close": "Schließen", + "Code formatted successfully": "Code erfolgreich formatiert", + "Collection": "Kollektion", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI-Basis-URL", + "ComfyUI Base URL is required.": "ComfyUI-Basis-URL wird benötigt.", + "Command": "Befehl", + "Concurrent Requests": "Anzahl gleichzeitiger Anfragen", + "Confirm": "Bestätigen", + "Confirm Password": "Passwort bestätigen", + "Confirm your action": "Bestätigen Sie Ihre Aktion.", + "Connections": "Verbindungen", + "Contact Admin for WebUI Access": "Kontaktieren Sie den Administrator für den Zugriff auf die Weboberfläche", + "Content": "Info", + "Content Extraction": "Inhaltsextraktion", + "Context Length": "Kontextlänge", + "Continue Response": "Antwort fortsetzen", + "Continue with {{provider}}": "Mit {{provider}} fortfahren", + "Controls": "", + "Copied shared chat URL to clipboard!": "Freigabelink in die Zwischenablage kopiert!", + "Copy": "Kopieren", + "Copy last code block": "Letzten Codeblock kopieren", + "Copy last response": "Letzte Antwort kopieren", + "Copy Link": "Link kopieren", + "Copying to clipboard was successful!": "Das Kopieren in die Zwischenablage war erfolgreich!", + "Create a model": "Ein Modell erstellen", + "Create Account": "Konto erstellen", + "Create new key": "Neuen Schlüssel erstellen", + "Create new secret key": "Neuen API-Schlüssel erstellen", + "Created at": "Erstellt am", + "Created At": "Erstellt am", + "Created by": "Erstellt von", + "CSV Import": "CSV-Import", + "Current Model": "Aktuelles Modell", + "Current Password": "Aktuelles Passwort", + "Custom": "Benutzerdefiniert", + "Customize models for a specific purpose": "Modelle für einen bestimmten Zweck anpassen", + "Dark": "Dunkel", + "Dashboard": "Übersicht", + "Database": "Datenbank", + "December": "Dezember", + "Default": "Standard", + "Default (Automatic1111)": "Standard (Automatic1111)", + "Default (SentenceTransformers)": "Standard (SentenceTransformers)", + "Default Model": "Standardmodell", + "Default model updated": "Standardmodell aktualisiert", + "Default Prompt Suggestions": "Prompt-Vorschläge", + "Default User Role": "Standardbenutzerrolle", + "delete": "löschen", + "Delete": "Löschen", + "Delete a model": "Ein Modell löschen", + "Delete All Chats": "Alle Unterhaltungen löschen", + "Delete chat": "Unterhaltung löschen", + "Delete Chat": "Unterhaltung löschen", + "Delete chat?": "Unterhaltung löschen?", + "Delete Doc": "Dokument löschen", + "Delete function?": "Funktion löschen?", + "Delete prompt?": "Prompt löschen?", + "delete this link": "diesen Link löschen", + "Delete tool?": "Werkzeug löschen?", + "Delete User": "Benutzer löschen", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} gelöscht", + "Deleted {{name}}": "{{name}} gelöscht", + "Description": "Beschreibung", + "Didn't fully follow instructions": "Nicht genau den Answeisungen gefolgt", + "Disabled": "", + "Discover a function": "Entdecken Sie weitere Funktionen", + "Discover a model": "Entdecken Sie weitere Modelle", + "Discover a prompt": "Entdecken Sie weitere Prompts", + "Discover a tool": "Entdecken Sie weitere Werkzeuge", + "Discover, download, and explore custom functions": "Entdecken und beziehen Sie benutzerdefinierte Funktionen", + "Discover, download, and explore custom prompts": "Entdecken und beziehen Sie benutzerdefinierte Prompts", + "Discover, download, and explore custom tools": "Entdecken und beziehen Sie benutzerdefinierte Werkzeuge", + "Discover, download, and explore model presets": "Entdecken und beziehen Sie benutzerdefinierte Modellvorlagen", + "Dismissible": "ausblendbar", + "Display Emoji in Call": "Emojis im Anruf anzeigen", + "Display the username instead of You in the Chat": "Soll \"Sie\" durch Ihren Benutzernamen ersetzt werden?", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokument", + "Document Settings": "Dokumenteinstellungen", + "Documentation": "Dokumentation", + "Documents": "Dokumente", + "does not make any external connections, and your data stays securely on your locally hosted server.": "stellt keine externen Verbindungen her, und Ihre Daten bleiben sicher auf Ihrem lokal gehosteten Server.", + "Don't Allow": "Verbieten", + "Don't have an account?": "Haben Sie noch kein Benutzerkonto?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "schlechter Schreibstil", + "Done": "Erledigt", + "Download": "Exportieren", + "Download canceled": "Exportierung abgebrochen", + "Download Database": "Datenbank exportieren", + "Drop any files here to add to the conversation": "Ziehen Sie beliebige Dateien hierher, um sie der Unterhaltung hinzuzufügen", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "z. B. '30s','10m'. Gültige Zeiteinheiten sind 's', 'm', 'h'.", + "Edit": "Bearbeiten", + "Edit Doc": "Dokument bearbeiten", + "Edit Memory": "Erinnerungen bearbeiten", + "Edit User": "Benutzer bearbeiten", + "ElevenLabs": "", + "Email": "E-Mail", + "Embedding Batch Size": "Embedding-Stapelgröße", + "Embedding Model": "Embedding-Modell", + "Embedding Model Engine": "Embedding-Modell-Engine", + "Embedding model set to \"{{embedding_model}}\"": "Embedding-Modell auf \"{{embedding_model}}\" gesetzt", + "Enable Chat History": "Unterhaltungshistorie aktivieren", + "Enable Community Sharing": "Community-Freigabe aktivieren", + "Enable New Sign Ups": "Registrierung erlauben", + "Enable Web Search": "Websuche aktivieren", + "Enabled": "", + "Engine": "Engine", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Stellen Sie sicher, dass Ihre CSV-Datei 4 Spalten in dieser Reihenfolge enthält: Name, E-Mail, Passwort, Rolle.", + "Enter {{role}} message here": "Geben Sie die {{role}}-Nachricht hier ein", + "Enter a detail about yourself for your LLMs to recall": "Geben Sie ein Detail über sich selbst ein, das Ihre Sprachmodelle (LLMs) sich merken sollen", + "Enter api auth string (e.g. username:password)": "Geben Sie die API-Authentifizierungszeichenfolge ein (z. B. Benutzername:Passwort)", + "Enter Brave Search API Key": "Geben Sie den Brave Search API-Schlüssel ein", + "Enter Chunk Overlap": "Geben Sie die Blocküberlappung ein", + "Enter Chunk Size": "Geben Sie die Blockgröße ein", + "Enter Github Raw URL": "Geben Sie die Github Raw-URL ein", + "Enter Google PSE API Key": "Geben Sie den Google PSE-API-Schlüssel ein", + "Enter Google PSE Engine Id": "Geben Sie die Google PSE-Engine-ID ein", + "Enter Image Size (e.g. 512x512)": "Geben Sie die Bildgröße ein (z. B. 512x512)", + "Enter language codes": "Geben Sie die Sprachcodes ein", + "Enter model tag (e.g. {{modelTag}})": "Gebn Sie den Model-Tag ein", + "Enter Number of Steps (e.g. 50)": "Geben Sie die Anzahl an Schritten ein (z. B. 50)", + "Enter Score": "Punktzahl eingeben", + "Enter Searxng Query URL": "Geben Sie die Searxng-Abfrage-URL ein", + "Enter Serper API Key": "Geben Sie den Serper-API-Schlüssel ein", + "Enter Serply API Key": "Geben Sie den", + "Enter Serpstack API Key": "Geben Sie den Serpstack-API-Schlüssel ein", + "Enter stop sequence": "Stop-Sequenz eingeben", + "Enter system prompt": "", + "Enter Tavily API Key": "Geben Sie den Tavily-API-Schlüssel ein", + "Enter Tika Server URL": "Geben Sie die Tika-Server-URL ein", + "Enter Top K": "Geben Sie Top K ein", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Geben Sie die URL ein (z. B. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Geben Sie die URL ein (z. B. http://localhost:11434)", + "Enter Your Email": "Geben Sie Ihre E-Mail-Adresse ein", + "Enter Your Full Name": "Geben Sie Ihren vollständigen Namen ein", + "Enter your message": "", + "Enter Your Password": "Geben Sie Ihr Passwort ein", + "Enter Your Role": "Geben Sie Ihre Rolle ein", + "Error": "Fehler", + "Experimental": "Experimentell", + "Export": "Exportieren", + "Export All Chats (All Users)": "Alle Unterhaltungen exportieren (alle Benutzer)", + "Export chat (.json)": "Unterhaltung exportieren (.json)", + "Export Chats": "Unterhaltungen exportieren", + "Export Documents Mapping": "Dokumentenzuordnung exportieren", + "Export Functions": "Funktionen exportieren", + "Export LiteLLM config.yaml": "LiteLLM-Konfiguration exportieren (config.yaml)", + "Export Models": "Modelle exportieren", + "Export Prompts": "Prompts exportieren", + "Export Tools": "Werkzeuge exportieren", + "External Models": "Externe Modelle", + "Failed to create API Key.": "Fehler beim Erstellen des API-Schlüssels.", + "Failed to read clipboard contents": "Fehler beim Abruf der Zwischenablage", + "Failed to update settings": "Fehler beim Aktualisieren der Einstellungen", + "February": "Februar", + "Feel free to add specific details": "Fühlen Sie sich frei, spezifische Details hinzuzufügen", + "File": "Datei", + "File Mode": "Datei-Modus", + "File not found.": "Datei nicht gefunden.", + "Files": "", + "Filter is now globally disabled": "Filter ist jetzt global deaktiviert", + "Filter is now globally enabled": "Filter ist jetzt global aktiviert", + "Filters": "Filter", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerabdruck-Spoofing erkannt: Initialen können nicht als Avatar verwendet werden. Standard-Avatar wird verwendet.", + "Fluidly stream large external response chunks": "Nahtlose Übertragung großer externer Antwortabschnitte", + "Focus chat input": "Chat-Eingabe fokussieren", + "Followed instructions perfectly": "Anweisungen perfekt befolgt", + "Form": "Formular", + "Format your variables using square brackets like this:": "Formatieren Sie Ihre Variablen mit eckigen Klammern wie folgt:", + "Frequency Penalty": "Frequenzstrafe", + "Function created successfully": "Funktion erfolgreich erstellt", + "Function deleted successfully": "Funktion erfolgreich gelöscht", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "Funktion erfolgreich aktualisiert", + "Functions": "Funktionen", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Funktionen erfolgreich importiert", + "General": "Allgemein", + "General Settings": "Allgemeine Einstellungen", + "Generate Image": "Bild erzeugen", + "Generating search query": "Suchanfrage wird erstellt", + "Generation Info": "Generierungsinformationen", + "Get up and running with": "", + "Global": "Global", + "Good Response": "Gute Antwort", + "Google PSE API Key": "Google PSE-API-Schlüssel", + "Google PSE Engine Id": "Google PSE-Engine-ID", + "h:mm a": "h:mm a", + "has no conversations.": "hat keine Unterhaltungen.", + "Hello, {{name}}": "Hallo, {{name}}", + "Help": "Hilfe", + "Hide": "Verbergen", + "Hide Model": "Modell ausblenden", + "How can I help you today?": "Wie kann ich Ihnen heute helfen?", + "Hybrid Search": "Hybride Suche", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Bildgenerierung (experimentell)", + "Image Generation Engine": "Bildgenerierungs-Engine", + "Image Settings": "Bildeinstellungen", + "Images": "Bilder", + "Import Chats": "Unterhaltungen importieren", + "Import Documents Mapping": "Dokumentenzuordnung importieren", + "Import Functions": "Funktionen importieren", + "Import Models": "Modelle importieren", + "Import Prompts": "Prompts importieren", + "Import Tools": "Werkzeuge importieren", + "Include `--api-auth` flag when running stable-diffusion-webui": "Fügen Sie beim Ausführen von stable-diffusion-webui die Option `--api-auth` hinzu", + "Include `--api` flag when running stable-diffusion-webui": "Fügen Sie beim Ausführen von stable-diffusion-webui die Option `--api` hinzu", + "Info": "Info", + "Input commands": "Eingabebefehle", + "Install from Github URL": "Installiere von der Github-URL", + "Instant Auto-Send After Voice Transcription": "Spracherkennung direkt absenden", + "Interface": "Benutzeroberfläche", + "Invalid Tag": "Ungültiger Tag", + "January": "Januar", + "join our Discord for help.": "Treten Sie unserem Discord bei, um Hilfe zu erhalten.", + "JSON": "JSON", + "JSON Preview": "JSON-Vorschau", + "July": "Juli", + "June": "Juni", + "JWT Expiration": "JWT-Ablauf", + "JWT Token": "JWT-Token", + "Keep Alive": "Verbindung aufrechterhalten", + "Keyboard shortcuts": "Tastenkombinationen", + "Knowledge": "Wissen", + "Language": "Sprache", + "large language models, locally.": "", + "Last Active": "Zuletzt aktiv", + "Last Modified": "Zuletzt bearbeitet", + "Light": "Hell", + "Listening...": "Höre zu...", + "LLMs can make mistakes. Verify important information.": "LLMs können Fehler machen. Überprüfe wichtige Informationen.", + "Local Models": "Lokale Modelle", + "LTR": "LTR", + "Made by OpenWebUI Community": "Von der OpenWebUI-Community", + "Make sure to enclose them with": "Umschließe Variablen mit", + "Manage": "Verwalten", + "Manage Models": "Modelle verwalten", + "Manage Ollama Models": "Ollama-Modelle verwalten", + "Manage Pipelines": "Pipelines verwalten", + "Manage Valves": "Valves verwalten", + "March": "März", + "Max Tokens (num_predict)": "Maximale Tokenanzahl (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Es können maximal 3 Modelle gleichzeitig heruntergeladen werden. Bitte versuchen Sie es später erneut.", + "May": "Mai", + "Memories accessible by LLMs will be shown here.": "Erinnerungen, die für Modelle zugänglich sind, werden hier angezeigt.", + "Memory": "Erinnerungen", + "Memory added successfully": "Erinnerung erfolgreich hinzugefügt", + "Memory cleared successfully": "Erinnerung erfolgreich gelöscht", + "Memory deleted successfully": "Erinnerung erfolgreich gelöscht", + "Memory updated successfully": "Erinnerung erfolgreich aktualisiert", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Nachrichten, die Sie nach der Erstellung Ihres Links senden, werden nicht geteilt. Nutzer mit der URL können die freigegebene Unterhaltung einsehen.", + "Minimum Score": "Mindestpunktzahl", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD MMMM YYYY", + "MMMM DD, YYYY HH:mm": "DD MMMM YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "DD MMMM YYYY HH:mm A", + "Model '{{modelName}}' has been successfully downloaded.": "Modell '{{modelName}}' wurde erfolgreich heruntergeladen.", + "Model '{{modelTag}}' is already in queue for downloading.": "Modell '{{modelTag}}' befindet sich bereits in der Warteschlange zum Herunterladen.", + "Model {{modelId}} not found": "Modell {{modelId}} nicht gefunden", + "Model {{modelName}} is not vision capable": "Das Modell {{modelName}} ist nicht für die Bildverarbeitung geeignet", + "Model {{name}} is now {{status}}": "Modell {{name}} ist jetzt {{status}}", + "Model created successfully!": "Modell erfolgreich erstellt!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modell-Dateisystempfad erkannt. Modellkurzname ist für das Update erforderlich, Fortsetzung nicht möglich.", + "Model ID": "Modell-ID", + "Model not selected": "Modell nicht ausgewählt", + "Model Params": "Modell-Params", + "Model updated successfully": "Modell erfolgreich aktualisiert", + "Model Whitelisting": "Modell-Whitelisting", + "Model(s) Whitelisted": "Modell(e) auf der Whitelist", + "Modelfile Content": "Modelfile-Inhalt", + "Models": "Modelle", + "More": "Mehr", + "Name": "Name", + "Name Tag": "Namens-Tag", + "Name your model": "Benennen Sie Ihr Modell", + "New Chat": "Neue Unterhaltung", + "New Password": "Neues Passwort", + "No content to speak": "Kein Inhalt zum Vorlesen", + "No documents found": "Keine Dokumente gefunden", + "No file selected": "Keine Datei ausgewählt", + "No results found": "Keine Ergebnisse gefunden", + "No search query generated": "Keine Suchanfrage generiert", + "No source available": "Keine Quelle verfügbar", + "No valves to update": "Keine Valves zum Aktualisieren", + "None": "Nichts", + "Not factually correct": "Nicht sachlich korrekt", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Hinweis: Wenn Sie eine Mindestpunktzahl festlegen, werden in der Suche nur Dokumente mit einer Punktzahl größer oder gleich der Mindestpunktzahl zurückgegeben.", + "Notifications": "Benachrichtigungen", + "November": "November", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth-ID", + "October": "Oktober", + "Off": "Aus", + "Okay, Let's Go!": "Okay, los geht's!", + "OLED Dark": "OLED-Dunkel", + "Ollama": "Ollama", + "Ollama API": "Ollama-API", + "Ollama API disabled": "Ollama-API deaktiviert", + "Ollama API is disabled": "Ollama-API ist deaktiviert.", + "Ollama Version": "Ollama-Version", + "On": "Ein", + "Only": "Nur", + "Only alphanumeric characters and hyphens are allowed in the command string.": "In der Befehlszeichenfolge sind nur alphanumerische Zeichen und Bindestriche erlaubt.", + "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.": "Ups! Bitte gedulden Sie sich einen Moment! Ihre Dateien sind noch in der Verarbeitung. Wir bereiten sie sorgfältig vor. Bitte haben Sie etwas Geduld, und wir werden Sie benachrichtigen, sobald sie fertig sind.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppla! Es scheint, dass die URL ungültig ist. Bitte überprüfen Sie diese und versuchen Sie es erneut.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Hoppla! Es gab einen Fehler in der vorherigen Antwort. Bitte versuchen Sie es erneut oder kontaktieren Sie den Administrator.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hoppla! Sie verwenden eine nicht unterstützte Methode (nur Frontend). Bitte stellen Sie die WebUI vom Backend bereit.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Neuen Chat öffnen", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Die installierte Open-WebUI-Version (v{{OPEN_WEBUI_VERSION}}) ist niedriger als die erforderliche Version (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI-API", + "OpenAI API Config": "OpenAI-API-Konfiguration", + "OpenAI API Key is required.": "OpenAI-API-Schlüssel erforderlich.", + "OpenAI URL/Key required.": "OpenAI-URL/Schlüssel erforderlich.", + "or": "oder", + "Other": "Andere", + "Password": "Passwort", + "PDF document (.pdf)": "PDF-Dokument (.pdf)", + "PDF Extract Images (OCR)": "Text von Bildern aus PDFs extrahieren (OCR)", + "pending": "ausstehend", + "Permission denied when accessing media devices": "Zugriff auf Mediengeräte verweigert", + "Permission denied when accessing microphone": "Zugriff auf das Mikrofon verweigert", + "Permission denied when accessing microphone: {{error}}": "Zugriff auf das Mikrofon verweigert: {{error}}", + "Personalization": "Personalisierung", + "Pin": "Anheften", + "Pinned": "Angeheftet", + "Pipeline deleted successfully": "Pipeline erfolgreich gelöscht", + "Pipeline downloaded successfully": "Pipeline erfolgreich heruntergeladen", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "Pipelines nicht erkannt", + "Pipelines Valves": "Pipeline Valves", + "Plain text (.txt)": "Nur Text (.txt)", + "Playground": "Testumgebung", + "Please carefully review the following warnings:": "", + "Positive attitude": "Positive Einstellung", + "Previous 30 days": "Vorherige 30 Tage", + "Previous 7 days": "Vorherige 7 Tage", + "Profile Image": "Profilbild", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (z. B. \"Erzähle mir eine interessante Tatsache über das Römische Reich\")", + "Prompt Content": "Prompt-Inhalt", + "Prompt suggestions": "Prompt-Vorschläge", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "\"{{searchValue}}\" von Ollama.com beziehen", + "Pull a model from Ollama.com": "Modell von Ollama.com beziehen", + "Query Params": "Abfrageparameter", + "RAG Template": "RAG-Vorlage", + "Read Aloud": "Vorlesen", + "Record voice": "Stimme aufnehmen", + "Redirecting you to OpenWebUI Community": "Sie werden zur OpenWebUI-Community weitergeleitet", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Abgelehnt, obwohl es nicht hätte abgelehnt werden sollen", + "Regenerate": "Neu generieren", + "Release Notes": "Veröffentlichungshinweise", + "Remove": "Entfernen", + "Remove Model": "Modell entfernen", + "Rename": "Umbenennen", + "Repeat Last N": "Wiederhole die letzten N", + "Request Mode": "Anforderungsmodus", + "Reranking Model": "Reranking-Modell", + "Reranking model disabled": "Reranking-Modell deaktiviert", + "Reranking model set to \"{{reranking_model}}\"": "Reranking-Modell \"{{reranking_model}}\" fesgelegt", + "Reset": "Zurücksetzen", + "Reset Upload Directory": "Upload-Verzeichnis zurücksetzen", + "Reset Vector Storage": "Vektorspeicher zurücksetzen", + "Response AutoCopy to Clipboard": "Antwort automatisch in die Zwischenablage kopieren", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Benachrichtigungen können nicht aktiviert werden, da die Website-Berechtigungen abgelehnt wurden. Bitte besuchen Sie Ihre Browser-Einstellungen, um den erforderlichen Zugriff zu gewähren.", + "Role": "Rolle", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Läuft", + "Save": "Speichern", + "Save & Create": "Erstellen", + "Save & Update": "Aktualisieren", + "Save Tag": "Tag speichern", + "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": "Das direkte Speichern von Unterhaltungen im Browser-Speicher wird nicht mehr unterstützt. Bitte nehmen Sie einen Moment Zeit, um Ihre Unterhaltungen zu exportieren und zu löschen, indem Sie auf die Schaltfläche unten klicken. Keine Sorge, Sie können Ihre Unterhaltungen problemlos über das Backend wieder importieren.", + "Scan": "Scannen", + "Scan complete!": "Scan abgeschlossen!", + "Scan for documents from {{path}}": "Dokumente im {{path}} scannen", + "Search": "Suchen", + "Search a model": "Modell suchen", + "Search Chats": "Unterhaltungen durchsuchen...", + "Search Documents": "Dokumente durchsuchen...", + "Search Functions": "Funktionen durchsuchen...", + "Search Models": "Modelle durchsuchen...", + "Search Prompts": "Prompts durchsuchen...", + "Search Query Generation Prompt": "Suchanfragengenerierungsvorlage", + "Search Query Generation Prompt Length Threshold": "Längenschwelle für Suchanfragengenerierung", + "Search Result Count": "Anzahl der Suchergebnisse", + "Search Tools": "Werkzeuge durchsuchen...", + "Searched {{count}} sites_one": "{{count}} Seite durchsucht", + "Searched {{count}} sites_other": "{{count}} Seiten durchsucht", + "Searching \"{{searchQuery}}\"": "Suche nach \"{{searchQuery}}\"", + "Searxng Query URL": "Searxng-Abfrage-URL", + "See readme.md for instructions": "Anleitung in readme.md anzeigen", + "See what's new": "Entdecken Sie die Neuigkeiten", + "Seed": "Seed", + "Select a base model": "Wählen Sie ein Basismodell", + "Select a engine": "Wählen Sie eine Engine", + "Select a function": "Wählen Sie eine Funktion", + "Select a mode": "Wählen Sie einen Modus", + "Select a model": "Wählen Sie ein Modell", + "Select a pipeline": "Wählen Sie eine Pipeline", + "Select a pipeline url": "Wählen Sie eine Pipeline-URL", + "Select a tool": "Wählen Sie ein Werkzeug", + "Select an Ollama instance": "Wählen Sie eine Ollama-Instanz", + "Select Documents": "Dokumente auswählen", + "Select model": "Modell auswählen", + "Select only one model to call": "Wählen Sie nur ein Modell zum Anrufen aus", + "Selected model(s) do not support image inputs": "Ihre ausgewählten Modelle unterstützen keine Bildeingaben", + "Send": "Senden", + "Send a Message": "Eine Nachricht senden", + "Send message": "Nachricht senden", + "September": "September", + "Serper API Key": "Serper-API-Schlüssel", + "Serply API Key": "Serply-API-Schlüssel", + "Serpstack API Key": "Serpstack-API-Schlüssel", + "Server connection verified": "Serververbindung überprüft", + "Set as default": "Als Standard festlegen", + "Set Default Model": "Standardmodell festlegen", + "Set embedding model (e.g. {{model}})": "Einbettungsmodell festlegen (z. B. {{model}})", + "Set Image Size": "Bildgröße festlegen", + "Set reranking model (e.g. {{model}})": "Rerankingmodell festlegen (z. B. {{model}})", + "Set Steps": "Schrittgröße festlegen", + "Set Task Model": "Aufgabenmodell festlegen", + "Set Voice": "Stimme festlegen", + "Settings": "Einstellungen", + "Settings saved successfully!": "Einstellungen erfolgreich gespeichert!", + "Settings updated successfully": "Einstellungen erfolgreich aktualisiert", + "Share": "Teilen", + "Share Chat": "Unterhaltung teilen", + "Share to OpenWebUI Community": "Mit OpenWebUI Community teilen", + "short-summary": "kurze-zusammenfassung", + "Show": "Anzeigen", + "Show Admin Details in Account Pending Overlay": "Admin-Details im Account-Pending-Overlay anzeigen", + "Show Model": "Modell anzeigen", + "Show shortcuts": "Verknüpfungen anzeigen", + "Show your support!": "Zeigen Sie Ihre Unterstützung!", + "Showcased creativity": "Kreativität gezeigt", + "Sign in": "Anmelden", + "Sign Out": "Abmelden", + "Sign up": "Registrieren", + "Signing in": "Anmeldung", + "Source": "Quelle", + "Speech recognition error: {{error}}": "Spracherkennungsfehler: {{error}}", + "Speech-to-Text Engine": "Sprache-zu-Text-Engine", + "Stop Sequence": "Stop-Sequenz", + "STT Model": "STT-Modell", + "STT Settings": "STT-Einstellungen", + "Submit": "Senden", + "Subtitle (e.g. about the Roman Empire)": "Untertitel (z. B. über das Römische Reich)", + "Success": "Erfolg", + "Successfully updated.": "Erfolgreich aktualisiert.", + "Suggested": "Vorgeschlagen", + "Support": "", + "Support this plugin:": "", + "System": "System", + "System Prompt": "System-Prompt", + "Tags": "Tags", + "Tap to interrupt": "Zum Unterbrechen tippen", + "Tavily API Key": "Tavily-API-Schlüssel", + "Tell us more:": "Erzähl uns mehr", + "Temperature": "Temperatur", + "Template": "Vorlage", + "Text Completion": "Textvervollständigung", + "Text-to-Speech Engine": "Text-zu-Sprache-Engine", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Danke für Ihr Feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Die Punktzahl sollte ein Wert zwischen 0,0 (0 %) und 1,0 (100 %) sein.", + "Theme": "Design", + "Thinking...": "Denke nach...", + "This action cannot be undone. Do you wish to continue?": "Diese Aktion kann nicht rückgängig gemacht werden. Möchten Sie fortfahren?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dies stellt sicher, dass Ihre wertvollen Unterhaltungen sicher in Ihrer Backend-Datenbank gespeichert werden. Vielen Dank!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Dies ist eine experimentelle Funktion, sie funktioniert möglicherweise nicht wie erwartet und kann jederzeit geändert werden.", + "This setting does not sync across browsers or devices.": "Diese Einstellung wird nicht zwischen Browsern oder Geräten synchronisiert.", + "This will delete": "Dies löscht", + "Thorough explanation": "Ausführliche Erklärung", + "Tika": "Tika", + "Tika Server URL required.": "Tika-Server-URL erforderlich.", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tipp: Aktualisieren Sie mehrere Variablenfelder nacheinander, indem Sie nach jedem Ersetzen die Tabulatortaste im Eingabefeld der Unterhaltung drücken.", + "Title": "Titel", + "Title (e.g. Tell me a fun fact)": "Titel (z. B. Erzähl mir einen lustigen Fakt)", + "Title Auto-Generation": "Unterhaltungstitel automatisch generieren", + "Title cannot be an empty string.": "Titel darf nicht leer sein.", + "Title Generation Prompt": "Prompt für Titelgenerierung", + "to": "für", + "To access the available model names for downloading,": "Um auf die verfügbaren Modellnamen zuzugreifen,", + "To access the GGUF models available for downloading,": "Um auf die verfügbaren GGUF-Modelle zuzugreifen,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Um auf das WebUI zugreifen zu können, wenden Sie sich bitte an einen Administrator. Administratoren können den Benutzerstatus über das Admin-Panel verwalten.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Um Dokumente hinzuzufügen, laden Sie sie zuerst im Arbeitsbereich „Dokumente“ hoch.", + "to chat input.": "zum Eingabefeld der Unterhaltung.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Um Filter auszuwählen, fügen Sie diese zunächst dem Arbeitsbereich „Funktionen“ hinzu.", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Um Toolkits auszuwählen, fügen Sie sie zunächst dem Arbeitsbereich „Werkzeuge“ hinzu.", + "Today": "Heute", + "Toggle settings": "Einstellungen umschalten", + "Toggle sidebar": "Seitenleiste umschalten", + "Tokens To Keep On Context Refresh (num_keep)": "Beizubehaltende Tokens bei Kontextaktualisierung (num_keep)", + "Tool created successfully": "Werkzeug erfolgreich erstellt", + "Tool deleted successfully": "Werkzeug erfolgreich gelöscht", + "Tool imported successfully": "Werkzeug erfolgreich importiert", + "Tool updated successfully": "Werkzeug erfolgreich aktualisiert", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Werkzeuge", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Probleme beim Zugriff auf Ollama?", + "TTS Model": "TTS-Modell", + "TTS Settings": "TTS-Einstellungen", + "TTS Voice": "TTS-Stimme", + "Type": "Art", + "Type Hugging Face Resolve (Download) URL": "Geben Sie die Hugging Face Resolve-URL ein", + "Uh-oh! There was an issue connecting to {{provider}}.": "Ups! Es gab ein Problem bei der Verbindung mit {{provider}}.", + "UI": "Oberfläche", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Unbekannter Dateityp '{{file_type}}'. Der Datei-Upload wird trotzdem fortgesetzt.", + "Unpin": "Lösen", + "Update": "Aktualisieren", + "Update and Copy Link": "Aktualisieren und Link kopieren", + "Update password": "Passwort aktualisieren", + "Updated at": "Aktualisiert am", + "Upload": "Hochladen", + "Upload a GGUF model": "GGUF-Model hochladen", + "Upload Files": "Datei(en) hochladen", + "Upload Pipeline": "Pipeline hochladen", + "Upload Progress": "Hochladefortschritt", + "URL Mode": "URL-Modus", + "Use '#' in the prompt input to load and select your documents.": "Verwenden Sie '#' in der Eingabeaufforderung, um Ihre Dokumente zu laden und auszuwählen.", + "Use Gravatar": "Gravatar verwenden", + "Use Initials": "Initialen verwenden", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "Benutzer", + "User location successfully retrieved.": "Benutzerstandort erfolgreich ermittelt.", + "User Permissions": "Benutzerberechtigungen", + "Users": "Benutzer", + "Utilize": "Verwende", + "Valid time units:": "Gültige Zeiteinheiten:", + "Valves": "Valves", + "Valves updated": "Valves aktualisiert", + "Valves updated successfully": "Valves erfolgreich aktualisiert", + "variable": "Variable", + "variable to have them replaced with clipboard content.": "Variable, um den Inhalt der Zwischenablage beim Nutzen des Prompts zu ersetzen.", + "Version": "Version", + "Voice": "Stimme", + "Warning": "Warnung", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Warnung: Wenn Sie das Einbettungsmodell aktualisieren oder ändern, müssen Sie alle Dokumente erneut importieren.", + "Web": "Web", + "Web API": "Web-API", + "Web Loader Settings": "Web Loader Einstellungen", + "Web Params": "Web Parameter", + "Web Search": "Websuche", + "Web Search Engine": "Suchmaschine", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI-Einstellungen", + "WebUI will make requests to": "WebUI sendet Anfragen an:", + "What’s New in": "Neuigkeiten von", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Wenn der Verlauf deaktiviert ist, werden neue Unterhaltungen in diesem Browser nicht im Verlauf Ihrer anderen Geräte erscheinen.", + "Whisper (Local)": "Whisper (lokal)", + "Widescreen Mode": "Breitbildmodus", + "Workspace": "Arbeitsbereich", + "Write a prompt suggestion (e.g. Who are you?)": "Schreiben Sie einen Promptvorschlag (z. B. Wer sind Sie?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Schreibe eine kurze Zusammenfassung in 50 Wörtern, die [Thema oder Schlüsselwort] zusammenfasst.", + "Yesterday": "Gestern", + "You": "Sie", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Personalisieren Sie Interaktionen mit LLMs, indem Sie über die Schaltfläche \"Verwalten\" Erinnerungen hinzufügen.", + "You cannot clone a base model": "Sie können Basismodelle nicht klonen", + "You have no archived conversations.": "Du hast keine archivierten Unterhaltungen.", + "You have shared this chat": "Sie haben diese Unterhaltung geteilt", + "You're a helpful assistant.": "Du bist ein hilfreicher Assistent.", + "You're now logged in.": "Sie sind jetzt eingeloggt.", + "Your account status is currently pending activation.": "Ihr Kontostatus ist derzeit ausstehend und wartet auf Aktivierung.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "YouTube-Ladeeinstellungen" +} diff --git a/src/lib/i18n/locales/dg-DG/translation.json b/src/lib/i18n/locales/dg-DG/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..1befb8086f6d3257157a069b7754ebb1570058d3 --- /dev/null +++ b/src/lib/i18n/locales/dg-DG/translation.json @@ -0,0 +1,716 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' for no expire. Much permanent, very wow.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(such e.g. `sh webui.sh --api`)", + "(latest)": "(much latest)", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "{{modelName}} is thinkin'...", + "{{user}}'s Chats": "", + "{{webUIName}} Backend Required": "{{webUIName}} Backend Much Required", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "such user", + "About": "Much About", + "Account": "Account", + "Account Activation Pending": "", + "Accurate information": "", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "Add short title for this prompt", + "Add a tag": "Add such tag", + "Add custom prompt": "", + "Add Docs": "Add Docs", + "Add Files": "Add Files", + "Add Memory": "", + "Add message": "Add Prompt", + "Add Model": "", + "Add Tag": "", + "Add Tags": "", + "Add User": "", + "Adjusting these settings will apply changes universally to all users.": "Adjusting these settings will apply changes to all users. Such universal, very wow.", + "admin": "admin", + "Admin": "", + "Admin Panel": "Admin Panel", + "Admin Settings": "Admin Settings", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Advanced Parameters", + "Advanced Params": "", + "all": "all", + "All Documents": "", + "All Users": "All Users", + "Allow": "Allow", + "Allow Chat Deletion": "Allow Delete Chats", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "so alpha, many hyphen", + "Already have an account?": "Such account exists?", + "an assistant": "such assistant", + "and": "and", + "and create a new shared link.": "", + "API Base URL": "API Base URL", + "API Key": "API Key", + "API Key created.": "", + "API keys": "", + "April": "", + "Archive": "", + "Archive All Chats": "", + "Archived Chats": "", + "are allowed - Activate this command by typing": "are allowed. Activate typing", + "Are you sure?": "Such certainty?", + "Attach file": "Attach file", + "Attention to detail": "", + "Audio": "Audio", + "Audio settings updated successfully": "", + "August": "", + "Auto-playback response": "Auto-playback response", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Base URL is required.", + "available!": "available! So excite!", + "Back": "Back", + "Bad Response": "", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "", + "Being lazy": "", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Cancel", + "Capabilities": "", + "Change Password": "Change Password", + "Chat": "Chat", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "Chat History", + "Chat History is off for this browser.": "Chat History off for this browser. Such sadness.", + "Chats": "Chats", + "Check Again": "Check Again", + "Check for updates": "Check for updates", + "Checking for updates...": "Checking for updates... Such anticipation...", + "Choose a model before saving...": "Choose model before saving... Wow choose first.", + "Chunk Overlap": "Chunk Overlap", + "Chunk Params": "Chunk Params", + "Chunk Size": "Chunk Size", + "Citation": "", + "Clear memory": "", + "Click here for help.": "Click for help. Much assist.", + "Click here to": "", + "Click here to download user import template file.": "", + "Click here to select": "Click to select", + "Click here to select a csv file.": "", + "Click here to select a py file.": "", + "Click here to select documents.": "Click to select documents", + "click here.": "click here. Such click.", + "Click on the user role button to change a user's role.": "Click user role button to change role.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "Close", + "Code formatted successfully": "", + "Collection": "Collection", + "ComfyUI": "", + "ComfyUI Base URL": "", + "ComfyUI Base URL is required.": "", + "Command": "Command", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "Confirm Password", + "Confirm your action": "", + "Connections": "Connections", + "Contact Admin for WebUI Access": "", + "Content": "Content", + "Content Extraction": "", + "Context Length": "Context Length", + "Continue Response": "", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "", + "Copy": "", + "Copy last code block": "Copy last code block", + "Copy last response": "Copy last response", + "Copy Link": "", + "Copying to clipboard was successful!": "Copying to clipboard was success! Very success!", + "Create a model": "", + "Create Account": "Create Account", + "Create new key": "", + "Create new secret key": "", + "Created at": "Created at", + "Created At": "", + "Created by": "", + "CSV Import": "", + "Current Model": "Current Model", + "Current Password": "Current Password", + "Custom": "Custom", + "Customize models for a specific purpose": "", + "Dark": "Dark", + "Dashboard": "", + "Database": "Database", + "December": "", + "Default": "Default", + "Default (Automatic1111)": "Default (Automatic1111)", + "Default (SentenceTransformers)": "", + "Default Model": "", + "Default model updated": "Default model much updated", + "Default Prompt Suggestions": "Default Prompt Suggestions", + "Default User Role": "Default User Role", + "delete": "delete", + "Delete": "", + "Delete a model": "Delete a model", + "Delete All Chats": "", + "Delete chat": "Delete chat", + "Delete Chat": "", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "", + "Delete tool?": "", + "Delete User": "", + "Deleted {{deleteModelTag}}": "Deleted {{deleteModelTag}}", + "Deleted {{name}}": "", + "Description": "Description", + "Didn't fully follow instructions": "", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "Discover a prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Discover, download, and explore custom prompts", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Discover, download, and explore model presets", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Display username instead of You in Chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Document", + "Document Settings": "Document Settings", + "Documentation": "", + "Documents": "Documents", + "does not make any external connections, and your data stays securely on your locally hosted server.": "does not connect external, data stays safe locally.", + "Don't Allow": "Don't Allow", + "Don't have an account?": "No account? Much sad.", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "", + "Done": "", + "Download": "", + "Download canceled": "", + "Download Database": "Download Database", + "Drop any files here to add to the conversation": "Drop files here to add to conversation", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "e.g. '30s','10m'. Much time units are 's', 'm', 'h'.", + "Edit": "", + "Edit Doc": "Edit Doge", + "Edit Memory": "", + "Edit User": "Edit Wowser", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "", + "Embedding Model": "", + "Embedding Model Engine": "", + "Embedding model set to \"{{embedding_model}}\"": "", + "Enable Chat History": "Activate Chat Story", + "Enable Community Sharing": "", + "Enable New Sign Ups": "Enable New Bark Ups", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "", + "Enter {{role}} message here": "Enter {{role}} bork here", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "Enter Overlap of Chunks", + "Enter Chunk Size": "Enter Size of Chunk", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "Enter Size of Wow (e.g. 512x512)", + "Enter language codes": "", + "Enter model tag (e.g. {{modelTag}})": "Enter model doge tag (e.g. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Enter Number of Steps (e.g. 50)", + "Enter Score": "", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "Enter stop bark", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Enter Top Wow", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Enter URL (e.g. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "", + "Enter Your Email": "Enter Your Dogemail", + "Enter Your Full Name": "Enter Your Full Wow", + "Enter your message": "", + "Enter Your Password": "Enter Your Barkword", + "Enter Your Role": "", + "Error": "", + "Experimental": "Much Experiment", + "Export": "", + "Export All Chats (All Users)": "Export All Chats (All Doggos)", + "Export chat (.json)": "", + "Export Chats": "Export Barks", + "Export Documents Mapping": "Export Mappings of Dogos", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "Export Promptos", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "", + "Failed to read clipboard contents": "Failed to read clipboard borks", + "Failed to update settings": "", + "February": "", + "Feel free to add specific details": "", + "File": "", + "File Mode": "Bark Mode", + "File not found.": "Bark not found.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint dogeing: Unable to use initials as avatar. Defaulting to default doge image.", + "Fluidly stream large external response chunks": "Fluidly wow big chunks", + "Focus chat input": "Focus chat bork", + "Followed instructions perfectly": "", + "Form": "", + "Format your variables using square brackets like this:": "Format variables using square brackets like wow:", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Woweral", + "General Settings": "General Doge Settings", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "", + "Get up and running with": "", + "Global": "", + "Good Response": "", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "", + "Hello, {{name}}": "Much helo, {{name}}", + "Help": "", + "Hide": "Hide", + "Hide Model": "", + "How can I help you today?": "How can I halp u today?", + "Hybrid Search": "", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Image Wow (Much Experiment)", + "Image Generation Engine": "Image Engine", + "Image Settings": "Settings for Wowmage", + "Images": "Wowmages", + "Import Chats": "Import Barks", + "Import Documents Mapping": "Import Doge Mapping", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "Import Promptos", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Include `--api` flag when running stable-diffusion-webui", + "Info": "", + "Input commands": "Input commands", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Interface", + "Invalid Tag": "", + "January": "", + "join our Discord for help.": "join our Discord for help.", + "JSON": "JSON", + "JSON Preview": "", + "July": "", + "June": "", + "JWT Expiration": "JWT Expire", + "JWT Token": "JWT Borken", + "Keep Alive": "Keep Wow", + "Keyboard shortcuts": "Keyboard Barkcuts", + "Knowledge": "", + "Language": "Doge Speak", + "large language models, locally.": "", + "Last Active": "", + "Last Modified": "", + "Light": "Light", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMs can make borks. Verify important info.", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "Made by OpenWebUI Community", + "Make sure to enclose them with": "Make sure to enclose them with", + "Manage": "", + "Manage Models": "Manage Wowdels", + "Manage Ollama Models": "Manage Ollama Wowdels", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximum of 3 models can be downloaded simultaneously. Please try again later.", + "May": "", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' has been successfully downloaded.", + "Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' is already in queue for downloading.", + "Model {{modelId}} not found": "Model {{modelId}} not found", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem bark detected. Model shortname is required for update, cannot continue.", + "Model ID": "", + "Model not selected": "Model not selected", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "Wowdel Whitelisting", + "Model(s) Whitelisted": "Wowdel(s) Whitelisted", + "Modelfile Content": "Modelfile Content", + "Models": "Wowdels", + "More": "", + "Name": "Name", + "Name Tag": "Name Tag", + "Name your model": "", + "New Chat": "New Bark", + "New Password": "New Barkword", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "", + "No search query generated": "", + "No source available": "No source available", + "No valves to update": "", + "None": "", + "Not factually correct": "", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "", + "Notifications": "Notifications", + "November": "", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "", + "Off": "Off", + "Okay, Let's Go!": "Okay, Let's Go!", + "OLED Dark": "OLED Dark", + "Ollama": "", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "Ollama Version", + "On": "On", + "Only": "Only", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Only wow characters and hyphens are allowed in the bork string.", + "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.": "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.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Open new bark", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "", + "OpenAI API Key is required.": "OpenAI Bark Key is required.", + "OpenAI URL/Key required.": "", + "or": "or", + "Other": "", + "Password": "Barkword", + "PDF document (.pdf)": "", + "PDF Extract Images (OCR)": "PDF Extract Wowmages (OCR)", + "pending": "pending", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Permission denied when accessing microphone: {{error}}", + "Personalization": "Personalization", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "Plain text (.txt)", + "Playground": "Playground", + "Please carefully review the following warnings:": "", + "Positive attitude": "", + "Previous 30 days": "", + "Previous 7 days": "", + "Profile Image": "", + "Prompt": "", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "", + "Prompt Content": "Prompt Content", + "Prompt suggestions": "Prompt wowgestions", + "Prompts": "Promptos", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "Pull a wowdel from Ollama.com", + "Query Params": "Query Bark", + "RAG Template": "RAG Template", + "Read Aloud": "", + "Record voice": "Record Bark", + "Redirecting you to OpenWebUI Community": "Redirecting you to OpenWebUI Community", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "", + "Regenerate": "", + "Release Notes": "Release Borks", + "Remove": "", + "Remove Model": "", + "Rename": "", + "Repeat Last N": "Repeat Last N", + "Request Mode": "Request Bark", + "Reranking Model": "", + "Reranking model disabled": "", + "Reranking model set to \"{{reranking_model}}\"": "", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Reset Vector Storage", + "Response AutoCopy to Clipboard": "Copy Bark Auto Bark", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Role", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Save much wow", + "Save & Create": "Save & Create much create", + "Save & Update": "Save & Update much update", + "Save Tag": "", + "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": "Saving chat logs in browser storage not support anymore. Pls download and delete your chat logs by click button below. Much easy re-import to backend through", + "Scan": "Scan much scan", + "Scan complete!": "Scan complete! Very wow!", + "Scan for documents from {{path}}": "Scan for documents from {{path}} wow", + "Search": "Search very search", + "Search a model": "", + "Search Chats": "", + "Search Documents": "Search Documents much find", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "Search Prompts much wow", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_few": "", + "Searched {{count}} sites_many": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "See readme.md for instructions wow", + "See what's new": "See what's new so amaze", + "Seed": "Seed very plant", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Select a mode very choose", + "Select a model": "Select a model much choice", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "Select an Ollama instance very choose", + "Select Documents": "", + "Select model": "Select model much choice", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "Send a Message much message", + "Send message": "Send message very send", + "September": "", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "Server connection verified much secure", + "Set as default": "Set as default very default", + "Set Default Model": "Set Default Model much model", + "Set embedding model (e.g. {{model}})": "", + "Set Image Size": "Set Image Size very size", + "Set reranking model (e.g. {{model}})": "", + "Set Steps": "Set Steps so many steps", + "Set Task Model": "", + "Set Voice": "Set Voice so speak", + "Settings": "Settings much settings", + "Settings saved successfully!": "Settings saved successfully! Very success!", + "Settings updated successfully": "", + "Share": "", + "Share Chat": "", + "Share to OpenWebUI Community": "Share to OpenWebUI Community much community", + "short-summary": "short-summary so short", + "Show": "Show much show", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Show shortcuts much shortcut", + "Show your support!": "", + "Showcased creativity": "", + "Sign in": "Sign in very sign", + "Sign Out": "Sign Out much logout", + "Sign up": "Sign up much join", + "Signing in": "", + "Source": "Source", + "Speech recognition error: {{error}}": "Speech recognition error: {{error}} so error", + "Speech-to-Text Engine": "Speech-to-Text Engine much speak", + "Stop Sequence": "Stop Sequence much stop", + "STT Model": "", + "STT Settings": "STT Settings very settings", + "Submit": "Submit much submit", + "Subtitle (e.g. about the Roman Empire)": "", + "Success": "Success very success", + "Successfully updated.": "Successfully updated. Very updated.", + "Suggested": "", + "Support": "", + "Support this plugin:": "", + "System": "System very system", + "System Prompt": "System Prompt much prompt", + "Tags": "Tags very tags", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "", + "Temperature": "Temperature very temp", + "Template": "Template much template", + "Text Completion": "Text Completion much complete", + "Text-to-Speech Engine": "Text-to-Speech Engine much speak", + "Tfs Z": "Tfs Z much Z", + "Thanks for your feedback!": "", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "", + "Theme": "Theme much theme", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "This ensures that your valuable conversations are securely saved to your backend database. Thank you! Much secure!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "This setting does not sync across browsers or devices. Very not sync.", + "This will delete": "", + "Thorough explanation": "", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement. Much tip!", + "Title": "Title very title", + "Title (e.g. Tell me a fun fact)": "", + "Title Auto-Generation": "Title Auto-Generation much auto-gen", + "Title cannot be an empty string.": "", + "Title Generation Prompt": "Title Generation Prompt very prompt", + "to": "to very to", + "To access the available model names for downloading,": "To access the available model names for downloading, much access", + "To access the GGUF models available for downloading,": "To access the GGUF models available for downloading, much access", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "to chat input. Very chat.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "", + "Toggle settings": "Toggle settings much toggle", + "Toggle sidebar": "Toggle sidebar much toggle", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K very top", + "Top P": "Top P very top", + "Trouble accessing Ollama?": "Trouble accessing Ollama? Much trouble?", + "TTS Model": "", + "TTS Settings": "TTS Settings much settings", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL much download", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! There was an issue connecting to {{provider}}. Much uh-oh!", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "", + "Update password": "Update password much change", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Upload a GGUF model very upload", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "Upload Progress much progress", + "URL Mode": "URL Mode much mode", + "Use '#' in the prompt input to load and select your documents.": "Use '#' in the prompt input to load and select your documents. Much use.", + "Use Gravatar": "Use Gravatar much avatar", + "Use Initials": "Use Initials much initial", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "user much user", + "User location successfully retrieved.": "", + "User Permissions": "User Permissions much permissions", + "Users": "Users much users", + "Utilize": "Utilize very use", + "Valid time units:": "Valid time units: much time", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variable very variable", + "variable to have them replaced with clipboard content.": "variable to have them replaced with clipboard content. Very replace.", + "Version": "Version much version", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "", + "Web": "Web very web", + "Web API": "", + "Web Loader Settings": "", + "Web Params": "", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "", + "WebUI Settings": "WebUI Settings much settings", + "WebUI will make requests to": "WebUI will make requests to much request", + "What’s New in": "What’s New in much new", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "When history is turned off, new chats on this browser won't appear in your history on any of your devices. Much history.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "Write a prompt suggestion (e.g. Who are you?) much suggest", + "Write a summary in 50 words that summarizes [topic or keyword].": "Write a summary in 50 words that summarizes [topic or keyword]. Much summarize.", + "Yesterday": "", + "You": "", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "", + "You have shared this chat": "", + "You're a helpful assistant.": "You're a helpful assistant. Much helpful.", + "You're now logged in.": "You're now logged in. Much logged.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "", + "Youtube Loader Settings": "" +} diff --git a/src/lib/i18n/locales/en-GB/translation.json b/src/lib/i18n/locales/en-GB/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..b8c97a3a885d677287ec2d38c6906e59ff7f1f4b --- /dev/null +++ b/src/lib/i18n/locales/en-GB/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "", + "(Beta)": "", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "", + "(latest)": "", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "", + "{{user}}'s Chats": "", + "{{webUIName}} Backend Required": "", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "", + "About": "", + "Account": "", + "Account Activation Pending": "", + "Accurate information": "", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "", + "Add a tag": "", + "Add custom prompt": "", + "Add Docs": "", + "Add Files": "", + "Add Memory": "", + "Add message": "", + "Add Model": "", + "Add Tag": "", + "Add Tags": "", + "Add User": "", + "Adjusting these settings will apply changes universally to all users.": "", + "admin": "", + "Admin": "", + "Admin Panel": "", + "Admin Settings": "", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "", + "Advanced Params": "", + "all": "", + "All Documents": "", + "All Users": "", + "Allow": "", + "Allow Chat Deletion": "", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "", + "Already have an account?": "", + "an assistant": "", + "and": "", + "and create a new shared link.": "", + "API Base URL": "", + "API Key": "", + "API Key created.": "", + "API keys": "", + "April": "", + "Archive": "", + "Archive All Chats": "", + "Archived Chats": "", + "are allowed - Activate this command by typing": "", + "Are you sure?": "", + "Attach file": "", + "Attention to detail": "", + "Audio": "", + "Audio settings updated successfully": "", + "August": "", + "Auto-playback response": "", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "", + "AUTOMATIC1111 Base URL is required.": "", + "available!": "", + "Back": "", + "Bad Response": "", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "", + "Being lazy": "", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "", + "Capabilities": "", + "Change Password": "", + "Chat": "", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "", + "Chat History is off for this browser.": "", + "Chats": "", + "Check Again": "", + "Check for updates": "", + "Checking for updates...": "", + "Choose a model before saving...": "", + "Chunk Overlap": "", + "Chunk Params": "", + "Chunk Size": "", + "Citation": "", + "Clear memory": "", + "Click here for help.": "", + "Click here to": "", + "Click here to download user import template file.": "", + "Click here to select": "", + "Click here to select a csv file.": "", + "Click here to select a py file.": "", + "Click here to select documents.": "", + "click here.": "", + "Click on the user role button to change a user's role.": "", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "", + "Code formatted successfully": "", + "Collection": "", + "ComfyUI": "", + "ComfyUI Base URL": "", + "ComfyUI Base URL is required.": "", + "Command": "", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "", + "Confirm your action": "", + "Connections": "", + "Contact Admin for WebUI Access": "", + "Content": "", + "Content Extraction": "", + "Context Length": "", + "Continue Response": "", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "", + "Copy": "", + "Copy last code block": "", + "Copy last response": "", + "Copy Link": "", + "Copying to clipboard was successful!": "", + "Create a model": "", + "Create Account": "", + "Create new key": "", + "Create new secret key": "", + "Created at": "", + "Created At": "", + "Created by": "", + "CSV Import": "", + "Current Model": "", + "Current Password": "", + "Custom": "", + "Customize models for a specific purpose": "", + "Dark": "", + "Dashboard": "", + "Database": "", + "December": "", + "Default": "", + "Default (Automatic1111)": "", + "Default (SentenceTransformers)": "", + "Default Model": "", + "Default model updated": "", + "Default Prompt Suggestions": "", + "Default User Role": "", + "delete": "", + "Delete": "", + "Delete a model": "", + "Delete All Chats": "", + "Delete chat": "", + "Delete Chat": "", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "", + "Delete tool?": "", + "Delete User": "", + "Deleted {{deleteModelTag}}": "", + "Deleted {{name}}": "", + "Description": "", + "Didn't fully follow instructions": "", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "", + "Document Settings": "", + "Documentation": "", + "Documents": "", + "does not make any external connections, and your data stays securely on your locally hosted server.": "", + "Don't Allow": "", + "Don't have an account?": "", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "", + "Done": "", + "Download": "", + "Download canceled": "", + "Download Database": "", + "Drop any files here to add to the conversation": "", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "", + "Edit": "", + "Edit Doc": "", + "Edit Memory": "", + "Edit User": "", + "ElevenLabs": "", + "Email": "", + "Embedding Batch Size": "", + "Embedding Model": "", + "Embedding Model Engine": "", + "Embedding model set to \"{{embedding_model}}\"": "", + "Enable Chat History": "", + "Enable Community Sharing": "", + "Enable New Sign Ups": "", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "", + "Enter {{role}} message here": "", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "", + "Enter Chunk Size": "", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "", + "Enter language codes": "", + "Enter model tag (e.g. {{modelTag}})": "", + "Enter Number of Steps (e.g. 50)": "", + "Enter Score": "", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "", + "Enter URL (e.g. http://127.0.0.1:7860/)": "", + "Enter URL (e.g. http://localhost:11434)": "", + "Enter Your Email": "", + "Enter Your Full Name": "", + "Enter your message": "", + "Enter Your Password": "", + "Enter Your Role": "", + "Error": "", + "Experimental": "", + "Export": "", + "Export All Chats (All Users)": "", + "Export chat (.json)": "", + "Export Chats": "", + "Export Documents Mapping": "", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "", + "Failed to read clipboard contents": "", + "Failed to update settings": "", + "February": "", + "Feel free to add specific details": "", + "File": "", + "File Mode": "", + "File not found.": "", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", + "Fluidly stream large external response chunks": "", + "Focus chat input": "", + "Followed instructions perfectly": "", + "Form": "", + "Format your variables using square brackets like this:": "", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "", + "General Settings": "", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "", + "Get up and running with": "", + "Global": "", + "Good Response": "", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "", + "Hello, {{name}}": "", + "Help": "", + "Hide": "", + "Hide Model": "", + "How can I help you today?": "", + "Hybrid Search": "", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "", + "Image Generation Engine": "", + "Image Settings": "", + "Images": "", + "Import Chats": "", + "Import Documents Mapping": "", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "", + "Info": "", + "Input commands": "", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "", + "Invalid Tag": "", + "January": "", + "join our Discord for help.": "", + "JSON": "", + "JSON Preview": "", + "July": "", + "June": "", + "JWT Expiration": "", + "JWT Token": "", + "Keep Alive": "", + "Keyboard shortcuts": "", + "Knowledge": "", + "Language": "", + "large language models, locally.": "", + "Last Active": "", + "Last Modified": "", + "Light": "", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "", + "Make sure to enclose them with": "", + "Manage": "", + "Manage Models": "", + "Manage Ollama Models": "", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "", + "May": "", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "", + "Mirostat": "", + "Mirostat Eta": "", + "Mirostat Tau": "", + "MMMM DD, YYYY": "", + "MMMM DD, YYYY HH:mm": "", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "", + "Model '{{modelTag}}' is already in queue for downloading.": "", + "Model {{modelId}} not found": "", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "", + "Model ID": "", + "Model not selected": "", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "", + "Model(s) Whitelisted": "", + "Modelfile Content": "", + "Models": "", + "More": "", + "Name": "", + "Name Tag": "", + "Name your model": "", + "New Chat": "", + "New Password": "", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "", + "No search query generated": "", + "No source available": "", + "No valves to update": "", + "None": "", + "Not factually correct": "", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "", + "Notifications": "", + "November": "", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "", + "Off": "", + "Okay, Let's Go!": "", + "OLED Dark": "", + "Ollama": "", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "", + "On": "", + "Only": "", + "Only alphanumeric characters and hyphens are allowed in the command string.": "", + "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.": "", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "", + "Open AI (Dall-E)": "", + "Open new chat": "", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "", + "OpenAI API": "", + "OpenAI API Config": "", + "OpenAI API Key is required.": "", + "OpenAI URL/Key required.": "", + "or": "", + "Other": "", + "Password": "", + "PDF document (.pdf)": "", + "PDF Extract Images (OCR)": "", + "pending": "", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "", + "Personalization": "", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "", + "Playground": "", + "Please carefully review the following warnings:": "", + "Positive attitude": "", + "Previous 30 days": "", + "Previous 7 days": "", + "Profile Image": "", + "Prompt": "", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "", + "Prompt Content": "", + "Prompt suggestions": "", + "Prompts": "", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "", + "Query Params": "", + "RAG Template": "", + "Read Aloud": "", + "Record voice": "", + "Redirecting you to OpenWebUI Community": "", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "", + "Regenerate": "", + "Release Notes": "", + "Remove": "", + "Remove Model": "", + "Rename": "", + "Repeat Last N": "", + "Request Mode": "", + "Reranking Model": "", + "Reranking model disabled": "", + "Reranking model set to \"{{reranking_model}}\"": "", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "", + "Response AutoCopy to Clipboard": "", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "", + "Rosé Pine": "", + "Rosé Pine Dawn": "", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "", + "Save & Create": "", + "Save & Update": "", + "Save Tag": "", + "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": "", + "Scan": "", + "Scan complete!": "", + "Scan for documents from {{path}}": "", + "Search": "", + "Search a model": "", + "Search Chats": "", + "Search Documents": "", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "", + "See what's new": "", + "Seed": "", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "", + "Select a model": "", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "", + "Select Documents": "", + "Select model": "", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "", + "Send message": "", + "September": "", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "", + "Set as default": "", + "Set Default Model": "", + "Set embedding model (e.g. {{model}})": "", + "Set Image Size": "", + "Set reranking model (e.g. {{model}})": "", + "Set Steps": "", + "Set Task Model": "", + "Set Voice": "", + "Settings": "", + "Settings saved successfully!": "", + "Settings updated successfully": "", + "Share": "", + "Share Chat": "", + "Share to OpenWebUI Community": "", + "short-summary": "", + "Show": "", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "", + "Show your support!": "", + "Showcased creativity": "", + "Sign in": "", + "Sign Out": "", + "Sign up": "", + "Signing in": "", + "Source": "", + "Speech recognition error: {{error}}": "", + "Speech-to-Text Engine": "", + "Stop Sequence": "", + "STT Model": "", + "STT Settings": "", + "Submit": "", + "Subtitle (e.g. about the Roman Empire)": "", + "Success": "", + "Successfully updated.": "", + "Suggested": "", + "Support": "", + "Support this plugin:": "", + "System": "", + "System Prompt": "", + "Tags": "", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "", + "Temperature": "", + "Template": "", + "Text Completion": "", + "Text-to-Speech Engine": "", + "Tfs Z": "", + "Thanks for your feedback!": "", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "", + "Theme": "", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "", + "This will delete": "", + "Thorough explanation": "", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "", + "Title": "", + "Title (e.g. Tell me a fun fact)": "", + "Title Auto-Generation": "", + "Title cannot be an empty string.": "", + "Title Generation Prompt": "", + "to": "", + "To access the available model names for downloading,": "", + "To access the GGUF models available for downloading,": "", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "", + "Toggle settings": "", + "Toggle sidebar": "", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "", + "Top P": "", + "Trouble accessing Ollama?": "", + "TTS Model": "", + "TTS Settings": "", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "", + "Uh-oh! There was an issue connecting to {{provider}}.": "", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "", + "Update password": "", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "", + "URL Mode": "", + "Use '#' in the prompt input to load and select your documents.": "", + "Use Gravatar": "", + "Use Initials": "", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "", + "User location successfully retrieved.": "", + "User Permissions": "", + "Users": "", + "Utilize": "", + "Valid time units:": "", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "", + "variable to have them replaced with clipboard content.": "", + "Version": "", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "", + "Web": "", + "Web API": "", + "Web Loader Settings": "", + "Web Params": "", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "", + "WebUI Settings": "", + "WebUI will make requests to": "", + "What’s New in": "", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "", + "Write a summary in 50 words that summarizes [topic or keyword].": "", + "Yesterday": "", + "You": "", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "", + "You have shared this chat": "", + "You're a helpful assistant.": "", + "You're now logged in.": "", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "", + "Youtube Loader Settings": "" +} diff --git a/src/lib/i18n/locales/en-US/translation.json b/src/lib/i18n/locales/en-US/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..b8c97a3a885d677287ec2d38c6906e59ff7f1f4b --- /dev/null +++ b/src/lib/i18n/locales/en-US/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "", + "(Beta)": "", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "", + "(latest)": "", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "", + "{{user}}'s Chats": "", + "{{webUIName}} Backend Required": "", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "", + "About": "", + "Account": "", + "Account Activation Pending": "", + "Accurate information": "", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "", + "Add a tag": "", + "Add custom prompt": "", + "Add Docs": "", + "Add Files": "", + "Add Memory": "", + "Add message": "", + "Add Model": "", + "Add Tag": "", + "Add Tags": "", + "Add User": "", + "Adjusting these settings will apply changes universally to all users.": "", + "admin": "", + "Admin": "", + "Admin Panel": "", + "Admin Settings": "", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "", + "Advanced Params": "", + "all": "", + "All Documents": "", + "All Users": "", + "Allow": "", + "Allow Chat Deletion": "", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "", + "Already have an account?": "", + "an assistant": "", + "and": "", + "and create a new shared link.": "", + "API Base URL": "", + "API Key": "", + "API Key created.": "", + "API keys": "", + "April": "", + "Archive": "", + "Archive All Chats": "", + "Archived Chats": "", + "are allowed - Activate this command by typing": "", + "Are you sure?": "", + "Attach file": "", + "Attention to detail": "", + "Audio": "", + "Audio settings updated successfully": "", + "August": "", + "Auto-playback response": "", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "", + "AUTOMATIC1111 Base URL is required.": "", + "available!": "", + "Back": "", + "Bad Response": "", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "", + "Being lazy": "", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "", + "Capabilities": "", + "Change Password": "", + "Chat": "", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "", + "Chat History is off for this browser.": "", + "Chats": "", + "Check Again": "", + "Check for updates": "", + "Checking for updates...": "", + "Choose a model before saving...": "", + "Chunk Overlap": "", + "Chunk Params": "", + "Chunk Size": "", + "Citation": "", + "Clear memory": "", + "Click here for help.": "", + "Click here to": "", + "Click here to download user import template file.": "", + "Click here to select": "", + "Click here to select a csv file.": "", + "Click here to select a py file.": "", + "Click here to select documents.": "", + "click here.": "", + "Click on the user role button to change a user's role.": "", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "", + "Code formatted successfully": "", + "Collection": "", + "ComfyUI": "", + "ComfyUI Base URL": "", + "ComfyUI Base URL is required.": "", + "Command": "", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "", + "Confirm your action": "", + "Connections": "", + "Contact Admin for WebUI Access": "", + "Content": "", + "Content Extraction": "", + "Context Length": "", + "Continue Response": "", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "", + "Copy": "", + "Copy last code block": "", + "Copy last response": "", + "Copy Link": "", + "Copying to clipboard was successful!": "", + "Create a model": "", + "Create Account": "", + "Create new key": "", + "Create new secret key": "", + "Created at": "", + "Created At": "", + "Created by": "", + "CSV Import": "", + "Current Model": "", + "Current Password": "", + "Custom": "", + "Customize models for a specific purpose": "", + "Dark": "", + "Dashboard": "", + "Database": "", + "December": "", + "Default": "", + "Default (Automatic1111)": "", + "Default (SentenceTransformers)": "", + "Default Model": "", + "Default model updated": "", + "Default Prompt Suggestions": "", + "Default User Role": "", + "delete": "", + "Delete": "", + "Delete a model": "", + "Delete All Chats": "", + "Delete chat": "", + "Delete Chat": "", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "", + "Delete tool?": "", + "Delete User": "", + "Deleted {{deleteModelTag}}": "", + "Deleted {{name}}": "", + "Description": "", + "Didn't fully follow instructions": "", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "", + "Document Settings": "", + "Documentation": "", + "Documents": "", + "does not make any external connections, and your data stays securely on your locally hosted server.": "", + "Don't Allow": "", + "Don't have an account?": "", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "", + "Done": "", + "Download": "", + "Download canceled": "", + "Download Database": "", + "Drop any files here to add to the conversation": "", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "", + "Edit": "", + "Edit Doc": "", + "Edit Memory": "", + "Edit User": "", + "ElevenLabs": "", + "Email": "", + "Embedding Batch Size": "", + "Embedding Model": "", + "Embedding Model Engine": "", + "Embedding model set to \"{{embedding_model}}\"": "", + "Enable Chat History": "", + "Enable Community Sharing": "", + "Enable New Sign Ups": "", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "", + "Enter {{role}} message here": "", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "", + "Enter Chunk Size": "", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "", + "Enter language codes": "", + "Enter model tag (e.g. {{modelTag}})": "", + "Enter Number of Steps (e.g. 50)": "", + "Enter Score": "", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "", + "Enter URL (e.g. http://127.0.0.1:7860/)": "", + "Enter URL (e.g. http://localhost:11434)": "", + "Enter Your Email": "", + "Enter Your Full Name": "", + "Enter your message": "", + "Enter Your Password": "", + "Enter Your Role": "", + "Error": "", + "Experimental": "", + "Export": "", + "Export All Chats (All Users)": "", + "Export chat (.json)": "", + "Export Chats": "", + "Export Documents Mapping": "", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "", + "Failed to read clipboard contents": "", + "Failed to update settings": "", + "February": "", + "Feel free to add specific details": "", + "File": "", + "File Mode": "", + "File not found.": "", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", + "Fluidly stream large external response chunks": "", + "Focus chat input": "", + "Followed instructions perfectly": "", + "Form": "", + "Format your variables using square brackets like this:": "", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "", + "General Settings": "", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "", + "Get up and running with": "", + "Global": "", + "Good Response": "", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "", + "Hello, {{name}}": "", + "Help": "", + "Hide": "", + "Hide Model": "", + "How can I help you today?": "", + "Hybrid Search": "", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "", + "Image Generation Engine": "", + "Image Settings": "", + "Images": "", + "Import Chats": "", + "Import Documents Mapping": "", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "", + "Info": "", + "Input commands": "", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "", + "Invalid Tag": "", + "January": "", + "join our Discord for help.": "", + "JSON": "", + "JSON Preview": "", + "July": "", + "June": "", + "JWT Expiration": "", + "JWT Token": "", + "Keep Alive": "", + "Keyboard shortcuts": "", + "Knowledge": "", + "Language": "", + "large language models, locally.": "", + "Last Active": "", + "Last Modified": "", + "Light": "", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "", + "Make sure to enclose them with": "", + "Manage": "", + "Manage Models": "", + "Manage Ollama Models": "", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "", + "May": "", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "", + "Mirostat": "", + "Mirostat Eta": "", + "Mirostat Tau": "", + "MMMM DD, YYYY": "", + "MMMM DD, YYYY HH:mm": "", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "", + "Model '{{modelTag}}' is already in queue for downloading.": "", + "Model {{modelId}} not found": "", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "", + "Model ID": "", + "Model not selected": "", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "", + "Model(s) Whitelisted": "", + "Modelfile Content": "", + "Models": "", + "More": "", + "Name": "", + "Name Tag": "", + "Name your model": "", + "New Chat": "", + "New Password": "", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "", + "No search query generated": "", + "No source available": "", + "No valves to update": "", + "None": "", + "Not factually correct": "", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "", + "Notifications": "", + "November": "", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "", + "Off": "", + "Okay, Let's Go!": "", + "OLED Dark": "", + "Ollama": "", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "", + "On": "", + "Only": "", + "Only alphanumeric characters and hyphens are allowed in the command string.": "", + "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.": "", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "", + "Open AI (Dall-E)": "", + "Open new chat": "", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "", + "OpenAI API": "", + "OpenAI API Config": "", + "OpenAI API Key is required.": "", + "OpenAI URL/Key required.": "", + "or": "", + "Other": "", + "Password": "", + "PDF document (.pdf)": "", + "PDF Extract Images (OCR)": "", + "pending": "", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "", + "Personalization": "", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "", + "Playground": "", + "Please carefully review the following warnings:": "", + "Positive attitude": "", + "Previous 30 days": "", + "Previous 7 days": "", + "Profile Image": "", + "Prompt": "", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "", + "Prompt Content": "", + "Prompt suggestions": "", + "Prompts": "", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "", + "Query Params": "", + "RAG Template": "", + "Read Aloud": "", + "Record voice": "", + "Redirecting you to OpenWebUI Community": "", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "", + "Regenerate": "", + "Release Notes": "", + "Remove": "", + "Remove Model": "", + "Rename": "", + "Repeat Last N": "", + "Request Mode": "", + "Reranking Model": "", + "Reranking model disabled": "", + "Reranking model set to \"{{reranking_model}}\"": "", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "", + "Response AutoCopy to Clipboard": "", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "", + "Rosé Pine": "", + "Rosé Pine Dawn": "", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "", + "Save & Create": "", + "Save & Update": "", + "Save Tag": "", + "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": "", + "Scan": "", + "Scan complete!": "", + "Scan for documents from {{path}}": "", + "Search": "", + "Search a model": "", + "Search Chats": "", + "Search Documents": "", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "", + "See what's new": "", + "Seed": "", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "", + "Select a model": "", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "", + "Select Documents": "", + "Select model": "", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "", + "Send message": "", + "September": "", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "", + "Set as default": "", + "Set Default Model": "", + "Set embedding model (e.g. {{model}})": "", + "Set Image Size": "", + "Set reranking model (e.g. {{model}})": "", + "Set Steps": "", + "Set Task Model": "", + "Set Voice": "", + "Settings": "", + "Settings saved successfully!": "", + "Settings updated successfully": "", + "Share": "", + "Share Chat": "", + "Share to OpenWebUI Community": "", + "short-summary": "", + "Show": "", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "", + "Show your support!": "", + "Showcased creativity": "", + "Sign in": "", + "Sign Out": "", + "Sign up": "", + "Signing in": "", + "Source": "", + "Speech recognition error: {{error}}": "", + "Speech-to-Text Engine": "", + "Stop Sequence": "", + "STT Model": "", + "STT Settings": "", + "Submit": "", + "Subtitle (e.g. about the Roman Empire)": "", + "Success": "", + "Successfully updated.": "", + "Suggested": "", + "Support": "", + "Support this plugin:": "", + "System": "", + "System Prompt": "", + "Tags": "", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "", + "Temperature": "", + "Template": "", + "Text Completion": "", + "Text-to-Speech Engine": "", + "Tfs Z": "", + "Thanks for your feedback!": "", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "", + "Theme": "", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "", + "This will delete": "", + "Thorough explanation": "", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "", + "Title": "", + "Title (e.g. Tell me a fun fact)": "", + "Title Auto-Generation": "", + "Title cannot be an empty string.": "", + "Title Generation Prompt": "", + "to": "", + "To access the available model names for downloading,": "", + "To access the GGUF models available for downloading,": "", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "", + "Toggle settings": "", + "Toggle sidebar": "", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "", + "Top P": "", + "Trouble accessing Ollama?": "", + "TTS Model": "", + "TTS Settings": "", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "", + "Uh-oh! There was an issue connecting to {{provider}}.": "", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "", + "Update password": "", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "", + "URL Mode": "", + "Use '#' in the prompt input to load and select your documents.": "", + "Use Gravatar": "", + "Use Initials": "", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "", + "User location successfully retrieved.": "", + "User Permissions": "", + "Users": "", + "Utilize": "", + "Valid time units:": "", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "", + "variable to have them replaced with clipboard content.": "", + "Version": "", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "", + "Web": "", + "Web API": "", + "Web Loader Settings": "", + "Web Params": "", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "", + "WebUI Settings": "", + "WebUI will make requests to": "", + "What’s New in": "", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "", + "Write a summary in 50 words that summarizes [topic or keyword].": "", + "Yesterday": "", + "You": "", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "", + "You have shared this chat": "", + "You're a helpful assistant.": "", + "You're now logged in.": "", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "", + "Youtube Loader Settings": "" +} diff --git a/src/lib/i18n/locales/es-ES/translation.json b/src/lib/i18n/locales/es-ES/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..da8ddd66bc257df0ebb0bee367c52ac88d2e5352 --- /dev/null +++ b/src/lib/i18n/locales/es-ES/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' para evitar expiración.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(p.ej. `sh webui.sh --api`)", + "(latest)": "(latest)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: No se puede eliminar un modelo base", + "{{modelName}} is thinking...": "{{modelName}} está pensando...", + "{{user}}'s Chats": "{{user}}'s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Servidor Requerido", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modelo de tareas se utiliza cuando se realizan tareas como la generación de títulos para chats y consultas de búsqueda web", + "a user": "un usuario", + "About": "Sobre nosotros", + "Account": "Cuenta", + "Account Activation Pending": "Activación de cuenta pendiente", + "Accurate information": "Información precisa", + "Actions": "", + "Active Users": "Usuarios activos", + "Add": "Agregar", + "Add a model id": "Adición de un identificador de modelo", + "Add a short description about what this model does": "Agregue una breve descripción sobre lo que hace este modelo", + "Add a short title for this prompt": "Agregue un título corto para este Prompt", + "Add a tag": "Agregar una etiqueta", + "Add custom prompt": "Agregar un prompt personalizado", + "Add Docs": "Agregar Documentos", + "Add Files": "Agregar Archivos", + "Add Memory": "Agregar Memoria", + "Add message": "Agregar Prompt", + "Add Model": "Agregar Modelo", + "Add Tag": "", + "Add Tags": "agregar etiquetas", + "Add User": "Agregar Usuario", + "Adjusting these settings will apply changes universally to all users.": "Ajustar estas opciones aplicará los cambios universalmente a todos los usuarios.", + "admin": "admin", + "Admin": "Admin", + "Admin Panel": "Panel de Administración", + "Admin Settings": "Configuración de Administrador", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Admins tienen acceso a todas las herramientas en todo momento; los usuarios necesitan herramientas asignadas por modelo en el espacio de trabajo.", + "Advanced Parameters": "Parámetros Avanzados", + "Advanced Params": "Parámetros avanzados", + "all": "todo", + "All Documents": "Todos los Documentos", + "All Users": "Todos los Usuarios", + "Allow": "Permitir", + "Allow Chat Deletion": "Permitir Borrar Chats", + "Allow non-local voices": "Permitir voces no locales", + "Allow User Location": "Permitir Ubicación del Usuario", + "Allow Voice Interruption in Call": "Permitir interrupción de voz en llamada", + "alphanumeric characters and hyphens": "caracteres alfanuméricos y guiones", + "Already have an account?": "¿Ya tienes una cuenta?", + "an assistant": "un asistente", + "and": "y", + "and create a new shared link.": "y crear un nuevo enlace compartido.", + "API Base URL": "Dirección URL de la API", + "API Key": "Clave de la API ", + "API Key created.": "Clave de la API creada.", + "API keys": "Claves de la API", + "April": "Abril", + "Archive": "Archivar", + "Archive All Chats": "Archivar todos los chats", + "Archived Chats": "Chats archivados", + "are allowed - Activate this command by typing": "están permitidos - Active este comando escribiendo", + "Are you sure?": "¿Está seguro?", + "Attach file": "Adjuntar archivo", + "Attention to detail": "Detalle preciso", + "Audio": "Audio", + "Audio settings updated successfully": "Opciones de audio actualizadas correctamente", + "August": "Agosto", + "Auto-playback response": "Respuesta de reproducción automática", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Dirección URL de AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "La dirección URL de AUTOMATIC1111 es requerida.", + "available!": "¡disponible!", + "Back": "Volver", + "Bad Response": "Respuesta incorrecta", + "Banners": "Banners", + "Base Model (From)": "Modelo base (desde)", + "Batch Size (num_batch)": "Tamaño del Batch (num_batch)", + "before": "antes", + "Being lazy": "Ser perezoso", + "Brave Search API Key": "Clave de API de Brave Search", + "Bypass SSL verification for Websites": "Desactivar la verificación SSL para sitios web", + "Call": "Llamada", + "Call feature is not supported when using Web STT engine": "La funcionalidad de llamada no puede usarse junto con el motor de STT Web", + "Camera": "Cámara", + "Cancel": "Cancelar", + "Capabilities": "Capacidades", + "Change Password": "Cambia la Contraseña", + "Chat": "Chat", + "Chat Background Image": "Imágen de fondo del Chat", + "Chat Bubble UI": "Burbuja de chat UI", + "Chat Controls": "", + "Chat direction": "Dirección del Chat", + "Chat History": "Historial del Chat", + "Chat History is off for this browser.": "El Historial del Chat está apagado para este navegador.", + "Chats": "Chats", + "Check Again": "Verifica de nuevo", + "Check for updates": "Verificar actualizaciones", + "Checking for updates...": "Verificando actualizaciones...", + "Choose a model before saving...": "Escoge un modelo antes de guardar los cambios...", + "Chunk Overlap": "Superposición de fragmentos", + "Chunk Params": "Parámetros de fragmentos", + "Chunk Size": "Tamaño de fragmentos", + "Citation": "Cita", + "Clear memory": "Liberar memoria", + "Click here for help.": "Presiona aquí para obtener ayuda.", + "Click here to": "Presiona aquí para", + "Click here to download user import template file.": "Presiona aquí para descargar el archivo de plantilla de importación de usuario.", + "Click here to select": "Presiona aquí para seleccionar", + "Click here to select a csv file.": "Presiona aquí para seleccionar un archivo csv.", + "Click here to select a py file.": "Presiona aquí para seleccionar un archivo py.", + "Click here to select documents.": "Presiona aquí para seleccionar documentos", + "click here.": "Presiona aquí.", + "Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar su rol.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permisos de escritura del portapapeles denegados. Por favor, comprueba las configuraciones de tu navegador para otorgar el acceso necesario.", + "Clone": "Clonar", + "Close": "Cerrar", + "Code formatted successfully": "Se ha formateado correctamente el código.", + "Collection": "Colección", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "ComfyUI Base URL es requerido.", + "Command": "Comando", + "Concurrent Requests": "Solicitudes simultáneas", + "Confirm": "Confirmar", + "Confirm Password": "Confirmar Contraseña", + "Confirm your action": "Confirma tu acción", + "Connections": "Conexiones", + "Contact Admin for WebUI Access": "Contacta el administrador para obtener acceso al WebUI", + "Content": "Contenido", + "Content Extraction": "", + "Context Length": "Longitud del contexto", + "Continue Response": "Continuar Respuesta", + "Continue with {{provider}}": "Continuar con {{provider}}", + "Controls": "", + "Copied shared chat URL to clipboard!": "¡URL de chat compartido copiado al portapapeles!", + "Copy": "Copiar", + "Copy last code block": "Copia el último bloque de código", + "Copy last response": "Copia la última respuesta", + "Copy Link": "Copiar enlace", + "Copying to clipboard was successful!": "¡La copia al portapapeles se ha realizado correctamente!", + "Create a model": "Crear un modelo", + "Create Account": "Crear una cuenta", + "Create new key": "Crear una nueva clave", + "Create new secret key": "Crear una nueva clave secreta", + "Created at": "Creado en", + "Created At": "Creado en", + "Created by": "Creado por", + "CSV Import": "Importa un CSV", + "Current Model": "Modelo Actual", + "Current Password": "Contraseña Actual", + "Custom": "Personalizado", + "Customize models for a specific purpose": "Personalizar modelos para un propósito específico", + "Dark": "Oscuro", + "Dashboard": "Panel de Control", + "Database": "Base de datos", + "December": "Diciembre", + "Default": "Por defecto", + "Default (Automatic1111)": "Por defecto (Automatic1111)", + "Default (SentenceTransformers)": "Por defecto (SentenceTransformers)", + "Default Model": "Modelo predeterminado", + "Default model updated": "El modelo por defecto ha sido actualizado", + "Default Prompt Suggestions": "Sugerencias de mensajes por defecto", + "Default User Role": "Rol por defecto para usuarios", + "delete": "borrar", + "Delete": "Borrar", + "Delete a model": "Borra un modelo", + "Delete All Chats": "Eliminar todos los chats", + "Delete chat": "Borrar chat", + "Delete Chat": "Borrar Chat", + "Delete chat?": "Borrar el chat?", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "Borrar este enlace", + "Delete tool?": "", + "Delete User": "Borrar Usuario", + "Deleted {{deleteModelTag}}": "Se borró {{deleteModelTag}}", + "Deleted {{name}}": "Eliminado {{nombre}}", + "Description": "Descripción", + "Didn't fully follow instructions": "No siguió las instrucciones", + "Disabled": "", + "Discover a function": "Descubre una función", + "Discover a model": "Descubrir un modelo", + "Discover a prompt": "Descubre un Prompt", + "Discover a tool": "Descubre una herramienta", + "Discover, download, and explore custom functions": "Descubre, descarga y explora funciones personalizadas", + "Discover, download, and explore custom prompts": "Descubre, descarga, y explora Prompts personalizados", + "Discover, download, and explore custom tools": "Descubre, descarga y explora herramientas personalizadas", + "Discover, download, and explore model presets": "Descubre, descarga y explora ajustes preestablecidos de modelos", + "Dismissible": "Desestimable", + "Display Emoji in Call": "Muestra Emoji en llamada", + "Display the username instead of You in the Chat": "Mostrar el nombre de usuario en lugar de Usted en el chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Documento", + "Document Settings": "Configuración del Documento", + "Documentation": "Documentación", + "Documents": "Documentos", + "does not make any external connections, and your data stays securely on your locally hosted server.": "no realiza ninguna conexión externa y sus datos permanecen seguros en su servidor alojado localmente.", + "Don't Allow": "No Permitir", + "Don't have an account?": "¿No tienes una cuenta?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "No te gusta el estilo?", + "Done": "Hecho", + "Download": "Descargar", + "Download canceled": "Descarga cancelada", + "Download Database": "Descarga la Base de Datos", + "Drop any files here to add to the conversation": "Suelta cualquier archivo aquí para agregarlo a la conversación", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "p.ej. '30s','10m'. Unidades válidas de tiempo son 's', 'm', 'h'.", + "Edit": "Editar", + "Edit Doc": "Editar Documento", + "Edit Memory": "Editar Memoria", + "Edit User": "Editar Usuario", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "Tamaño de Embedding", + "Embedding Model": "Modelo de Embedding", + "Embedding Model Engine": "Motor de Modelo de Embedding", + "Embedding model set to \"{{embedding_model}}\"": "Modelo de Embedding configurado a \"{{embedding_model}}\"", + "Enable Chat History": "Activa el Historial de Chat", + "Enable Community Sharing": "Habilitar el uso compartido de la comunidad", + "Enable New Sign Ups": "Habilitar Nuevos Registros", + "Enable Web Search": "Habilitar la búsqueda web", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Asegúrese de que su archivo CSV incluya 4 columnas en este orden: Nombre, Correo Electrónico, Contraseña, Rol.", + "Enter {{role}} message here": "Ingrese el mensaje {{role}} aquí", + "Enter a detail about yourself for your LLMs to recall": "Ingrese un detalle sobre usted para que sus LLMs recuerden", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Ingresa la clave de API de Brave Search", + "Enter Chunk Overlap": "Ingresar superposición de fragmentos", + "Enter Chunk Size": "Ingrese el tamaño del fragmento", + "Enter Github Raw URL": "Ingresa la URL sin procesar de Github", + "Enter Google PSE API Key": "Ingrese la clave API de Google PSE", + "Enter Google PSE Engine Id": "Introduzca el ID del motor PSE de Google", + "Enter Image Size (e.g. 512x512)": "Ingrese el tamaño de la imagen (p.ej. 512x512)", + "Enter language codes": "Ingrese códigos de idioma", + "Enter model tag (e.g. {{modelTag}})": "Ingrese la etiqueta del modelo (p.ej. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Ingrese el número de pasos (p.ej., 50)", + "Enter Score": "Ingrese la puntuación", + "Enter Searxng Query URL": "Introduzca la URL de consulta de Searxng", + "Enter Serper API Key": "Ingrese la clave API de Serper", + "Enter Serply API Key": "Ingrese la clave API de Serply", + "Enter Serpstack API Key": "Ingrese la clave API de Serpstack", + "Enter stop sequence": "Ingrese la secuencia de parada", + "Enter system prompt": "", + "Enter Tavily API Key": "Ingrese la clave API de Tavily", + "Enter Tika Server URL": "", + "Enter Top K": "Ingrese el Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Ingrese la URL (p.ej., http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Ingrese la URL (p.ej., http://localhost:11434)", + "Enter Your Email": "Ingrese su correo electrónico", + "Enter Your Full Name": "Ingrese su nombre completo", + "Enter your message": "", + "Enter Your Password": "Ingrese su contraseña", + "Enter Your Role": "Ingrese su rol", + "Error": "Error", + "Experimental": "Experimental", + "Export": "Exportar", + "Export All Chats (All Users)": "Exportar todos los chats (Todos los usuarios)", + "Export chat (.json)": "Exportar chat (.json)", + "Export Chats": "Exportar Chats", + "Export Documents Mapping": "Exportar el mapeo de documentos", + "Export Functions": "Exportar Funciones", + "Export LiteLLM config.yaml": "Exportar LiteLLM config.yaml", + "Export Models": "Exportar Modelos", + "Export Prompts": "Exportar Prompts", + "Export Tools": "Exportar Herramientas", + "External Models": "Modelos Externos", + "Failed to create API Key.": "No se pudo crear la clave API.", + "Failed to read clipboard contents": "No se pudo leer el contenido del portapapeles", + "Failed to update settings": "Falla al actualizar los ajustes", + "February": "Febrero", + "Feel free to add specific details": "Libre de agregar detalles específicos", + "File": "Archivo", + "File Mode": "Modo de archivo", + "File not found.": "Archivo no encontrado.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "Filtros", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Se detectó suplantación de huellas: No se pueden usar las iniciales como avatar. Por defecto se utiliza la imagen de perfil predeterminada.", + "Fluidly stream large external response chunks": "Transmita con fluidez grandes fragmentos de respuesta externa", + "Focus chat input": "Enfoca la entrada del chat", + "Followed instructions perfectly": "Siguió las instrucciones perfectamente", + "Form": "De", + "Format your variables using square brackets like this:": "Formatea tus variables usando corchetes de la siguiente manera:", + "Frequency Penalty": "Penalización de frecuencia", + "Function created successfully": "Función creada exitosamente", + "Function deleted successfully": "Función borrada exitosamente", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "Función actualizada exitosamente", + "Functions": "Funciones", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Funciones importadas exitosamente", + "General": "General", + "General Settings": "Opciones Generales", + "Generate Image": "Generar imagen", + "Generating search query": "Generación de consultas de búsqueda", + "Generation Info": "Información de Generación", + "Get up and running with": "", + "Global": "", + "Good Response": "Buena Respuesta", + "Google PSE API Key": "Clave API de Google PSE", + "Google PSE Engine Id": "ID del motor PSE de Google", + "h:mm a": "h:mm a", + "has no conversations.": "no tiene conversaciones.", + "Hello, {{name}}": "Hola, {{name}}", + "Help": "Ayuda", + "Hide": "Esconder", + "Hide Model": "Esconder Modelo", + "How can I help you today?": "¿Cómo puedo ayudarte hoy?", + "Hybrid Search": "Búsqueda Híbrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Generación de imágenes (experimental)", + "Image Generation Engine": "Motor de generación de imágenes", + "Image Settings": "Ajustes de la Imágen", + "Images": "Imágenes", + "Import Chats": "Importar chats", + "Import Documents Mapping": "Importar Mapeo de Documentos", + "Import Functions": "Importar Funciones", + "Import Models": "Importar modelos", + "Import Prompts": "Importar Prompts", + "Import Tools": "Importar Herramientas", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Incluir el indicador `--api` al ejecutar stable-diffusion-webui", + "Info": "Información", + "Input commands": "Ingresar comandos", + "Install from Github URL": "Instalar desde la URL de Github", + "Instant Auto-Send After Voice Transcription": "Auto-Enviar Después de la Transcripción de Voz", + "Interface": "Interfaz", + "Invalid Tag": "Etiqueta Inválida", + "January": "Enero", + "join our Discord for help.": "Únase a nuestro Discord para obtener ayuda.", + "JSON": "JSON", + "JSON Preview": "Vista previa de JSON", + "July": "Julio", + "June": "Junio", + "JWT Expiration": "Expiración del JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Mantener Vivo", + "Keyboard shortcuts": "Atajos de teclado", + "Knowledge": "Conocimiento", + "Language": "Lenguaje", + "large language models, locally.": "", + "Last Active": "Última Actividad", + "Last Modified": "Modificado por última vez", + "Light": "Claro", + "Listening...": "Escuchando...", + "LLMs can make mistakes. Verify important information.": "Los LLM pueden cometer errores. Verifica la información importante.", + "Local Models": "Modelos locales", + "LTR": "LTR", + "Made by OpenWebUI Community": "Hecho por la comunidad de OpenWebUI", + "Make sure to enclose them with": "Asegúrese de adjuntarlos con", + "Manage": "Gestionar", + "Manage Models": "Administrar Modelos", + "Manage Ollama Models": "Administrar Modelos Ollama", + "Manage Pipelines": "Administrar Pipelines", + "Manage Valves": "Gestionar Valves", + "March": "Marzo", + "Max Tokens (num_predict)": "Máximo de fichas (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Se pueden descargar un máximo de 3 modelos simultáneamente. Por favor, inténtelo de nuevo más tarde.", + "May": "Mayo", + "Memories accessible by LLMs will be shown here.": "Las memorias accesibles por los LLMs se mostrarán aquí.", + "Memory": "Memoria", + "Memory added successfully": "Memoria añadida correctamente", + "Memory cleared successfully": "Memoria liberada correctamente", + "Memory deleted successfully": "Memoria borrada correctamente", + "Memory updated successfully": "Memoria actualizada correctamente", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Los mensajes que envíe después de crear su enlace no se compartirán. Los usuarios con el enlace podrán ver el chat compartido.", + "Minimum Score": "Puntuación mínima", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "El modelo '{{modelName}}' se ha descargado correctamente.", + "Model '{{modelTag}}' is already in queue for downloading.": "El modelo '{{modelTag}}' ya está en cola para descargar.", + "Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado", + "Model {{modelName}} is not vision capable": "El modelo {{modelName}} no es capaz de ver", + "Model {{name}} is now {{status}}": "El modelo {{name}} ahora es {{status}}", + "Model created successfully!": "Modelo creado correctamente!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.", + "Model ID": "ID del modelo", + "Model not selected": "Modelo no seleccionado", + "Model Params": "Parámetros del modelo", + "Model updated successfully": "Modelo actualizado correctamente", + "Model Whitelisting": "Listado de Modelos habilitados", + "Model(s) Whitelisted": "Modelo(s) habilitados", + "Modelfile Content": "Contenido del Modelfile", + "Models": "Modelos", + "More": "Más", + "Name": "Nombre", + "Name Tag": "Nombre de etiqueta", + "Name your model": "Asigne un nombre a su modelo", + "New Chat": "Nuevo Chat", + "New Password": "Nueva Contraseña", + "No content to speak": "No hay contenido para hablar", + "No documents found": "No se han encontrado documentos", + "No file selected": "Ningún archivo fué seleccionado", + "No results found": "No se han encontrado resultados", + "No search query generated": "No se ha generado ninguna consulta de búsqueda", + "No source available": "No hay fuente disponible", + "No valves to update": "No valves para actualizar", + "None": "Ninguno", + "Not factually correct": "No es correcto en todos los aspectos", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Si estableces una puntuación mínima, la búsqueda sólo devolverá documentos con una puntuación mayor o igual a la puntuación mínima.", + "Notifications": "Notificaciones", + "November": "Noviembre", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth ID", + "October": "Octubre", + "Off": "Desactivado", + "Okay, Let's Go!": "Bien, ¡Vamos!", + "OLED Dark": "OLED oscuro", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API de Ollama deshabilitada", + "Ollama API is disabled": "API de Ollama desactivada", + "Ollama Version": "Versión de Ollama", + "On": "Activado", + "Only": "Solamente", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo se permiten caracteres alfanuméricos y guiones en la cadena 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.": "¡Ups! ¡Agárrate fuerte! Tus archivos todavía están en el horno de procesamiento. Los estamos cocinando a la perfección. Tenga paciencia y le avisaremos una vez que estén listos.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "¡Ups! Parece que la URL no es válida. Vuelva a verificar e inténtelo nuevamente.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "¡Oops! Hubo un error en la respuesta anterior. Intente de nuevo o póngase en contacto con el administrador.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Por favor ejecute la WebUI desde el backend.", + "Open AI (Dall-E)": "Abrir AI (Dall-E)", + "Open new chat": "Abrir nuevo chat", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API Config", + "OpenAI API Key is required.": "La Clave de la API de OpenAI es requerida.", + "OpenAI URL/Key required.": "URL/Clave de OpenAI es requerida.", + "or": "o", + "Other": "Otro", + "Password": "Contraseña", + "PDF document (.pdf)": "PDF document (.pdf)", + "PDF Extract Images (OCR)": "Extraer imágenes de PDF (OCR)", + "pending": "pendiente", + "Permission denied when accessing media devices": "Permiso denegado al acceder a los dispositivos", + "Permission denied when accessing microphone": "Permiso denegado al acceder a la micrófono", + "Permission denied when accessing microphone: {{error}}": "Permiso denegado al acceder al micrófono: {{error}}", + "Personalization": "Personalización", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "Pipeline borrada exitosamente", + "Pipeline downloaded successfully": "Pipeline descargada exitosamente", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "Pipeline No Detectada", + "Pipelines Valves": "Tuberías Válvulas", + "Plain text (.txt)": "Texto plano (.txt)", + "Playground": "Patio de juegos", + "Please carefully review the following warnings:": "", + "Positive attitude": "Actitud positiva", + "Previous 30 days": "Últimos 30 días", + "Previous 7 days": "Últimos 7 días", + "Profile Image": "Imagen de perfil", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (por ejemplo, cuéntame una cosa divertida sobre el Imperio Romano)", + "Prompt Content": "Contenido del Prompt", + "Prompt suggestions": "Sugerencias de Prompts", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Extraer \"{{searchValue}}\" de Ollama.com", + "Pull a model from Ollama.com": "Obtener un modelo de Ollama.com", + "Query Params": "Parámetros de consulta", + "RAG Template": "Plantilla de RAG", + "Read Aloud": "Leer al oído", + "Record voice": "Grabar voz", + "Redirecting you to OpenWebUI Community": "Redireccionándote a la comunidad OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referirse a usted mismo como \"Usuario\" (por ejemplo, \"El usuario está aprendiendo Español\")", + "Refused when it shouldn't have": "Rechazado cuando no debería", + "Regenerate": "Regenerar", + "Release Notes": "Notas de la versión", + "Remove": "Eliminar", + "Remove Model": "Eliminar modelo", + "Rename": "Renombrar", + "Repeat Last N": "Repetir las últimas N", + "Request Mode": "Modo de petición", + "Reranking Model": "Modelo de reranking", + "Reranking model disabled": "Modelo de reranking deshabilitado", + "Reranking model set to \"{{reranking_model}}\"": "Modelo de reranking establecido en \"{{reranking_model}}\"", + "Reset": "Reiniciar", + "Reset Upload Directory": "Reiniciar Directorio de carga", + "Reset Vector Storage": "Restablecer almacenamiento vectorial", + "Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Las notificaciones de respuesta no pueden activarse debido a que los permisos del sitio web han sido denegados. Por favor, visite las configuraciones de su navegador para otorgar el acceso necesario.", + "Role": "Rol", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Ejecutando", + "Save": "Guardar", + "Save & Create": "Guardar y Crear", + "Save & Update": "Guardar y Actualizar", + "Save Tag": "", + "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": "Ya no se admite guardar registros de chat directamente en el almacenamiento de su navegador. Tómese un momento para descargar y eliminar sus registros de chat haciendo clic en el botón a continuación. No te preocupes, puedes volver a importar fácilmente tus registros de chat al backend a través de", + "Scan": "Escanear", + "Scan complete!": "¡Escaneo completado!", + "Scan for documents from {{path}}": "Escanear en busca de documentos desde {{path}}", + "Search": "Buscar", + "Search a model": "Buscar un modelo", + "Search Chats": "Chats de búsqueda", + "Search Documents": "Buscar Documentos", + "Search Functions": "Funciones de Búsqueda", + "Search Models": "Modelos de búsqueda", + "Search Prompts": "Buscar Prompts", + "Search Query Generation Prompt": "Búsqueda de consulta de generación de prompts", + "Search Query Generation Prompt Length Threshold": "Nivel de longitud de Búsqueda de consulta de generación de prompts", + "Search Result Count": "Recuento de resultados de búsqueda", + "Search Tools": "Búsqueda de herramientas", + "Searched {{count}} sites_one": "Buscado {{count}} sites_one", + "Searched {{count}} sites_many": "Buscado {{count}} sites_many", + "Searched {{count}} sites_other": "Buscó {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "Buscando \"{{searchQuery}}\"", + "Searxng Query URL": "Searxng URL de consulta", + "See readme.md for instructions": "Vea el readme.md para instrucciones", + "See what's new": "Ver las novedades", + "Seed": "Seed", + "Select a base model": "Seleccionar un modelo base", + "Select a engine": "Busca un motor", + "Select a function": "Busca una función", + "Select a mode": "Selecciona un modo", + "Select a model": "Selecciona un modelo", + "Select a pipeline": "Selección de una Pipeline", + "Select a pipeline url": "Selección de una dirección URL de Pipeline", + "Select a tool": "Busca una herramienta", + "Select an Ollama instance": "Seleccione una instancia de Ollama", + "Select Documents": "Seleccionar Documentos", + "Select model": "Selecciona un modelo", + "Select only one model to call": "Selecciona sólo un modelo para llamar", + "Selected model(s) do not support image inputs": "Los modelos seleccionados no admiten entradas de imagen", + "Send": "Enviar", + "Send a Message": "Enviar un Mensaje", + "Send message": "Enviar Mensaje", + "September": "Septiembre", + "Serper API Key": "Clave API de Serper", + "Serply API Key": "Clave API de Serply", + "Serpstack API Key": "Clave API de Serpstack", + "Server connection verified": "Conexión del servidor verificada", + "Set as default": "Establecer por defecto", + "Set Default Model": "Establecer modelo predeterminado", + "Set embedding model (e.g. {{model}})": "Establecer modelo de embedding (ej. {{model}})", + "Set Image Size": "Establecer tamaño de imagen", + "Set reranking model (e.g. {{model}})": "Establecer modelo de reranking (ej. {{model}})", + "Set Steps": "Establecer Pasos", + "Set Task Model": "Establecer modelo de tarea", + "Set Voice": "Establecer la voz", + "Settings": "Configuración", + "Settings saved successfully!": "¡Configuración guardada con éxito!", + "Settings updated successfully": "¡Configuración actualizada con éxito!", + "Share": "Compartir", + "Share Chat": "Compartir Chat", + "Share to OpenWebUI Community": "Compartir con la comunidad OpenWebUI", + "short-summary": "resumen-corto", + "Show": "Mostrar", + "Show Admin Details in Account Pending Overlay": "Mostrar detalles de administración en la capa de espera de la cuenta", + "Show Model": "Mostrar Modelo", + "Show shortcuts": "Mostrar atajos", + "Show your support!": "¡Muestra tu apoyo!", + "Showcased creativity": "Creatividad mostrada", + "Sign in": "Iniciar sesión", + "Sign Out": "Cerrar sesión", + "Sign up": "Crear una cuenta", + "Signing in": "Iniciando sesión", + "Source": "Fuente", + "Speech recognition error: {{error}}": "Error de reconocimiento de voz: {{error}}", + "Speech-to-Text Engine": "Motor de voz a texto", + "Stop Sequence": "Detener secuencia", + "STT Model": "Modelo STT", + "STT Settings": "Configuraciones de STT", + "Submit": "Enviar", + "Subtitle (e.g. about the Roman Empire)": "Subtítulo (por ejemplo, sobre el Imperio Romano)", + "Success": "Éxito", + "Successfully updated.": "Actualizado exitosamente.", + "Suggested": "Sugerido", + "Support": "", + "Support this plugin:": "", + "System": "Sistema", + "System Prompt": "Prompt del sistema", + "Tags": "Etiquetas", + "Tap to interrupt": "Toca para interrumpir", + "Tavily API Key": "Clave API de Tavily", + "Tell us more:": "Dinos más:", + "Temperature": "Temperatura", + "Template": "Plantilla", + "Text Completion": "Finalización de texto", + "Text-to-Speech Engine": "Motor de texto a voz", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "¡Gracias por tu retroalimentación!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "El puntaje debe ser un valor entre 0.0 (0%) y 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "Pensando...", + "This action cannot be undone. Do you wish to continue?": "Esta acción no se puede deshacer. ¿Desea continuar?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Esto garantiza que sus valiosas conversaciones se guarden de forma segura en su base de datos en el backend. ¡Gracias!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Esta es una característica experimental que puede no funcionar como se esperaba y está sujeto a cambios en cualquier momento.", + "This setting does not sync across browsers or devices.": "Esta configuración no se sincroniza entre navegadores o dispositivos.", + "This will delete": "Esto eliminará", + "Thorough explanation": "Explicación exhaustiva", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Consejo: Actualice múltiples variables consecutivamente presionando la tecla tab en la entrada del chat después de cada reemplazo.", + "Title": "Título", + "Title (e.g. Tell me a fun fact)": "Título (por ejemplo, cuéntame una curiosidad)", + "Title Auto-Generation": "Generación automática de títulos", + "Title cannot be an empty string.": "El título no puede ser una cadena vacía.", + "Title Generation Prompt": "Prompt de generación de título", + "to": "para", + "To access the available model names for downloading,": "Para acceder a los nombres de modelos disponibles para descargar,", + "To access the GGUF models available for downloading,": "Para acceder a los modelos GGUF disponibles para descargar,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acceder al interfaz de usuario web, por favor contacte al administrador. Los administradores pueden administrar los estados de los usuarios desde el panel de administración.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Para agregar documentos aquí, subalos al área de trabajo \"Documentos\" primero.", + "to chat input.": "a la entrada del chat.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Para seleccionar filtros aquí, agreguelos al área de trabajo \"Funciones\" primero.", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Para seleccionar herramientas aquí, agreguelas al área de trabajo \"Herramientas\" primero.", + "Today": "Hoy", + "Toggle settings": "Alternar configuración", + "Toggle sidebar": "Alternar barra lateral", + "Tokens To Keep On Context Refresh (num_keep)": "Tokens a mantener en el contexto de actualización (num_keep)", + "Tool created successfully": "Herramienta creada con éxito", + "Tool deleted successfully": "Herramienta eliminada con éxito", + "Tool imported successfully": "Herramienta importada con éxito", + "Tool updated successfully": "Herramienta actualizada con éxito", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Herramientas", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "¿Problemas para acceder a Ollama?", + "TTS Model": "Modelo TTS", + "TTS Settings": "Configuración de TTS", + "TTS Voice": "Voz del TTS", + "Type": "Tipo", + "Type Hugging Face Resolve (Download) URL": "Escriba la URL (Descarga) de Hugging Face Resolve", + "Uh-oh! There was an issue connecting to {{provider}}.": "¡Uh oh! Hubo un problema al conectarse a {{provider}}.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Tipo de archivo desconocido '{{file_type}}'. Procediendo con la carga del archivo de todos modos.", + "Unpin": "", + "Update": "Actualizar", + "Update and Copy Link": "Actualizar y copiar enlace", + "Update password": "Actualizar contraseña", + "Updated at": "Actualizado en", + "Upload": "Subir", + "Upload a GGUF model": "Subir un modelo GGUF", + "Upload Files": "Subir archivos", + "Upload Pipeline": "Subir Pipeline", + "Upload Progress": "Progreso de carga", + "URL Mode": "Modo de URL", + "Use '#' in the prompt input to load and select your documents.": "Utilice '#' en el prompt para cargar y seleccionar sus documentos.", + "Use Gravatar": "Usar Gravatar", + "Use Initials": "Usar Iniciales", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "usuario", + "User location successfully retrieved.": "Localización del usuario recuperada con éxito.", + "User Permissions": "Permisos de usuario", + "Users": "Usuarios", + "Utilize": "Utilizar", + "Valid time units:": "Unidades válidas de tiempo:", + "Valves": "Valves", + "Valves updated": "Valves actualizados", + "Valves updated successfully": "Valves actualizados con éxito", + "variable": "variable", + "variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.", + "Version": "Versión", + "Voice": "Voz", + "Warning": "Advertencia", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Advertencia: Si actualiza o cambia su modelo de inserción, necesitará volver a importar todos los documentos.", + "Web": "Web", + "Web API": "API Web", + "Web Loader Settings": "Web Loader Settings", + "Web Params": "Web Params", + "Web Search": "Búsqueda en la Web", + "Web Search Engine": "Motor de búsqueda web", + "Webhook URL": "Webhook URL", + "WebUI Settings": "Configuración del WebUI", + "WebUI will make requests to": "WebUI realizará solicitudes a", + "What’s New in": "Novedades en", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Cuando el historial está desactivado, los nuevos chats en este navegador no aparecerán en el historial de ninguno de sus dispositivos..", + "Whisper (Local)": "Whisper (Local)", + "Widescreen Mode": "Modo de pantalla ancha", + "Workspace": "Espacio de trabajo", + "Write a prompt suggestion (e.g. Who are you?)": "Escribe una sugerencia para un prompt (por ejemplo, ¿quién eres?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Escribe un resumen en 50 palabras que resuma [tema o palabra clave].", + "Yesterday": "Ayer", + "You": "Usted", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Puede personalizar sus interacciones con LLMs añadiendo memorias a través del botón 'Gestionar' debajo, haciendo que sean más útiles y personalizados para usted.", + "You cannot clone a base model": "No se puede clonar un modelo base", + "You have no archived conversations.": "No tiene conversaciones archivadas.", + "You have shared this chat": "Usted ha compartido esta conversación", + "You're a helpful assistant.": "Usted es un asistente útil.", + "You're now logged in.": "Usted ahora está conectado.", + "Your account status is currently pending activation.": "El estado de su cuenta actualmente se encuentra pendiente de activación.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Configuración del cargador de Youtube" +} diff --git a/src/lib/i18n/locales/fa-IR/translation.json b/src/lib/i18n/locales/fa-IR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..445960366d0de666befd07e6810af9d1e21b4f5b --- /dev/null +++ b/src/lib/i18n/locales/fa-IR/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' یا '-1' برای غیر فعال کردن انقضا.", + "(Beta)": "(بتا)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", + "(latest)": "(آخرین)", + "{{ models }}": "{{ مدل }}", + "{{ owner }}: You cannot delete a base model": "{{ مالک }}: شما نمیتوانید یک مدل پایه را حذف کنید", + "{{modelName}} is thinking...": "{{modelName}} در حال فکر کردن است...", + "{{user}}'s Chats": "{{user}} چت ها", + "{{webUIName}} Backend Required": "بکند {{webUIName}} نیاز است.", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "یک مدل وظیفه هنگام انجام وظایف مانند تولید عناوین برای چت ها و نمایش های جستجوی وب استفاده می شود.", + "a user": "یک کاربر", + "About": "درباره", + "Account": "حساب کاربری", + "Account Activation Pending": "", + "Accurate information": "اطلاعات دقیق", + "Actions": "", + "Active Users": "", + "Add": "اضافه کردن", + "Add a model id": "افزودن شناسه مدل", + "Add a short description about what this model does": "اضافه کردن توضیحات کوتاه در مورد انچه که این مدل انجام می دهد", + "Add a short title for this prompt": "یک عنوان کوتاه برای این درخواست اضافه کنید", + "Add a tag": "اضافه کردن یک تگ", + "Add custom prompt": "اضافه کردن یک درخواست سفارشی", + "Add Docs": "اضافه کردن اسناد", + "Add Files": "اضافه کردن فایل\u200cها", + "Add Memory": "اضافه کردن یادگیری", + "Add message": "اضافه کردن پیغام", + "Add Model": "اضافه کردن مدل", + "Add Tag": "", + "Add Tags": "اضافه کردن تگ\u200cها", + "Add User": "اضافه کردن کاربر", + "Adjusting these settings will apply changes universally to all users.": "با تنظیم این تنظیمات، تغییرات به طور کلی برای همه کاربران اعمال می شود.", + "admin": "مدیر", + "Admin": "", + "Admin Panel": "پنل مدیریت", + "Admin Settings": "تنظیمات مدیریت", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "پارامترهای پیشرفته", + "Advanced Params": "پارام های پیشرفته", + "all": "همه", + "All Documents": "تمام سند ها", + "All Users": "همه کاربران", + "Allow": "اجازه دادن", + "Allow Chat Deletion": "اجازه حذف گپ", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "حروف الفبایی و خط فاصله", + "Already have an account?": "از قبل حساب کاربری دارید؟", + "an assistant": "یک دستیار", + "and": "و", + "and create a new shared link.": "و یک لینک به اشتراک گذاری جدید ایجاد کنید.", + "API Base URL": "API Base URL", + "API Key": "API Key", + "API Key created.": "API Key created.", + "API keys": "API keys", + "April": "ژوئن", + "Archive": "آرشیو", + "Archive All Chats": "بایگانی همه گفتگوها", + "Archived Chats": "آرشیو تاریخچه چت", + "are allowed - Activate this command by typing": "مجاز هستند - این دستور را با تایپ کردن این فعال کنید:", + "Are you sure?": "آیا مطمئن هستید؟", + "Attach file": "پیوست فایل", + "Attention to detail": "دقیق", + "Audio": "صدا", + "Audio settings updated successfully": "", + "August": "آگوست", + "Auto-playback response": "پخش خودکار پاسخ ", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "پایه URL AUTOMATIC1111 ", + "AUTOMATIC1111 Base URL is required.": "به URL پایه AUTOMATIC1111 مورد نیاز است.", + "available!": "در دسترس!", + "Back": "بازگشت", + "Bad Response": "پاسخ خوب نیست", + "Banners": "بنر", + "Base Model (From)": "مدل پایه (از)", + "Batch Size (num_batch)": "", + "before": "قبل", + "Being lazy": "حالت سازنده", + "Brave Search API Key": "کلید API جستجوی شجاع", + "Bypass SSL verification for Websites": "عبور از تأیید SSL برای وب سایت ها", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "لغو", + "Capabilities": "قابلیت", + "Change Password": "تغییر رمز عبور", + "Chat": "گپ", + "Chat Background Image": "", + "Chat Bubble UI": "UI\u200cی\u200c گفتگو\u200c", + "Chat Controls": "", + "Chat direction": "جهت\u200cگفتگو", + "Chat History": "تاریخچه\u200cی گفتگو", + "Chat History is off for this browser.": "سابقه گپ برای این مرورگر خاموش است.", + "Chats": "گپ\u200cها", + "Check Again": "چک مجدد", + "Check for updates": "بررسی به\u200cروزرسانی", + "Checking for updates...": "در حال بررسی برای به\u200cروزرسانی..", + "Choose a model before saving...": "قبل از ذخیره یک مدل را انتخاب کنید...", + "Chunk Overlap": "همپوشانی تکه", + "Chunk Params": "پارامترهای تکه", + "Chunk Size": "اندازه تکه", + "Citation": "استناد", + "Clear memory": "", + "Click here for help.": "برای کمک اینجا را کلیک کنید.", + "Click here to": "برای کمک اینجا را کلیک کنید.", + "Click here to download user import template file.": "", + "Click here to select": "برای انتخاب اینجا کلیک کنید", + "Click here to select a csv file.": "برای انتخاب یک فایل csv اینجا را کلیک کنید.", + "Click here to select a py file.": "", + "Click here to select documents.": "برای انتخاب اسناد اینجا را کلیک کنید.", + "click here.": "اینجا کلیک کنید.", + "Click on the user role button to change a user's role.": "برای تغییر نقش کاربر، روی دکمه نقش کاربر کلیک کنید.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "کلون", + "Close": "بسته", + "Code formatted successfully": "", + "Collection": "مجموعه", + "ComfyUI": "کومیوآی", + "ComfyUI Base URL": "URL پایه کومیوآی", + "ComfyUI Base URL is required.": "URL پایه کومیوآی الزامی است.", + "Command": "دستور", + "Concurrent Requests": "درخواست های همزمان", + "Confirm": "", + "Confirm Password": "تایید رمز عبور", + "Confirm your action": "", + "Connections": "ارتباطات", + "Contact Admin for WebUI Access": "", + "Content": "محتوا", + "Content Extraction": "", + "Context Length": "طول زمینه", + "Continue Response": "ادامه پاسخ", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL چت به کلیپ بورد کپی شد!", + "Copy": "کپی", + "Copy last code block": "کپی آخرین بلوک کد", + "Copy last response": "کپی آخرین پاسخ", + "Copy Link": "کپی لینک", + "Copying to clipboard was successful!": "کپی کردن در کلیپ بورد با موفقیت انجام شد!", + "Create a model": "ایجاد یک مدل", + "Create Account": "ساخت حساب کاربری", + "Create new key": "ساخت کلید جدید", + "Create new secret key": "ساخت کلید gehez جدید", + "Created at": "ایجاد شده در", + "Created At": "ایجاد شده در", + "Created by": "", + "CSV Import": "", + "Current Model": "مدل فعلی", + "Current Password": "رمز عبور فعلی", + "Custom": "دلخواه", + "Customize models for a specific purpose": "سفارشی کردن مدل ها برای یک هدف خاص", + "Dark": "تیره", + "Dashboard": "", + "Database": "پایگاه داده", + "December": "دسامبر", + "Default": "پیشفرض", + "Default (Automatic1111)": "پیشفرض (Automatic1111)", + "Default (SentenceTransformers)": "پیشفرض (SentenceTransformers)", + "Default Model": "مدل پیشفرض", + "Default model updated": "مدل پیشفرض به\u200cروزرسانی شد", + "Default Prompt Suggestions": "پیشنهادات پرامپت پیش فرض", + "Default User Role": "نقش کاربر پیش فرض", + "delete": "حذف", + "Delete": "حذف", + "Delete a model": "حذف یک مدل", + "Delete All Chats": "حذف همه گفتگوها", + "Delete chat": "حذف گپ", + "Delete Chat": "حذف گپ", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "حذف این لینک", + "Delete tool?": "", + "Delete User": "حذف کاربر", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} پاک شد", + "Deleted {{name}}": "حذف شده {{name}}", + "Description": "توضیحات", + "Didn't fully follow instructions": "نمی تواند دستورالعمل را کامل پیگیری کند", + "Disabled": "", + "Discover a function": "", + "Discover a model": "کشف یک مدل", + "Discover a prompt": "یک اعلان را کشف کنید", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "پرامپت\u200cهای سفارشی را کشف، دانلود و کاوش کنید", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "پیش تنظیمات مدل را کشف، دانلود و کاوش کنید", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "نمایش نام کاربری به جای «شما» در چت", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "سند", + "Document Settings": "تنظیمات سند", + "Documentation": "", + "Documents": "اسناد", + "does not make any external connections, and your data stays securely on your locally hosted server.": "هیچ اتصال خارجی ایجاد نمی کند و داده های شما به طور ایمن در سرور میزبان محلی شما باقی می ماند.", + "Don't Allow": "اجازه نده", + "Don't have an account?": "حساب کاربری ندارید؟", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "نظری ندارید؟", + "Done": "", + "Download": "دانلود کن", + "Download canceled": "دانلود لغو شد", + "Download Database": "دانلود پایگاه داده", + "Drop any files here to add to the conversation": "هر فایلی را اینجا رها کنید تا به مکالمه اضافه شود", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "به طور مثال '30s','10m'. واحد\u200cهای زمانی معتبر 's', 'm', 'h' هستند.", + "Edit": "ویرایش", + "Edit Doc": "ویرایش سند", + "Edit Memory": "", + "Edit User": "ویرایش کاربر", + "ElevenLabs": "", + "Email": "ایمیل", + "Embedding Batch Size": "", + "Embedding Model": "مدل پیدائش", + "Embedding Model Engine": "محرک مدل پیدائش", + "Embedding model set to \"{{embedding_model}}\"": "مدل پیدائش را به \"{{embedding_model}}\" تنظیم کنید", + "Enable Chat History": "تاریخچه چت را فعال کنید", + "Enable Community Sharing": "فعالسازی اشتراک انجمن", + "Enable New Sign Ups": "فعال کردن ثبت نام\u200cهای جدید", + "Enable Web Search": "فعالسازی جستجوی وب", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "اطمینان حاصل کنید که فایل CSV شما شامل چهار ستون در این ترتیب است: نام، ایمیل، رمز عبور، نقش.", + "Enter {{role}} message here": "پیام {{role}} را اینجا وارد کنید", + "Enter a detail about yourself for your LLMs to recall": "برای ذخیره سازی اطلاعات خود، یک توضیح کوتاه درباره خود را وارد کنید", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "کلید API جستجوی شجاع را وارد کنید", + "Enter Chunk Overlap": "مقدار Chunk Overlap را وارد کنید", + "Enter Chunk Size": "مقدار Chunk Size را وارد کنید", + "Enter Github Raw URL": "ادرس Github Raw را وارد کنید", + "Enter Google PSE API Key": "کلید API گوگل PSE را وارد کنید", + "Enter Google PSE Engine Id": "شناسه موتور PSE گوگل را وارد کنید", + "Enter Image Size (e.g. 512x512)": "اندازه تصویر را وارد کنید (مثال: 512x512)", + "Enter language codes": "کد زبان را وارد کنید", + "Enter model tag (e.g. {{modelTag}})": "تگ مدل را وارد کنید (مثلا {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "تعداد گام ها را وارد کنید (مثال: 50)", + "Enter Score": "امتیاز را وارد کنید", + "Enter Searxng Query URL": "نشانی وب پرسوجوی Searxng را وارد کنید", + "Enter Serper API Key": "کلید API Serper را وارد کنید", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "کلید API Serpstack را وارد کنید", + "Enter stop sequence": "توالی توقف را وارد کنید", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "مقدار Top K را وارد کنید", + "Enter URL (e.g. http://127.0.0.1:7860/)": "مقدار URL را وارد کنید (مثال http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "مقدار URL را وارد کنید (مثال http://localhost:11434)", + "Enter Your Email": "ایمیل خود را وارد کنید", + "Enter Your Full Name": "نام کامل خود را وارد کنید", + "Enter your message": "", + "Enter Your Password": "رمز عبور خود را وارد کنید", + "Enter Your Role": "نقش خود را وارد کنید", + "Error": "خطا", + "Experimental": "آزمایشی", + "Export": "صادرات", + "Export All Chats (All Users)": "اکسپورت از همه گپ\u200cها(همه کاربران)", + "Export chat (.json)": "", + "Export Chats": "اکسپورت از گپ\u200cها", + "Export Documents Mapping": "اکسپورت از نگاشت اسناد", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "مدل های صادرات", + "Export Prompts": "اکسپورت از پرامپت\u200cها", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "ایجاد کلید API با خطا مواجه شد.", + "Failed to read clipboard contents": "خواندن محتوای کلیپ بورد ناموفق بود", + "Failed to update settings": "", + "February": "فوری", + "Feel free to add specific details": "اگر به دلخواه، معلومات خاصی اضافه کنید", + "File": "", + "File Mode": "حالت فایل", + "File not found.": "فایل یافت نشد.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "فانگ سرفیس شناسایی شد: نمی توان از نمایه شما به عنوان آواتار استفاده کرد. پیش فرض به عکس پروفایل پیش فرض برگشت داده شد.", + "Fluidly stream large external response chunks": "تکه های پاسخ خارجی بزرگ را به صورت سیال پخش کنید", + "Focus chat input": "فوکوس کردن ورودی گپ", + "Followed instructions perfectly": "دستورالعمل ها را کاملا دنبال کرد", + "Form": "", + "Format your variables using square brackets like this:": "متغیرهای خود را با استفاده از براکت مربع به شکل زیر قالب بندی کنید:", + "Frequency Penalty": "مجازات فرکانس", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "عمومی", + "General Settings": "تنظیمات عمومی", + "Generate Image": "", + "Generating search query": "در حال تولید پرسوجوی جستجو", + "Generation Info": "اطلاعات تولید", + "Get up and running with": "", + "Global": "", + "Good Response": "پاسخ خوب", + "Google PSE API Key": "گوگل PSE API کلید", + "Google PSE Engine Id": "شناسه موتور PSE گوگل", + "h:mm a": "h:mm a", + "has no conversations.": "ندارد.", + "Hello, {{name}}": "سلام، {{name}}", + "Help": "کمک", + "Hide": "پنهان", + "Hide Model": "", + "How can I help you today?": "امروز چطور می توانم کمک تان کنم؟", + "Hybrid Search": "جستجوی همزمان", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "تولید تصویر (آزمایشی)", + "Image Generation Engine": "موتور تولید تصویر", + "Image Settings": "تنظیمات تصویر", + "Images": "تصاویر", + "Import Chats": "ایمپورت گپ\u200cها", + "Import Documents Mapping": "ایمپورت نگاشت اسناد", + "Import Functions": "", + "Import Models": "واردات مدلها", + "Import Prompts": "ایمپورت پرامپت\u200cها", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "فلگ `--api` را هنکام اجرای stable-diffusion-webui استفاده کنید.", + "Info": "اطلاعات", + "Input commands": "ورودی دستورات", + "Install from Github URL": "نصب از ادرس Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "رابط", + "Invalid Tag": "تگ نامعتبر", + "January": "ژانویه", + "join our Discord for help.": "برای کمک به دیسکورد ما بپیوندید.", + "JSON": "JSON", + "JSON Preview": "پیش نمایش JSON", + "July": "ژوئن", + "June": "جولای", + "JWT Expiration": "JWT انقضای", + "JWT Token": "JWT توکن", + "Keep Alive": "Keep Alive", + "Keyboard shortcuts": "میانبرهای صفحه کلید", + "Knowledge": "", + "Language": "زبان", + "large language models, locally.": "", + "Last Active": "آخرین فعال", + "Last Modified": "", + "Light": "روشن", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "مدل\u200cهای زبانی بزرگ می\u200cتوانند اشتباه کنند. اطلاعات مهم را راستی\u200cآزمایی کنید.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "ساخته شده توسط OpenWebUI Community", + "Make sure to enclose them with": "مطمئن شوید که آنها را با این محصور کنید:", + "Manage": "", + "Manage Models": "مدیریت مدل\u200cها", + "Manage Ollama Models": "مدیریت مدل\u200cهای اولاما", + "Manage Pipelines": "مدیریت خطوط لوله", + "Manage Valves": "", + "March": "مارچ", + "Max Tokens (num_predict)": "توکنهای بیشینه (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "حداکثر 3 مدل را می توان به طور همزمان دانلود کرد. لطفاً بعداً دوباره امتحان کنید.", + "May": "ماهی", + "Memories accessible by LLMs will be shown here.": "حافظه های دسترسی به LLMs در اینجا نمایش داده می شوند.", + "Memory": "حافظه", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "پیام های شما بعد از ایجاد لینک شما به اشتراک نمی گردد. کاربران با لینک URL می توانند چت اشتراک را مشاهده کنند.", + "Minimum Score": "نماد کمینه", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "مدل '{{modelName}}' با موفقیت دانلود شد.", + "Model '{{modelTag}}' is already in queue for downloading.": "مدل '{{modelTag}}' در حال حاضر در صف برای دانلود است.", + "Model {{modelId}} not found": "مدل {{modelId}} یافت نشد", + "Model {{modelName}} is not vision capable": "مدل {{modelName}} قادر به بینایی نیست", + "Model {{name}} is now {{status}}": "مدل {{name}} در حال حاضر {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "مسیر فایل سیستم مدل یافت شد. برای بروزرسانی نیاز است نام کوتاه مدل وجود داشته باشد.", + "Model ID": "شناسه مدل", + "Model not selected": "مدل انتخاب نشده", + "Model Params": "مدل پارامز", + "Model updated successfully": "", + "Model Whitelisting": "لیست سفید مدل", + "Model(s) Whitelisted": "مدل در لیست سفید ثبت شد", + "Modelfile Content": "محتویات فایل مدل", + "Models": "مدل\u200cها", + "More": "بیشتر", + "Name": "نام", + "Name Tag": "نام تگ", + "Name your model": "نام مدل خود را", + "New Chat": "گپ جدید", + "New Password": "رمز عبور جدید", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "نتیجه\u200cای یافت نشد", + "No search query generated": "پرسوجوی جستجویی ایجاد نشده است", + "No source available": "منبعی در دسترس نیست", + "No valves to update": "", + "None": "هیچ کدام", + "Not factually correct": "اشتباهی فکری نیست", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "توجه: اگر حداقل نمره را تعیین کنید، جستجو تنها اسنادی را با نمره بیشتر یا برابر با حداقل نمره باز می گرداند.", + "Notifications": "اعلان", + "November": "نوامبر", + "num_thread (Ollama)": "num_thread (اولاما)", + "OAuth ID": "", + "October": "اکتبر", + "Off": "خاموش", + "Okay, Let's Go!": "باشه، بزن بریم!", + "OLED Dark": "OLED تیره", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API Ollama غیرفعال شد", + "Ollama API is disabled": "", + "Ollama Version": "نسخه اولاما", + "On": "روشن", + "Only": "فقط", + "Only alphanumeric characters and hyphens are allowed in the command string.": "فقط کاراکترهای الفبایی و خط فاصله در رشته فرمان مجاز هستند.", + "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.": "اوه! فایل های شما هنوز در فر پردازش هستند. ما آنها را کامل می پزیم. لطفا صبور باشید، به محض آماده شدن به شما اطلاع خواهیم داد.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "اوه! به نظر می رسد URL نامعتبر است. لطفاً دوباره بررسی کنید و دوباره امتحان کنید.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "اوه! شما از یک روش پشتیبانی نشده (فقط frontend) استفاده می کنید. لطفاً WebUI را از بکند اجرا کنید.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "باز کردن گپ جدید", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API Config", + "OpenAI API Key is required.": "مقدار کلید OpenAI API مورد نیاز است.", + "OpenAI URL/Key required.": "URL/Key OpenAI مورد نیاز است.", + "or": "روشن", + "Other": "دیگر", + "Password": "رمز عبور", + "PDF document (.pdf)": "PDF سند (.pdf)", + "PDF Extract Images (OCR)": "استخراج تصاویر از PDF (OCR)", + "pending": "در انتظار", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "هنگام دسترسی به میکروفون، اجازه داده نشد: {{error}}", + "Personalization": "شخصی سازی", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "خط لوله", + "Pipelines Not Detected": "", + "Pipelines Valves": "شیرالات خطوط لوله", + "Plain text (.txt)": "متن ساده (.txt)", + "Playground": "زمین بازی", + "Please carefully review the following warnings:": "", + "Positive attitude": "نظرات مثبت", + "Previous 30 days": "30 روز قبل", + "Previous 7 days": "7 روز قبل", + "Profile Image": "تصویر پروفایل", + "Prompt": "پیشنهاد", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "پیشنهاد (برای مثال: به من بگوید چیزی که برای من یک کاربرد داره درباره ایران)", + "Prompt Content": "محتویات پرامپت", + "Prompt suggestions": "پیشنهادات پرامپت", + "Prompts": "پرامپت\u200cها", + "Pull \"{{searchValue}}\" from Ollama.com": "بازگرداندن \"{{searchValue}}\" از Ollama.com", + "Pull a model from Ollama.com": "دریافت یک مدل از Ollama.com", + "Query Params": "پارامترهای پرس و جو", + "RAG Template": "RAG الگوی", + "Read Aloud": "خواندن به صورت صوتی", + "Record voice": "ضبط صدا", + "Redirecting you to OpenWebUI Community": "در حال هدایت به OpenWebUI Community", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "رد شده زمانی که باید نباشد", + "Regenerate": "ری\u200cسازی", + "Release Notes": "یادداشت\u200cهای انتشار", + "Remove": "حذف", + "Remove Model": "حذف مدل", + "Rename": "تغییر نام", + "Repeat Last N": "Repeat Last N", + "Request Mode": "حالت درخواست", + "Reranking Model": "مدل ری\u200cشناسی مجدد غیرفعال است", + "Reranking model disabled": "مدل ری\u200cشناسی مجدد غیرفعال است", + "Reranking model set to \"{{reranking_model}}\"": "مدل ری\u200cشناسی مجدد به \"{{reranking_model}}\" تنظیم شده است", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "بازنشانی ذخیره سازی برداری", + "Response AutoCopy to Clipboard": "کپی خودکار پاسخ به کلیپ بورد", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "نقش", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "ذخیره", + "Save & Create": "ذخیره و ایجاد", + "Save & Update": "ذخیره و به\u200cروزرسانی", + "Save Tag": "", + "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": "ذخیره گزارش\u200cهای چت مستقیماً در حافظه مرورگر شما دیگر پشتیبانی نمی\u200cشود. لطفاً با کلیک بر روی دکمه زیر، چند لحظه برای دانلود و حذف گزارش های چت خود وقت بگذارید. نگران نباشید، شما به راحتی می توانید گزارش های چت خود را از طریق بکند دوباره وارد کنید", + "Scan": "اسکن", + "Scan complete!": "اسکن کامل شد!", + "Scan for documents from {{path}}": "اسکن اسناد از {{path}}", + "Search": "جستجو", + "Search a model": "جستجوی مدل", + "Search Chats": "جستجو گپ ها", + "Search Documents": "جستجوی اسناد", + "Search Functions": "", + "Search Models": "مدل های جستجو", + "Search Prompts": "جستجوی پرامپت\u200cها", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "تعداد نتایج جستجو", + "Search Tools": "", + "Searched {{count}} sites_one": "جستجو {{count}} sites_one", + "Searched {{count}} sites_other": "جستجو {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "نشانی وب جستجوی Searxng", + "See readme.md for instructions": "برای مشاهده دستورالعمل\u200cها به readme.md مراجعه کنید", + "See what's new": "ببینید موارد جدید چه بوده", + "Seed": "Seed", + "Select a base model": "انتخاب یک مدل پایه", + "Select a engine": "", + "Select a function": "", + "Select a mode": "یک حالت انتخاب کنید", + "Select a model": "انتخاب یک مدل", + "Select a pipeline": "انتخاب یک خط لوله", + "Select a pipeline url": "یک ادرس خط لوله را انتخاب کنید", + "Select a tool": "", + "Select an Ollama instance": "انتخاب یک نمونه از اولاما", + "Select Documents": "", + "Select model": "انتخاب یک مدل", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "مدل) های (انتخاب شده ورودیهای تصویر را پشتیبانی نمیکند", + "Send": "ارسال", + "Send a Message": "ارسال یک پیام", + "Send message": "ارسال پیام", + "September": "سپتامبر", + "Serper API Key": "کلید API Serper", + "Serply API Key": "", + "Serpstack API Key": "کلید API Serpstack", + "Server connection verified": "اتصال سرور تأیید شد", + "Set as default": "تنظیم به عنوان پیشفرض", + "Set Default Model": "تنظیم مدل پیش فرض", + "Set embedding model (e.g. {{model}})": "تنظیم مدل پیچشی (برای مثال {{model}})", + "Set Image Size": "تنظیم اندازه تصویر", + "Set reranking model (e.g. {{model}})": "تنظیم مدل ری\u200cراینگ (برای مثال {{model}})", + "Set Steps": "تنظیم گام\u200cها", + "Set Task Model": "تنظیم مدل تکلیف", + "Set Voice": "تنظیم صدا", + "Settings": "تنظیمات", + "Settings saved successfully!": "تنظیمات با موفقیت ذخیره شد!", + "Settings updated successfully": "", + "Share": "اشتراک\u200cگذاری", + "Share Chat": "اشتراک\u200cگذاری چت", + "Share to OpenWebUI Community": "اشتراک گذاری با OpenWebUI Community", + "short-summary": "خلاصه کوتاه", + "Show": "نمایش", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "نمایش میانبرها", + "Show your support!": "", + "Showcased creativity": "ایده\u200cآفرینی", + "Sign in": "ورود", + "Sign Out": "خروج", + "Sign up": "ثبت نام", + "Signing in": "ورود", + "Source": "منبع", + "Speech recognition error: {{error}}": "خطای تشخیص گفتار: {{error}}", + "Speech-to-Text Engine": "موتور گفتار به متن", + "Stop Sequence": "توالی توقف", + "STT Model": "", + "STT Settings": "STT تنظیمات", + "Submit": "ارسال", + "Subtitle (e.g. about the Roman Empire)": "زیرنویس (برای مثال: درباره رمانی)", + "Success": "موفقیت", + "Successfully updated.": "با موفقیت به روز شد", + "Suggested": "پیشنهادی", + "Support": "", + "Support this plugin:": "", + "System": "سیستم", + "System Prompt": "پرامپت سیستم", + "Tags": "تگ\u200cها", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "بیشتر بگویید:", + "Temperature": "دما", + "Template": "الگو", + "Text Completion": "تکمیل متن", + "Text-to-Speech Engine": "موتور تبدیل متن به گفتار", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "با تشکر از بازخورد شما!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "امتیاز باید یک مقدار بین 0.0 (0%) و 1.0 (100%) باشد.", + "Theme": "قالب", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "این تضمین می کند که مکالمات ارزشمند شما به طور ایمن در پایگاه داده بکند ذخیره می شود. تشکر!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "این تنظیم در مرورگرها یا دستگاه\u200cها همگام\u200cسازی نمی\u200cشود.", + "This will delete": "", + "Thorough explanation": "توضیح کامل", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "با فشردن کلید Tab در ورودی چت پس از هر بار تعویض، چندین متغیر را به صورت متوالی به روزرسانی کنید.", + "Title": "عنوان", + "Title (e.g. Tell me a fun fact)": "عنوان (برای مثال: به من بگوید چیزی که دوست دارید)", + "Title Auto-Generation": "تولید خودکار عنوان", + "Title cannot be an empty string.": "عنوان نمی تواند یک رشته خالی باشد.", + "Title Generation Prompt": "پرامپت تولید عنوان", + "to": "به", + "To access the available model names for downloading,": "برای دسترسی به نام مدل های موجود برای دانلود،", + "To access the GGUF models available for downloading,": "برای دسترسی به مدل\u200cهای GGUF موجود برای دانلود،", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "در ورودی گپ.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "امروز", + "Toggle settings": "نمایش/عدم نمایش تنظیمات", + "Toggle sidebar": "نمایش/عدم نمایش نوار کناری", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "در دسترسی به اولاما مشکل دارید؟", + "TTS Model": "", + "TTS Settings": "تنظیمات TTS", + "TTS Voice": "", + "Type": "نوع", + "Type Hugging Face Resolve (Download) URL": "مقدار URL دانلود (Resolve) Hugging Face را وارد کنید", + "Uh-oh! There was an issue connecting to {{provider}}.": "اوه اوه! مشکلی در اتصال به {{provider}} وجود داشت.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "به روزرسانی و کپی لینک", + "Update password": "به روزرسانی رمزعبور", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "آپلود یک مدل GGUF", + "Upload Files": "بارگذاری پروندهها", + "Upload Pipeline": "", + "Upload Progress": "پیشرفت آپلود", + "URL Mode": "حالت URL", + "Use '#' in the prompt input to load and select your documents.": "در پرامپت از '#' برای لود و انتخاب اسناد خود استفاده کنید.", + "Use Gravatar": "استفاده از گراواتار", + "Use Initials": "استفاده از آبزوده", + "use_mlock (Ollama)": "use_mlock (اولاما)", + "use_mmap (Ollama)": "use_mmap (اولاما)", + "user": "کاربر", + "User location successfully retrieved.": "", + "User Permissions": "مجوزهای کاربر", + "Users": "کاربران", + "Utilize": "استفاده کنید", + "Valid time units:": "واحدهای زمانی معتبر:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "متغیر", + "variable to have them replaced with clipboard content.": "متغیر برای جایگزینی آنها با محتوای کلیپ بورد.", + "Version": "نسخه", + "Voice": "", + "Warning": "هشدار", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "هشدار: اگر شما به روز کنید یا تغییر دهید مدل شما، باید تمام سند ها را مجددا وارد کنید.", + "Web": "وب", + "Web API": "", + "Web Loader Settings": "تنظیمات لودر وب", + "Web Params": "پارامترهای وب", + "Web Search": "جستجوی وب", + "Web Search Engine": "موتور جستجوی وب", + "Webhook URL": "URL وبهوک", + "WebUI Settings": "تنظیمات WebUI", + "WebUI will make requests to": "WebUI درخواست\u200cها را ارسال خواهد کرد به", + "What’s New in": "موارد جدید در", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "وقتی سابقه خاموش است، چت\u200cهای جدید در این مرورگر در سابقه شما در هیچ یک از دستگاه\u200cهایتان ظاهر نمی\u200cشوند.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "محیط کار", + "Write a prompt suggestion (e.g. Who are you?)": "یک پیشنهاد پرامپت بنویسید (مثلاً شما کی هستید؟)", + "Write a summary in 50 words that summarizes [topic or keyword].": "خلاصه ای در 50 کلمه بنویسید که [موضوع یا کلمه کلیدی] را خلاصه کند.", + "Yesterday": "دیروز", + "You": "شما", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "شما نمیتوانید یک مدل پایه را کلون کنید", + "You have no archived conversations.": "شما هیچ گفتگوی ذخیره شده ندارید.", + "You have shared this chat": "شما این گفتگو را به اشتراک گذاشته اید", + "You're a helpful assistant.": "تو یک دستیار سودمند هستی.", + "You're now logged in.": "شما اکنون وارد شده\u200cاید.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "یوتیوب", + "Youtube Loader Settings": "تنظیمات لودر یوتیوب" +} diff --git a/src/lib/i18n/locales/fi-FI/translation.json b/src/lib/i18n/locales/fi-FI/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..fb4f08089a8efdb0242edffd59c2457878fd9a6d --- /dev/null +++ b/src/lib/i18n/locales/fi-FI/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' tai '-1' jottei vanhene.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(esim. `sh webui.sh --api`)", + "(latest)": "(uusin)", + "{{ models }}": "{{ mallit }}", + "{{ owner }}: You cannot delete a base model": "{{ omistaja }}: Perusmallia ei voi poistaa", + "{{modelName}} is thinking...": "{{modelName}} miettii...", + "{{user}}'s Chats": "{{user}}:n keskustelut", + "{{webUIName}} Backend Required": "{{webUIName}} backend vaaditaan", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Tehtävämallia käytetään tehtävien suorittamiseen, kuten otsikoiden luomiseen keskusteluille ja verkkohakukyselyille", + "a user": "käyttäjä", + "About": "Tietoja", + "Account": "Tili", + "Account Activation Pending": "", + "Accurate information": "Tarkkaa tietoa", + "Actions": "", + "Active Users": "", + "Add": "Lisää", + "Add a model id": "Mallitunnuksen lisääminen", + "Add a short description about what this model does": "Lisää lyhyt kuvaus siitä, mitä tämä malli tekee", + "Add a short title for this prompt": "Lisää lyhyt otsikko tälle kehotteelle", + "Add a tag": "Lisää tagi", + "Add custom prompt": "Lisää mukautettu kehote", + "Add Docs": "Lisää asiakirjoja", + "Add Files": "Lisää tiedostoja", + "Add Memory": "Lisää muistia", + "Add message": "Lisää viesti", + "Add Model": "Lisää malli", + "Add Tag": "", + "Add Tags": "Lisää tageja", + "Add User": "Lisää käyttäjä", + "Adjusting these settings will apply changes universally to all users.": "Näiden asetusten säätäminen vaikuttaa kaikkiin käyttäjiin.", + "admin": "hallinta", + "Admin": "", + "Admin Panel": "Hallintapaneeli", + "Admin Settings": "Hallinta-asetukset", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Edistyneet parametrit", + "Advanced Params": "Edistyneet parametrit", + "all": "kaikki", + "All Documents": "Kaikki asiakirjat", + "All Users": "Kaikki käyttäjät", + "Allow": "Salli", + "Allow Chat Deletion": "Salli keskustelujen poisto", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "kirjaimia, numeroita ja väliviivoja", + "Already have an account?": "Onko sinulla jo tili?", + "an assistant": "avustaja", + "and": "ja", + "and create a new shared link.": "ja luo uusi jaettu linkki.", + "API Base URL": "APIn perus-URL", + "API Key": "API-avain", + "API Key created.": "API-avain luotu.", + "API keys": "API-avaimet", + "April": "huhtikuu", + "Archive": "Arkisto", + "Archive All Chats": "Arkistoi kaikki keskustelut", + "Archived Chats": "Arkistoidut keskustelut", + "are allowed - Activate this command by typing": "ovat sallittuja - Aktivoi tämä komento kirjoittamalla", + "Are you sure?": "Oletko varma?", + "Attach file": "Liitä tiedosto", + "Attention to detail": "Huomio yksityiskohtiin", + "Audio": "Ääni", + "Audio settings updated successfully": "", + "August": "elokuu", + "Auto-playback response": "Soita vastaus automaattisesti", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111-perus-URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111-perus-URL vaaditaan.", + "available!": "saatavilla!", + "Back": "Takaisin", + "Bad Response": "Epäkelpo vastaus", + "Banners": "Bannerit", + "Base Model (From)": "Perusmalli (alkaen)", + "Batch Size (num_batch)": "", + "before": "ennen", + "Being lazy": "Oli laiska", + "Brave Search API Key": "Brave Search API -avain", + "Bypass SSL verification for Websites": "Ohita SSL-varmennus verkkosivustoille", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Peruuta", + "Capabilities": "Ominaisuuksia", + "Change Password": "Vaihda salasana", + "Chat": "Keskustelu", + "Chat Background Image": "", + "Chat Bubble UI": "Keskustelu-pallojen käyttöliittymä", + "Chat Controls": "", + "Chat direction": "Keskustelun suunta", + "Chat History": "Keskusteluhistoria", + "Chat History is off for this browser.": "Keskusteluhistoria on pois päältä tällä selaimella.", + "Chats": "Keskustelut", + "Check Again": "Tarkista uudelleen", + "Check for updates": "Tarkista päivitykset", + "Checking for updates...": "Tarkistetaan päivityksiä...", + "Choose a model before saving...": "Valitse malli ennen tallentamista...", + "Chunk Overlap": "Osien päällekkäisyys", + "Chunk Params": "Osien parametrit", + "Chunk Size": "Osien koko", + "Citation": "Sitaatti", + "Clear memory": "", + "Click here for help.": "Klikkaa tästä saadaksesi apua.", + "Click here to": "Klikkaa tästä", + "Click here to download user import template file.": "", + "Click here to select": "Klikkaa tästä valitaksesi", + "Click here to select a csv file.": "Klikkaa tästä valitaksesi CSV-tiedosto.", + "Click here to select a py file.": "", + "Click here to select documents.": "Klikkaa tästä valitaksesi asiakirjoja.", + "click here.": "klikkaa tästä.", + "Click on the user role button to change a user's role.": "Klikkaa käyttäjän roolipainiketta vaihtaaksesi käyttäjän roolia.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Klooni", + "Close": "Sulje", + "Code formatted successfully": "", + "Collection": "Kokoelma", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI-perus-URL", + "ComfyUI Base URL is required.": "ComfyUI-perus-URL vaaditaan.", + "Command": "Komento", + "Concurrent Requests": "Samanaikaiset pyynnöt", + "Confirm": "", + "Confirm Password": "Vahvista salasana", + "Confirm your action": "", + "Connections": "Yhteydet", + "Contact Admin for WebUI Access": "", + "Content": "Sisältö", + "Content Extraction": "", + "Context Length": "Kontekstin pituus", + "Continue Response": "Jatka vastausta", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Jaettu keskustelulinkki kopioitu leikepöydälle!", + "Copy": "Kopioi", + "Copy last code block": "Kopioi viimeisin koodilohko", + "Copy last response": "Kopioi viimeisin vastaus", + "Copy Link": "Kopioi linkki", + "Copying to clipboard was successful!": "Kopioiminen leikepöydälle onnistui!", + "Create a model": "Mallin luominen", + "Create Account": "Luo tili", + "Create new key": "Luo uusi avain", + "Create new secret key": "Luo uusi salainen avain", + "Created at": "Luotu", + "Created At": "Luotu", + "Created by": "", + "CSV Import": "", + "Current Model": "Nykyinen malli", + "Current Password": "Nykyinen salasana", + "Custom": "Mukautettu", + "Customize models for a specific purpose": "Mallien mukauttaminen tiettyyn tarkoitukseen", + "Dark": "Tumma", + "Dashboard": "", + "Database": "Tietokanta", + "December": "joulukuu", + "Default": "Oletus", + "Default (Automatic1111)": "Oletus (AUTOMATIC1111)", + "Default (SentenceTransformers)": "Oletus (SentenceTransformers)", + "Default Model": "Oletusmalli", + "Default model updated": "Oletusmalli päivitetty", + "Default Prompt Suggestions": "Oletuskehotteiden ehdotukset", + "Default User Role": "Oletuskäyttäjärooli", + "delete": "poista", + "Delete": "Poista", + "Delete a model": "Poista malli", + "Delete All Chats": "Poista kaikki keskustelut", + "Delete chat": "Poista keskustelu", + "Delete Chat": "Poista keskustelu", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "poista tämä linkki", + "Delete tool?": "", + "Delete User": "Poista käyttäjä", + "Deleted {{deleteModelTag}}": "Poistettu {{deleteModelTag}}", + "Deleted {{name}}": "Poistettu {{nimi}}", + "Description": "Kuvaus", + "Didn't fully follow instructions": "Ei noudattanut ohjeita täysin", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Tutustu malliin", + "Discover a prompt": "Löydä kehote", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Löydä ja lataa mukautettuja kehotteita", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Löydä ja lataa mallien esiasetuksia", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Näytä käyttäjänimi keskustelussa", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Asiakirja", + "Document Settings": "Asiakirja-asetukset", + "Documentation": "", + "Documents": "Asiakirjat", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ei tee ulkoisia yhteyksiä, ja tietosi pysyvät turvallisesti paikallisesti isännöidyllä palvelimellasi.", + "Don't Allow": "Älä salli", + "Don't have an account?": "Eikö sinulla ole tiliä?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "En pidä tyylistä", + "Done": "", + "Download": "Lataa", + "Download canceled": "Lataus peruutettu", + "Download Database": "Lataa tietokanta", + "Drop any files here to add to the conversation": "Pudota tiedostoja tähän lisätäksesi ne keskusteluun", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "esim. '30s', '10m'. Kelpoiset aikayksiköt ovat 's', 'm', 'h'.", + "Edit": "Muokkaa", + "Edit Doc": "Muokkaa asiakirjaa", + "Edit Memory": "", + "Edit User": "Muokkaa käyttäjää", + "ElevenLabs": "", + "Email": "Sähköposti", + "Embedding Batch Size": "", + "Embedding Model": "Upotusmalli", + "Embedding Model Engine": "Upotusmallin moottori", + "Embedding model set to \"{{embedding_model}}\"": "\"{{embedding_model}}\" valittu upotusmalliksi", + "Enable Chat History": "Ota keskusteluhistoria käyttöön", + "Enable Community Sharing": "Ota yhteisön jakaminen käyttöön", + "Enable New Sign Ups": "Salli uudet rekisteröitymiset", + "Enable Web Search": "Ota verkkohaku käyttöön", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Varmista, että CSV-tiedostossasi on 4 saraketta seuraavassa järjestyksessä: Nimi, Sähköposti, Salasana, Rooli.", + "Enter {{role}} message here": "Kirjoita {{role}} viesti tähän", + "Enter a detail about yourself for your LLMs to recall": "Kirjoita tieto itseestäsi LLM:ien muistamiseksi", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Anna Brave Search API -avain", + "Enter Chunk Overlap": "Syötä osien päällekkäisyys", + "Enter Chunk Size": "Syötä osien koko", + "Enter Github Raw URL": "Kirjoita Github Raw URL-osoite", + "Enter Google PSE API Key": "Anna Google PSE API -avain", + "Enter Google PSE Engine Id": "Anna Google PSE -moottorin tunnus", + "Enter Image Size (e.g. 512x512)": "Syötä kuvan koko (esim. 512x512)", + "Enter language codes": "Syötä kielikoodit", + "Enter model tag (e.g. {{modelTag}})": "Syötä mallitagi (esim. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Syötä askelien määrä (esim. 50)", + "Enter Score": "Syötä pisteet", + "Enter Searxng Query URL": "Kirjoita Searxng-kyselyn URL-osoite", + "Enter Serper API Key": "Anna Serper API -avain", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Anna Serpstack API -avain", + "Enter stop sequence": "Syötä lopetussekvenssi", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Syötä Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Syötä URL (esim. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Syötä URL (esim. http://localhost:11434)", + "Enter Your Email": "Syötä sähköpostiosoitteesi", + "Enter Your Full Name": "Syötä koko nimesi", + "Enter your message": "", + "Enter Your Password": "Syötä salasanasi", + "Enter Your Role": "Syötä roolisi", + "Error": "Virhe", + "Experimental": "Kokeellinen", + "Export": "Vienti", + "Export All Chats (All Users)": "Vie kaikki keskustelut (kaikki käyttäjät)", + "Export chat (.json)": "", + "Export Chats": "Vie keskustelut", + "Export Documents Mapping": "Vie asiakirjakartoitus", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Vie malleja", + "Export Prompts": "Vie kehotteet", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "API-avaimen luonti epäonnistui.", + "Failed to read clipboard contents": "Leikepöydän sisällön lukeminen epäonnistui", + "Failed to update settings": "", + "February": "helmikuu", + "Feel free to add specific details": "Voit lisätä tarkempia tietoja", + "File": "", + "File Mode": "Tiedostotila", + "File not found.": "Tiedostoa ei löytynyt.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Sormenjäljen väärentäminen havaittu: Ei voi käyttää alkukirjaimia avatarina. Käytetään oletusprofiilikuvaa.", + "Fluidly stream large external response chunks": "Virtaa suuria ulkoisia vastausosia joustavasti", + "Focus chat input": "Fokusoi syöttökenttään", + "Followed instructions perfectly": "Noudatti ohjeita täydellisesti", + "Form": "", + "Format your variables using square brackets like this:": "Muotoile muuttujat hakasulkeilla näin:", + "Frequency Penalty": "Taajuussakko", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Yleinen", + "General Settings": "Yleisasetukset", + "Generate Image": "", + "Generating search query": "Hakukyselyn luominen", + "Generation Info": "Generointitiedot", + "Get up and running with": "", + "Global": "", + "Good Response": "Hyvä vastaus", + "Google PSE API Key": "Google PSE API -avain", + "Google PSE Engine Id": "Google PSE -moduulin tunnus", + "h:mm a": "h:mm a", + "has no conversations.": "ei ole keskusteluja.", + "Hello, {{name}}": "Terve, {{name}}", + "Help": "Apua", + "Hide": "Piilota", + "Hide Model": "", + "How can I help you today?": "Kuinka voin auttaa tänään?", + "Hybrid Search": "Hybridihaku", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Kuvagenerointi (kokeellinen)", + "Image Generation Engine": "Kuvagenerointimoottori", + "Image Settings": "Kuva-asetukset", + "Images": "Kuvat", + "Import Chats": "Tuo keskustelut", + "Import Documents Mapping": "Tuo asiakirjakartoitus", + "Import Functions": "", + "Import Models": "Mallien tuominen", + "Import Prompts": "Tuo kehotteita", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Sisällytä `--api`-parametri suorittaessasi stable-diffusion-webui", + "Info": "Info", + "Input commands": "Syötä komennot", + "Install from Github URL": "Asenna Githubin URL-osoitteesta", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Käyttöliittymä", + "Invalid Tag": "Virheellinen tagi", + "January": "tammikuu", + "join our Discord for help.": "liity Discordiimme saadaksesi apua.", + "JSON": "JSON", + "JSON Preview": "JSON-esikatselu", + "July": "heinäkuu", + "June": "kesäkuu", + "JWT Expiration": "JWT:n vanheneminen", + "JWT Token": "JWT-token", + "Keep Alive": "Pysy aktiivisena", + "Keyboard shortcuts": "Pikanäppäimet", + "Knowledge": "", + "Language": "Kieli", + "large language models, locally.": "", + "Last Active": "Viimeksi aktiivinen", + "Last Modified": "", + "Light": "Vaalea", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "Kielimallit voivat tehdä virheitä. Varmista tärkeät tiedot.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Tehnyt OpenWebUI-yhteisö", + "Make sure to enclose them with": "Varmista, että suljet ne", + "Manage": "", + "Manage Models": "Hallitse malleja", + "Manage Ollama Models": "Hallitse Ollama-malleja", + "Manage Pipelines": "Hallitse putkia", + "Manage Valves": "", + "March": "maaliskuu", + "Max Tokens (num_predict)": "Tokenien enimmäismäärä (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Enintään 3 mallia voidaan ladata samanaikaisesti. Yritä myöhemmin uudelleen.", + "May": "toukokuu", + "Memories accessible by LLMs will be shown here.": "Muistitiedostot, joita LLM-ohjelmat käyttävät, näkyvät tässä.", + "Memory": "Muisti", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Linkin luomisen jälkeen lähettämiäsi viestejä ei jaeta. Käyttäjät, joilla on URL-osoite, voivat tarkastella jaettua keskustelua.", + "Minimum Score": "Vähimmäispisteet", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD MMMM YYYY", + "MMMM DD, YYYY HH:mm": "DD MMMM YYYY, HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Malli '{{modelName}}' ladattiin onnistuneesti.", + "Model '{{modelTag}}' is already in queue for downloading.": "Malli '{{modelTag}}' on jo jonossa ladattavaksi.", + "Model {{modelId}} not found": "Mallia {{modelId}} ei löytynyt", + "Model {{modelName}} is not vision capable": "Malli {{modelName}} ei kykene näkökykyyn", + "Model {{name}} is now {{status}}": "Malli {{name}} on nyt {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Mallin tiedostojärjestelmäpolku havaittu. Mallin lyhytnimi vaaditaan päivitykseen, ei voi jatkaa.", + "Model ID": "Mallin tunnus", + "Model not selected": "Mallia ei valittu", + "Model Params": "Mallin parametrit", + "Model updated successfully": "", + "Model Whitelisting": "Mallin sallimislista", + "Model(s) Whitelisted": "Malli(t) sallittu", + "Modelfile Content": "Mallitiedoston sisältö", + "Models": "Mallit", + "More": "Lisää", + "Name": "Nimi", + "Name Tag": "Nimitagi", + "Name your model": "Mallin nimeäminen", + "New Chat": "Uusi keskustelu", + "New Password": "Uusi salasana", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Ei tuloksia", + "No search query generated": "Hakukyselyä ei luotu", + "No source available": "Ei lähdettä saatavilla", + "No valves to update": "", + "None": "Ei lainkaan", + "Not factually correct": "Ei faktisesti oikein", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Huom: Jos asetat vähimmäispisteet, haku palauttaa vain asiakirjat, joiden pisteet ovat suurempia tai yhtä suuria kuin vähimmäispistemäärä.", + "Notifications": "Ilmoitukset", + "November": "marraskuu", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "lokakuu", + "Off": "Pois", + "Okay, Let's Go!": "Eikun menoksi!", + "OLED Dark": "OLED-tumma", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API poistettu käytöstä", + "Ollama API is disabled": "", + "Ollama Version": "Ollama-versio", + "On": "Päällä", + "Only": "Vain", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Vain kirjaimet, numerot ja väliviivat ovat sallittuja komentosarjassa.", + "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.": "Hetki pieni, tiedostosi ovat yhä leivinuunissa. Odota kärsivällisesti, ja ilmoitamme, kun ne ovat valmiita.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Hups! Näyttää siltä, että URL on virheellinen. Tarkista se ja yritä uudelleen.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hupsista! Käytät ei-tuettua menetelmää. WebUI pitää palvella backendista.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Avaa uusi keskustelu", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API -asetukset", + "OpenAI API Key is required.": "OpenAI API -avain vaaditaan.", + "OpenAI URL/Key required.": "OpenAI URL/ -avain vaaditaan.", + "or": "tai", + "Other": "Muu", + "Password": "Salasana", + "PDF document (.pdf)": "PDF-tiedosto (.pdf)", + "PDF Extract Images (OCR)": "PDF-tiedoston kuvien erottelu (OCR)", + "pending": "odottaa", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Mikrofonin käyttöoikeus evätty: {{error}}", + "Personalization": "Henkilökohtaisuus", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Putkistot", + "Pipelines Not Detected": "", + "Pipelines Valves": "Putkistot Venttiilit", + "Plain text (.txt)": "Pelkkä teksti (.txt)", + "Playground": "Leikkipaikka", + "Please carefully review the following warnings:": "", + "Positive attitude": "Positiivinen asenne", + "Previous 30 days": "Edelliset 30 päivää", + "Previous 7 days": "Edelliset 7 päivää", + "Profile Image": "Profiilikuva", + "Prompt": "Kehote", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Kehote (esim. Kerro hauska fakta Turusta)", + "Prompt Content": "Kehotteen sisältö", + "Prompt suggestions": "Kehotteen ehdotukset", + "Prompts": "Kehotteet", + "Pull \"{{searchValue}}\" from Ollama.com": "Lataa \"{{searchValue}}\" Ollama.comista", + "Pull a model from Ollama.com": "Lataa malli Ollama.comista", + "Query Params": "Kyselyparametrit", + "RAG Template": "RAG-malline", + "Read Aloud": "Lue ääneen", + "Record voice": "Nauhoita ääni", + "Redirecting you to OpenWebUI Community": "Ohjataan sinut OpenWebUI-yhteisöön", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Kieltäytyi, vaikka ei olisi pitänyt", + "Regenerate": "Uudelleenluo", + "Release Notes": "Julkaisutiedot", + "Remove": "Poista", + "Remove Model": "Poista malli", + "Rename": "Nimeä uudelleen", + "Repeat Last N": "Viimeinen N -toisto", + "Request Mode": "Pyyntötila", + "Reranking Model": "Uudelleenpisteytysmalli", + "Reranking model disabled": "Uudelleenpisteytysmalli poistettu käytöstä", + "Reranking model set to \"{{reranking_model}}\"": "\"{{reranking_model}}\" valittu uudelleenpisteytysmalliksi", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Tyhjennä vektorivarasto", + "Response AutoCopy to Clipboard": "Vastauksen automaattikopiointi leikepöydälle", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Rooli", + "Rosé Pine": "Rosee-mänty", + "Rosé Pine Dawn": "Aamuinen Rosee-mänty", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Tallenna", + "Save & Create": "Tallenna ja luo", + "Save & Update": "Tallenna ja päivitä", + "Save Tag": "", + "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": "Keskustelulokien tallentaminen suoraan selaimen tallennustilaan ei ole enää tuettua. Lataa ja poista keskustelulokit napsauttamalla alla olevaa painiketta. Älä huoli, voit helposti tuoda keskustelulokit takaisin backendiin", + "Scan": "Skannaa", + "Scan complete!": "Skannaus valmis!", + "Scan for documents from {{path}}": "Skannaa asiakirjoja polusta {{path}}", + "Search": "Haku", + "Search a model": "Hae mallia", + "Search Chats": "Etsi chatteja", + "Search Documents": "Hae asiakirjoja", + "Search Functions": "", + "Search Models": "Hae malleja", + "Search Prompts": "Hae kehotteita", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Hakutulosten määrä", + "Search Tools": "", + "Searched {{count}} sites_one": "Haettu {{count}} sites_one", + "Searched {{count}} sites_other": "Haku {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng-kyselyn URL-osoite", + "See readme.md for instructions": "Katso lisää ohjeita readme.md:stä", + "See what's new": "Katso, mitä uutta", + "Seed": "Siemen", + "Select a base model": "Valitse perusmalli", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Valitse tila", + "Select a model": "Valitse malli", + "Select a pipeline": "Valitse putki", + "Select a pipeline url": "Valitse putken URL-osoite", + "Select a tool": "", + "Select an Ollama instance": "Valitse Ollama-instanssi", + "Select Documents": "", + "Select model": "Valitse malli", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Valitut mallit eivät tue kuvasyötteitä", + "Send": "Lähetä", + "Send a Message": "Lähetä viesti", + "Send message": "Lähetä viesti", + "September": "syyskuu", + "Serper API Key": "Serper API -avain", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API -avain", + "Server connection verified": "Palvelinyhteys varmennettu", + "Set as default": "Aseta oletukseksi", + "Set Default Model": "Aseta oletusmalli", + "Set embedding model (e.g. {{model}})": "Aseta upotusmalli (esim. {{model}})", + "Set Image Size": "Aseta kuvan koko", + "Set reranking model (e.g. {{model}})": "Aseta uudelleenpisteytysmalli (esim. {{model}})", + "Set Steps": "Aseta askelmäärä", + "Set Task Model": "Aseta tehtävämalli", + "Set Voice": "Aseta puheääni", + "Settings": "Asetukset", + "Settings saved successfully!": "Asetukset tallennettu onnistuneesti!", + "Settings updated successfully": "", + "Share": "Jaa", + "Share Chat": "Jaa keskustelu", + "Share to OpenWebUI Community": "Jaa OpenWebUI-yhteisöön", + "short-summary": "lyhyt-yhteenveto", + "Show": "Näytä", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Näytä pikanäppäimet", + "Show your support!": "", + "Showcased creativity": "Näytti luovuutta", + "Sign in": "Kirjaudu sisään", + "Sign Out": "Kirjaudu ulos", + "Sign up": "Rekisteröidy", + "Signing in": "Kirjaudutaan sisään", + "Source": "Lähde", + "Speech recognition error: {{error}}": "Puheentunnistusvirhe: {{error}}", + "Speech-to-Text Engine": "Puheentunnistusmoottori", + "Stop Sequence": "Lopetussekvenssi", + "STT Model": "", + "STT Settings": "Puheentunnistusasetukset", + "Submit": "Lähetä", + "Subtitle (e.g. about the Roman Empire)": "Alaotsikko (esim. Rooman valtakunnasta)", + "Success": "Onnistui", + "Successfully updated.": "Päivitetty onnistuneesti.", + "Suggested": "Suositeltu", + "Support": "", + "Support this plugin:": "", + "System": "Järjestelmä", + "System Prompt": "Järjestelmäkehote", + "Tags": "Tagit", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Kerro lisää:", + "Temperature": "Lämpötila", + "Template": "Malline", + "Text Completion": "Tekstin täydennys", + "Text-to-Speech Engine": "Puhemoottori", + "Tfs Z": "TFS Z", + "Thanks for your feedback!": "Kiitos palautteestasi!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Pisteytyksen tulee olla arvo välillä 0.0 (0%) ja 1.0 (100%).", + "Theme": "Teema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Tämä varmistaa, että arvokkaat keskustelusi tallennetaan turvallisesti backend-tietokantaasi. Kiitos!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Tämä asetus ei synkronoidu selainten tai laitteiden välillä.", + "This will delete": "", + "Thorough explanation": "Perusteellinen selitys", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Vinkki: Päivitä useita muuttujapaikkoja peräkkäin painamalla tabulaattoria keskustelusyötteessä jokaisen korvauksen jälkeen.", + "Title": "Otsikko", + "Title (e.g. Tell me a fun fact)": "Otsikko (esim. Kerro hauska fakta)", + "Title Auto-Generation": "Otsikon automaattinen luonti", + "Title cannot be an empty string.": "Otsikko ei voi olla tyhjä.", + "Title Generation Prompt": "Otsikon luontikehote", + "to": "->", + "To access the available model names for downloading,": "Päästäksesi käsiksi ladattavissa oleviin mallinimiin,", + "To access the GGUF models available for downloading,": "Päästäksesi käsiksi ladattavissa oleviin GGUF-malleihin,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "keskustelusyötteeseen.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Tänään", + "Toggle settings": "Kytke asetukset", + "Toggle sidebar": "Kytke sivupalkki", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Ongelmia Ollama-yhteydessä?", + "TTS Model": "", + "TTS Settings": "Puheentuottamisasetukset", + "TTS Voice": "", + "Type": "Tyyppi", + "Type Hugging Face Resolve (Download) URL": "Kirjoita Hugging Face -resolve-osoite", + "Uh-oh! There was an issue connecting to {{provider}}.": "Voi ei! Yhteysongelma {{provider}}:n kanssa.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Päivitä ja kopioi linkki", + "Update password": "Päivitä salasana", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Lataa GGUF-malli", + "Upload Files": "Lataa tiedostoja", + "Upload Pipeline": "", + "Upload Progress": "Latauksen eteneminen", + "URL Mode": "URL-tila", + "Use '#' in the prompt input to load and select your documents.": "Käytä '#' syötteessä ladataksesi ja valitaksesi asiakirjoja.", + "Use Gravatar": "Käytä Gravataria", + "Use Initials": "Käytä alkukirjaimia", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "käyttäjä", + "User location successfully retrieved.": "", + "User Permissions": "Käyttäjäoikeudet", + "Users": "Käyttäjät", + "Utilize": "Käytä", + "Valid time units:": "Kelvolliset aikayksiköt:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "muuttuja", + "variable to have them replaced with clipboard content.": "muuttuja korvataan leikepöydän sisällöllä.", + "Version": "Versio", + "Voice": "", + "Warning": "Varoitus", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Varoitus: Jos päivität tai vaihdat upotusmallia, sinun on tuotava kaikki asiakirjat uudelleen.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Web Loader asetukset", + "Web Params": "Web-parametrit", + "Web Search": "Web-haku", + "Web Search Engine": "Web-hakukone", + "Webhook URL": "Webhook-URL", + "WebUI Settings": "WebUI-asetukset", + "WebUI will make requests to": "WebUI tekee pyyntöjä", + "What’s New in": "Mitä uutta", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kun historia on pois päältä, uudet keskustelut tässä selaimessa eivät näy historiassasi millään laitteellasi.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Työtilat", + "Write a prompt suggestion (e.g. Who are you?)": "Kirjoita ehdotettu kehote (esim. Kuka olet?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Kirjoita 50 sanan yhteenveto, joka tiivistää [aihe tai avainsana].", + "Yesterday": "Eilen", + "You": "Sinä", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Perusmallia ei voi kloonata", + "You have no archived conversations.": "Sinulla ei ole arkistoituja keskusteluja.", + "You have shared this chat": "Olet jakanut tämän keskustelun", + "You're a helpful assistant.": "Olet avulias apulainen.", + "You're now logged in.": "Olet nyt kirjautunut sisään.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube Loader-asetukset" +} diff --git a/src/lib/i18n/locales/fr-CA/translation.json b/src/lib/i18n/locales/fr-CA/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..8e85da16445b8c911e007c93a1ea02b64b2ec2c5 --- /dev/null +++ b/src/lib/i18n/locales/fr-CA/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": " 's', 'm', 'h', 'd', 'w' ou '-1' pour une durée illimitée.", + "(Beta)": "(Version bêta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(par ex. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(par exemple `sh webui.sh --api`)", + "(latest)": "(dernier)", + "{{ models }}": "{{ modèles }}", + "{{ owner }}: You cannot delete a base model": "{{ propriétaire }} : Vous ne pouvez pas supprimer un modèle de base.", + "{{modelName}} is thinking...": "{{modelName}} est en train de réfléchir...", + "{{user}}'s Chats": "Discussions de {{user}}", + "{{webUIName}} Backend Required": "Backend {{webUIName}} requis", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modèle de tâche est utilisé lors de l’exécution de tâches telles que la génération de titres pour les conversations et les requêtes de recherche sur le web.", + "a user": "un utilisateur", + "About": "À propos", + "Account": "Compte", + "Account Activation Pending": "Activation du compte en attente", + "Accurate information": "Information exacte", + "Actions": "", + "Active Users": "Utilisateurs actifs", + "Add": "Ajouter", + "Add a model id": "Ajouter un identifiant de modèle", + "Add a short description about what this model does": "Ajoutez une brève description de ce que fait ce modèle.", + "Add a short title for this prompt": "Ajoutez un bref titre pour cette prompt.", + "Add a tag": "Ajouter une balise", + "Add custom prompt": "Ajouter une prompt personnalisée", + "Add Docs": "Ajouter de la documentation", + "Add Files": "Ajouter des fichiers", + "Add Memory": "Ajouter de la mémoire", + "Add message": "Ajouter un message", + "Add Model": "Ajouter un modèle", + "Add Tag": "", + "Add Tags": "Ajouter des balises", + "Add User": "Ajouter un Utilisateur", + "Adjusting these settings will apply changes universally to all users.": "L'ajustement de ces paramètres appliquera universellement les changements à tous les utilisateurs.", + "admin": "administrateur", + "Admin": "Administrateur", + "Admin Panel": "Tableau de bord administrateur", + "Admin Settings": "Paramètres d'administration", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; les utilisateurs ont besoin d'outils affectés par modèle dans l'espace de travail.", + "Advanced Parameters": "Paramètres avancés", + "Advanced Params": "Paramètres avancés", + "all": "toutes", + "All Documents": "Tous les documents", + "All Users": "Tous les Utilisateurs", + "Allow": "Autoriser", + "Allow Chat Deletion": "Autoriser la suppression de l'historique de chat", + "Allow non-local voices": "Autoriser les voix non locales", + "Allow User Location": "Autoriser l'emplacement de l'utilisateur", + "Allow Voice Interruption in Call": "Autoriser l'interruption vocale pendant un appel", + "alphanumeric characters and hyphens": "caractères alphanumériques et tirets", + "Already have an account?": "Avez-vous déjà un compte ?", + "an assistant": "un assistant", + "and": "et", + "and create a new shared link.": "et créer un nouveau lien partagé.", + "API Base URL": "URL de base de l'API", + "API Key": "Clé d'API", + "API Key created.": "Clé d'API générée.", + "API keys": "Clés d'API", + "April": "Avril", + "Archive": "Archivage", + "Archive All Chats": "Archiver toutes les conversations", + "Archived Chats": "Conversations archivées", + "are allowed - Activate this command by typing": "sont autorisés - Activer cette commande en tapant", + "Are you sure?": "Êtes-vous certain ?", + "Attach file": "Joindre un document", + "Attention to detail": "Attention aux détails", + "Audio": "Audio", + "Audio settings updated successfully": "Les paramètres audio ont été mis à jour avec succès", + "August": "Août", + "Auto-playback response": "Réponse de lecture automatique", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Chaîne d'authentification de l'API", + "AUTOMATIC1111 Base URL": "URL de base AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "L'URL de base {AUTOMATIC1111} est requise.", + "available!": "disponible !", + "Back": "Retour en arrière", + "Bad Response": "Mauvaise réponse", + "Banners": "Banniers", + "Base Model (From)": "Modèle de base (à partir de)", + "Batch Size (num_batch)": "Taille du lot (num_batch)", + "before": "avant", + "Being lazy": "Être fainéant", + "Brave Search API Key": "Clé API Brave Search", + "Bypass SSL verification for Websites": "Bypasser la vérification SSL pour les sites web", + "Call": "Appeler", + "Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT", + "Camera": "Appareil photo", + "Cancel": "Annuler", + "Capabilities": "Capacités", + "Change Password": "Changer le mot de passe", + "Chat": "Chat", + "Chat Background Image": "Image d'arrière-plan de la fenêtre de chat", + "Chat Bubble UI": "Bulles de discussion", + "Chat Controls": "", + "Chat direction": "Direction du chat", + "Chat History": "Historique de discussion", + "Chat History is off for this browser.": "L'historique de chat est désactivé pour ce navigateur", + "Chats": "Conversations", + "Check Again": "Vérifiez à nouveau.", + "Check for updates": "Vérifier les mises à jour disponibles", + "Checking for updates...": "Recherche de mises à jour...", + "Choose a model before saving...": "Choisissez un modèle avant de sauvegarder...", + "Chunk Overlap": "Chevauchement de blocs", + "Chunk Params": "Paramètres d'encombrement", + "Chunk Size": "Taille de bloc", + "Citation": "Citation", + "Clear memory": "Libérer la mémoire", + "Click here for help.": "Cliquez ici pour obtenir de l'aide.", + "Click here to": "Cliquez ici pour", + "Click here to download user import template file.": "Cliquez ici pour télécharger le fichier modèle d'importation utilisateur.", + "Click here to select": "Cliquez ici pour sélectionner", + "Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier CSV.", + "Click here to select a py file.": "Cliquez ici pour sélectionner un fichier .py.", + "Click here to select documents.": "Cliquez ici pour sélectionner les documents.", + "click here.": "cliquez ici.", + "Click on the user role button to change a user's role.": "Cliquez sur le bouton de rôle d'utilisateur pour modifier le rôle d'un utilisateur.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "L'autorisation d'écriture du presse-papier a été refusée. Veuillez vérifier les paramètres de votre navigateur pour accorder l'accès nécessaire.", + "Clone": "Copie conforme", + "Close": "Fermer", + "Code formatted successfully": "Le code a été formaté avec succès", + "Collection": "Collection", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL de base ComfyUI", + "ComfyUI Base URL is required.": "L'URL de base ComfyUI est requise.", + "Command": "Commande", + "Concurrent Requests": "Demandes concurrentes", + "Confirm": "Confirmer", + "Confirm Password": "Confirmer le mot de passe", + "Confirm your action": "Confirmez votre action", + "Connections": "Connexions", + "Contact Admin for WebUI Access": "Contacter l'administrateur pour l'accès à l'interface Web", + "Content": "Contenu", + "Content Extraction": "", + "Context Length": "Longueur du contexte", + "Continue Response": "Continuer la réponse", + "Continue with {{provider}}": "Continuer avec {{provider}}", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL du chat copiée dans le presse-papiers\u00a0!", + "Copy": "Copie", + "Copy last code block": "Copier le dernier bloc de code", + "Copy last response": "Copier la dernière réponse", + "Copy Link": "Copier le lien", + "Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !", + "Create a model": "Créer un modèle", + "Create Account": "Créer un compte", + "Create new key": "Créer une nouvelle clé principale", + "Create new secret key": "Créer une nouvelle clé secrète", + "Created at": "Créé à", + "Created At": "Créé le", + "Created by": "Créé par", + "CSV Import": "Import CSV", + "Current Model": "Modèle actuel amélioré", + "Current Password": "Mot de passe actuel", + "Custom": "Sur mesure", + "Customize models for a specific purpose": "Personnaliser les modèles pour une fonction spécifique", + "Dark": "Obscur", + "Dashboard": "Tableau de bord", + "Database": "Base de données", + "December": "Décembre", + "Default": "Par défaut", + "Default (Automatic1111)": "Par défaut (Automatic1111)", + "Default (SentenceTransformers)": "Par défaut (Sentence Transformers)", + "Default Model": "Modèle standard", + "Default model updated": "Modèle par défaut mis à jour", + "Default Prompt Suggestions": "Suggestions de prompts par défaut", + "Default User Role": "Rôle utilisateur par défaut", + "delete": "supprimer", + "Delete": "Supprimer", + "Delete a model": "Supprimer un modèle", + "Delete All Chats": "Supprimer toutes les conversations", + "Delete chat": "Supprimer la conversation", + "Delete Chat": "Supprimer la Conversation", + "Delete chat?": "Supprimer la conversation ?", + "Delete Doc": "", + "Delete function?": "Supprimer la fonction ?", + "Delete prompt?": "Supprimer la prompt ?", + "delete this link": "supprimer ce lien", + "Delete tool?": "Effacer l'outil ?", + "Delete User": "Supprimer le compte d'utilisateur", + "Deleted {{deleteModelTag}}": "Supprimé {{deleteModelTag}}", + "Deleted {{name}}": "Supprimé {{name}}", + "Description": "Description", + "Didn't fully follow instructions": "N'a pas entièrement respecté les instructions", + "Disabled": "", + "Discover a function": "Découvrez une fonction", + "Discover a model": "Découvrir un modèle", + "Discover a prompt": "Découvrir une suggestion", + "Discover a tool": "Découvrez un outil", + "Discover, download, and explore custom functions": "Découvrez, téléchargez et explorez des fonctions personnalisées", + "Discover, download, and explore custom prompts": "Découvrez, téléchargez et explorez des prompts personnalisés", + "Discover, download, and explore custom tools": "Découvrez, téléchargez et explorez des outils personnalisés", + "Discover, download, and explore model presets": "Découvrir, télécharger et explorer des préréglages de modèles", + "Dismissible": "Fermeture", + "Display Emoji in Call": "Afficher les emojis pendant l'appel", + "Display the username instead of You in the Chat": "Afficher le nom d'utilisateur à la place de \"Vous\" dans le Chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Document", + "Document Settings": "Paramètres du document", + "Documentation": "Documentation", + "Documents": "Documents", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ne fait aucune connexion externe et garde vos données en sécurité sur votre serveur local.", + "Don't Allow": "Ne pas autoriser", + "Don't have an account?": "Vous n'avez pas de compte ?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "N'apprécie pas le style", + "Done": "Terminé", + "Download": "Télécharger", + "Download canceled": "Téléchargement annulé", + "Download Database": "Télécharger la base de données", + "Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.", + "Edit": "Modifier", + "Edit Doc": "Modifier le document", + "Edit Memory": "Modifier la mémoire", + "Edit User": "Modifier l'utilisateur", + "ElevenLabs": "", + "Email": "E-mail", + "Embedding Batch Size": "Taille du lot d'encodage", + "Embedding Model": "Modèle d'embedding", + "Embedding Model Engine": "Moteur de modèle d'encodage", + "Embedding model set to \"{{embedding_model}}\"": "Modèle d'encodage défini sur « {{embedding_model}} »", + "Enable Chat History": "Activer l'historique de conversation", + "Enable Community Sharing": "Activer le partage communautaire", + "Enable New Sign Ups": "Activer les nouvelles inscriptions", + "Enable Web Search": "Activer la recherche sur le Web", + "Enabled": "", + "Engine": "Moteur", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Vérifiez que votre fichier CSV comprenne les 4 colonnes dans cet ordre : Name, Email, Password, Role.", + "Enter {{role}} message here": "Entrez le message {{role}} ici", + "Enter a detail about yourself for your LLMs to recall": "Saisissez un détail sur vous-même que vos LLMs pourront se rappeler", + "Enter api auth string (e.g. username:password)": "Entrez la chaîne d'authentification de l'API (par ex. nom d'utilisateur:mot de passe)", + "Enter Brave Search API Key": "Entrez la clé API Brave Search", + "Enter Chunk Overlap": "Entrez le chevauchement de chunk", + "Enter Chunk Size": "Entrez la taille de bloc", + "Enter Github Raw URL": "Entrez l'URL brute de GitHub", + "Enter Google PSE API Key": "Entrez la clé API Google PSE", + "Enter Google PSE Engine Id": "Entrez l'identifiant du moteur Google PSE", + "Enter Image Size (e.g. 512x512)": "Entrez la taille de l'image (par ex. 512x512)", + "Enter language codes": "Entrez les codes de langue", + "Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Entrez le nombre de pas (par ex. 50)", + "Enter Score": "Entrez votre score", + "Enter Searxng Query URL": "Entrez l'URL de la requête Searxng", + "Enter Serper API Key": "Entrez la clé API Serper", + "Enter Serply API Key": "Entrez la clé API Serply", + "Enter Serpstack API Key": "Entrez la clé API Serpstack", + "Enter stop sequence": "Entrez la séquence d'arrêt", + "Enter system prompt": "", + "Enter Tavily API Key": "Entrez la clé API Tavily", + "Enter Tika Server URL": "", + "Enter Top K": "Entrez les Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Entrez l'URL (par ex. {http://127.0.0.1:7860/})", + "Enter URL (e.g. http://localhost:11434)": "Entrez l'URL (par ex. http://localhost:11434)", + "Enter Your Email": "Entrez votre adresse e-mail", + "Enter Your Full Name": "Entrez votre nom complet", + "Enter your message": "", + "Enter Your Password": "Entrez votre mot de passe", + "Enter Your Role": "Entrez votre rôle", + "Error": "Erreur", + "Experimental": "Expérimental", + "Export": "Exportation", + "Export All Chats (All Users)": "Exporter toutes les conversations (tous les utilisateurs)", + "Export chat (.json)": "Exporter la discussion (.json)", + "Export Chats": "Exporter les conversations", + "Export Documents Mapping": "Exportez la correspondance des documents", + "Export Functions": "Exportez les Fonctions", + "Export LiteLLM config.yaml": "Exportez le fichier LiteLLM config.yaml", + "Export Models": "Exporter les modèles", + "Export Prompts": "Exporter les Prompts", + "Export Tools": "Outils d'exportation", + "External Models": "Modèles externes", + "Failed to create API Key.": "Échec de la création de la clé API.", + "Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers", + "Failed to update settings": "Échec de la mise à jour des paramètres", + "February": "Février", + "Feel free to add specific details": "N'hésitez pas à ajouter des détails spécifiques", + "File": "Fichier", + "File Mode": "Mode fichier", + "File not found.": "Fichier introuvable.", + "Files": "", + "Filter is now globally disabled": "Le filtre est maintenant désactivé globalement", + "Filter is now globally enabled": "Le filtre est désormais activé globalement", + "Filters": "Filtres", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.", + "Fluidly stream large external response chunks": "Diffuser de manière fluide de larges portions de réponses externes", + "Focus chat input": "Se concentrer sur le chat en entrée", + "Followed instructions perfectly": "A parfaitement suivi les instructions", + "Form": "Formulaire", + "Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :", + "Frequency Penalty": "Pénalité de fréquence", + "Function created successfully": "La fonction a été créée avec succès", + "Function deleted successfully": "Fonction supprimée avec succès", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "La fonction a été mise à jour avec succès", + "Functions": "Fonctions", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Fonctions importées avec succès", + "General": "Général", + "General Settings": "Paramètres Généraux", + "Generate Image": "Générer une image", + "Generating search query": "Génération d'une requête de recherche", + "Generation Info": "Informations sur la génération", + "Get up and running with": "", + "Global": "Mondial", + "Good Response": "Bonne réponse", + "Google PSE API Key": "Clé API Google PSE", + "Google PSE Engine Id": "ID du moteur de recherche personnalisé de Google", + "h:mm a": "h:mm a", + "has no conversations.": "n'a aucune conversation.", + "Hello, {{name}}": "Bonjour, {{name}}.", + "Help": "Aide", + "Hide": "Cacher", + "Hide Model": "Masquer le modèle", + "How can I help you today?": "Comment puis-je vous être utile aujourd'hui ?", + "Hybrid Search": "Recherche hybride", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Génération d'images (expérimental)", + "Image Generation Engine": "Moteur de génération d'images", + "Image Settings": "Paramètres de l'image", + "Images": "Images", + "Import Chats": "Importer les discussions", + "Import Documents Mapping": "Import de la correspondance des documents", + "Import Functions": "Import de fonctions", + "Import Models": "Importer des modèles", + "Import Prompts": "Importer des Enseignes", + "Import Tools": "Outils d'importation", + "Include `--api-auth` flag when running stable-diffusion-webui": "Inclure le drapeau `--api-auth` lors de l'exécution de stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Inclure le drapeau `--api` lorsque vous exécutez stable-diffusion-webui", + "Info": "Info", + "Input commands": "Entrez les commandes", + "Install from Github URL": "Installer depuis l'URL GitHub", + "Instant Auto-Send After Voice Transcription": "Envoi automatique instantané après transcription vocale", + "Interface": "Interface utilisateur", + "Invalid Tag": "Étiquette non valide", + "January": "Janvier", + "join our Discord for help.": "Rejoignez notre Discord pour obtenir de l'aide.", + "JSON": "JSON", + "JSON Preview": "Aperçu JSON", + "July": "Juillet", + "June": "Juin", + "JWT Expiration": "Expiration du jeton JWT", + "JWT Token": "Jeton JWT", + "Keep Alive": "Rester connecté", + "Keyboard shortcuts": "Raccourcis clavier", + "Knowledge": "Connaissance", + "Language": "Langue", + "large language models, locally.": "", + "Last Active": "Dernière activité", + "Last Modified": "Dernière modification", + "Light": "Lumineux", + "Listening...": "En train d'écouter...", + "LLMs can make mistakes. Verify important information.": "Les LLM peuvent faire des erreurs. Vérifiez les informations importantes.", + "Local Models": "Modèles locaux", + "LTR": "LTR", + "Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI", + "Make sure to enclose them with": "Assurez-vous de les inclure dans", + "Manage": "Gérer", + "Manage Models": "Gérer les Modèles", + "Manage Ollama Models": "Gérer les modèles Ollama", + "Manage Pipelines": "Gérer les pipelines", + "Manage Valves": "Gérer les vannes", + "March": "Mars", + "Max Tokens (num_predict)": "Tokens maximaux (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Un maximum de 3 modèles peut être téléchargé en même temps. Veuillez réessayer ultérieurement.", + "May": "Mai", + "Memories accessible by LLMs will be shown here.": "Les mémoires accessibles par les LLMs seront affichées ici.", + "Memory": "Mémoire", + "Memory added successfully": "Mémoire ajoutée avec succès", + "Memory cleared successfully": "La mémoire a été effacée avec succès", + "Memory deleted successfully": "La mémoire a été supprimée avec succès", + "Memory updated successfully": "La mémoire a été mise à jour avec succès", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Les messages que vous envoyez après avoir créé votre lien ne seront pas partagés. Les utilisateurs disposant de l'URL pourront voir le chat partagé.", + "Minimum Score": "Score minimal", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MM DD, AAAA", + "MMMM DD, YYYY HH:mm": "MM MDDD, AAAA HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "jj MM, aaaa HH:mm:ss", + "Model '{{modelName}}' has been successfully downloaded.": "Le modèle '{{modelName}}' a été téléchargé avec succès.", + "Model '{{modelTag}}' is already in queue for downloading.": "Le modèle '{{modelTag}}' est déjà dans la file d'attente pour le téléchargement.", + "Model {{modelId}} not found": "Modèle {{modelId}} introuvable", + "Model {{modelName}} is not vision capable": "Le modèle {{modelName}} n'a pas de capacités visuelles", + "Model {{name}} is now {{status}}": "Le modèle {{name}} est désormais {{status}}.", + "Model created successfully!": "Le modèle a été créé avec succès !", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Chemin du système de fichiers de modèle détecté. Le nom court du modèle est requis pour la mise à jour, l'opération ne peut pas être poursuivie.", + "Model ID": "ID du modèle", + "Model not selected": "Modèle non sélectionné", + "Model Params": "Paramètres du modèle", + "Model updated successfully": "Le modèle a été mis à jour avec succès", + "Model Whitelisting": "Liste blanche de modèles", + "Model(s) Whitelisted": "Modèle(s) Autorisé(s)", + "Modelfile Content": "Contenu du Fichier de Modèle", + "Models": "Modèles", + "More": "Plus de", + "Name": "Nom", + "Name Tag": "Étiquette de nom", + "Name your model": "Nommez votre modèle", + "New Chat": "Nouvelle conversation", + "New Password": "Nouveau mot de passe", + "No content to speak": "Rien à signaler", + "No documents found": "Aucun document trouvé", + "No file selected": "Aucun fichier sélectionné", + "No results found": "Aucun résultat trouvé", + "No search query generated": "Aucune requête de recherche générée", + "No source available": "Aucune source n'est disponible", + "No valves to update": "Aucune vanne à mettre à jour", + "None": "Aucun", + "Not factually correct": "Non factuellement correct", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Note : Si vous définissez un score minimum, seuls les documents ayant un score supérieur ou égal à ce score minimum seront retournés par la recherche.", + "Notifications": "Notifications", + "November": "Novembre", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "ID OAuth", + "October": "Octobre", + "Off": "Désactivé", + "Okay, Let's Go!": "D'accord, on y va !", + "OLED Dark": "Noir OLED", + "Ollama": "Ollama", + "Ollama API": "API Ollama", + "Ollama API disabled": "API Ollama désactivée", + "Ollama API is disabled": "L'API Ollama est désactivée", + "Ollama Version": "Version Ollama améliorée", + "On": "Activé", + "Only": "Seulement", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Seuls les caractères alphanumériques et les tirets sont autorisés dans la chaîne de commande.", + "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.": "Oups ! Un instant ! Vos fichiers sont toujours en train d'être traités. Nous les perfectionnons pour vous. Veuillez patienter, nous vous informerons dès qu'ils seront prêts.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oups ! Il semble que l'URL soit invalide. Veuillez vérifier à nouveau et réessayer.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Oops ! Il y a eu une erreur dans la réponse précédente. Veuillez réessayer ou contacter l'administrateur.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups\u00a0! Vous utilisez une méthode non prise en charge (frontend uniquement). Veuillez servir l'interface Web à partir du backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Ouvrir une nouvelle discussion", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Configuration de l'API OpenAI", + "OpenAI API Key is required.": "Une clé API OpenAI est requise.", + "OpenAI URL/Key required.": "URL/Clé OpenAI requise.", + "or": "ou", + "Other": "Autre", + "Password": "Mot de passe", + "PDF document (.pdf)": "Document au format PDF (.pdf)", + "PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)", + "pending": "en attente", + "Permission denied when accessing media devices": "Accès aux appareils multimédias refusé", + "Permission denied when accessing microphone": "Autorisation refusée lors de l'accès au micro", + "Permission denied when accessing microphone: {{error}}": "Permission refusée lors de l'accès au microphone : {{error}}", + "Personalization": "Personnalisation", + "Pin": "Épingler", + "Pinned": "Épinglé", + "Pipeline deleted successfully": "Le pipeline a été supprimé avec succès", + "Pipeline downloaded successfully": "Le pipeline a été téléchargé avec succès", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "Aucun pipelines détecté", + "Pipelines Valves": "Vannes de Pipelines", + "Plain text (.txt)": "Texte simple (.txt)", + "Playground": "Aire de jeux", + "Please carefully review the following warnings:": "", + "Positive attitude": "Attitude positive", + "Previous 30 days": "30 derniers jours", + "Previous 7 days": "7 derniers jours", + "Profile Image": "Image de profil", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (par ex. Dites-moi un fait amusant à propos de l'Empire romain)", + "Prompt Content": "Contenu du prompt", + "Prompt suggestions": "Suggestions pour le prompt", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Récupérer « {{searchValue}} » depuis Ollama.com", + "Pull a model from Ollama.com": "Télécharger un modèle depuis Ollama.com", + "Query Params": "Paramètres de requête", + "RAG Template": "Modèle RAG", + "Read Aloud": "Lire à haute voix", + "Record voice": "Enregistrer la voix", + "Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)", + "Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être", + "Regenerate": "Regénérer", + "Release Notes": "Notes de publication", + "Remove": "Retirer", + "Remove Model": "Retirer le modèle", + "Rename": "Renommer", + "Repeat Last N": "Répéter les N derniers", + "Request Mode": "Mode de Requête", + "Reranking Model": "Modèle de ré-ranking", + "Reranking model disabled": "Modèle de ré-ranking désactivé", + "Reranking model set to \"{{reranking_model}}\"": "Modèle de ré-ranking défini sur « {{reranking_model}} »", + "Reset": "Réinitialiser", + "Reset Upload Directory": "Répertoire de téléchargement réinitialisé", + "Reset Vector Storage": "Réinitialiser le stockage des vecteurs", + "Response AutoCopy to Clipboard": "Copie automatique de la réponse vers le presse-papiers", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de réponse ne peuvent pas être activées car les autorisations du site web ont été refusées. Veuillez visiter les paramètres de votre navigateur pour accorder l'accès nécessaire.", + "Role": "Rôle", + "Rosé Pine": "Pin rosé", + "Rosé Pine Dawn": "Aube de Pin Rosé", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Courir", + "Save": "Enregistrer", + "Save & Create": "Enregistrer & Créer", + "Save & Update": "Enregistrer & Mettre à jour", + "Save Tag": "", + "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": "La sauvegarde des journaux de discussion directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un instant pour télécharger et supprimer vos journaux de discussion en cliquant sur le bouton ci-dessous. Pas de soucis, vous pouvez facilement les réimporter depuis le backend via l'interface ci-dessous", + "Scan": "Scanner", + "Scan complete!": "Scan terminé !", + "Scan for documents from {{path}}": "Scanner des documents depuis {{path}}", + "Search": "Recherche", + "Search a model": "Rechercher un modèle", + "Search Chats": "Rechercher des conversations", + "Search Documents": "Recherche de documents", + "Search Functions": "Fonctions de recherche", + "Search Models": "Rechercher des modèles", + "Search Prompts": "Recherche de prompts", + "Search Query Generation Prompt": "Génération d'interrogation de recherche", + "Search Query Generation Prompt Length Threshold": "Seuil de longueur de prompt de génération de requête de recherche", + "Search Result Count": "Nombre de résultats de recherche", + "Search Tools": "Outils de recherche", + "Searched {{count}} sites_one": "Recherché {{count}} site(s)_one", + "Searched {{count}} sites_many": "Recherché {{count}} sites_many", + "Searched {{count}} sites_other": "Recherché {{count}} sites_autres", + "Searching \"{{searchQuery}}\"": "Recherche de « {{searchQuery}} »", + "Searxng Query URL": "URL de recherche Searxng", + "See readme.md for instructions": "Voir le fichier readme.md pour les instructions", + "See what's new": "Découvrez les nouvelles fonctionnalités", + "Seed": "Graine", + "Select a base model": "Sélectionnez un modèle de base", + "Select a engine": "Sélectionnez un moteur", + "Select a function": "Sélectionnez une fonction", + "Select a mode": "Choisissez un mode", + "Select a model": "Sélectionnez un modèle", + "Select a pipeline": "Sélectionnez un pipeline", + "Select a pipeline url": "Sélectionnez l'URL du pipeline", + "Select a tool": "Sélectionnez un outil", + "Select an Ollama instance": "Sélectionnez une instance Ollama", + "Select Documents": "Sélectionnez des documents", + "Select model": "Sélectionnez un modèle", + "Select only one model to call": "Sélectionnez seulement un modèle pour appeler", + "Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images", + "Send": "Envoyer", + "Send a Message": "Envoyer un message", + "Send message": "Envoyer un message", + "September": "Septembre", + "Serper API Key": "Clé API Serper", + "Serply API Key": "Clé API Serply", + "Serpstack API Key": "Clé API Serpstack", + "Server connection verified": "Connexion au serveur vérifiée", + "Set as default": "Définir comme valeur par défaut", + "Set Default Model": "Définir le modèle par défaut", + "Set embedding model (e.g. {{model}})": "Définir le modèle d'encodage (par ex. {{model}})", + "Set Image Size": "Définir la taille de l'image", + "Set reranking model (e.g. {{model}})": "Définir le modèle de reclassement (par ex. {{model}})", + "Set Steps": "Définir les étapes", + "Set Task Model": "Définir le modèle de tâche", + "Set Voice": "Définir la voix", + "Settings": "Paramètres", + "Settings saved successfully!": "Paramètres enregistrés avec succès !", + "Settings updated successfully": "Les paramètres ont été mis à jour avec succès", + "Share": "Partager", + "Share Chat": "Partage de conversation", + "Share to OpenWebUI Community": "Partager avec la communauté OpenWebUI", + "short-summary": "résumé concis", + "Show": "Montrer", + "Show Admin Details in Account Pending Overlay": "Afficher les détails de l'administrateur dans la superposition en attente du compte", + "Show Model": "Montrer le modèle", + "Show shortcuts": "Afficher les raccourcis", + "Show your support!": "Montre ton soutien !", + "Showcased creativity": "Créativité mise en avant", + "Sign in": "S'identifier", + "Sign Out": "Déconnexion", + "Sign up": "Inscrivez-vous", + "Signing in": "Connexion en cours", + "Source": "Source", + "Speech recognition error: {{error}}": "Erreur de reconnaissance vocale\u00a0: {{error}}", + "Speech-to-Text Engine": "Moteur de reconnaissance vocale", + "Stop Sequence": "Séquence d'arrêt", + "STT Model": "Modèle de STT", + "STT Settings": "Paramètres de STT", + "Submit": "Soumettre", + "Subtitle (e.g. about the Roman Empire)": "Sous-titres (par ex. sur l'Empire romain)", + "Success": "Réussite", + "Successfully updated.": "Mise à jour réussie.", + "Suggested": "Sugéré", + "Support": "", + "Support this plugin:": "", + "System": "Système", + "System Prompt": "Prompt du système", + "Tags": "Balises", + "Tap to interrupt": "Appuyez pour interrompre", + "Tavily API Key": "Clé API Tavily", + "Tell us more:": "Dites-nous en plus à ce sujet : ", + "Temperature": "Température", + "Template": "Template", + "Text Completion": "Complétion de texte", + "Text-to-Speech Engine": "Moteur de synthèse vocale", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Merci pour vos commentaires !", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Le score doit être une valeur comprise entre 0,0 (0\u00a0%) et 1,0 (100\u00a0%).", + "Theme": "Thème", + "Thinking...": "En train de réfléchir...", + "This action cannot be undone. Do you wish to continue?": "Cette action ne peut pas être annulée. Souhaitez-vous continuer ?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos conversations précieuses soient sauvegardées en toute sécurité dans votre base de données backend. Merci !", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Il s'agit d'une fonctionnalité expérimentale, elle peut ne pas fonctionner comme prévu et est sujette à modification à tout moment.", + "This setting does not sync across browsers or devices.": "Ce paramètre ne se synchronise pas entre les navigateurs ou les appareils.", + "This will delete": "Cela supprimera", + "Thorough explanation": "Explication approfondie", + "Tika": "Tika", + "Tika Server URL required.": "URL du serveur Tika requise.", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Conseil\u00a0: mettez à jour plusieurs emplacements de variables consécutivement en appuyant sur la touche Tab dans l’entrée de chat après chaque remplacement.", + "Title": "Titre", + "Title (e.g. Tell me a fun fact)": "Titre (par ex. raconte-moi un fait amusant)", + "Title Auto-Generation": "Génération automatique de titres", + "Title cannot be an empty string.": "Le titre ne peut pas être une chaîne de caractères vide.", + "Title Generation Prompt": "Prompt de génération de titre", + "to": "à", + "To access the available model names for downloading,": "Pour accéder aux noms des modèles disponibles en téléchargement,", + "To access the GGUF models available for downloading,": "Pour accéder aux modèles GGUF disponibles en téléchargement,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pour accéder à l'interface Web, veuillez contacter l'administrateur. Les administrateurs peuvent gérer les statuts des utilisateurs depuis le panneau d'administration.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Pour ajouter des documents ici, téléchargez-les d'abord dans l'espace de travail « Documents ». ", + "to chat input.": "à l'entrée de discussion.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Pour sélectionner des filtres ici, ajoutez-les d'abord à l'espace de travail « Fonctions ». ", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Pour sélectionner des toolkits ici, ajoutez-les d'abord à l'espace de travail « Outils ». ", + "Today": "Aujourd'hui", + "Toggle settings": "Basculer les paramètres", + "Toggle sidebar": "Basculer la barre latérale", + "Tokens To Keep On Context Refresh (num_keep)": "Jeton à conserver pour l'actualisation du contexte (num_keep)", + "Tool created successfully": "L'outil a été créé avec succès", + "Tool deleted successfully": "Outil supprimé avec succès", + "Tool imported successfully": "Outil importé avec succès", + "Tool updated successfully": "L'outil a été mis à jour avec succès", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Outils", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Rencontrez-vous des difficultés pour accéder à Ollama ?", + "TTS Model": "Modèle de synthèse vocale", + "TTS Settings": "Paramètres de synthèse vocale", + "TTS Voice": "Voix TTS", + "Type": "Type", + "Type Hugging Face Resolve (Download) URL": "Entrez l'URL de Téléchargement Hugging Face Resolve", + "Uh-oh! There was an issue connecting to {{provider}}.": "Oh non ! Un problème est survenu lors de la connexion à {{provider}}.", + "UI": "Interface utilisateur", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Type de fichier inconnu '{{file_type}}'. Continuons tout de même le téléchargement du fichier.", + "Unpin": "", + "Update": "Mise à jour", + "Update and Copy Link": "Mettre à jour et copier le lien", + "Update password": "Mettre à jour le mot de passe", + "Updated at": "Mise à jour le", + "Upload": "Télécharger", + "Upload a GGUF model": "Téléverser un modèle GGUF", + "Upload Files": "Télécharger des fichiers", + "Upload Pipeline": "Pipeline de téléchargement", + "Upload Progress": "Progression de l'envoi", + "URL Mode": "Mode d'URL", + "Use '#' in the prompt input to load and select your documents.": "Utilisez '#' dans l'entrée de prompt pour charger et sélectionner vos documents.", + "Use Gravatar": "Utilisez Gravatar", + "Use Initials": "Utiliser les initiales", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "utiliser mmap (Ollama)", + "user": "utilisateur", + "User location successfully retrieved.": "L'emplacement de l'utilisateur a été récupéré avec succès.", + "User Permissions": "Permissions utilisateur", + "Users": "Utilisateurs", + "Utilize": "Utilisez", + "Valid time units:": "Unités de temps valides\u00a0:", + "Valves": "Vannes", + "Valves updated": "Vannes mises à jour", + "Valves updated successfully": "Les vannes ont été mises à jour avec succès", + "variable": "variable", + "variable to have them replaced with clipboard content.": "variable pour qu'elles soient remplacées par le contenu du presse-papiers.", + "Version": "Version améliorée", + "Voice": "Voix", + "Warning": "Avertissement !", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avertissement : Si vous mettez à jour ou modifiez votre modèle d'encodage, vous devrez réimporter tous les documents.", + "Web": "Web", + "Web API": "API Web", + "Web Loader Settings": "Paramètres du chargeur web", + "Web Params": "Paramètres Web", + "Web Search": "Recherche Web", + "Web Search Engine": "Moteur de recherche Web", + "Webhook URL": "URL du webhook", + "WebUI Settings": "Paramètres de WebUI", + "WebUI will make requests to": "WebUI effectuera des requêtes vers", + "What’s New in": "Quoi de neuf", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Lorsque l'historique est désactivé, les nouvelles conversations sur ce navigateur ne seront pas enregistrés dans votre historique sur aucun de vos appareils.", + "Whisper (Local)": "Whisper (local)", + "Widescreen Mode": "Mode Grand Écran", + "Workspace": "Espace de travail", + "Write a prompt suggestion (e.g. Who are you?)": "Écrivez une suggestion de prompt (par exemple : Qui êtes-vous ?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Rédigez un résumé de 50 mots qui résume [sujet ou mot-clé].", + "Yesterday": "Hier", + "You": "Vous", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Vous pouvez personnaliser vos interactions avec les LLM en ajoutant des souvenirs via le bouton 'Gérer' ci-dessous, ce qui les rendra plus utiles et adaptés à vos besoins.", + "You cannot clone a base model": "Vous ne pouvez pas cloner un modèle de base", + "You have no archived conversations.": "Vous n'avez aucune conversation archivée", + "You have shared this chat": "Vous avez partagé cette conversation.", + "You're a helpful assistant.": "Vous êtes un assistant serviable.", + "You're now logged in.": "Vous êtes désormais connecté.", + "Your account status is currently pending activation.": "Votre statut de compte est actuellement en attente d'activation.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "Paramètres de l'outil de téléchargement YouTube" +} diff --git a/src/lib/i18n/locales/fr-FR/translation.json b/src/lib/i18n/locales/fr-FR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..c2d5ecd66c64e22ed0cfe545f28beea023291a79 --- /dev/null +++ b/src/lib/i18n/locales/fr-FR/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": " 's', 'm', 'h', 'd', 'w' ou '-1' pour une durée illimitée.", + "(Beta)": "(Version bêta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(par ex. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(par exemple `sh webui.sh --api`)", + "(latest)": "(dernier)", + "{{ models }}": "{{ modèles }}", + "{{ owner }}: You cannot delete a base model": "{{ propriétaire }} : Vous ne pouvez pas supprimer un modèle de base.", + "{{modelName}} is thinking...": "{{modelName}} est en train de réfléchir...", + "{{user}}'s Chats": "Discussions de {{user}}", + "{{webUIName}} Backend Required": "Backend {{webUIName}} requis", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modèle de tâche est utilisé lors de l’exécution de tâches telles que la génération de titres pour les conversations et les requêtes de recherche sur le web.", + "a user": "un utilisateur", + "About": "À propos", + "Account": "Compte", + "Account Activation Pending": "Activation du compte en attente", + "Accurate information": "Information exacte", + "Actions": "", + "Active Users": "Utilisateurs actifs", + "Add": "Ajouter", + "Add a model id": "Ajouter un identifiant de modèle", + "Add a short description about what this model does": "Ajoutez une brève description de ce que fait ce modèle.", + "Add a short title for this prompt": "Ajoutez un bref titre pour cette prompt.", + "Add a tag": "Ajouter une balise", + "Add custom prompt": "Ajouter une prompt personnalisée", + "Add Docs": "Ajouter de la documentation", + "Add Files": "Ajouter des fichiers", + "Add Memory": "Ajouter de la mémoire", + "Add message": "Ajouter un message", + "Add Model": "Ajouter un modèle", + "Add Tag": "", + "Add Tags": "Ajouter des balises", + "Add User": "Ajouter un Utilisateur", + "Adjusting these settings will apply changes universally to all users.": "L'ajustement de ces paramètres appliquera universellement les changements à tous les utilisateurs.", + "admin": "administrateur", + "Admin": "Administrateur", + "Admin Panel": "Tableau de bord administrateur", + "Admin Settings": "Paramètres d'administration", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Les administrateurs ont accès à tous les outils en tout temps ; les utilisateurs ont besoin d'outils affectés par modèle dans l'espace de travail.", + "Advanced Parameters": "Paramètres avancés", + "Advanced Params": "Paramètres avancés", + "all": "toutes", + "All Documents": "Tous les documents", + "All Users": "Tous les Utilisateurs", + "Allow": "Autoriser", + "Allow Chat Deletion": "Autoriser la suppression de l'historique de chat", + "Allow non-local voices": "Autoriser les voix non locales", + "Allow User Location": "Autoriser l'emplacement de l'utilisateur", + "Allow Voice Interruption in Call": "Autoriser l'interruption vocale pendant un appel", + "alphanumeric characters and hyphens": "caractères alphanumériques et tirets", + "Already have an account?": "Avez-vous déjà un compte ?", + "an assistant": "un assistant", + "and": "et", + "and create a new shared link.": "et créer un nouveau lien partagé.", + "API Base URL": "URL de base de l'API", + "API Key": "Clé d'API", + "API Key created.": "Clé d'API générée.", + "API keys": "Clés d'API", + "April": "Avril", + "Archive": "Archivage", + "Archive All Chats": "Archiver toutes les conversations", + "Archived Chats": "Conversations archivées", + "are allowed - Activate this command by typing": "sont autorisés - Activer cette commande en tapant", + "Are you sure?": "Êtes-vous certain ?", + "Attach file": "Joindre un document", + "Attention to detail": "Attention aux détails", + "Audio": "Audio", + "Audio settings updated successfully": "Les paramètres audio ont été mis à jour avec succès", + "August": "Août", + "Auto-playback response": "Réponse de lecture automatique", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Chaîne d'authentification de l'API", + "AUTOMATIC1111 Base URL": "URL de base AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "L'URL de base {AUTOMATIC1111} est requise.", + "available!": "disponible !", + "Back": "Retour en arrière", + "Bad Response": "Mauvaise réponse", + "Banners": "Banniers", + "Base Model (From)": "Modèle de base (à partir de)", + "Batch Size (num_batch)": "Taille du lot (num_batch)", + "before": "avant", + "Being lazy": "Être fainéant", + "Brave Search API Key": "Clé API Brave Search", + "Bypass SSL verification for Websites": "Bypasser la vérification SSL pour les sites web", + "Call": "Appeler", + "Call feature is not supported when using Web STT engine": "La fonction d'appel n'est pas prise en charge lors de l'utilisation du moteur Web STT", + "Camera": "Appareil photo", + "Cancel": "Annuler", + "Capabilities": "Capacités", + "Change Password": "Changer le mot de passe", + "Chat": "Chat", + "Chat Background Image": "Image d'arrière-plan de la fenêtre de chat", + "Chat Bubble UI": "Bulles de discussion", + "Chat Controls": "", + "Chat direction": "Direction du chat", + "Chat History": "Historique de discussion", + "Chat History is off for this browser.": "L'historique de chat est désactivé pour ce navigateur", + "Chats": "Conversations", + "Check Again": "Vérifiez à nouveau.", + "Check for updates": "Vérifier les mises à jour disponibles", + "Checking for updates...": "Recherche de mises à jour...", + "Choose a model before saving...": "Choisissez un modèle avant de sauvegarder...", + "Chunk Overlap": "Chevauchement de blocs", + "Chunk Params": "Paramètres d'encombrement", + "Chunk Size": "Taille de bloc", + "Citation": "Citation", + "Clear memory": "Libérer la mémoire", + "Click here for help.": "Cliquez ici pour obtenir de l'aide.", + "Click here to": "Cliquez ici pour", + "Click here to download user import template file.": "Cliquez ici pour télécharger le fichier modèle d'importation utilisateur.", + "Click here to select": "Cliquez ici pour sélectionner", + "Click here to select a csv file.": "Cliquez ici pour sélectionner un fichier CSV.", + "Click here to select a py file.": "Cliquez ici pour sélectionner un fichier .py.", + "Click here to select documents.": "Cliquez ici pour sélectionner les documents.", + "click here.": "cliquez ici.", + "Click on the user role button to change a user's role.": "Cliquez sur le bouton de rôle d'utilisateur pour modifier le rôle d'un utilisateur.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "L'autorisation d'écriture du presse-papier a été refusée. Veuillez vérifier les paramètres de votre navigateur pour accorder l'accès nécessaire.", + "Clone": "Copie conforme", + "Close": "Fermer", + "Code formatted successfully": "Le code a été formaté avec succès", + "Collection": "Collection", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL de base ComfyUI", + "ComfyUI Base URL is required.": "L'URL de base ComfyUI est requise.", + "Command": "Commande", + "Concurrent Requests": "Demandes concurrentes", + "Confirm": "Confirmer", + "Confirm Password": "Confirmer le mot de passe", + "Confirm your action": "Confirmez votre action", + "Connections": "Connexions", + "Contact Admin for WebUI Access": "Contacter l'administrateur pour l'accès à l'interface Web", + "Content": "Contenu", + "Content Extraction": "", + "Context Length": "Longueur du contexte", + "Continue Response": "Continuer la réponse", + "Continue with {{provider}}": "Continuer avec {{provider}}", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL du chat copiée dans le presse-papiers\u00a0!", + "Copy": "Copie", + "Copy last code block": "Copier le dernier bloc de code", + "Copy last response": "Copier la dernière réponse", + "Copy Link": "Copier le lien", + "Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !", + "Create a model": "Créer un modèle", + "Create Account": "Créer un compte", + "Create new key": "Créer une nouvelle clé principale", + "Create new secret key": "Créer une nouvelle clé secrète", + "Created at": "Créé à", + "Created At": "Créé le", + "Created by": "Créé par", + "CSV Import": "Import CSV", + "Current Model": "Modèle actuel amélioré", + "Current Password": "Mot de passe actuel", + "Custom": "Sur mesure", + "Customize models for a specific purpose": "Personnaliser les modèles pour une fonction spécifique", + "Dark": "Obscur", + "Dashboard": "Tableau de bord", + "Database": "Base de données", + "December": "Décembre", + "Default": "Par défaut", + "Default (Automatic1111)": "Par défaut (Automatic1111)", + "Default (SentenceTransformers)": "Par défaut (Sentence Transformers)", + "Default Model": "Modèle standard", + "Default model updated": "Modèle par défaut mis à jour", + "Default Prompt Suggestions": "Suggestions de prompts par défaut", + "Default User Role": "Rôle utilisateur par défaut", + "delete": "supprimer", + "Delete": "Supprimer", + "Delete a model": "Supprimer un modèle", + "Delete All Chats": "Supprimer toutes les conversations", + "Delete chat": "Supprimer la conversation", + "Delete Chat": "Supprimer la Conversation", + "Delete chat?": "Supprimer la conversation ?", + "Delete Doc": "", + "Delete function?": "Supprimer la fonction ?", + "Delete prompt?": "Supprimer la prompt ?", + "delete this link": "supprimer ce lien", + "Delete tool?": "Effacer l'outil ?", + "Delete User": "Supprimer le compte d'utilisateur", + "Deleted {{deleteModelTag}}": "Supprimé {{deleteModelTag}}", + "Deleted {{name}}": "Supprimé {{name}}", + "Description": "Description", + "Didn't fully follow instructions": "N'a pas entièrement respecté les instructions", + "Disabled": "", + "Discover a function": "Découvrez une fonction", + "Discover a model": "Découvrir un modèle", + "Discover a prompt": "Découvrir une suggestion", + "Discover a tool": "Découvrez un outil", + "Discover, download, and explore custom functions": "Découvrez, téléchargez et explorez des fonctions personnalisées", + "Discover, download, and explore custom prompts": "Découvrez, téléchargez et explorez des prompts personnalisés", + "Discover, download, and explore custom tools": "Découvrez, téléchargez et explorez des outils personnalisés", + "Discover, download, and explore model presets": "Découvrir, télécharger et explorer des préréglages de modèles", + "Dismissible": "Fermeture", + "Display Emoji in Call": "Afficher les emojis pendant l'appel", + "Display the username instead of You in the Chat": "Afficher le nom d'utilisateur à la place de \"Vous\" dans le Chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Document", + "Document Settings": "Paramètres du document", + "Documentation": "Documentation", + "Documents": "Documents", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ne fait aucune connexion externe et garde vos données en sécurité sur votre serveur local.", + "Don't Allow": "Ne pas autoriser", + "Don't have an account?": "Vous n'avez pas de compte ?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "N'apprécie pas le style", + "Done": "Terminé", + "Download": "Télécharger", + "Download canceled": "Téléchargement annulé", + "Download Database": "Télécharger la base de données", + "Drop any files here to add to the conversation": "Déposez des fichiers ici pour les ajouter à la conversation", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "par ex. '30s', '10 min'. Les unités de temps valides sont 's', 'm', 'h'.", + "Edit": "Modifier", + "Edit Doc": "Modifier le document", + "Edit Memory": "Modifier la mémoire", + "Edit User": "Modifier l'utilisateur", + "ElevenLabs": "", + "Email": "E-mail", + "Embedding Batch Size": "Taille du lot d'encodage", + "Embedding Model": "Modèle d'embedding", + "Embedding Model Engine": "Moteur de modèle d'encodage", + "Embedding model set to \"{{embedding_model}}\"": "Modèle d'encodage défini sur « {{embedding_model}} »", + "Enable Chat History": "Activer l'historique de conversation", + "Enable Community Sharing": "Activer le partage communautaire", + "Enable New Sign Ups": "Activer les nouvelles inscriptions", + "Enable Web Search": "Activer la recherche web", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Vérifiez que votre fichier CSV comprenne les 4 colonnes dans cet ordre : Name, Email, Password, Role.", + "Enter {{role}} message here": "Entrez le message {{role}} ici", + "Enter a detail about yourself for your LLMs to recall": "Saisissez un détail sur vous-même que vos LLMs pourront se rappeler", + "Enter api auth string (e.g. username:password)": "Entrez la chaîne d'authentification de l'API (par ex. nom d'utilisateur:mot de passe)", + "Enter Brave Search API Key": "Entrez la clé API Brave Search", + "Enter Chunk Overlap": "Entrez le chevauchement de chunk", + "Enter Chunk Size": "Entrez la taille de bloc", + "Enter Github Raw URL": "Entrez l'URL brute de GitHub", + "Enter Google PSE API Key": "Entrez la clé API Google PSE", + "Enter Google PSE Engine Id": "Entrez l'identifiant du moteur Google PSE", + "Enter Image Size (e.g. 512x512)": "Entrez la taille de l'image (par ex. 512x512)", + "Enter language codes": "Entrez les codes de langue", + "Enter model tag (e.g. {{modelTag}})": "Entrez l'étiquette du modèle (par ex. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Entrez le nombre de pas (par ex. 50)", + "Enter Score": "Entrez votre score", + "Enter Searxng Query URL": "Entrez l'URL de la requête Searxng", + "Enter Serper API Key": "Entrez la clé API Serper", + "Enter Serply API Key": "Entrez la clé API Serply", + "Enter Serpstack API Key": "Entrez la clé API Serpstack", + "Enter stop sequence": "Entrez la séquence d'arrêt", + "Enter system prompt": "", + "Enter Tavily API Key": "Entrez la clé API Tavily", + "Enter Tika Server URL": "", + "Enter Top K": "Entrez les Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Entrez l'URL (par ex. {http://127.0.0.1:7860/})", + "Enter URL (e.g. http://localhost:11434)": "Entrez l'URL (par ex. http://localhost:11434)", + "Enter Your Email": "Entrez votre adresse e-mail", + "Enter Your Full Name": "Entrez votre nom complet", + "Enter your message": "", + "Enter Your Password": "Entrez votre mot de passe", + "Enter Your Role": "Entrez votre rôle", + "Error": "Erreur", + "Experimental": "Expérimental", + "Export": "Exportation", + "Export All Chats (All Users)": "Exporter toutes les conversations (tous les utilisateurs)", + "Export chat (.json)": "Exporter la discussion (.json)", + "Export Chats": "Exporter les conversations", + "Export Documents Mapping": "Exportez la correspondance des documents", + "Export Functions": "Exportez les Fonctions", + "Export LiteLLM config.yaml": "Exportez le fichier LiteLLM config.yaml", + "Export Models": "Exporter les modèles", + "Export Prompts": "Exporter les Prompts", + "Export Tools": "Outils d'exportation", + "External Models": "Modèles externes", + "Failed to create API Key.": "Échec de la création de la clé API.", + "Failed to read clipboard contents": "Échec de la lecture du contenu du presse-papiers", + "Failed to update settings": "Échec de la mise à jour des paramètres", + "February": "Février", + "Feel free to add specific details": "N'hésitez pas à ajouter des détails spécifiques", + "File": "Fichier", + "File Mode": "Mode fichier", + "File not found.": "Fichier introuvable.", + "Files": "", + "Filter is now globally disabled": "Le filtre est maintenant désactivé globalement", + "Filter is now globally enabled": "Le filtre est désormais activé globalement", + "Filters": "Filtres", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Spoofing détecté : impossible d'utiliser les initiales comme avatar. Retour à l'image de profil par défaut.", + "Fluidly stream large external response chunks": "Diffuser de manière fluide de larges portions de réponses externes", + "Focus chat input": "Se concentrer sur le chat en entrée", + "Followed instructions perfectly": "A parfaitement suivi les instructions", + "Form": "Formulaire", + "Format your variables using square brackets like this:": "Formatez vos variables en utilisant des crochets comme suit :", + "Frequency Penalty": "Pénalité de fréquence", + "Function created successfully": "La fonction a été créée avec succès", + "Function deleted successfully": "Fonction supprimée avec succès", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "La fonction a été mise à jour avec succès", + "Functions": "Fonctions", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Fonctions importées avec succès", + "General": "Général", + "General Settings": "Paramètres Généraux", + "Generate Image": "Générer une image", + "Generating search query": "Génération d'une requête de recherche", + "Generation Info": "Informations sur la génération", + "Get up and running with": "", + "Global": "Mondial", + "Good Response": "Bonne réponse", + "Google PSE API Key": "Clé API Google PSE", + "Google PSE Engine Id": "ID du moteur de recherche personnalisé de Google", + "h:mm a": "h:mm a", + "has no conversations.": "n'a aucune conversation.", + "Hello, {{name}}": "Bonjour, {{name}}.", + "Help": "Aide", + "Hide": "Cacher", + "Hide Model": "Masquer le modèle", + "How can I help you today?": "Comment puis-je vous être utile aujourd'hui ?", + "Hybrid Search": "Recherche hybride", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Génération d'images (expérimental)", + "Image Generation Engine": "Moteur de génération d'images", + "Image Settings": "Paramètres de l'image", + "Images": "Images", + "Import Chats": "Importer les discussions", + "Import Documents Mapping": "Import de la correspondance des documents", + "Import Functions": "Import de fonctions", + "Import Models": "Importer des modèles", + "Import Prompts": "Importer des Enseignes", + "Import Tools": "Outils d'importation", + "Include `--api-auth` flag when running stable-diffusion-webui": "Inclure le drapeau `--api-auth` lors de l'exécution de stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Inclure le drapeau `--api` lorsque vous exécutez stable-diffusion-webui", + "Info": "Info", + "Input commands": "Entrez les commandes", + "Install from Github URL": "Installer depuis l'URL GitHub", + "Instant Auto-Send After Voice Transcription": "Envoi automatique instantané après transcription vocale", + "Interface": "Interface utilisateur", + "Invalid Tag": "Étiquette non valide", + "January": "Janvier", + "join our Discord for help.": "Rejoignez notre Discord pour obtenir de l'aide.", + "JSON": "JSON", + "JSON Preview": "Aperçu JSON", + "July": "Juillet", + "June": "Juin", + "JWT Expiration": "Expiration du jeton JWT", + "JWT Token": "Jeton JWT", + "Keep Alive": "Rester connecté", + "Keyboard shortcuts": "Raccourcis clavier", + "Knowledge": "Connaissance", + "Language": "Langue", + "large language models, locally.": "", + "Last Active": "Dernière activité", + "Last Modified": "Dernière modification", + "Light": "Lumineux", + "Listening...": "En train d'écouter...", + "LLMs can make mistakes. Verify important information.": "Les LLM peuvent faire des erreurs. Vérifiez les informations importantes.", + "Local Models": "Modèles locaux", + "LTR": "LTR", + "Made by OpenWebUI Community": "Réalisé par la communauté OpenWebUI", + "Make sure to enclose them with": "Assurez-vous de les inclure dans", + "Manage": "Gérer", + "Manage Models": "Gérer les Modèles", + "Manage Ollama Models": "Gérer les modèles Ollama", + "Manage Pipelines": "Gérer les pipelines", + "Manage Valves": "Gérer les vannes", + "March": "Mars", + "Max Tokens (num_predict)": "Tokens maximaux (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Un maximum de 3 modèles peut être téléchargé en même temps. Veuillez réessayer ultérieurement.", + "May": "Mai", + "Memories accessible by LLMs will be shown here.": "Les mémoires accessibles par les LLMs seront affichées ici.", + "Memory": "Mémoire", + "Memory added successfully": "Mémoire ajoutée avec succès", + "Memory cleared successfully": "La mémoire a été effacée avec succès", + "Memory deleted successfully": "La mémoire a été supprimée avec succès", + "Memory updated successfully": "La mémoire a été mise à jour avec succès", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Les messages que vous envoyez après avoir créé votre lien ne seront pas partagés. Les utilisateurs disposant de l'URL pourront voir le chat partagé.", + "Minimum Score": "Score minimal", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MM DD, AAAA", + "MMMM DD, YYYY HH:mm": "MM MDDD, AAAA HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "jj MM, aaaa HH:mm:ss", + "Model '{{modelName}}' has been successfully downloaded.": "Le modèle '{{modelName}}' a été téléchargé avec succès.", + "Model '{{modelTag}}' is already in queue for downloading.": "Le modèle '{{modelTag}}' est déjà dans la file d'attente pour le téléchargement.", + "Model {{modelId}} not found": "Modèle {{modelId}} introuvable", + "Model {{modelName}} is not vision capable": "Le modèle {{modelName}} n'a pas de capacités visuelles", + "Model {{name}} is now {{status}}": "Le modèle {{name}} est désormais {{status}}.", + "Model created successfully!": "Le modèle a été créé avec succès !", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Chemin du système de fichiers de modèle détecté. Le nom court du modèle est requis pour la mise à jour, l'opération ne peut pas être poursuivie.", + "Model ID": "ID du modèle", + "Model not selected": "Modèle non sélectionné", + "Model Params": "Paramètres du modèle", + "Model updated successfully": "Le modèle a été mis à jour avec succès", + "Model Whitelisting": "Liste blanche de modèles", + "Model(s) Whitelisted": "Modèle(s) Autorisé(s)", + "Modelfile Content": "Contenu du Fichier de Modèle", + "Models": "Modèles", + "More": "Plus de", + "Name": "Nom", + "Name Tag": "Étiquette de nom", + "Name your model": "Nommez votre modèle", + "New Chat": "Nouvelle conversation", + "New Password": "Nouveau mot de passe", + "No content to speak": "Rien à signaler", + "No documents found": "Aucun document trouvé", + "No file selected": "Aucun fichier sélectionné", + "No results found": "Aucun résultat trouvé", + "No search query generated": "Aucune requête de recherche générée", + "No source available": "Aucune source n'est disponible", + "No valves to update": "Aucune vanne à mettre à jour", + "None": "Aucun", + "Not factually correct": "Non factuellement correct", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Note : Si vous définissez un score minimum, seuls les documents ayant un score supérieur ou égal à ce score minimum seront retournés par la recherche.", + "Notifications": "Notifications", + "November": "Novembre", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "ID OAuth", + "October": "Octobre", + "Off": "Désactivé", + "Okay, Let's Go!": "D'accord, on y va !", + "OLED Dark": "Noir OLED", + "Ollama": "Ollama", + "Ollama API": "API Ollama", + "Ollama API disabled": "API Ollama désactivée", + "Ollama API is disabled": "L'API Ollama est désactivée", + "Ollama Version": "Version Ollama améliorée", + "On": "Activé", + "Only": "Seulement", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Seuls les caractères alphanumériques et les tirets sont autorisés dans la chaîne de commande.", + "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.": "Oups ! Un instant ! Vos fichiers sont toujours en train d'être traités. Nous les perfectionnons pour vous. Veuillez patienter, nous vous informerons dès qu'ils seront prêts.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oups ! Il semble que l'URL soit invalide. Veuillez vérifier à nouveau et réessayer.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Oops ! Il y a eu une erreur dans la réponse précédente. Veuillez réessayer ou contacter l'administrateur.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oups\u00a0! Vous utilisez une méthode non prise en charge (frontend uniquement). Veuillez servir l'interface Web à partir du backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Ouvrir une nouvelle discussion", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "La version Open WebUI (v{{OPEN_WEBUI_VERSION}}) est inférieure à la version requise (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Configuration de l'API OpenAI", + "OpenAI API Key is required.": "Une clé API OpenAI est requise.", + "OpenAI URL/Key required.": "URL/Clé OpenAI requise.", + "or": "ou", + "Other": "Autre", + "Password": "Mot de passe", + "PDF document (.pdf)": "Document au format PDF (.pdf)", + "PDF Extract Images (OCR)": "Extraction d'images PDF (OCR)", + "pending": "en attente", + "Permission denied when accessing media devices": "Accès aux appareils multimédias refusé", + "Permission denied when accessing microphone": "Autorisation refusée lors de l'accès au micro", + "Permission denied when accessing microphone: {{error}}": "Permission refusée lors de l'accès au microphone : {{error}}", + "Personalization": "Personnalisation", + "Pin": "Épingler", + "Pinned": "Épinglé", + "Pipeline deleted successfully": "Le pipeline a été supprimé avec succès", + "Pipeline downloaded successfully": "Le pipeline a été téléchargé avec succès", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "Aucun pipelines détecté", + "Pipelines Valves": "Vannes de Pipelines", + "Plain text (.txt)": "Texte simple (.txt)", + "Playground": "Aire de jeux", + "Please carefully review the following warnings:": "", + "Positive attitude": "Attitude positive", + "Previous 30 days": "30 derniers jours", + "Previous 7 days": "7 derniers jours", + "Profile Image": "Image de profil", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (par ex. Dites-moi un fait amusant à propos de l'Empire romain)", + "Prompt Content": "Contenu du prompt", + "Prompt suggestions": "Suggestions pour le prompt", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Récupérer « {{searchValue}} » depuis Ollama.com", + "Pull a model from Ollama.com": "Télécharger un modèle depuis Ollama.com", + "Query Params": "Paramètres de requête", + "RAG Template": "Modèle RAG", + "Read Aloud": "Lire à haute voix", + "Record voice": "Enregistrer la voix", + "Redirecting you to OpenWebUI Community": "Redirection vers la communauté OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Désignez-vous comme « Utilisateur » (par ex. « L'utilisateur apprend l'espagnol »)", + "Refused when it shouldn't have": "Refusé alors qu'il n'aurait pas dû l'être", + "Regenerate": "Regénérer", + "Release Notes": "Notes de publication", + "Remove": "Retirer", + "Remove Model": "Retirer le modèle", + "Rename": "Renommer", + "Repeat Last N": "Répéter les N derniers", + "Request Mode": "Mode de Requête", + "Reranking Model": "Modèle de ré-ranking", + "Reranking model disabled": "Modèle de ré-ranking désactivé", + "Reranking model set to \"{{reranking_model}}\"": "Modèle de ré-ranking défini sur « {{reranking_model}} »", + "Reset": "Réinitialiser", + "Reset Upload Directory": "Répertoire de téléchargement réinitialisé", + "Reset Vector Storage": "Réinitialiser le stockage des vecteurs", + "Response AutoCopy to Clipboard": "Copie automatique de la réponse vers le presse-papiers", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Les notifications de réponse ne peuvent pas être activées car les autorisations du site web ont été refusées. Veuillez visiter les paramètres de votre navigateur pour accorder l'accès nécessaire.", + "Role": "Rôle", + "Rosé Pine": "Pin rosé", + "Rosé Pine Dawn": "Aube de Pin Rosé", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Courir", + "Save": "Enregistrer", + "Save & Create": "Enregistrer & Créer", + "Save & Update": "Enregistrer & Mettre à jour", + "Save Tag": "", + "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": "La sauvegarde des journaux de discussion directement dans le stockage de votre navigateur n'est plus prise en charge. Veuillez prendre un instant pour télécharger et supprimer vos journaux de discussion en cliquant sur le bouton ci-dessous. Pas de soucis, vous pouvez facilement les réimporter depuis le backend via l'interface ci-dessous", + "Scan": "Scanner", + "Scan complete!": "Scan terminé !", + "Scan for documents from {{path}}": "Scanner des documents depuis {{path}}", + "Search": "Recherche", + "Search a model": "Rechercher un modèle", + "Search Chats": "Rechercher des conversations", + "Search Documents": "Recherche de documents", + "Search Functions": "Fonctions de recherche", + "Search Models": "Rechercher des modèles", + "Search Prompts": "Recherche de prompts", + "Search Query Generation Prompt": "Génération d'interrogation de recherche", + "Search Query Generation Prompt Length Threshold": "Seuil de longueur de prompt de génération de requête de recherche", + "Search Result Count": "Nombre de résultats de recherche", + "Search Tools": "Outils de recherche", + "Searched {{count}} sites_one": "Recherché {{count}} site(s)_one", + "Searched {{count}} sites_many": "Recherché {{count}} sites_many", + "Searched {{count}} sites_other": "Recherché {{count}} sites_autres", + "Searching \"{{searchQuery}}\"": "Recherche de « {{searchQuery}} »", + "Searxng Query URL": "URL de recherche Searxng", + "See readme.md for instructions": "Voir le fichier readme.md pour les instructions", + "See what's new": "Découvrez les nouvelles fonctionnalités", + "Seed": "Graine", + "Select a base model": "Sélectionnez un modèle de base", + "Select a engine": "Sélectionnez un moteur", + "Select a function": "Sélectionnez une fonction", + "Select a mode": "Choisissez un mode", + "Select a model": "Sélectionnez un modèle", + "Select a pipeline": "Sélectionnez un pipeline", + "Select a pipeline url": "Sélectionnez l'URL du pipeline", + "Select a tool": "Sélectionnez un outil", + "Select an Ollama instance": "Sélectionnez une instance Ollama", + "Select Documents": "Sélectionnez des documents", + "Select model": "Sélectionnez un modèle", + "Select only one model to call": "Sélectionnez seulement un modèle pour appeler", + "Selected model(s) do not support image inputs": "Les modèle(s) sélectionné(s) ne prennent pas en charge les entrées d'images", + "Send": "Envoyer", + "Send a Message": "Envoyer un message", + "Send message": "Envoyer un message", + "September": "Septembre", + "Serper API Key": "Clé API Serper", + "Serply API Key": "Clé API Serply", + "Serpstack API Key": "Clé API Serpstack", + "Server connection verified": "Connexion au serveur vérifiée", + "Set as default": "Définir comme valeur par défaut", + "Set Default Model": "Définir le modèle par défaut", + "Set embedding model (e.g. {{model}})": "Définir le modèle d'encodage (par ex. {{model}})", + "Set Image Size": "Définir la taille de l'image", + "Set reranking model (e.g. {{model}})": "Définir le modèle de reclassement (par ex. {{model}})", + "Set Steps": "Définir les étapes", + "Set Task Model": "Définir le modèle de tâche", + "Set Voice": "Définir la voix", + "Settings": "Paramètres", + "Settings saved successfully!": "Paramètres enregistrés avec succès !", + "Settings updated successfully": "Les paramètres ont été mis à jour avec succès", + "Share": "Partager", + "Share Chat": "Partage de conversation", + "Share to OpenWebUI Community": "Partager avec la communauté OpenWebUI", + "short-summary": "résumé concis", + "Show": "Montrer", + "Show Admin Details in Account Pending Overlay": "Afficher les détails de l'administrateur dans la superposition en attente du compte", + "Show Model": "Montrer le modèle", + "Show shortcuts": "Afficher les raccourcis", + "Show your support!": "Montre ton soutien !", + "Showcased creativity": "Créativité mise en avant", + "Sign in": "S'identifier", + "Sign Out": "Déconnexion", + "Sign up": "Inscrivez-vous", + "Signing in": "Connexion en cours", + "Source": "Source", + "Speech recognition error: {{error}}": "Erreur de reconnaissance vocale\u00a0: {{error}}", + "Speech-to-Text Engine": "Moteur de reconnaissance vocale", + "Stop Sequence": "Séquence d'arrêt", + "STT Model": "Modèle de STT", + "STT Settings": "Paramètres de STT", + "Submit": "Soumettre", + "Subtitle (e.g. about the Roman Empire)": "Sous-titres (par ex. sur l'Empire romain)", + "Success": "Réussite", + "Successfully updated.": "Mise à jour réussie.", + "Suggested": "Sugéré", + "Support": "", + "Support this plugin:": "", + "System": "Système", + "System Prompt": "Prompt du système", + "Tags": "Balises", + "Tap to interrupt": "Appuyez pour interrompre", + "Tavily API Key": "Clé API Tavily", + "Tell us more:": "Dites-nous en plus à ce sujet : ", + "Temperature": "Température", + "Template": "Template", + "Text Completion": "Complétion de texte", + "Text-to-Speech Engine": "Moteur de synthèse vocale", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Merci pour vos commentaires !", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Le score doit être une valeur comprise entre 0,0 (0\u00a0%) et 1,0 (100\u00a0%).", + "Theme": "Thème", + "Thinking...": "En train de réfléchir...", + "This action cannot be undone. Do you wish to continue?": "Cette action ne peut pas être annulée. Souhaitez-vous continuer ?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Cela garantit que vos conversations précieuses soient sauvegardées en toute sécurité dans votre base de données backend. Merci !", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Il s'agit d'une fonctionnalité expérimentale, elle peut ne pas fonctionner comme prévu et est sujette à modification à tout moment.", + "This setting does not sync across browsers or devices.": "Ce paramètre ne se synchronise pas entre les navigateurs ou les appareils.", + "This will delete": "Cela supprimera", + "Thorough explanation": "Explication approfondie", + "Tika": "Tika", + "Tika Server URL required.": "URL du serveur Tika requise.", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Conseil\u00a0: mettez à jour plusieurs emplacements de variables consécutivement en appuyant sur la touche Tab dans l’entrée de chat après chaque remplacement.", + "Title": "Titre", + "Title (e.g. Tell me a fun fact)": "Titre (par ex. raconte-moi un fait amusant)", + "Title Auto-Generation": "Génération automatique de titres", + "Title cannot be an empty string.": "Le titre ne peut pas être une chaîne de caractères vide.", + "Title Generation Prompt": "Prompt de génération de titre", + "to": "à", + "To access the available model names for downloading,": "Pour accéder aux noms des modèles disponibles en téléchargement,", + "To access the GGUF models available for downloading,": "Pour accéder aux modèles GGUF disponibles en téléchargement,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Pour accéder à l'interface Web, veuillez contacter l'administrateur. Les administrateurs peuvent gérer les statuts des utilisateurs depuis le panneau d'administration.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Pour ajouter des documents ici, téléchargez-les d'abord dans l'espace de travail « Documents ». ", + "to chat input.": "à l'entrée de discussion.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Pour sélectionner des filtres ici, ajoutez-les d'abord à l'espace de travail « Fonctions ». ", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Pour sélectionner des toolkits ici, ajoutez-les d'abord à l'espace de travail « Outils ». ", + "Today": "Aujourd'hui", + "Toggle settings": "Basculer les paramètres", + "Toggle sidebar": "Basculer la barre latérale", + "Tokens To Keep On Context Refresh (num_keep)": "Jeton à conserver pour l'actualisation du contexte (num_keep)", + "Tool created successfully": "L'outil a été créé avec succès", + "Tool deleted successfully": "Outil supprimé avec succès", + "Tool imported successfully": "Outil importé avec succès", + "Tool updated successfully": "L'outil a été mis à jour avec succès", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Outils", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Rencontrez-vous des difficultés pour accéder à Ollama ?", + "TTS Model": "Modèle de synthèse vocale", + "TTS Settings": "Paramètres de synthèse vocale", + "TTS Voice": "Voix TTS", + "Type": "Type", + "Type Hugging Face Resolve (Download) URL": "Entrez l'URL de Téléchargement Hugging Face Resolve", + "Uh-oh! There was an issue connecting to {{provider}}.": "Oh non ! Un problème est survenu lors de la connexion à {{provider}}.", + "UI": "Interface utilisateur", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Type de fichier inconnu '{{file_type}}'. Continuons tout de même le téléchargement du fichier.", + "Unpin": "", + "Update": "Mise à jour", + "Update and Copy Link": "Mettre à jour et copier le lien", + "Update password": "Mettre à jour le mot de passe", + "Updated at": "Mise à jour le", + "Upload": "Télécharger", + "Upload a GGUF model": "Téléverser un modèle GGUF", + "Upload Files": "Télécharger des fichiers", + "Upload Pipeline": "Pipeline de téléchargement", + "Upload Progress": "Progression de l'envoi", + "URL Mode": "Mode d'URL", + "Use '#' in the prompt input to load and select your documents.": "Utilisez '#' dans l'entrée de prompt pour charger et sélectionner vos documents.", + "Use Gravatar": "Utilisez Gravatar", + "Use Initials": "Utiliser les initiales", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "utiliser mmap (Ollama)", + "user": "utilisateur", + "User location successfully retrieved.": "L'emplacement de l'utilisateur a été récupéré avec succès.", + "User Permissions": "Permissions utilisateur", + "Users": "Utilisateurs", + "Utilize": "Utilisez", + "Valid time units:": "Unités de temps valides\u00a0:", + "Valves": "Vannes", + "Valves updated": "Vannes mises à jour", + "Valves updated successfully": "Les vannes ont été mises à jour avec succès", + "variable": "variable", + "variable to have them replaced with clipboard content.": "variable pour qu'elles soient remplacées par le contenu du presse-papiers.", + "Version": "Version améliorée", + "Voice": "Voix", + "Warning": "Avertissement !", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Avertissement : Si vous mettez à jour ou modifiez votre modèle d'encodage, vous devrez réimporter tous les documents.", + "Web": "Web", + "Web API": "API Web", + "Web Loader Settings": "Paramètres du chargeur web", + "Web Params": "Paramètres Web", + "Web Search": "Recherche Web", + "Web Search Engine": "Moteur de recherche Web", + "Webhook URL": "URL du webhook", + "WebUI Settings": "Paramètres de WebUI", + "WebUI will make requests to": "WebUI effectuera des requêtes vers", + "What’s New in": "Quoi de neuf", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Lorsque l'historique est désactivé, les nouvelles conversations sur ce navigateur ne seront pas enregistrés dans votre historique sur aucun de vos appareils.", + "Whisper (Local)": "Whisper (local)", + "Widescreen Mode": "Mode Grand Écran", + "Workspace": "Espace de travail", + "Write a prompt suggestion (e.g. Who are you?)": "Écrivez une suggestion de prompt (par exemple : Qui êtes-vous ?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Rédigez un résumé de 50 mots qui résume [sujet ou mot-clé].", + "Yesterday": "Hier", + "You": "Vous", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Vous pouvez personnaliser vos interactions avec les LLM en ajoutant des souvenirs via le bouton 'Gérer' ci-dessous, ce qui les rendra plus utiles et adaptés à vos besoins.", + "You cannot clone a base model": "Vous ne pouvez pas cloner un modèle de base", + "You have no archived conversations.": "Vous n'avez aucune conversation archivée", + "You have shared this chat": "Vous avez partagé cette conversation.", + "You're a helpful assistant.": "Vous êtes un assistant serviable.", + "You're now logged in.": "Vous êtes désormais connecté.", + "Your account status is currently pending activation.": "Votre statut de compte est actuellement en attente d'activation.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "Paramètres de l'outil de téléchargement YouTube" +} diff --git a/src/lib/i18n/locales/he-IL/translation.json b/src/lib/i18n/locales/he-IL/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..2f400366562eb2b306dba16f5d5e413cb8d58f8b --- /dev/null +++ b/src/lib/i18n/locales/he-IL/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' או '-1' ללא תפוגה.", + "(Beta)": "(בטא)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(למשל `sh webui.sh --api`)", + "(latest)": "(האחרון)", + "{{ models }}": "{{ דגמים }}", + "{{ owner }}: You cannot delete a base model": "{{ בעלים }}: לא ניתן למחוק מודל בסיס", + "{{modelName}} is thinking...": "{{modelName}} חושב...", + "{{user}}'s Chats": "צ'אטים של {{user}}", + "{{webUIName}} Backend Required": "נדרש Backend של {{webUIName}}", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "מודל משימה משמש בעת ביצוע משימות כגון יצירת כותרות עבור צ'אטים ושאילתות חיפוש באינטרנט", + "a user": "משתמש", + "About": "אודות", + "Account": "חשבון", + "Account Activation Pending": "", + "Accurate information": "מידע מדויק", + "Actions": "", + "Active Users": "", + "Add": "הוסף", + "Add a model id": "הוספת מזהה דגם", + "Add a short description about what this model does": "הוסף תיאור קצר אודות אופן הפעולה של מודל זה", + "Add a short title for this prompt": "הוסף כותרת קצרה לפקודה זו", + "Add a tag": "הוסף תג", + "Add custom prompt": "הוסף פקודה מותאמת אישית", + "Add Docs": "הוסף מסמכים", + "Add Files": "הוסף קבצים", + "Add Memory": "הוסף זיכרון", + "Add message": "הוסף הודעה", + "Add Model": "הוסף מודל", + "Add Tag": "", + "Add Tags": "הוסף תגים", + "Add User": "הוסף משתמש", + "Adjusting these settings will apply changes universally to all users.": "התאמת הגדרות אלו תחול על כל המשתמשים.", + "admin": "מנהל", + "Admin": "", + "Admin Panel": "לוח בקרה למנהל", + "Admin Settings": "הגדרות מנהל", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "פרמטרים מתקדמים", + "Advanced Params": "פרמטרים מתקדמים", + "all": "הכל", + "All Documents": "כל המסמכים", + "All Users": "כל המשתמשים", + "Allow": "אפשר", + "Allow Chat Deletion": "אפשר מחיקת צ'אט", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "תווים אלפאנומריים ומקפים", + "Already have an account?": "כבר יש לך חשבון?", + "an assistant": "עוזר", + "and": "וגם", + "and create a new shared link.": "וצור קישור משותף חדש.", + "API Base URL": "כתובת URL בסיסית ל-API", + "API Key": "מפתח API", + "API Key created.": "מפתח API נוצר.", + "API keys": "מפתחות API", + "April": "אפריל", + "Archive": "ארכיון", + "Archive All Chats": "אחסן בארכיון את כל הצ'אטים", + "Archived Chats": "צ'אטים מאורכבים", + "are allowed - Activate this command by typing": "מותרים - הפעל פקודה זו על ידי הקלדה", + "Are you sure?": "האם אתה בטוח?", + "Attach file": "צרף קובץ", + "Attention to detail": "תשומת לב לפרטים", + "Audio": "אודיו", + "Audio settings updated successfully": "", + "August": "אוגוסט", + "Auto-playback response": "תגובת השמעה אוטומטית", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "כתובת URL בסיסית של AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "נדרשת כתובת URL בסיסית של AUTOMATIC1111", + "available!": "זמין!", + "Back": "חזור", + "Bad Response": "תגובה שגויה", + "Banners": "באנרים", + "Base Model (From)": "דגם בסיס (מ)", + "Batch Size (num_batch)": "", + "before": "לפני", + "Being lazy": "להיות עצלן", + "Brave Search API Key": "מפתח API של חיפוש אמיץ", + "Bypass SSL verification for Websites": "עקוף אימות SSL עבור אתרים", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "בטל", + "Capabilities": "יכולות", + "Change Password": "שנה סיסמה", + "Chat": "צ'אט", + "Chat Background Image": "", + "Chat Bubble UI": "UI של תיבת הדיבור", + "Chat Controls": "", + "Chat direction": "כיוון צ'אט", + "Chat History": "היסטוריית צ'אט", + "Chat History is off for this browser.": "היסטוריית הצ'אט כבויה לדפדפן זה.", + "Chats": "צ'אטים", + "Check Again": "בדוק שוב", + "Check for updates": "בדוק עדכונים", + "Checking for updates...": "בודק עדכונים...", + "Choose a model before saving...": "בחר מודל לפני השמירה...", + "Chunk Overlap": "חפיפת נתונים", + "Chunk Params": "פרמטרי נתונים", + "Chunk Size": "גודל נתונים", + "Citation": "ציטוט", + "Clear memory": "", + "Click here for help.": "לחץ כאן לעזרה.", + "Click here to": "לחץ כאן כדי", + "Click here to download user import template file.": "", + "Click here to select": "לחץ כאן לבחירה", + "Click here to select a csv file.": "לחץ כאן לבחירת קובץ csv.", + "Click here to select a py file.": "", + "Click here to select documents.": "לחץ כאן לבחירת מסמכים.", + "click here.": "לחץ כאן.", + "Click on the user role button to change a user's role.": "לחץ על כפתור תפקיד המשתמש כדי לשנות את תפקיד המשתמש.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "שיבוט", + "Close": "סגור", + "Code formatted successfully": "", + "Collection": "אוסף", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "כתובת URL בסיסית של ComfyUI", + "ComfyUI Base URL is required.": "נדרשת כתובת URL בסיסית של ComfyUI", + "Command": "פקודה", + "Concurrent Requests": "בקשות בו-זמניות", + "Confirm": "", + "Confirm Password": "אשר סיסמה", + "Confirm your action": "", + "Connections": "חיבורים", + "Contact Admin for WebUI Access": "", + "Content": "תוכן", + "Content Extraction": "", + "Context Length": "אורך הקשר", + "Continue Response": "המשך תגובה", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "העתקת כתובת URL של צ'אט משותף ללוח!", + "Copy": "העתק", + "Copy last code block": "העתק את בלוק הקוד האחרון", + "Copy last response": "העתק את התגובה האחרונה", + "Copy Link": "העתק קישור", + "Copying to clipboard was successful!": "ההעתקה ללוח הייתה מוצלחת!", + "Create a model": "יצירת מודל", + "Create Account": "צור חשבון", + "Create new key": "צור מפתח חדש", + "Create new secret key": "צור מפתח סודי חדש", + "Created at": "נוצר ב", + "Created At": "נוצר ב", + "Created by": "", + "CSV Import": "", + "Current Model": "המודל הנוכחי", + "Current Password": "הסיסמה הנוכחית", + "Custom": "מותאם אישית", + "Customize models for a specific purpose": "התאמה אישית של מודלים למטרה ספציפית", + "Dark": "כהה", + "Dashboard": "", + "Database": "מסד נתונים", + "December": "דצמבר", + "Default": "ברירת מחדל", + "Default (Automatic1111)": "ברירת מחדל (Automatic1111)", + "Default (SentenceTransformers)": "ברירת מחדל (SentenceTransformers)", + "Default Model": "מודל ברירת מחדל", + "Default model updated": "המודל המוגדר כברירת מחדל עודכן", + "Default Prompt Suggestions": "הצעות ברירת מחדל לפקודות", + "Default User Role": "תפקיד משתמש ברירת מחדל", + "delete": "מחק", + "Delete": "מחק", + "Delete a model": "מחק מודל", + "Delete All Chats": "מחק את כל הצ'אטים", + "Delete chat": "מחק צ'אט", + "Delete Chat": "מחק צ'אט", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "מחק את הקישור הזה", + "Delete tool?": "", + "Delete User": "מחק משתמש", + "Deleted {{deleteModelTag}}": "נמחק {{deleteModelTag}}", + "Deleted {{name}}": "נמחק {{name}}", + "Description": "תיאור", + "Didn't fully follow instructions": "לא עקב אחרי ההוראות באופן מלא", + "Disabled": "", + "Discover a function": "", + "Discover a model": "גלה מודל", + "Discover a prompt": "גלה פקודה", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "גלה, הורד, וחקור פקודות מותאמות אישית", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "גלה, הורד, וחקור הגדרות מודל מוגדרות מראש", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "הצג את שם המשתמש במקום 'אתה' בצ'אט", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "מסמך", + "Document Settings": "הגדרות מסמך", + "Documentation": "", + "Documents": "מסמכים", + "does not make any external connections, and your data stays securely on your locally hosted server.": "לא מבצע חיבורים חיצוניים, והנתונים שלך נשמרים באופן מאובטח בשרת המקומי שלך.", + "Don't Allow": "אל תאפשר", + "Don't have an account?": "אין לך חשבון?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "לא אוהב את הסגנון", + "Done": "", + "Download": "הורד", + "Download canceled": "ההורדה בוטלה", + "Download Database": "הורד מסד נתונים", + "Drop any files here to add to the conversation": "גרור כל קובץ לכאן כדי להוסיף לשיחה", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "למשל '30s', '10m'. יחידות זמן חוקיות הן 's', 'm', 'h'.", + "Edit": "ערוך", + "Edit Doc": "ערוך מסמך", + "Edit Memory": "", + "Edit User": "ערוך משתמש", + "ElevenLabs": "", + "Email": "דוא\"ל", + "Embedding Batch Size": "", + "Embedding Model": "מודל הטמעה", + "Embedding Model Engine": "מנוע מודל הטמעה", + "Embedding model set to \"{{embedding_model}}\"": "מודל ההטמעה הוגדר ל-\"{{embedding_model}}\"", + "Enable Chat History": "הפעל היסטוריית צ'אט", + "Enable Community Sharing": "הפיכת שיתוף קהילה לזמין", + "Enable New Sign Ups": "אפשר הרשמות חדשות", + "Enable Web Search": "הפיכת חיפוש באינטרנט לזמין", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "ודא שקובץ ה-CSV שלך כולל 4 עמודות בסדר הבא: שם, דוא\"ל, סיסמה, תפקיד.", + "Enter {{role}} message here": "הזן הודעת {{role}} כאן", + "Enter a detail about yourself for your LLMs to recall": "הזן פרטים על עצמך כדי שLLMs יזכור", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "הזן מפתח API של חיפוש אמיץ", + "Enter Chunk Overlap": "הזן חפיפת נתונים", + "Enter Chunk Size": "הזן גודל נתונים", + "Enter Github Raw URL": "הזן כתובת URL של Github Raw", + "Enter Google PSE API Key": "הזן מפתח API של Google PSE", + "Enter Google PSE Engine Id": "הזן את מזהה מנוע PSE של Google", + "Enter Image Size (e.g. 512x512)": "הזן גודל תמונה (למשל 512x512)", + "Enter language codes": "הזן קודי שפה", + "Enter model tag (e.g. {{modelTag}})": "הזן תג מודל (למשל {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "הזן מספר שלבים (למשל 50)", + "Enter Score": "הזן ציון", + "Enter Searxng Query URL": "הזן כתובת URL של שאילתת Searxng", + "Enter Serper API Key": "הזן מפתח API של Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "הזן מפתח API של Serpstack", + "Enter stop sequence": "הזן רצף עצירה", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "הזן Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "הזן כתובת URL (למשל http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "הזן כתובת URL (למשל http://localhost:11434)", + "Enter Your Email": "הזן את דוא\"ל שלך", + "Enter Your Full Name": "הזן את שמך המלא", + "Enter your message": "", + "Enter Your Password": "הזן את הסיסמה שלך", + "Enter Your Role": "הזן את התפקיד שלך", + "Error": "שגיאה", + "Experimental": "ניסיוני", + "Export": "ייצא", + "Export All Chats (All Users)": "ייצוא כל הצ'אטים (כל המשתמשים)", + "Export chat (.json)": "", + "Export Chats": "ייצוא צ'אטים", + "Export Documents Mapping": "ייצוא מיפוי מסמכים", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "ייצוא מודלים", + "Export Prompts": "ייצוא פקודות", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "יצירת מפתח API נכשלה.", + "Failed to read clipboard contents": "קריאת תוכן הלוח נכשלה", + "Failed to update settings": "", + "February": "פברואר", + "Feel free to add specific details": "נא להוסיף פרטים ספציפיים לפי רצון", + "File": "", + "File Mode": "מצב קובץ", + "File not found.": "הקובץ לא נמצא.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "התגלתה הזיית טביעת אצבע: לא ניתן להשתמש בראשי תיבות כאווטאר. משתמש בתמונת פרופיל ברירת מחדל.", + "Fluidly stream large external response chunks": "שידור נתונים חיצוניים בקצב רציף", + "Focus chat input": "מיקוד הקלט לצ'אט", + "Followed instructions perfectly": "עקב אחר ההוראות במושלמות", + "Form": "", + "Format your variables using square brackets like this:": "עצב את המשתנים שלך באמצעות סוגריים מרובעים כך:", + "Frequency Penalty": "עונש תדירות", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "כללי", + "General Settings": "הגדרות כלליות", + "Generate Image": "", + "Generating search query": "יצירת שאילתת חיפוש", + "Generation Info": "מידע על היצירה", + "Get up and running with": "", + "Global": "", + "Good Response": "תגובה טובה", + "Google PSE API Key": "מפתח API של Google PSE", + "Google PSE Engine Id": "מזהה מנוע PSE של Google", + "h:mm a": "h:mm a", + "has no conversations.": "אין שיחות.", + "Hello, {{name}}": "שלום, {{name}}", + "Help": "עזרה", + "Hide": "הסתר", + "Hide Model": "", + "How can I help you today?": "כיצד אוכל לעזור לך היום?", + "Hybrid Search": "חיפוש היברידי", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "יצירת תמונות (ניסיוני)", + "Image Generation Engine": "מנוע יצירת תמונות", + "Image Settings": "הגדרות תמונה", + "Images": "תמונות", + "Import Chats": "יבוא צ'אטים", + "Import Documents Mapping": "יבוא מיפוי מסמכים", + "Import Functions": "", + "Import Models": "ייבוא דגמים", + "Import Prompts": "יבוא פקודות", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "כלול את הדגל `--api` בעת הרצת stable-diffusion-webui", + "Info": "מידע", + "Input commands": "פקודות קלט", + "Install from Github URL": "התקן מכתובת URL של Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "ממשק", + "Invalid Tag": "תג לא חוקי", + "January": "ינואר", + "join our Discord for help.": "הצטרף ל-Discord שלנו לעזרה.", + "JSON": "JSON", + "JSON Preview": "תצוגה מקדימה של JSON", + "July": "יולי", + "June": "יוני", + "JWT Expiration": "תפוגת JWT", + "JWT Token": "אסימון JWT", + "Keep Alive": "השאר פעיל", + "Keyboard shortcuts": "קיצורי מקלדת", + "Knowledge": "", + "Language": "שפה", + "large language models, locally.": "", + "Last Active": "פעיל לאחרונה", + "Last Modified": "", + "Light": "בהיר", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "מודלים בשפה טבעית יכולים לטעות. אמת מידע חשוב.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "נוצר על ידי קהילת OpenWebUI", + "Make sure to enclose them with": "ודא להקיף אותם עם", + "Manage": "", + "Manage Models": "נהל מודלים", + "Manage Ollama Models": "נהל מודלים של Ollama", + "Manage Pipelines": "ניהול צינורות", + "Manage Valves": "", + "March": "מרץ", + "Max Tokens (num_predict)": "מקסימום אסימונים (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "ניתן להוריד מקסימום 3 מודלים בו זמנית. אנא נסה שוב מאוחר יותר.", + "May": "מאי", + "Memories accessible by LLMs will be shown here.": "מזכירים נגישים על ידי LLMs יוצגו כאן.", + "Memory": "זיכרון", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "הודעות שתשלח לאחר יצירת הקישור לא ישותפו. משתמשים עם כתובת האתר יוכלו לצפות בצ'אט המשותף.", + "Minimum Score": "ציון מינימלי", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD בMMMM, YYYY", + "MMMM DD, YYYY HH:mm": "DD בMMMM, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "המודל '{{modelName}}' הורד בהצלחה.", + "Model '{{modelTag}}' is already in queue for downloading.": "המודל '{{modelTag}}' כבר בתור להורדה.", + "Model {{modelId}} not found": "המודל {{modelId}} לא נמצא", + "Model {{modelName}} is not vision capable": "דגם {{modelName}} אינו בעל יכולת ראייה", + "Model {{name}} is now {{status}}": "דגם {{name}} הוא כעת {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "נתיב מערכת הקבצים של המודל זוהה. נדרש שם קצר של המודל לעדכון, לא ניתן להמשיך.", + "Model ID": "מזהה דגם", + "Model not selected": "לא נבחר מודל", + "Model Params": "פרמס מודל", + "Model updated successfully": "", + "Model Whitelisting": "רישום לבן של מודלים", + "Model(s) Whitelisted": "מודלים שנכללו ברשימה הלבנה", + "Modelfile Content": "תוכן קובץ מודל", + "Models": "מודלים", + "More": "עוד", + "Name": "שם", + "Name Tag": "תג שם", + "Name your model": "תן שם לדגם שלך", + "New Chat": "צ'אט חדש", + "New Password": "סיסמה חדשה", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "לא נמצאו תוצאות", + "No search query generated": "לא נוצרה שאילתת חיפוש", + "No source available": "אין מקור זמין", + "No valves to update": "", + "None": "ללא", + "Not factually correct": "לא נכון מבחינה עובדתית", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "הערה: אם תקבע ציון מינימלי, החיפוש יחזיר רק מסמכים עם ציון שגבוה או שווה לציון המינימלי.", + "Notifications": "התראות", + "November": "נובמבר", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "אוקטובר", + "Off": "כבוי", + "Okay, Let's Go!": "בסדר, בואו נתחיל!", + "OLED Dark": "OLED כהה", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API מושבת", + "Ollama API is disabled": "", + "Ollama Version": "גרסת Ollama", + "On": "פועל", + "Only": "רק", + "Only alphanumeric characters and hyphens are allowed in the command string.": "רק תווים אלפאנומריים ומקפים מותרים במחרוזת הפקודה.", + "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.": "אופס! תחזיק מעמד! הקבצים שלך עדיין בתהליך העיבוד. אנו מבשלים אותם לשלמות. נא להתאזר בסבלנות ונודיע לך ברגע שיהיו מוכנים.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "אופס! נראה שהכתובת URL אינה תקינה. אנא בדוק שוב ונסה שנית.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "אופס! אתה משתמש בשיטה לא נתמכת (רק חזית). אנא שרת את ממשק המשתמש האינטרנטי מהשרת האחורי.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "פתח צ'אט חדש", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "API של OpenAI", + "OpenAI API Config": "תצורת API של OpenAI", + "OpenAI API Key is required.": "נדרש מפתח API של OpenAI.", + "OpenAI URL/Key required.": "נדרשת כתובת URL/מפתח של OpenAI.", + "or": "או", + "Other": "אחר", + "Password": "סיסמה", + "PDF document (.pdf)": "מסמך PDF (.pdf)", + "PDF Extract Images (OCR)": "חילוץ תמונות מ-PDF (OCR)", + "pending": "ממתין", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "ההרשאה נדחתה בעת גישה למיקרופון: {{error}}", + "Personalization": "תאור", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "צינורות", + "Pipelines Not Detected": "", + "Pipelines Valves": "צינורות שסתומים", + "Plain text (.txt)": "טקסט פשוט (.txt)", + "Playground": "אזור משחקים", + "Please carefully review the following warnings:": "", + "Positive attitude": "גישה חיובית", + "Previous 30 days": "30 הימים הקודמים", + "Previous 7 days": "7 הימים הקודמים", + "Profile Image": "תמונת פרופיל", + "Prompt": "פקודה", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "פקודה (למשל, ספר לי עובדה מעניינת על האימפריה הרומית)", + "Prompt Content": "תוכן הפקודה", + "Prompt suggestions": "הצעות לפקודות", + "Prompts": "פקודות", + "Pull \"{{searchValue}}\" from Ollama.com": "משוך \"{{searchValue}}\" מ-Ollama.com", + "Pull a model from Ollama.com": "משוך מודל מ-Ollama.com", + "Query Params": "פרמטרי שאילתה", + "RAG Template": "תבנית RAG", + "Read Aloud": "קרא בקול", + "Record voice": "הקלט קול", + "Redirecting you to OpenWebUI Community": "מפנה אותך לקהילת OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "נדחה כאשר לא היה צריך", + "Regenerate": "הפק מחדש", + "Release Notes": "הערות שחרור", + "Remove": "הסר", + "Remove Model": "הסר מודל", + "Rename": "שנה שם", + "Repeat Last N": "חזור על ה-N האחרונים", + "Request Mode": "מצב בקשה", + "Reranking Model": "מודל דירוג מחדש", + "Reranking model disabled": "מודל דירוג מחדש מושבת", + "Reranking model set to \"{{reranking_model}}\"": "מודל דירוג מחדש הוגדר ל-\"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "איפוס אחסון וקטורים", + "Response AutoCopy to Clipboard": "העתקה אוטומטית של תגובה ללוח", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "תפקיד", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "שמור", + "Save & Create": "שמור וצור", + "Save & Update": "שמור ועדכן", + "Save Tag": "", + "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": "שמירת יומני צ'אט ישירות באחסון הדפדפן שלך אינה נתמכת יותר. אנא הקדש רגע להוריד ולמחוק את יומני הצ'אט שלך על ידי לחיצה על הכפתור למטה. אל דאגה, באפשרותך לייבא מחדש בקלות את יומני הצ'אט שלך לשרת האחורי דרך", + "Scan": "סרוק", + "Scan complete!": "הסריקה הושלמה!", + "Scan for documents from {{path}}": "סרוק מסמכים מ-{{path}}", + "Search": "חפש", + "Search a model": "חפש מודל", + "Search Chats": "חיפוש צ'אטים", + "Search Documents": "חפש מסמכים", + "Search Functions": "", + "Search Models": "חיפוש מודלים", + "Search Prompts": "חפש פקודות", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "ספירת תוצאות חיפוש", + "Search Tools": "", + "Searched {{count}} sites_one": "חיפש {{count}} sites_one", + "Searched {{count}} sites_two": "חיפש {{count}} sites_two", + "Searched {{count}} sites_other": "חיפש {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "כתובת URL של שאילתת Searxng", + "See readme.md for instructions": "ראה את readme.md להוראות", + "See what's new": "ראה מה חדש", + "Seed": "זרע", + "Select a base model": "בחירת מודל בסיס", + "Select a engine": "", + "Select a function": "", + "Select a mode": "בחר מצב", + "Select a model": "בחר מודל", + "Select a pipeline": "בחר קו צינור", + "Select a pipeline url": "בחר כתובת URL של קו צינור", + "Select a tool": "", + "Select an Ollama instance": "בחר מופע של Ollama", + "Select Documents": "", + "Select model": "בחר מודל", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "דגמים נבחרים אינם תומכים בקלט תמונה", + "Send": "שלח", + "Send a Message": "שלח הודעה", + "Send message": "שלח הודעה", + "September": "ספטמבר", + "Serper API Key": "מפתח Serper API", + "Serply API Key": "", + "Serpstack API Key": "מפתח API של Serpstack", + "Server connection verified": "החיבור לשרת אומת", + "Set as default": "הגדר כברירת מחדל", + "Set Default Model": "הגדר מודל ברירת מחדל", + "Set embedding model (e.g. {{model}})": "הגדר מודל הטמעה (למשל {{model}})", + "Set Image Size": "הגדר גודל תמונה", + "Set reranking model (e.g. {{model}})": "הגדר מודל דירוג מחדש (למשל {{model}})", + "Set Steps": "הגדר שלבים", + "Set Task Model": "הגדרת מודל משימה", + "Set Voice": "הגדר קול", + "Settings": "הגדרות", + "Settings saved successfully!": "ההגדרות נשמרו בהצלחה!", + "Settings updated successfully": "", + "Share": "שתף", + "Share Chat": "שתף צ'אט", + "Share to OpenWebUI Community": "שתף לקהילת OpenWebUI", + "short-summary": "סיכום קצר", + "Show": "הצג", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "הצג קיצורי דרך", + "Show your support!": "", + "Showcased creativity": "הצגת יצירתיות", + "Sign in": "הירשם", + "Sign Out": "התנתקות", + "Sign up": "הרשמה", + "Signing in": "כניסה", + "Source": "מקור", + "Speech recognition error: {{error}}": "שגיאת תחקור שמע: {{error}}", + "Speech-to-Text Engine": "מנוע תחקור שמע", + "Stop Sequence": "סידור עצירה", + "STT Model": "", + "STT Settings": "הגדרות חקירה של TTS", + "Submit": "שלח", + "Subtitle (e.g. about the Roman Empire)": "תחקור (לדוגמה: על מעמד הרומי)", + "Success": "הצלחה", + "Successfully updated.": "עדכון הצלחה.", + "Suggested": "מומלץ", + "Support": "", + "Support this plugin:": "", + "System": "מערכת", + "System Prompt": "תגובת מערכת", + "Tags": "תגיות", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "תרשמו יותר:", + "Temperature": "טמפרטורה", + "Template": "תבנית", + "Text Completion": "תחילת טקסט", + "Text-to-Speech Engine": "מנוע טקסט לדיבור", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "תודה על המשוב שלך!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "ציון צריך להיות ערך בין 0.0 (0%) ל-1.0 (100%)", + "Theme": "נושא", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "פעולה זו מבטיחה שהשיחות בעלות הערך שלך יישמרו באופן מאובטח במסד הנתונים העורפי שלך. תודה!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "הגדרה זו אינה מסתנכרנת בין דפדפנים או מכשירים.", + "This will delete": "", + "Thorough explanation": "תיאור מפורט", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "טיפ: עדכן חריצים משתנים מרובים ברציפות על-ידי לחיצה על מקש Tab בקלט הצ'אט לאחר כל החלפה.", + "Title": "שם", + "Title (e.g. Tell me a fun fact)": "שם (לדוגמה: תרגום)", + "Title Auto-Generation": "יצירת שם אוטומטית", + "Title cannot be an empty string.": "שם לא יכול להיות מחרוזת ריקה.", + "Title Generation Prompt": "שאלה ליצירת שם", + "to": "ל", + "To access the available model names for downloading,": "כדי לגשת לשמות הדגמים הזמינים להורדה,", + "To access the GGUF models available for downloading,": "כדי לגשת לדגמי GGUF הזמינים להורדה,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "לקלטת שיחה.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "היום", + "Toggle settings": "החלפת מצב של הגדרות", + "Toggle sidebar": "החלפת מצב של סרגל הצד", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "קשה לגשת לOllama?", + "TTS Model": "", + "TTS Settings": "הגדרות TTS", + "TTS Voice": "", + "Type": "סוג", + "Type Hugging Face Resolve (Download) URL": "הקלד כתובת URL של פתרון פנים מחבק (הורד)", + "Uh-oh! There was an issue connecting to {{provider}}.": "או-הו! אירעה בעיה בהתחברות ל- {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "עדכן ושכפל קישור", + "Update password": "עדכן סיסמה", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "העלה מודל GGUF", + "Upload Files": "העלאת קבצים", + "Upload Pipeline": "", + "Upload Progress": "תקדמות העלאה", + "URL Mode": "מצב URL", + "Use '#' in the prompt input to load and select your documents.": "השתמש ב- '#' בקלט הבקשה כדי לטעון ולבחור את המסמכים שלך.", + "Use Gravatar": "שימוש ב Gravatar", + "Use Initials": "שימוש ב initials", + "use_mlock (Ollama)": "use_mlock (אולמה)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "משתמש", + "User location successfully retrieved.": "", + "User Permissions": "הרשאות משתמש", + "Users": "משתמשים", + "Utilize": "שימוש", + "Valid time units:": "יחידות זמן תקינות:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "משתנה", + "variable to have them replaced with clipboard content.": "משתנה להחליפו ב- clipboard תוכן.", + "Version": "גרסה", + "Voice": "", + "Warning": "אזהרה", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "אזהרה: אם תעדכן או תשנה את מודל ההטבעה שלך, יהיה עליך לייבא מחדש את כל המסמכים.", + "Web": "רשת", + "Web API": "", + "Web Loader Settings": "הגדרות טעינת אתר", + "Web Params": "פרמטרים Web", + "Web Search": "חיפוש באינטרנט", + "Web Search Engine": "מנוע חיפוש באינטרנט", + "Webhook URL": "URL Webhook", + "WebUI Settings": "הגדרות WebUI", + "WebUI will make requests to": "WebUI יבקש לבקש", + "What’s New in": "מה חדש ב", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "כאשר ההיסטוריה מושבתת, צ'אטים חדשים בדפדפן זה לא יופיעו בהיסטוריה שלך באף אחד מהמכשירים שלך.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "סביבה", + "Write a prompt suggestion (e.g. Who are you?)": "כתוב הצעה מהירה (למשל, מי אתה?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "כתוב סיכום ב-50 מילים שמסכם [נושא או מילת מפתח].", + "Yesterday": "אתמול", + "You": "אתה", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "לא ניתן לשכפל מודל בסיס", + "You have no archived conversations.": "אין לך שיחות בארכיון.", + "You have shared this chat": "שיתפת את השיחה הזו", + "You're a helpful assistant.": "אתה עוזר מועיל.", + "You're now logged in.": "כעת אתה מחובר.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "הגדרות Youtube Loader" +} diff --git a/src/lib/i18n/locales/hi-IN/translation.json b/src/lib/i18n/locales/hi-IN/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..04d70f9747d2c849e8527c0b534095d8533abf24 --- /dev/null +++ b/src/lib/i18n/locales/hi-IN/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' बिना किसी समाप्ति के", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", + "(latest)": "(latest)", + "{{ models }}": "{{ मॉडल }}", + "{{ owner }}: You cannot delete a base model": "{{ मालिक }}: आप बेस मॉडल को हटा नहीं सकते", + "{{modelName}} is thinking...": "{{modelName}} सोच रहा है...", + "{{user}}'s Chats": "{{user}} की चैट", + "{{webUIName}} Backend Required": "{{webUIName}} बैकएंड आवश्यक", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "चैट और वेब खोज क्वेरी के लिए शीर्षक उत्पन्न करने जैसे कार्य करते समय कार्य मॉडल का उपयोग किया जाता है", + "a user": "एक उपयोगकर्ता", + "About": "हमारे बारे में", + "Account": "खाता", + "Account Activation Pending": "", + "Accurate information": "सटीक जानकारी", + "Actions": "", + "Active Users": "", + "Add": "जोड़ें", + "Add a model id": "मॉडल आईडी जोड़ना", + "Add a short description about what this model does": "इस मॉडल के बारे में एक संक्षिप्त विवरण जोड़ें", + "Add a short title for this prompt": "इस संकेत के लिए एक संक्षिप्त शीर्षक जोड़ें", + "Add a tag": "एक टैग जोड़े", + "Add custom prompt": "अनुकूल संकेत जोड़ें", + "Add Docs": "दस्तावेज़ जोड़ें", + "Add Files": "फाइलें जोड़ें", + "Add Memory": "मेमोरी जोड़ें", + "Add message": "संदेश डालें", + "Add Model": "मॉडल जोड़ें", + "Add Tag": "", + "Add Tags": "टैगों को जोड़ें", + "Add User": "उपयोगकर्ता जोड़ें", + "Adjusting these settings will apply changes universally to all users.": "इन सेटिंग्स को समायोजित करने से परिवर्तन सभी उपयोगकर्ताओं पर सार्वभौमिक रूप से लागू होंगे।", + "admin": "व्यवस्थापक", + "Admin": "", + "Admin Panel": "व्यवस्थापक पैनल", + "Admin Settings": "व्यवस्थापक सेटिंग्स", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "उन्नत पैरामीटर", + "Advanced Params": "उन्नत परम", + "all": "सभी", + "All Documents": "सभी डॉक्यूमेंट्स", + "All Users": "सभी उपयोगकर्ता", + "Allow": "अनुमति दें", + "Allow Chat Deletion": "चैट हटाने की अनुमति दें", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "अल्फ़ान्यूमेरिक वर्ण और हाइफ़न", + "Already have an account?": "क्या आपके पास पहले से एक खाता मौजूद है?", + "an assistant": "एक सहायक", + "and": "और", + "and create a new shared link.": "और एक नई साझा लिंक बनाएं.", + "API Base URL": "एपीआई बेस यूआरएल", + "API Key": "एपीआई कुंजी", + "API Key created.": "एपीआई कुंजी बनाई गई", + "API keys": "एपीआई कुंजियाँ", + "April": "अप्रैल", + "Archive": "पुरालेख", + "Archive All Chats": "सभी चैट संग्रहीत करें", + "Archived Chats": "संग्रहीत चैट", + "are allowed - Activate this command by typing": "अनुमति है - टाइप करके इस कमांड को सक्रिय करें", + "Are you sure?": "क्या आपको यकीन है?", + "Attach file": "फ़ाइल atta", + "Attention to detail": "विस्तार पर ध्यान", + "Audio": "ऑडियो", + "Audio settings updated successfully": "", + "August": "अगस्त", + "Auto-playback response": "ऑटो-प्लेबैक प्रतिक्रिया", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 बेस यूआरएल", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 का बेस यूआरएल आवश्यक है।", + "available!": "उपलब्ध!", + "Back": "पीछे", + "Bad Response": "ख़राब प्रतिक्रिया", + "Banners": "बैनर", + "Base Model (From)": "बेस मॉडल (से)", + "Batch Size (num_batch)": "", + "before": "पहले", + "Being lazy": "आलसी होना", + "Brave Search API Key": "Brave सर्च एपीआई कुंजी", + "Bypass SSL verification for Websites": "वेबसाइटों के लिए SSL सुनिश्चिती को छोड़ें", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "रद्द करें", + "Capabilities": "क्षमताओं", + "Change Password": "पासवर्ड बदलें", + "Chat": "चैट करें", + "Chat Background Image": "", + "Chat Bubble UI": "चैट बॉली", + "Chat Controls": "", + "Chat direction": "चैट दिशा", + "Chat History": "चैट का इतिहास", + "Chat History is off for this browser.": "इस ब्राउज़र के लिए चैट इतिहास बंद है।", + "Chats": "सभी चैट", + "Check Again": "फिर से जाँचो", + "Check for updates": "अपडेट के लिए जाँच", + "Checking for updates...": "अपडेट के लिए जांच कर रहा है...", + "Choose a model before saving...": "सहेजने से पहले एक मॉडल चुनें...", + "Chunk Overlap": "चंक ओवरलैप", + "Chunk Params": "चंक पैरामीटर्स", + "Chunk Size": "चंक आकार", + "Citation": "उद्धरण", + "Clear memory": "", + "Click here for help.": "सहायता के लिए यहां क्लिक करें।", + "Click here to": "यहां क्लिक करें", + "Click here to download user import template file.": "", + "Click here to select": "चयन करने के लिए यहां क्लिक करें।", + "Click here to select a csv file.": "सीएसवी फ़ाइल का चयन करने के लिए यहां क्लिक करें।", + "Click here to select a py file.": "", + "Click here to select documents.": "दस्तावेज़ चुनने के लिए यहां क्लिक करें।", + "click here.": "यहाँ क्लिक करें।", + "Click on the user role button to change a user's role.": "उपयोगकर्ता की भूमिका बदलने के लिए उपयोगकर्ता भूमिका बटन पर क्लिक करें।", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "क्लोन", + "Close": "बंद करना", + "Code formatted successfully": "", + "Collection": "संग्रह", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI बेस यूआरएल", + "ComfyUI Base URL is required.": "ComfyUI का बेस यूआरएल आवश्यक है", + "Command": "कमांड", + "Concurrent Requests": "समवर्ती अनुरोध", + "Confirm": "", + "Confirm Password": "पासवर्ड की पुष्टि कीजिये", + "Confirm your action": "", + "Connections": "सम्बन्ध", + "Contact Admin for WebUI Access": "", + "Content": "सामग्री", + "Content Extraction": "", + "Context Length": "प्रसंग की लंबाई", + "Continue Response": "प्रतिक्रिया जारी रखें", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "साझा चैट URL को क्लिपबोर्ड पर कॉपी किया गया!", + "Copy": "कॉपी", + "Copy last code block": "अंतिम कोड ब्लॉक कॉपी करें", + "Copy last response": "अंतिम प्रतिक्रिया कॉपी करें", + "Copy Link": "लिंक को कॉपी करें", + "Copying to clipboard was successful!": "क्लिपबोर्ड पर कॉपी बनाना सफल रहा!", + "Create a model": "एक मॉडल बनाएं", + "Create Account": "खाता बनाएं", + "Create new key": "नया क्रिप्टोग्राफिक क्षेत्र बनाएं", + "Create new secret key": "नया क्रिप्टोग्राफिक क्षेत्र बनाएं", + "Created at": "किस समय बनाया गया", + "Created At": "किस समय बनाया गया", + "Created by": "", + "CSV Import": "", + "Current Model": "वर्तमान मॉडल", + "Current Password": "वर्तमान पासवर्ड", + "Custom": "कस्टम संस्करण", + "Customize models for a specific purpose": "एक विशिष्ट उद्देश्य के लिए मॉडल अनुकूलित करें", + "Dark": "डार्क", + "Dashboard": "", + "Database": "डेटाबेस", + "December": "डिसेंबर", + "Default": "डिफ़ॉल्ट", + "Default (Automatic1111)": "डिफ़ॉल्ट (Automatic1111)", + "Default (SentenceTransformers)": "डिफ़ॉल्ट (SentenceTransformers)", + "Default Model": "डिफ़ॉल्ट मॉडल", + "Default model updated": "डिफ़ॉल्ट मॉडल अपडेट किया गया", + "Default Prompt Suggestions": "डिफ़ॉल्ट प्रॉम्प्ट सुझाव", + "Default User Role": "डिफ़ॉल्ट उपयोगकर्ता भूमिका", + "delete": "डिलीट", + "Delete": "डिलीट", + "Delete a model": "एक मॉडल हटाएँ", + "Delete All Chats": "सभी चैट हटाएं", + "Delete chat": "चैट हटाएं", + "Delete Chat": "चैट हटाएं", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "इस लिंक को हटाएं", + "Delete tool?": "", + "Delete User": "उपभोक्ता मिटायें", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} हटा दिया गया", + "Deleted {{name}}": "{{name}} हटा दिया गया", + "Description": "विवरण", + "Didn't fully follow instructions": "निर्देशों का पूरी तरह से पालन नहीं किया", + "Disabled": "", + "Discover a function": "", + "Discover a model": "एक मॉडल की खोज करें", + "Discover a prompt": "प्रॉम्प्ट खोजें", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "कस्टम प्रॉम्प्ट को खोजें, डाउनलोड करें और एक्सप्लोर करें", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "मॉडल प्रीसेट खोजें, डाउनलोड करें और एक्सप्लोर करें", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "चैट में 'आप' के स्थान पर उपयोगकर्ता नाम प्रदर्शित करें", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "दस्तावेज़", + "Document Settings": "दस्तावेज़ सेटिंग्स", + "Documentation": "", + "Documents": "दस्तावेज़", + "does not make any external connections, and your data stays securely on your locally hosted server.": "कोई बाहरी कनेक्शन नहीं बनाता है, और आपका डेटा आपके स्थानीय रूप से होस्ट किए गए सर्वर पर सुरक्षित रूप से रहता है।", + "Don't Allow": "अनुमति न दें", + "Don't have an account?": "कोई खाता नहीं है?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "शैली पसंद नहीं है", + "Done": "", + "Download": "डाउनलोड", + "Download canceled": "डाउनलोड रद्द किया गया", + "Download Database": "डेटाबेस डाउनलोड करें", + "Drop any files here to add to the conversation": "बातचीत में जोड़ने के लिए कोई भी फ़ाइल यहां छोड़ें", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "जैसे '30s', '10m', मान्य समय इकाइयाँ 's', 'm', 'h' हैं।", + "Edit": "संपादित करें", + "Edit Doc": "दस्तावेज़ संपादित करें", + "Edit Memory": "", + "Edit User": "यूजर को संपादित करो", + "ElevenLabs": "", + "Email": "ईमेल", + "Embedding Batch Size": "", + "Embedding Model": "मॉडेल अनुकूलन", + "Embedding Model Engine": "एंबेडिंग मॉडल इंजन", + "Embedding model set to \"{{embedding_model}}\"": "एम्बेडिंग मॉडल को \"{{embedding_model}}\" पर सेट किया गया", + "Enable Chat History": "चैट इतिहास सक्रिय करें", + "Enable Community Sharing": "समुदाय साझाकरण सक्षम करें", + "Enable New Sign Ups": "नए साइन अप सक्रिय करें", + "Enable Web Search": "वेब खोज सक्षम करें", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "सुनिश्चित करें कि आपकी CSV फ़ाइल में इस क्रम में 4 कॉलम शामिल हैं: नाम, ईमेल, पासवर्ड, भूमिका।", + "Enter {{role}} message here": "यहां {{role}} संदेश दर्ज करें", + "Enter a detail about yourself for your LLMs to recall": "अपने एलएलएम को याद करने के लिए अपने बारे में एक विवरण दर्ज करें", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Brave सर्च एपीआई कुंजी डालें", + "Enter Chunk Overlap": "चंक ओवरलैप दर्ज करें", + "Enter Chunk Size": "खंड आकार दर्ज करें", + "Enter Github Raw URL": "Github Raw URL दर्ज करें", + "Enter Google PSE API Key": "Google PSE API कुंजी दर्ज करें", + "Enter Google PSE Engine Id": "Google PSE इंजन आईडी दर्ज करें", + "Enter Image Size (e.g. 512x512)": "छवि का आकार दर्ज करें (उदा. 512x512)", + "Enter language codes": "भाषा कोड दर्ज करें", + "Enter model tag (e.g. {{modelTag}})": "Model tag दर्ज करें (उदा. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "चरणों की संख्या दर्ज करें (उदा. 50)", + "Enter Score": "स्कोर दर्ज करें", + "Enter Searxng Query URL": "Searxng क्वेरी URL दर्ज करें", + "Enter Serper API Key": "Serper API कुंजी दर्ज करें", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "सर्पस्टैक एपीआई कुंजी दर्ज करें", + "Enter stop sequence": "स्टॉप अनुक्रम दर्ज करें", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "शीर्ष K दर्ज करें", + "Enter URL (e.g. http://127.0.0.1:7860/)": "यूआरएल दर्ज करें (उदा. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "यूआरएल दर्ज करें (उदा. http://localhost:11434)", + "Enter Your Email": "अपना ईमेल दर्ज करें", + "Enter Your Full Name": "अपना पूरा नाम भरें", + "Enter your message": "", + "Enter Your Password": "अपना पासवर्ड भरें", + "Enter Your Role": "अपनी भूमिका दर्ज करें", + "Error": "चूक", + "Experimental": "प्रयोगात्मक", + "Export": "निर्यातित माल", + "Export All Chats (All Users)": "सभी चैट निर्यात करें (सभी उपयोगकर्ताओं की)", + "Export chat (.json)": "", + "Export Chats": "चैट निर्यात करें", + "Export Documents Mapping": "निर्यात दस्तावेज़ मैपिंग", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "निर्यात मॉडल", + "Export Prompts": "प्रॉम्प्ट निर्यात करें", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "एपीआई कुंजी बनाने में विफल.", + "Failed to read clipboard contents": "क्लिपबोर्ड सामग्री पढ़ने में विफल", + "Failed to update settings": "", + "February": "फरवरी", + "Feel free to add specific details": "विशिष्ट विवरण जोड़ने के लिए स्वतंत्र महसूस करें", + "File": "", + "File Mode": "फ़ाइल मोड", + "File not found.": "फ़ाइल प्राप्त नहीं हुई।", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "फ़िंगरप्रिंट स्पूफ़िंग का पता चला: प्रारंभिक अक्षरों को अवतार के रूप में उपयोग करने में असमर्थ। प्रोफ़ाइल छवि को डिफ़ॉल्ट पर डिफ़ॉल्ट किया जा रहा है.", + "Fluidly stream large external response chunks": "बड़े बाह्य प्रतिक्रिया खंडों को तरल रूप से प्रवाहित करें", + "Focus chat input": "चैट इनपुट पर फ़ोकस करें", + "Followed instructions perfectly": "निर्देशों का पूर्णतः पालन किया", + "Form": "", + "Format your variables using square brackets like this:": "वर्गाकार कोष्ठकों का उपयोग करके अपने चरों को इस प्रकार प्रारूपित करें :", + "Frequency Penalty": "फ्रीक्वेंसी पेनल्टी", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "सामान्य", + "General Settings": "सामान्य सेटिंग्स", + "Generate Image": "", + "Generating search query": "खोज क्वेरी जनरेट करना", + "Generation Info": "जनरेशन की जानकारी", + "Get up and running with": "", + "Global": "", + "Good Response": "अच्छी प्रतिक्रिया", + "Google PSE API Key": "Google PSE API कुंजी", + "Google PSE Engine Id": "Google PSE इंजन आईडी", + "h:mm a": "h:mm a", + "has no conversations.": "कोई बातचीत नहीं है", + "Hello, {{name}}": "नमस्ते, {{name}}", + "Help": "मदद", + "Hide": "छुपाएं", + "Hide Model": "", + "How can I help you today?": "आज मैं आपकी कैसे मदद कर सकता हूँ?", + "Hybrid Search": "हाइब्रिड खोज", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "छवि निर्माण (प्रायोगिक)", + "Image Generation Engine": "छवि निर्माण इंजन", + "Image Settings": "छवि सेटिंग्स", + "Images": "इमेजिस", + "Import Chats": "चैट आयात करें", + "Import Documents Mapping": "दस्तावेज़ मैपिंग आयात करें", + "Import Functions": "", + "Import Models": "आयात मॉडल", + "Import Prompts": "प्रॉम्प्ट आयात करें", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui चलाते समय `--api` ध्वज शामिल करें", + "Info": "सूचना-विषयक", + "Input commands": "इनपुट क命", + "Install from Github URL": "Github URL से इंस्टॉल करें", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "इंटरफेस", + "Invalid Tag": "अवैध टैग", + "January": "जनवरी", + "join our Discord for help.": "मदद के लिए हमारे डिस्कोर्ड में शामिल हों।", + "JSON": "ज्ञान प्रकार", + "JSON Preview": "JSON पूर्वावलोकन", + "July": "जुलाई", + "June": "जुन", + "JWT Expiration": "JWT समाप्ति", + "JWT Token": "जट टोकन", + "Keep Alive": "क्रियाशील रहो", + "Keyboard shortcuts": "कीबोर्ड शॉर्टकट", + "Knowledge": "", + "Language": "भाषा", + "large language models, locally.": "", + "Last Active": "पिछली बार सक्रिय", + "Last Modified": "", + "Light": "सुन", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "एलएलएम गलतियाँ कर सकते हैं। महत्वपूर्ण जानकारी सत्यापित करें.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "OpenWebUI समुदाय द्वारा निर्मित", + "Make sure to enclose them with": "उन्हें संलग्न करना सुनिश्चित करें", + "Manage": "", + "Manage Models": "मॉडल प्रबंधित करें", + "Manage Ollama Models": "Ollama मॉडल प्रबंधित करें", + "Manage Pipelines": "पाइपलाइनों का प्रबंधन करें", + "Manage Valves": "", + "March": "मार्च", + "Max Tokens (num_predict)": "अधिकतम टोकन (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "अधिकतम 3 मॉडल एक साथ डाउनलोड किये जा सकते हैं। कृपया बाद में पुन: प्रयास करें।", + "May": "मेई", + "Memories accessible by LLMs will be shown here.": "एलएलएम द्वारा सुलभ यादें यहां दिखाई जाएंगी।", + "Memory": "मेमोरी", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "अपना लिंक बनाने के बाद आपके द्वारा भेजे गए संदेश साझा नहीं किए जाएंगे। यूआरएल वाले यूजर्स शेयर की गई चैट देख पाएंगे।", + "Minimum Score": "न्यूनतम स्कोर", + "Mirostat": "मिरोस्टा", + "Mirostat Eta": "मिरोस्टा ईटा", + "Mirostat Tau": "मिरोस्तात ताऊ", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "मॉडल '{{modelName}}' सफलतापूर्वक डाउनलोड हो गया है।", + "Model '{{modelTag}}' is already in queue for downloading.": "मॉडल '{{modelTag}}' पहले से ही डाउनलोड करने के लिए कतार में है।", + "Model {{modelId}} not found": "मॉडल {{modelId}} नहीं मिला", + "Model {{modelName}} is not vision capable": "मॉडल {{modelName}} दृष्टि सक्षम नहीं है", + "Model {{name}} is now {{status}}": "मॉडल {{name}} अब {{status}} है", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "मॉडल फ़ाइल सिस्टम पथ का पता चला. अद्यतन के लिए मॉडल संक्षिप्त नाम आवश्यक है, जारी नहीं रखा जा सकता।", + "Model ID": "मॉडल आईडी", + "Model not selected": "मॉडल चयनित नहीं है", + "Model Params": "मॉडल Params", + "Model updated successfully": "", + "Model Whitelisting": "मॉडल श्वेतसूचीकरण करें", + "Model(s) Whitelisted": "मॉडल श्वेतसूची में है", + "Modelfile Content": "मॉडल फ़ाइल सामग्री", + "Models": "सभी मॉडल", + "More": "और..", + "Name": "नाम", + "Name Tag": "नाम टैग", + "Name your model": "अपने मॉडल को नाम दें", + "New Chat": "नई चैट", + "New Password": "नया पासवर्ड", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "कोई परिणाम नहीं मिला", + "No search query generated": "कोई खोज क्वेरी जनरेट नहीं हुई", + "No source available": "कोई स्रोत उपलब्ध नहीं है", + "No valves to update": "", + "None": "कोई नहीं", + "Not factually correct": "तथ्यात्मक रूप से सही नहीं है", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ध्यान दें: यदि आप न्यूनतम स्कोर निर्धारित करते हैं, तो खोज केवल न्यूनतम स्कोर से अधिक या उसके बराबर स्कोर वाले दस्तावेज़ वापस लाएगी।", + "Notifications": "सूचनाएं", + "November": "नवंबर", + "num_thread (Ollama)": "num_thread (ओलामा)", + "OAuth ID": "", + "October": "अक्टूबर", + "Off": "बंद", + "Okay, Let's Go!": "ठीक है, चलिए चलते हैं!", + "OLED Dark": "OLEDescuro", + "Ollama": "Ollama", + "Ollama API": "ओलामा एपीआई", + "Ollama API disabled": "ओलामा एपीआई अक्षम", + "Ollama API is disabled": "", + "Ollama Version": "Ollama Version", + "On": "चालू", + "Only": "केवल", + "Only alphanumeric characters and hyphens are allowed in the command string.": "कमांड स्ट्रिंग में केवल अल्फ़ान्यूमेरिक वर्ण और हाइफ़न की अनुमति है।", + "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.": "उफ़! कृपया प्रतीक्षा करें, आपकी फ़ाइलें अभी भी प्रसंस्करण ओवन में हैं। हम उन्हें पूर्णता से पका रहे हैं। कृपया धैर्य रखें और जब वे तैयार हो जाएंगे तो हम आपको बता देंगे।", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "उफ़! ऐसा लगता है कि यूआरएल अमान्य है. कृपया दोबारा जांचें और पुनः प्रयास करें।", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "उफ़! आप एक असमर्थित विधि (केवल फ्रंटएंड) का उपयोग कर रहे हैं। कृपया बैकएंड से WebUI सर्वे करें।", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "नई चैट खोलें", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API कॉन्फिग", + "OpenAI API Key is required.": "OpenAI API कुंजी आवश्यक है", + "OpenAI URL/Key required.": "OpenAI URL/Key आवश्यक है।", + "or": "या", + "Other": "अन्य", + "Password": "पासवर्ड", + "PDF document (.pdf)": "PDF दस्तावेज़ (.pdf)", + "PDF Extract Images (OCR)": "PDF छवियाँ निकालें (OCR)", + "pending": "लंबित", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "माइक्रोफ़ोन तक पहुँचने पर अनुमति अस्वीकृत: {{error}}", + "Personalization": "पेरसनलाइज़मेंट", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "पाइपलाइनों", + "Pipelines Not Detected": "", + "Pipelines Valves": "पाइपलाइन वाल्व", + "Plain text (.txt)": "सादा पाठ (.txt)", + "Playground": "कार्यक्षेत्र", + "Please carefully review the following warnings:": "", + "Positive attitude": "सकारात्मक रवैया", + "Previous 30 days": "पिछले 30 दिन", + "Previous 7 days": "पिछले 7 दिन", + "Profile Image": "प्रोफ़ाइल छवि", + "Prompt": "प्रचलन", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "प्रॉम्प्ट (उदाहरण के लिए मुझे रोमन साम्राज्य के बारे में एक मजेदार तथ्य बताएं)", + "Prompt Content": "प्रॉम्प्ट सामग्री", + "Prompt suggestions": "प्रॉम्प्ट सुझाव", + "Prompts": "प्रॉम्प्ट", + "Pull \"{{searchValue}}\" from Ollama.com": "\"{{searchValue}}\" को Ollama.com से खींचें", + "Pull a model from Ollama.com": "Ollama.com से एक मॉडल खींचें", + "Query Params": "क्वेरी पैरामीटर", + "RAG Template": "RAG टेम्पलेट", + "Read Aloud": "जोर से पढ़ें", + "Record voice": "आवाज रिकॉर्ड करना", + "Redirecting you to OpenWebUI Community": "आपको OpenWebUI समुदाय पर पुनर्निर्देशित किया जा रहा है", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "जब ऐसा नहीं होना चाहिए था तो मना कर दिया", + "Regenerate": "पुनः जेनरेट", + "Release Notes": "रिलीज नोट्स", + "Remove": "हटा दें", + "Remove Model": "मोडेल हटाएँ", + "Rename": "नाम बदलें", + "Repeat Last N": "अंतिम N दोहराएँ", + "Request Mode": "अनुरोध मोड", + "Reranking Model": "रीरैकिंग मोड", + "Reranking model disabled": "पुनर्रैंकिंग मॉडल अक्षम किया गया", + "Reranking model set to \"{{reranking_model}}\"": "रीरैंकिंग मॉडल को \"{{reranking_model}}\" पर \u200b\u200bसेट किया गया", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "वेक्टर संग्रहण रीसेट करें", + "Response AutoCopy to Clipboard": "क्लिपबोर्ड पर प्रतिक्रिया ऑटोकॉपी", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "भूमिका", + "Rosé Pine": "रोसे पिन", + "Rosé Pine Dawn": "रोसे पिन डेन", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "सहेजें", + "Save & Create": "सहेजें और बनाएं", + "Save & Update": "सहेजें और अपडेट करें", + "Save Tag": "", + "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": "चैट लॉग को सीधे आपके ब्राउज़र के स्टोरेज में सहेजना अब समर्थित नहीं है। कृपया नीचे दिए गए बटन पर क्लिक करके डाउनलोड करने और अपने चैट लॉग को हटाने के लिए कुछ समय दें। चिंता न करें, आप आसानी से अपने चैट लॉग को बैकएंड पर पुनः आयात कर सकते हैं", + "Scan": "स्कैन", + "Scan complete!": "स्कैन पूरा हुआ!", + "Scan for documents from {{path}}": "{{path}} से दस्तावेज़ों को स्कैन करें", + "Search": "खोजें", + "Search a model": "एक मॉडल खोजें", + "Search Chats": "चैट खोजें", + "Search Documents": "दस्तावेज़ खोजें", + "Search Functions": "", + "Search Models": "मॉडल खोजें", + "Search Prompts": "प्रॉम्प्ट खोजें", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "खोज परिणामों की संख्या", + "Search Tools": "", + "Searched {{count}} sites_one": "{{count}} sites_one खोजा गया", + "Searched {{count}} sites_other": "{{count}} sites_other खोजा गया", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng क्वेरी URL", + "See readme.md for instructions": "निर्देशों के लिए readme.md देखें", + "See what's new": "देखें, क्या नया है", + "Seed": "सीड्\u200c", + "Select a base model": "एक आधार मॉडल का चयन करें", + "Select a engine": "", + "Select a function": "", + "Select a mode": "एक मोड चुनें", + "Select a model": "एक मॉडल चुनें", + "Select a pipeline": "एक पाइपलाइन का चयन करें", + "Select a pipeline url": "एक पाइपलाइन url चुनें", + "Select a tool": "", + "Select an Ollama instance": "एक Ollama Instance चुनें", + "Select Documents": "", + "Select model": "मॉडल चुनें", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "चयनित मॉडल छवि इनपुट का समर्थन नहीं करते हैं", + "Send": "भेज", + "Send a Message": "एक संदेश भेजो", + "Send message": "मेसेज भेजें", + "September": "सितंबर", + "Serper API Key": "Serper API कुंजी", + "Serply API Key": "", + "Serpstack API Key": "सर्पस्टैक एपीआई कुंजी", + "Server connection verified": "सर्वर कनेक्शन सत्यापित", + "Set as default": "डिफाल्ट के रूप में सेट", + "Set Default Model": "डिफ़ॉल्ट मॉडल सेट करें", + "Set embedding model (e.g. {{model}})": "ईम्बेडिंग मॉडल सेट करें (उदाहरण: {{model}})", + "Set Image Size": "छवि का आकार सेट करें", + "Set reranking model (e.g. {{model}})": "रीकरण मॉडल सेट करें (उदाहरण: {{model}})", + "Set Steps": "चरण निर्धारित करें", + "Set Task Model": "कार्य मॉडल सेट करें", + "Set Voice": "आवाज सेट करें", + "Settings": "सेटिंग्स", + "Settings saved successfully!": "सेटिंग्स सफलतापूर्वक सहेजी गईं!", + "Settings updated successfully": "", + "Share": "साझा करें", + "Share Chat": "चैट साझा करें", + "Share to OpenWebUI Community": "OpenWebUI समुदाय में साझा करें", + "short-summary": "संक्षिप्त सारांश", + "Show": "दिखाओ", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "शॉर्टकट दिखाएँ", + "Show your support!": "", + "Showcased creativity": "रचनात्मकता का प्रदर्शन किया", + "Sign in": "साइन इन", + "Sign Out": "साइन आउट", + "Sign up": "साइन अप", + "Signing in": "साइन इन हो रहा है", + "Source": "स्रोत", + "Speech recognition error: {{error}}": "वाक् पहचान त्रुटि: {{error}}", + "Speech-to-Text Engine": "वाक्-से-पाठ इंजन", + "Stop Sequence": "अनुक्रम रोकें", + "STT Model": "", + "STT Settings": "STT सेटिंग्स ", + "Submit": "सबमिट करें", + "Subtitle (e.g. about the Roman Empire)": "उपशीर्षक (जैसे रोमन साम्राज्य के बारे में)", + "Success": "संपन्न", + "Successfully updated.": "सफलतापूर्वक उत्परिवर्तित।", + "Suggested": "सुझावी", + "Support": "", + "Support this plugin:": "", + "System": "सिस्टम", + "System Prompt": "सिस्टम प्रॉम्प्ट", + "Tags": "टैग", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "हमें और अधिक बताएँ:", + "Temperature": "टेंपेरेचर", + "Template": "टेम्पलेट", + "Text Completion": "पाठ समापन", + "Text-to-Speech Engine": "टेक्स्ट-टू-स्पीच इंजन", + "Tfs Z": "टफ्स Z", + "Thanks for your feedback!": "आपकी प्रतिक्रिया के लिए धन्यवाद!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "स्कोर का मान 0.0 (0%) और 1.0 (100%) के बीच होना चाहिए।", + "Theme": "थीम", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "यह सुनिश्चित करता है कि आपकी मूल्यवान बातचीत आपके बैकएंड डेटाबेस में सुरक्षित रूप से सहेजी गई है। धन्यवाद!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "यह सेटिंग सभी ब्राउज़रों या डिवाइसों में समन्वयित नहीं होती है", + "This will delete": "", + "Thorough explanation": "विस्तृत व्याख्या", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "टिप: प्रत्येक प्रतिस्थापन के बाद चैट इनपुट में टैब कुंजी दबाकर लगातार कई वैरिएबल स्लॉट अपडेट करें।", + "Title": "शीर्षक", + "Title (e.g. Tell me a fun fact)": "शीर्षक (उदा. मुझे एक मज़ेदार तथ्य बताएं)", + "Title Auto-Generation": "शीर्षक ऑटो-जेनरेशन", + "Title cannot be an empty string.": "शीर्षक नहीं खाली पाठ हो सकता है.", + "Title Generation Prompt": "शीर्षक जनरेशन प्रॉम्प्ट", + "to": "तक", + "To access the available model names for downloading,": "डाउनलोड करने के लिए उपलब्ध मॉडल नामों तक पहुंचने के लिए,", + "To access the GGUF models available for downloading,": "डाउनलोडिंग के लिए उपलब्ध GGUF मॉडल तक पहुँचने के लिए,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "इनपुट चैट करने के लिए.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "आज", + "Toggle settings": "सेटिंग्स टॉगल करें", + "Toggle sidebar": "साइडबार टॉगल करें", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "शीर्ष K", + "Top P": "शीर्ष P", + "Trouble accessing Ollama?": "Ollama तक पहुँचने में परेशानी हो रही है?", + "TTS Model": "", + "TTS Settings": "TTS सेटिंग्स", + "TTS Voice": "", + "Type": "प्रकार", + "Type Hugging Face Resolve (Download) URL": "हगिंग फेस रिज़ॉल्व (डाउनलोड) यूआरएल टाइप करें", + "Uh-oh! There was an issue connecting to {{provider}}.": "उह ओह! {{provider}} से कनेक्ट करने में एक समस्या थी।", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "अपडेट करें और लिंक कॉपी करें", + "Update password": "पासवर्ड अपडेट करें", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "GGUF मॉडल अपलोड करें", + "Upload Files": "फ़ाइलें अपलोड करें", + "Upload Pipeline": "", + "Upload Progress": "प्रगति अपलोड करें", + "URL Mode": "URL मोड", + "Use '#' in the prompt input to load and select your documents.": "अपने दस्तावेज़ों को लोड करने और चुनने के लिए शीघ्र इनपुट में '#' का उपयोग करें।", + "Use Gravatar": "Gravatar का प्रयोग करें", + "Use Initials": "प्रथमाक्षर का प्रयोग करें", + "use_mlock (Ollama)": "use_mlock (ओलामा)", + "use_mmap (Ollama)": "use_mmap (ओलामा)", + "user": "उपयोगकर्ता", + "User location successfully retrieved.": "", + "User Permissions": "उपयोगकर्ता अनुमतियाँ", + "Users": "उपयोगकर्ताओं", + "Utilize": "उपयोग करें", + "Valid time units:": "मान्य समय इकाइयाँ:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "वेरिएबल", + "variable to have them replaced with clipboard content.": "उन्हें क्लिपबोर्ड सामग्री से बदलने के लिए वेरिएबल।", + "Version": "संस्करण", + "Voice": "", + "Warning": "चेतावनी", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "चेतावनी: यदि आप अपने एम्बेडिंग मॉडल को अपडेट या बदलते हैं, तो आपको सभी दस्तावेज़ों को फिर से आयात करने की आवश्यकता होगी।", + "Web": "वेब", + "Web API": "", + "Web Loader Settings": "वेब लोडर सेटिंग्स", + "Web Params": "वेब पैरामीटर", + "Web Search": "वेब खोज", + "Web Search Engine": "वेब खोज इंजन", + "Webhook URL": "वेबहुक URL", + "WebUI Settings": "WebUI सेटिंग्स", + "WebUI will make requests to": "WebUI अनुरोध करेगा", + "What’s New in": "इसमें नया क्या है", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "जब इतिहास बंद हो जाता है, तो इस ब्राउज़र पर नई चैट आपके किसी भी डिवाइस पर इतिहास में दिखाई नहीं देंगी।", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "वर्कस्पेस", + "Write a prompt suggestion (e.g. Who are you?)": "एक त्वरित सुझाव लिखें (जैसे कि आप कौन हैं?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "50 शब्दों में एक सारांश लिखें जो [विषय या कीवर्ड] का सारांश प्रस्तुत करता हो।", + "Yesterday": "कल", + "You": "आप", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "आप बेस मॉडल का क्लोन नहीं बना सकते", + "You have no archived conversations.": "आपको कोई अंकित चैट नहीं है।", + "You have shared this chat": "आपने इस चैट को शेयर किया है", + "You're a helpful assistant.": "आप एक सहायक सहायक हैं", + "You're now logged in.": "अब आप लॉग इन हो गए हैं", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "यूट्यूब लोडर सेटिंग्स" +} diff --git a/src/lib/i18n/locales/hr-HR/translation.json b/src/lib/i18n/locales/hr-HR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..a4bbbc5eecd06ad57e130419c5dd87a1534dd131 --- /dev/null +++ b/src/lib/i18n/locales/hr-HR/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ili '-1' za bez isteka.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(npr. `sh webui.sh --api`)", + "(latest)": "(najnovije)", + "{{ models }}": "{{ modeli }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Ne možete obrisati osnovni model", + "{{modelName}} is thinking...": "{{modelName}} razmišlja...", + "{{user}}'s Chats": "Razgovori korisnika {{user}}", + "{{webUIName}} Backend Required": "{{webUIName}} Backend je potreban", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Model zadatka koristi se pri izvođenju zadataka kao što su generiranje naslova za razgovore i upite za pretraživanje weba", + "a user": "korisnik", + "About": "O aplikaciji", + "Account": "Račun", + "Account Activation Pending": "", + "Accurate information": "Točne informacije", + "Actions": "", + "Active Users": "Aktivni korisnici", + "Add": "Dodaj", + "Add a model id": "Dodavanje ID-a modela", + "Add a short description about what this model does": "Dodajte kratak opis funkcija ovog modela", + "Add a short title for this prompt": "Dodajte kratki naslov za ovaj prompt", + "Add a tag": "Dodaj oznaku", + "Add custom prompt": "Dodaj prilagođeni prompt", + "Add Docs": "Dodaj dokumente", + "Add Files": "Dodaj datoteke", + "Add Memory": "Dodaj memoriju", + "Add message": "Dodaj poruku", + "Add Model": "Dodaj model", + "Add Tag": "", + "Add Tags": "Dodaj oznake", + "Add User": "Dodaj korisnika", + "Adjusting these settings will apply changes universally to all users.": "Podešavanje će se primijeniti univerzalno na sve korisnike.", + "admin": "administrator", + "Admin": "Admin", + "Admin Panel": "Admin ploča", + "Admin Settings": "Admin postavke", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Napredni parametri", + "Advanced Params": "Napredni parametri", + "all": "sve", + "All Documents": "Svi dokumenti", + "All Users": "Svi korisnici", + "Allow": "Dopusti", + "Allow Chat Deletion": "Dopusti brisanje razgovora", + "Allow non-local voices": "Dopusti nelokalne glasove", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "alfanumerički znakovi i crtice", + "Already have an account?": "Već imate račun?", + "an assistant": "asistent", + "and": "i", + "and create a new shared link.": "i stvorite novu dijeljenu vezu.", + "API Base URL": "Osnovni URL API-ja", + "API Key": "API ključ", + "API Key created.": "API ključ je stvoren.", + "API keys": "API ključevi", + "April": "Travanj", + "Archive": "Arhiva", + "Archive All Chats": "Arhivirajte sve razgovore", + "Archived Chats": "Arhivirani razgovori", + "are allowed - Activate this command by typing": "su dopušteni - Aktivirajte ovu naredbu upisivanjem", + "Are you sure?": "Jeste li sigurni?", + "Attach file": "Priloži datoteku", + "Attention to detail": "Pažnja na detalje", + "Audio": "Audio", + "Audio settings updated successfully": "", + "August": "Kolovoz", + "Auto-playback response": "Automatska reprodukcija odgovora", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 osnovni URL", + "AUTOMATIC1111 Base URL is required.": "Potreban je AUTOMATIC1111 osnovni URL.", + "available!": "dostupno!", + "Back": "Natrag", + "Bad Response": "Loš odgovor", + "Banners": "Baneri", + "Base Model (From)": "Osnovni model (Od)", + "Batch Size (num_batch)": "", + "before": "prije", + "Being lazy": "Biti lijen", + "Brave Search API Key": "Brave tražilica - API ključ", + "Bypass SSL verification for Websites": "Zaobiđi SSL provjeru za web stranice", + "Call": "Poziv", + "Call feature is not supported when using Web STT engine": "Značajka poziva nije podržana kada se koristi Web STT mehanizam", + "Camera": "Kamera", + "Cancel": "Otkaži", + "Capabilities": "Mogućnosti", + "Change Password": "Promijeni lozinku", + "Chat": "Razgovor", + "Chat Background Image": "", + "Chat Bubble UI": "Razgovor - Bubble UI", + "Chat Controls": "", + "Chat direction": "Razgovor - smijer", + "Chat History": "Povijest razgovora", + "Chat History is off for this browser.": "Povijest razgovora je isključena za ovaj preglednik.", + "Chats": "Razgovori", + "Check Again": "Provjeri ponovo", + "Check for updates": "Provjeri za ažuriranja", + "Checking for updates...": "Provjeravam ažuriranja...", + "Choose a model before saving...": "Odaberite model prije spremanja...", + "Chunk Overlap": "Preklapanje dijelova", + "Chunk Params": "Parametri dijelova", + "Chunk Size": "Veličina dijela", + "Citation": "Citiranje", + "Clear memory": "Očisti memoriju", + "Click here for help.": "Kliknite ovdje za pomoć.", + "Click here to": "Kliknite ovdje za", + "Click here to download user import template file.": "", + "Click here to select": "Kliknite ovdje za odabir", + "Click here to select a csv file.": "Kliknite ovdje da odaberete csv datoteku.", + "Click here to select a py file.": "", + "Click here to select documents.": "Kliknite ovdje da odaberete dokumente.", + "click here.": "kliknite ovdje.", + "Click on the user role button to change a user's role.": "Kliknite na gumb uloge korisnika za promjenu uloge korisnika.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Kloniraj", + "Close": "Zatvori", + "Code formatted successfully": "", + "Collection": "Kolekcija", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI osnovni URL", + "ComfyUI Base URL is required.": "Potreban je ComfyUI osnovni URL.", + "Command": "Naredba", + "Concurrent Requests": "Istodobni zahtjevi", + "Confirm": "", + "Confirm Password": "Potvrdite lozinku", + "Confirm your action": "", + "Connections": "Povezivanja", + "Contact Admin for WebUI Access": "Kontaktirajte admina za WebUI pristup", + "Content": "Sadržaj", + "Content Extraction": "", + "Context Length": "Dužina konteksta", + "Continue Response": "Nastavi odgovor", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL dijeljenog razgovora kopiran u međuspremnik!", + "Copy": "Kopiraj", + "Copy last code block": "Kopiraj zadnji blok koda", + "Copy last response": "Kopiraj zadnji odgovor", + "Copy Link": "Kopiraj vezu", + "Copying to clipboard was successful!": "Kopiranje u međuspremnik je uspješno!", + "Create a model": "Izradite model", + "Create Account": "Stvori račun", + "Create new key": "Stvori novi ključ", + "Create new secret key": "Stvori novi tajni ključ", + "Created at": "Stvoreno", + "Created At": "Stvoreno", + "Created by": "", + "CSV Import": "", + "Current Model": "Trenutni model", + "Current Password": "Trenutna lozinka", + "Custom": "Prilagođeno", + "Customize models for a specific purpose": "Prilagodba modela za određenu svrhu", + "Dark": "Tamno", + "Dashboard": "Radna ploča", + "Database": "Baza podataka", + "December": "Prosinac", + "Default": "Zadano", + "Default (Automatic1111)": "Zadano (Automatic1111)", + "Default (SentenceTransformers)": "Zadano (SentenceTransformers)", + "Default Model": "Zadani model", + "Default model updated": "Zadani model ažuriran", + "Default Prompt Suggestions": "Zadani prijedlozi prompta", + "Default User Role": "Zadana korisnička uloga", + "delete": "izbriši", + "Delete": "Izbriši", + "Delete a model": "Izbriši model", + "Delete All Chats": "Izbriši sve razgovore", + "Delete chat": "Izbriši razgovor", + "Delete Chat": "Izbriši razgovor", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "izbriši ovu vezu", + "Delete tool?": "", + "Delete User": "Izbriši korisnika", + "Deleted {{deleteModelTag}}": "Izbrisan {{deleteModelTag}}", + "Deleted {{name}}": "Izbrisano {{name}}", + "Description": "Opis", + "Didn't fully follow instructions": "Nije u potpunosti slijedio upute", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Otkrijte model", + "Discover a prompt": "Otkrijte prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Otkrijte, preuzmite i istražite prilagođene prompte", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Otkrijte, preuzmite i istražite unaprijed postavljene modele", + "Dismissible": "Odbaciti", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Prikaži korisničko ime umjesto Vas u razgovoru", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokument", + "Document Settings": "Postavke dokumenta", + "Documentation": "Dokumentacija", + "Documents": "Dokumenti", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ne uspostavlja vanjske veze, a vaši podaci ostaju sigurno na vašem lokalno hostiranom poslužitelju.", + "Don't Allow": "Ne dopuštaj", + "Don't have an account?": "Nemate račun?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Ne sviđa mi se stil", + "Done": "", + "Download": "Preuzimanje", + "Download canceled": "Preuzimanje otkazano", + "Download Database": "Preuzmi bazu podataka", + "Drop any files here to add to the conversation": "Spustite bilo koje datoteke ovdje za dodavanje u razgovor", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "npr. '30s','10m'. Važeće vremenske jedinice su 's', 'm', 'h'.", + "Edit": "Uredi", + "Edit Doc": "Uredi dokument", + "Edit Memory": "", + "Edit User": "Uredi korisnika", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "Embedding - Veličina batch-a", + "Embedding Model": "Embedding model", + "Embedding Model Engine": "Embedding model pogon", + "Embedding model set to \"{{embedding_model}}\"": "Embedding model postavljen na \"{{embedding_model}}\"", + "Enable Chat History": "Omogući povijest razgovora", + "Enable Community Sharing": "Omogući zajedničko korištenje zajednice", + "Enable New Sign Ups": "Omogući nove prijave", + "Enable Web Search": "Omogući pretraživanje weba", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Provjerite da vaša CSV datoteka uključuje 4 stupca u ovom redoslijedu: Name, Email, Password, Role.", + "Enter {{role}} message here": "Unesite {{role}} poruku ovdje", + "Enter a detail about yourself for your LLMs to recall": "Unesite pojedinosti o sebi da bi učitali memoriju u LLM", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Unesite Brave Search API ključ", + "Enter Chunk Overlap": "Unesite preklapanje dijelova", + "Enter Chunk Size": "Unesite veličinu dijela", + "Enter Github Raw URL": "Unesite Github sirovi URL", + "Enter Google PSE API Key": "Unesite Google PSE API ključ", + "Enter Google PSE Engine Id": "Unesite ID Google PSE motora", + "Enter Image Size (e.g. 512x512)": "Unesite veličinu slike (npr. 512x512)", + "Enter language codes": "Unesite kodove jezika", + "Enter model tag (e.g. {{modelTag}})": "Unesite oznaku modela (npr. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Unesite broj koraka (npr. 50)", + "Enter Score": "Unesite ocjenu", + "Enter Searxng Query URL": "Unesite URL upita Searxng", + "Enter Serper API Key": "Unesite Serper API ključ", + "Enter Serply API Key": "Unesite Serply API ključ", + "Enter Serpstack API Key": "Unesite Serpstack API ključ", + "Enter stop sequence": "Unesite sekvencu zaustavljanja", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Unesite Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Unesite URL (npr. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Unesite URL (npr. http://localhost:11434)", + "Enter Your Email": "Unesite svoj email", + "Enter Your Full Name": "Unesite svoje puno ime", + "Enter your message": "", + "Enter Your Password": "Unesite svoju lozinku", + "Enter Your Role": "Unesite svoju ulogu", + "Error": "Greška", + "Experimental": "Eksperimentalno", + "Export": "Izvoz", + "Export All Chats (All Users)": "Izvoz svih razgovora (svi korisnici)", + "Export chat (.json)": "Izvoz četa (.json)", + "Export Chats": "Izvoz razgovora", + "Export Documents Mapping": "Izvoz mapiranja dokumenata", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Izvoz modela", + "Export Prompts": "Izvoz prompta", + "Export Tools": "Izvoz alata", + "External Models": "Vanjski modeli", + "Failed to create API Key.": "Neuspješno stvaranje API ključa.", + "Failed to read clipboard contents": "Neuspješno čitanje sadržaja međuspremnika", + "Failed to update settings": "Greška kod ažuriranja postavki", + "February": "Veljača", + "Feel free to add specific details": "Slobodno dodajte specifične detalje", + "File": "", + "File Mode": "Način datoteke", + "File not found.": "Datoteka nije pronađena.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Otkriveno krivotvorenje otisaka prstiju: Nemoguće je koristiti inicijale kao avatar. Postavljanje na zadanu profilnu sliku.", + "Fluidly stream large external response chunks": "Glavno strujanje velikih vanjskih dijelova odgovora", + "Focus chat input": "Fokusiraj unos razgovora", + "Followed instructions perfectly": "Savršeno slijedio upute", + "Form": "", + "Format your variables using square brackets like this:": "Formatirajte svoje varijable pomoću uglatih zagrada ovako:", + "Frequency Penalty": "Kazna za učestalost", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Općenito", + "General Settings": "Opće postavke", + "Generate Image": "Gneriraj sliku", + "Generating search query": "Generiranje upita za pretraživanje", + "Generation Info": "Informacije o generaciji", + "Get up and running with": "", + "Global": "", + "Good Response": "Dobar odgovor", + "Google PSE API Key": "Google PSE API ključ", + "Google PSE Engine Id": "ID Google PSE modula", + "h:mm a": "h:mm a", + "has no conversations.": "nema razgovora.", + "Hello, {{name}}": "Bok, {{name}}", + "Help": "Pomoć", + "Hide": "Sakrij", + "Hide Model": "", + "How can I help you today?": "Kako vam mogu pomoći danas?", + "Hybrid Search": "Hibridna pretraga", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Generiranje slika (eksperimentalno)", + "Image Generation Engine": "Stroj za generiranje slika", + "Image Settings": "Postavke slike", + "Images": "Slike", + "Import Chats": "Uvoz razgovora", + "Import Documents Mapping": "Uvoz mapiranja dokumenata", + "Import Functions": "", + "Import Models": "Uvoz modela", + "Import Prompts": "Uvoz prompta", + "Import Tools": "Uvoz alata", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Uključite zastavicu `--api` prilikom pokretanja stable-diffusion-webui", + "Info": "Informacije", + "Input commands": "Unos naredbi", + "Install from Github URL": "Instaliraj s Github URL-a", + "Instant Auto-Send After Voice Transcription": "Trenutačno automatsko slanje nakon glasovne transkripcije", + "Interface": "Sučelje", + "Invalid Tag": "Nevažeća oznaka", + "January": "Siječanj", + "join our Discord for help.": "pridružite se našem Discordu za pomoć.", + "JSON": "JSON", + "JSON Preview": "JSON pretpregled", + "July": "Srpanj", + "June": "Lipanj", + "JWT Expiration": "Isticanje JWT-a", + "JWT Token": "JWT token", + "Keep Alive": "Održavanje živim", + "Keyboard shortcuts": "Tipkovnički prečaci", + "Knowledge": "Znanje", + "Language": "Jezik", + "large language models, locally.": "", + "Last Active": "Zadnja aktivnost", + "Last Modified": "", + "Light": "Svijetlo", + "Listening...": "Slušam...", + "LLMs can make mistakes. Verify important information.": "LLM-ovi mogu pogriješiti. Provjerite važne informacije.", + "Local Models": "Lokalni modeli", + "LTR": "LTR", + "Made by OpenWebUI Community": "Izradio OpenWebUI Community", + "Make sure to enclose them with": "Provjerite da ih zatvorite s", + "Manage": "Upravljaj", + "Manage Models": "Upravljanje modelima", + "Manage Ollama Models": "Upravljanje Ollama modelima", + "Manage Pipelines": "Upravljanje cjevovodima", + "Manage Valves": "", + "March": "Ožujak", + "Max Tokens (num_predict)": "Maksimalan broj tokena (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksimalno 3 modela se mogu preuzeti istovremeno. Pokušajte ponovo kasnije.", + "May": "Svibanj", + "Memories accessible by LLMs will be shown here.": "Ovdje će biti prikazana memorija kojoj mogu pristupiti LLM-ovi.", + "Memory": "Memorija", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Poruke koje pošaljete nakon stvaranja veze neće se dijeliti. Korisnici s URL-om moći će vidjeti zajednički chat.", + "Minimum Score": "Minimalna ocjena", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' je uspješno preuzet.", + "Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' je već u redu za preuzimanje.", + "Model {{modelId}} not found": "Model {{modelId}} nije pronađen", + "Model {{modelName}} is not vision capable": "Model {{modelName}} ne čita vizualne impute", + "Model {{name}} is now {{status}}": "Model {{name}} sada je {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Otkriven put datotečnog sustava modela. Kratko ime modela je potrebno za ažuriranje, nije moguće nastaviti.", + "Model ID": "ID modela", + "Model not selected": "Model nije odabran", + "Model Params": "Model parametri", + "Model updated successfully": "", + "Model Whitelisting": "Model - Bijela lista", + "Model(s) Whitelisted": "Model(i) na bijeloj listi", + "Modelfile Content": "Sadržaj datoteke modela", + "Models": "Modeli", + "More": "Više", + "Name": "Ime", + "Name Tag": "Naziv oznake", + "Name your model": "Dodijelite naziv modelu", + "New Chat": "Novi razgovor", + "New Password": "Nova lozinka", + "No content to speak": "", + "No documents found": "Dokumenti nisu pronađeni", + "No file selected": "", + "No results found": "Nema rezultata", + "No search query generated": "Nije generiran upit za pretraživanje", + "No source available": "Nema dostupnog izvora", + "No valves to update": "", + "None": "Ništa", + "Not factually correct": "Nije činjenično točno", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Napomena: Ako postavite minimalnu ocjenu, pretraga će vratiti samo dokumente s ocjenom većom ili jednakom minimalnoj ocjeni.", + "Notifications": "Obavijesti", + "November": "Studeni", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Listopad", + "Off": "Isključeno", + "Okay, Let's Go!": "U redu, idemo!", + "OLED Dark": "OLED Tamno", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API je onemogućen", + "Ollama API is disabled": "Ollama API je onemogućen", + "Ollama Version": "Ollama verzija", + "On": "Uključeno", + "Only": "Samo", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Samo alfanumerički znakovi i crtice su dopušteni u naredbenom nizu.", + "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.": "Ups! Držite se! Vaše datoteke su još uvijek u procesu obrade. Pečemo ih do savršenstva. Molimo vas da budete strpljivi i obavijestit ćemo vas kada budu spremne.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Izgleda da je URL nevažeći. Molimo provjerite ponovno i pokušajte ponovo.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Koristite nepodržanu metodu (samo frontend). Molimo poslužite WebUI s backend-a.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Otvorite novi razgovor", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API konfiguracija", + "OpenAI API Key is required.": "Potreban je OpenAI API ključ.", + "OpenAI URL/Key required.": "Potreban je OpenAI URL/ključ.", + "or": "ili", + "Other": "Ostalo", + "Password": "Lozinka", + "PDF document (.pdf)": "PDF dokument (.pdf)", + "PDF Extract Images (OCR)": "PDF izdvajanje slika (OCR)", + "pending": "u tijeku", + "Permission denied when accessing media devices": "Dopuštenje je odbijeno prilikom pristupa medijskim uređajima", + "Permission denied when accessing microphone": "Dopuštenje je odbijeno prilikom pristupa mikrofonu", + "Permission denied when accessing microphone: {{error}}": "Pristup mikrofonu odbijen: {{error}}", + "Personalization": "Prilagodba", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Cjevovodi", + "Pipelines Not Detected": "", + "Pipelines Valves": "Ventili za cjevovode", + "Plain text (.txt)": "Običan tekst (.txt)", + "Playground": "Igralište", + "Please carefully review the following warnings:": "", + "Positive attitude": "Pozitivan stav", + "Previous 30 days": "Prethodnih 30 dana", + "Previous 7 days": "Prethodnih 7 dana", + "Profile Image": "Profilna slika", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (npr. Reci mi zanimljivost o Rimskom carstvu)", + "Prompt Content": "Sadržaj prompta", + "Prompt suggestions": "Prijedlozi prompta", + "Prompts": "Prompti", + "Pull \"{{searchValue}}\" from Ollama.com": "Povucite \"{{searchValue}}\" s Ollama.com", + "Pull a model from Ollama.com": "Povucite model s Ollama.com", + "Query Params": "Parametri upita", + "RAG Template": "RAG predložak", + "Read Aloud": "Čitaj naglas", + "Record voice": "Snimanje glasa", + "Redirecting you to OpenWebUI Community": "Preusmjeravanje na OpenWebUI zajednicu", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Nazivajte se \"Korisnik\" (npr. \"Korisnik uči španjolski\")", + "Refused when it shouldn't have": "Odbijen kada nije trebao biti", + "Regenerate": "Regeneriraj", + "Release Notes": "Bilješke o izdanju", + "Remove": "Ukloni", + "Remove Model": "Ukloni model", + "Rename": "Preimenuj", + "Repeat Last N": "Ponovi zadnjih N", + "Request Mode": "Način zahtjeva", + "Reranking Model": "Model za ponovno rangiranje", + "Reranking model disabled": "Model za ponovno rangiranje onemogućen", + "Reranking model set to \"{{reranking_model}}\"": "Model za ponovno rangiranje postavljen na \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "Poništi upload direktorij", + "Reset Vector Storage": "Resetiraj pohranu vektora", + "Response AutoCopy to Clipboard": "Automatsko kopiranje odgovora u međuspremnik", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Uloga", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Pokrenuto", + "Save": "Spremi", + "Save & Create": "Spremi i stvori", + "Save & Update": "Spremi i ažuriraj", + "Save Tag": "", + "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": "Spremanje zapisnika razgovora izravno u pohranu vašeg preglednika više nije podržano. Molimo vas da odvojite trenutak za preuzimanje i brisanje zapisnika razgovora klikom na gumb ispod. Ne brinite, možete lako ponovno uvesti zapisnike razgovora u backend putem", + "Scan": "Skeniraj", + "Scan complete!": "Skeniranje dovršeno!", + "Scan for documents from {{path}}": "Skeniraj dokumente s {{path}}", + "Search": "Pretraga", + "Search a model": "Pretraži model", + "Search Chats": "Pretraži razgovore", + "Search Documents": "Pretraga dokumenata", + "Search Functions": "", + "Search Models": "Pretražite modele", + "Search Prompts": "Pretraga prompta", + "Search Query Generation Prompt": "Upit za generiranje upita za pretraživanje", + "Search Query Generation Prompt Length Threshold": "Prag duljine upita za generiranje upita za pretraživanje", + "Search Result Count": "Broj rezultata pretraživanja", + "Search Tools": "Alati za pretraživanje", + "Searched {{count}} sites_one": "Pretraženo {{count}} sites_one", + "Searched {{count}} sites_few": "Pretraženo {{count}} sites_few", + "Searched {{count}} sites_other": "Pretraženo {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng URL upita", + "See readme.md for instructions": "Pogledajte readme.md za upute", + "See what's new": "Pogledajte što je novo", + "Seed": "Sjeme", + "Select a base model": "Odabir osnovnog modela", + "Select a engine": "Odaberite pogon", + "Select a function": "", + "Select a mode": "Odaberite način", + "Select a model": "Odaberite model", + "Select a pipeline": "Odabir kanala", + "Select a pipeline url": "Odabir URL-a kanala", + "Select a tool": "", + "Select an Ollama instance": "Odaberite Ollama instancu", + "Select Documents": "Odaberite dokumente", + "Select model": "Odaberite model", + "Select only one model to call": "Odaberite samo jedan model za poziv", + "Selected model(s) do not support image inputs": "Odabrani modeli ne podržavaju unose slika", + "Send": "Pošalji", + "Send a Message": "Pošaljite poruku", + "Send message": "Pošalji poruku", + "September": "Rujan", + "Serper API Key": "Serper API ključ", + "Serply API Key": "Serply API ključ", + "Serpstack API Key": "Serpstack API API ključ", + "Server connection verified": "Veza s poslužiteljem potvrđena", + "Set as default": "Postavi kao zadano", + "Set Default Model": "Postavi zadani model", + "Set embedding model (e.g. {{model}})": "Postavi model za embedding (npr. {{model}})", + "Set Image Size": "Postavi veličinu slike", + "Set reranking model (e.g. {{model}})": "Postavi model za ponovno rangiranje (npr. {{model}})", + "Set Steps": "Postavi korake", + "Set Task Model": "Postavite model zadatka", + "Set Voice": "Postavi glas", + "Settings": "Postavke", + "Settings saved successfully!": "Postavke su uspješno spremljene!", + "Settings updated successfully": "Postavke uspješno ažurirane", + "Share": "Podijeli", + "Share Chat": "Podijeli razgovor", + "Share to OpenWebUI Community": "Podijeli u OpenWebUI zajednici", + "short-summary": "kratki sažetak", + "Show": "Pokaži", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Pokaži prečace", + "Show your support!": "", + "Showcased creativity": "Prikazana kreativnost", + "Sign in": "Prijava", + "Sign Out": "Odjava", + "Sign up": "Registracija", + "Signing in": "Prijava", + "Source": "Izvor", + "Speech recognition error: {{error}}": "Pogreška prepoznavanja govora: {{error}}", + "Speech-to-Text Engine": "Stroj za prepoznavanje govora", + "Stop Sequence": "Zaustavi sekvencu", + "STT Model": "STT model", + "STT Settings": "STT postavke", + "Submit": "Pošalji", + "Subtitle (e.g. about the Roman Empire)": "Podnaslov (npr. o Rimskom carstvu)", + "Success": "Uspjeh", + "Successfully updated.": "Uspješno ažurirano.", + "Suggested": "Predloženo", + "Support": "", + "Support this plugin:": "", + "System": "Sustav", + "System Prompt": "Sistemski prompt", + "Tags": "Oznake", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Recite nam više:", + "Temperature": "Temperatura", + "Template": "Predložak", + "Text Completion": "Dovršavanje teksta", + "Text-to-Speech Engine": "Stroj za pretvorbu teksta u govor", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Hvala na povratnim informacijama!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Ocjena treba biti vrijednost između 0,0 (0%) i 1,0 (100%).", + "Theme": "Tema", + "Thinking...": "Razmišljam", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ovo osigurava da su vaši vrijedni razgovori sigurno spremljeni u bazu podataka. Hvala vam!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Ovo je eksperimentalna značajka, možda neće funkcionirati prema očekivanjima i podložna je promjenama u bilo kojem trenutku.", + "This setting does not sync across browsers or devices.": "Ova postavka se ne sinkronizira između preglednika ili uređaja.", + "This will delete": "", + "Thorough explanation": "Detaljno objašnjenje", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Savjet: Ažurirajte više mjesta za varijable uzastopno pritiskom na tipku tab u unosu razgovora nakon svake zamjene.", + "Title": "Naslov", + "Title (e.g. Tell me a fun fact)": "Naslov (npr. Reci mi zanimljivost)", + "Title Auto-Generation": "Automatsko generiranje naslova", + "Title cannot be an empty string.": "Naslov ne može biti prazni niz.", + "Title Generation Prompt": "Prompt za generiranje naslova", + "to": "do", + "To access the available model names for downloading,": "Za pristup dostupnim nazivima modela za preuzimanje,", + "To access the GGUF models available for downloading,": "Za pristup GGUF modelima dostupnim za preuzimanje,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Za pristup WebUI-u obratite se administratoru. Administratori mogu upravljati statusima korisnika s Admin panela.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Da biste ovdje dodali dokumente, prvo ih prenesite u radni prostor \"Dokumenti\".", + "to chat input.": "u unos razgovora.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Danas", + "Toggle settings": "Prebaci postavke", + "Toggle sidebar": "Prebaci bočnu traku", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Alati", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemi s pristupom Ollama?", + "TTS Model": "TTS model", + "TTS Settings": "TTS postavke", + "TTS Voice": "TTS glas", + "Type": "Tip", + "Type Hugging Face Resolve (Download) URL": "Upišite Hugging Face Resolve (Download) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Pojavio se problem s povezivanjem na {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Ažuriraj i kopiraj vezu", + "Update password": "Ažuriraj lozinku", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Učitaj GGUF model", + "Upload Files": "Prijenos datoteka", + "Upload Pipeline": "Prijenos kanala", + "Upload Progress": "Napredak učitavanja", + "URL Mode": "URL način", + "Use '#' in the prompt input to load and select your documents.": "Koristite '#' u unosu prompta za učitavanje i odabir vaših dokumenata.", + "Use Gravatar": "Koristi Gravatar", + "Use Initials": "Koristi inicijale", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "korisnik", + "User location successfully retrieved.": "", + "User Permissions": "Korisnička dopuštenja", + "Users": "Korisnici", + "Utilize": "Iskoristi", + "Valid time units:": "Važeće vremenske jedinice:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "varijabla", + "variable to have them replaced with clipboard content.": "varijabla za zamjenu sadržajem međuspremnika.", + "Version": "Verzija", + "Voice": "", + "Warning": "Upozorenje", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Upozorenje: Ako ažurirate ili promijenite svoj model za umetanje, morat ćete ponovno uvesti sve dokumente.", + "Web": "Web", + "Web API": "Web API", + "Web Loader Settings": "Postavke web učitavanja", + "Web Params": "Web parametri", + "Web Search": "Internet pretraga", + "Web Search Engine": "Web tražilica", + "Webhook URL": "URL webkuke", + "WebUI Settings": "WebUI postavke", + "WebUI will make requests to": "WebUI će slati zahtjeve na", + "What’s New in": "Što je novo u", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kada je povijest isključena, novi razgovori na ovom pregledniku neće se pojaviti u vašoj povijesti na bilo kojem od vaših uređaja.", + "Whisper (Local)": "Whisper (lokalno)", + "Widescreen Mode": "Mod širokog zaslona", + "Workspace": "Radna ploča", + "Write a prompt suggestion (e.g. Who are you?)": "Napišite prijedlog prompta (npr. Tko si ti?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Napišite sažetak u 50 riječi koji sažima [temu ili ključnu riječ].", + "Yesterday": "Jučer", + "You": "Vi", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Možete personalizirati svoje interakcije s LLM-ima dodavanjem uspomena putem gumba 'Upravljanje' u nastavku, čineći ih korisnijima i prilagođenijima vama.", + "You cannot clone a base model": "Ne možete klonirati osnovni model", + "You have no archived conversations.": "Nemate arhiviranih razgovora.", + "You have shared this chat": "Podijelili ste ovaj razgovor", + "You're a helpful assistant.": "Vi ste korisni asistent.", + "You're now logged in.": "Sada ste prijavljeni.", + "Your account status is currently pending activation.": "Status vašeg računa trenutno čeka aktivaciju.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "YouTube postavke učitavanja" +} diff --git a/src/lib/i18n/locales/id-ID/translation.json b/src/lib/i18n/locales/id-ID/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..092e760ca5d849ca79e96ddc4254cda83046a0c1 --- /dev/null +++ b/src/lib/i18n/locales/id-ID/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' atau '-1' untuk tidak ada kedaluwarsa.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(contoh: `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(contoh: `sh webui.sh --api`)", + "(latest)": "(terbaru)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Anda tidak dapat menghapus model dasar", + "{{modelName}} is thinking...": "{{modelName}} sedang berpikir...", + "{{user}}'s Chats": "Obrolan {{user}}", + "{{webUIName}} Backend Required": "{{webUIName}} Diperlukan Backend", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Model tugas digunakan saat melakukan tugas seperti membuat judul untuk obrolan dan kueri penelusuran web", + "a user": "seorang pengguna", + "About": "Tentang", + "Account": "Akun", + "Account Activation Pending": "Aktivasi Akun Tertunda", + "Accurate information": "Informasi yang akurat", + "Actions": "", + "Active Users": "Pengguna Aktif", + "Add": "Tambah", + "Add a model id": "Tambahkan id model", + "Add a short description about what this model does": "Tambahkan deskripsi singkat tentang apa yang dilakukan model ini", + "Add a short title for this prompt": "Tambahkan judul singkat untuk prompt ini", + "Add a tag": "Menambahkan tag", + "Add custom prompt": "Tambahkan prompt khusus", + "Add Docs": "Tambahkan Dokumen", + "Add Files": "Menambahkan File", + "Add Memory": "Menambahkan Memori", + "Add message": "Tambahkan pesan", + "Add Model": "Tambahkan Model", + "Add Tag": "", + "Add Tags": "Tambahkan Tag", + "Add User": "Tambah Pengguna", + "Adjusting these settings will apply changes universally to all users.": "Menyesuaikan pengaturan ini akan menerapkan perubahan secara universal ke semua pengguna.", + "admin": "admin", + "Admin": "Admin", + "Admin Panel": "Panel Admin", + "Admin Settings": "Pengaturan Admin", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Admin memiliki akses ke semua alat setiap saat; pengguna memerlukan alat yang ditetapkan per model di ruang kerja.", + "Advanced Parameters": "Parameter Lanjutan", + "Advanced Params": "Parameter Lanjutan", + "all": "semua", + "All Documents": "Semua Dokumen", + "All Users": "Semua Pengguna", + "Allow": "Mengizinkan", + "Allow Chat Deletion": "Izinkan Penghapusan Obrolan", + "Allow non-local voices": "Izinkan suara non-lokal", + "Allow User Location": "Izinkan Lokasi Pengguna", + "Allow Voice Interruption in Call": "Izinkan Gangguan Suara dalam Panggilan", + "alphanumeric characters and hyphens": "karakter alfanumerik dan tanda hubung", + "Already have an account?": "Sudah memiliki akun?", + "an assistant": "asisten", + "and": "dan", + "and create a new shared link.": "dan membuat tautan bersama baru.", + "API Base URL": "URL Dasar API", + "API Key": "Kunci API", + "API Key created.": "Kunci API dibuat.", + "API keys": "Kunci API", + "April": "April", + "Archive": "Arsipkan", + "Archive All Chats": "Arsipkan Semua Obrolan", + "Archived Chats": "Obrolan yang Diarsipkan", + "are allowed - Activate this command by typing": "diizinkan - Aktifkan perintah ini dengan mengetik", + "Are you sure?": "Apakah Anda yakin?", + "Attach file": "Lampirkan file", + "Attention to detail": "Perhatian terhadap detail", + "Audio": "Audio", + "Audio settings updated successfully": "Pengaturan audio berhasil diperbarui", + "August": "Agustus", + "Auto-playback response": "Respons pemutaran otomatis", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Api Auth String", + "AUTOMATIC1111 Base URL": "URL Dasar AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 URL Dasar diperlukan.", + "available!": "tersedia!", + "Back": "Kembali", + "Bad Response": "Respons Buruk", + "Banners": "Spanduk", + "Base Model (From)": "Model Dasar (Dari)", + "Batch Size (num_batch)": "Ukuran Batch (num_batch)", + "before": "sebelum", + "Being lazy": "Menjadi malas", + "Brave Search API Key": "Kunci API Pencarian Berani", + "Bypass SSL verification for Websites": "Lewati verifikasi SSL untuk Situs Web", + "Call": "Panggilan", + "Call feature is not supported when using Web STT engine": "Fitur panggilan tidak didukung saat menggunakan mesin Web STT", + "Camera": "Kamera", + "Cancel": "Batal", + "Capabilities": "Kemampuan", + "Change Password": "Ubah Kata Sandi", + "Chat": "Obrolan", + "Chat Background Image": "Gambar Latar Belakang Obrolan", + "Chat Bubble UI": "UI Gelembung Obrolan", + "Chat Controls": "", + "Chat direction": "Arah obrolan", + "Chat History": "Riwayat Obrolan", + "Chat History is off for this browser.": "Riwayat Obrolan tidak aktif untuk browser ini.", + "Chats": "Obrolan", + "Check Again": "Periksa Lagi", + "Check for updates": "Memeriksa pembaruan", + "Checking for updates...": "Memeriksa pembaruan...", + "Choose a model before saving...": "Pilih model sebelum menyimpan...", + "Chunk Overlap": "Tumpang Tindih Potongan", + "Chunk Params": "Parameter Potongan", + "Chunk Size": "Ukuran Potongan", + "Citation": "Kutipan", + "Clear memory": "Menghapus memori", + "Click here for help.": "Klik di sini untuk bantuan.", + "Click here to": "Klik di sini untuk", + "Click here to download user import template file.": "Klik di sini untuk mengunduh file templat impor pengguna.", + "Click here to select": "Klik di sini untuk memilih", + "Click here to select a csv file.": "Klik di sini untuk memilih file csv.", + "Click here to select a py file.": "Klik di sini untuk memilih file py.", + "Click here to select documents.": "Klik di sini untuk memilih dokumen.", + "click here.": "Klik di sini.", + "Click on the user role button to change a user's role.": "Klik tombol peran pengguna untuk mengubah peran pengguna.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Izin menulis papan klip ditolak. Periksa pengaturan peramban Anda untuk memberikan akses yang diperlukan.", + "Clone": "Kloning", + "Close": "Tutup", + "Code formatted successfully": "Kode berhasil diformat", + "Collection": "Koleksi", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL Dasar ComfyUI", + "ComfyUI Base URL is required.": "URL Dasar ComfyUI diperlukan.", + "Command": "Perintah", + "Concurrent Requests": "Permintaan Bersamaan", + "Confirm": "Konfirmasi", + "Confirm Password": "Konfirmasi Kata Sandi", + "Confirm your action": "Konfirmasi tindakan Anda", + "Connections": "Koneksi", + "Contact Admin for WebUI Access": "Hubungi Admin untuk Akses WebUI", + "Content": "Konten", + "Content Extraction": "", + "Context Length": "Panjang Konteks", + "Continue Response": "Lanjutkan Tanggapan", + "Continue with {{provider}}": "Lanjutkan dengan {{penyedia}}", + "Controls": "", + "Copied shared chat URL to clipboard!": "Menyalin URL obrolan bersama ke papan klip!", + "Copy": "Menyalin", + "Copy last code block": "Salin blok kode terakhir", + "Copy last response": "Salin tanggapan terakhir", + "Copy Link": "Salin Tautan", + "Copying to clipboard was successful!": "Penyalinan ke papan klip berhasil!", + "Create a model": "Buat model", + "Create Account": "Buat Akun", + "Create new key": "Buat kunci baru", + "Create new secret key": "Buat kunci rahasia baru", + "Created at": "Dibuat di", + "Created At": "Dibuat di", + "Created by": "Dibuat oleh", + "CSV Import": "Impor CSV", + "Current Model": "Model Saat Ini", + "Current Password": "Kata Sandi Saat Ini", + "Custom": "Kustom", + "Customize models for a specific purpose": "Menyesuaikan model untuk tujuan tertentu", + "Dark": "Gelap", + "Dashboard": "Dasbor", + "Database": "Basis data", + "December": "Desember", + "Default": "Default", + "Default (Automatic1111)": "Default (Automatic1111)", + "Default (SentenceTransformers)": "Default (Pengubah Kalimat)", + "Default Model": "Model Default", + "Default model updated": "Model default diperbarui", + "Default Prompt Suggestions": "Saran Permintaan Default", + "Default User Role": "Peran Pengguna Default", + "delete": "Hapus", + "Delete": "Menghapus", + "Delete a model": "Menghapus model", + "Delete All Chats": "Menghapus Semua Obrolan", + "Delete chat": "Menghapus obrolan", + "Delete Chat": "Menghapus Obrolan", + "Delete chat?": "Menghapus obrolan?", + "Delete Doc": "", + "Delete function?": "Fungsi hapus?", + "Delete prompt?": "Perintah hapus?", + "delete this link": "hapus tautan ini", + "Delete tool?": "Hapus alat?", + "Delete User": "Menghapus Pengguna", + "Deleted {{deleteModelTag}}": "Menghapus {{deleteModelTag}}", + "Deleted {{name}}": "Menghapus {{name}}", + "Description": "Deskripsi", + "Didn't fully follow instructions": "Tidak sepenuhnya mengikuti instruksi", + "Disabled": "", + "Discover a function": "Menemukan sebuah fungsi", + "Discover a model": "Menemukan sebuah model", + "Discover a prompt": "Temukan petunjuk", + "Discover a tool": "Menemukan alat", + "Discover, download, and explore custom functions": "Menemukan, mengunduh, dan menjelajahi fungsi khusus", + "Discover, download, and explore custom prompts": "Temukan, unduh, dan jelajahi prompt khusus", + "Discover, download, and explore custom tools": "Menemukan, mengunduh, dan menjelajahi alat khusus", + "Discover, download, and explore model presets": "Menemukan, mengunduh, dan menjelajahi preset model", + "Dismissible": "Tidak dapat digunakan", + "Display Emoji in Call": "Menampilkan Emoji dalam Panggilan", + "Display the username instead of You in the Chat": "Menampilkan nama pengguna, bukan Anda di Obrolan", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokumen", + "Document Settings": "Pengaturan Dokumen", + "Documentation": "Dokumentasi", + "Documents": "Dokumen", + "does not make any external connections, and your data stays securely on your locally hosted server.": "tidak membuat koneksi eksternal apa pun, dan data Anda tetap aman di server yang dihosting secara lokal.", + "Don't Allow": "Jangan Izinkan", + "Don't have an account?": "Tidak memiliki akun?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Tidak suka gayanya", + "Done": "Selesai", + "Download": "Unduh", + "Download canceled": "Unduh dibatalkan", + "Download Database": "Unduh Basis Data", + "Drop any files here to add to the conversation": "Letakkan file apa pun di sini untuk ditambahkan ke percakapan", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "misalnya '30-an', '10m'. Satuan waktu yang valid adalah 's', 'm', 'h'.", + "Edit": "Edit", + "Edit Doc": "Edit Dokumen", + "Edit Memory": "Edit Memori", + "Edit User": "Edit Pengguna", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "Menyematkan Ukuran Batch", + "Embedding Model": "Model Penyematan", + "Embedding Model Engine": "Mesin Model Penyematan", + "Embedding model set to \"{{embedding_model}}\"": "Model penyematan diatur ke \"{{embedding_model}}\"", + "Enable Chat History": "Aktifkan Riwayat Obrolan", + "Enable Community Sharing": "Aktifkan Berbagi Komunitas", + "Enable New Sign Ups": "Aktifkan Pendaftaran Baru", + "Enable Web Search": "Aktifkan Pencarian Web", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Pastikan file CSV Anda menyertakan 4 kolom dengan urutan sebagai berikut: Nama, Email, Kata Sandi, Peran.", + "Enter {{role}} message here": "Masukkan pesan {{role}} di sini", + "Enter a detail about yourself for your LLMs to recall": "Masukkan detail tentang diri Anda untuk diingat oleh LLM Anda", + "Enter api auth string (e.g. username:password)": "Masukkan string pengesahan API (misalnya nama pengguna: kata sandi)", + "Enter Brave Search API Key": "Masukkan Kunci API Pencarian Berani", + "Enter Chunk Overlap": "Masukkan Tumpang Tindih Chunk", + "Enter Chunk Size": "Masukkan Ukuran Potongan", + "Enter Github Raw URL": "Masukkan URL Mentah Github", + "Enter Google PSE API Key": "Masukkan Kunci API Google PSE", + "Enter Google PSE Engine Id": "Masukkan Id Mesin Google PSE", + "Enter Image Size (e.g. 512x512)": "Masukkan Ukuran Gambar (mis. 512x512)", + "Enter language codes": "Masukkan kode bahasa", + "Enter model tag (e.g. {{modelTag}})": "Masukkan tag model (misalnya {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Masukkan Jumlah Langkah (mis. 50)", + "Enter Score": "Masukkan Skor", + "Enter Searxng Query URL": "Masukkan URL Kueri Searxng", + "Enter Serper API Key": "Masukkan Kunci API Serper", + "Enter Serply API Key": "Masukkan Kunci API Serply", + "Enter Serpstack API Key": "Masukkan Kunci API Serpstack", + "Enter stop sequence": "Masukkan urutan berhenti", + "Enter system prompt": "", + "Enter Tavily API Key": "Masukkan Kunci API Tavily", + "Enter Tika Server URL": "", + "Enter Top K": "Masukkan Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Masukkan URL (mis. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Masukkan URL (mis. http://localhost:11434)", + "Enter Your Email": "Masukkan Email Anda", + "Enter Your Full Name": "Masukkan Nama Lengkap Anda", + "Enter your message": "", + "Enter Your Password": "Masukkan Kata Sandi Anda", + "Enter Your Role": "Masukkan Peran Anda", + "Error": "Kesalahan", + "Experimental": "Percobaan", + "Export": "Ekspor", + "Export All Chats (All Users)": "Ekspor Semua Obrolan (Semua Pengguna)", + "Export chat (.json)": "Ekspor obrolan (.json)", + "Export Chats": "Ekspor Obrolan", + "Export Documents Mapping": "Pemetaan Dokumen Ekspor", + "Export Functions": "Fungsi Ekspor", + "Export LiteLLM config.yaml": "Ekspor LiteLLM config.yaml", + "Export Models": "Model Ekspor", + "Export Prompts": "Perintah Ekspor", + "Export Tools": "Alat Ekspor", + "External Models": "Model Eksternal", + "Failed to create API Key.": "Gagal membuat API Key.", + "Failed to read clipboard contents": "Gagal membaca konten papan klip", + "Failed to update settings": "Gagal memperbarui pengaturan", + "February": "Februari", + "Feel free to add specific details": "Jangan ragu untuk menambahkan detail spesifik", + "File": "Berkas", + "File Mode": "Mode File", + "File not found.": "File tidak ditemukan.", + "Files": "", + "Filter is now globally disabled": "Filter sekarang dinonaktifkan secara global", + "Filter is now globally enabled": "Filter sekarang diaktifkan secara global", + "Filters": "Filter", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Pemalsuan sidik jari terdeteksi: Tidak dapat menggunakan inisial sebagai avatar. Default ke gambar profil default.", + "Fluidly stream large external response chunks": "Mengalirkan potongan respons eksternal yang besar dengan lancar", + "Focus chat input": "Memfokuskan input obrolan", + "Followed instructions perfectly": "Mengikuti instruksi dengan sempurna", + "Form": "Formulir", + "Format your variables using square brackets like this:": "Format variabel Anda menggunakan tanda kurung siku seperti ini:", + "Frequency Penalty": "Penalti Frekuensi", + "Function created successfully": "Fungsi berhasil dibuat", + "Function deleted successfully": "Fungsi berhasil dihapus", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "Fungsi berhasil diperbarui", + "Functions": "Fungsi", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Fungsi berhasil diimpor", + "General": "Umum", + "General Settings": "Pengaturan Umum", + "Generate Image": "Menghasilkan Gambar", + "Generating search query": "Membuat kueri penelusuran", + "Generation Info": "Info Pembuatan", + "Get up and running with": "", + "Global": "Global", + "Good Response": "Respons yang Baik", + "Google PSE API Key": "Kunci API Google PSE", + "Google PSE Engine Id": "Id Mesin Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "tidak memiliki percakapan.", + "Hello, {{name}}": "Halo, {{name}}", + "Help": "Bantuan", + "Hide": "Sembunyikan", + "Hide Model": "Sembunyikan Model", + "How can I help you today?": "Ada yang bisa saya bantu hari ini?", + "Hybrid Search": "Pencarian Hibrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Pembuatan Gambar (Eksperimental)", + "Image Generation Engine": "Mesin Pembuat Gambar", + "Image Settings": "Pengaturan Gambar", + "Images": "Gambar", + "Import Chats": "Impor Obrolan", + "Import Documents Mapping": "Pemetaan Dokumen Impor", + "Import Functions": "Fungsi Impor", + "Import Models": "Model Impor", + "Import Prompts": "Petunjuk Impor", + "Import Tools": "Alat Impor", + "Include `--api-auth` flag when running stable-diffusion-webui": "Sertakan bendera `--api-auth` saat menjalankan stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Sertakan bendera `--api` saat menjalankan stable-diffusion-webui", + "Info": "Info", + "Input commands": "Perintah masukan", + "Install from Github URL": "Instal dari URL Github", + "Instant Auto-Send After Voice Transcription": "Kirim Otomatis Instan Setelah Transkripsi Suara", + "Interface": "Antarmuka", + "Invalid Tag": "Tag tidak valid", + "January": "Januari", + "join our Discord for help.": "bergabunglah dengan Discord kami untuk mendapatkan bantuan.", + "JSON": "JSON", + "JSON Preview": "Pratinjau JSON", + "July": "Juli", + "June": "Juni", + "JWT Expiration": "Kedaluwarsa JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Tetap Hidup", + "Keyboard shortcuts": "Pintasan keyboard", + "Knowledge": "Pengetahuan", + "Language": "Bahasa", + "large language models, locally.": "", + "Last Active": "Terakhir Aktif", + "Last Modified": "Terakhir Dimodifikasi", + "Light": "Cahaya", + "Listening...": "Mendengarkan", + "LLMs can make mistakes. Verify important information.": "LLM dapat membuat kesalahan. Verifikasi informasi penting.", + "Local Models": "Model Lokal", + "LTR": "LTR", + "Made by OpenWebUI Community": "Dibuat oleh Komunitas OpenWebUI", + "Make sure to enclose them with": "Pastikan untuk melampirkannya dengan", + "Manage": "Mengelola", + "Manage Models": "Kelola Model", + "Manage Ollama Models": "Mengelola Model Ollama", + "Manage Pipelines": "Mengelola Saluran Pipa", + "Manage Valves": "Kelola Katup", + "March": "Maret", + "Max Tokens (num_predict)": "Token Maksimal (num_prediksi)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksimal 3 model dapat diunduh secara bersamaan. Silakan coba lagi nanti.", + "May": "Mei", + "Memories accessible by LLMs will be shown here.": "Memori yang dapat diakses oleh LLM akan ditampilkan di sini.", + "Memory": "Memori", + "Memory added successfully": "Memori berhasil ditambahkan", + "Memory cleared successfully": "Memori berhasil dihapus", + "Memory deleted successfully": "Memori berhasil dihapus", + "Memory updated successfully": "Memori berhasil diperbarui", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Pesan yang Anda kirim setelah membuat tautan tidak akan dibagikan. Pengguna yang memiliki URL tersebut akan dapat melihat obrolan yang dibagikan.", + "Minimum Score": "Skor Minimum", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH: mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY jj: mm: dd A", + "Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' telah berhasil diunduh.", + "Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' sudah berada dalam antrean untuk diunduh.", + "Model {{modelId}} not found": "Model {{modelId}} tidak ditemukan", + "Model {{modelName}} is not vision capable": "Model {{modelName}} tidak dapat dilihat", + "Model {{name}} is now {{status}}": "Model {{name}} sekarang menjadi {{status}}", + "Model created successfully!": "Model berhasil dibuat!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Jalur sistem berkas model terdeteksi. Nama pendek model diperlukan untuk pembaruan, tidak dapat dilanjutkan.", + "Model ID": "ID Model", + "Model not selected": "Model tidak dipilih", + "Model Params": "Parameter Model", + "Model updated successfully": "Model berhasil diperbarui", + "Model Whitelisting": "Daftar Putih Model", + "Model(s) Whitelisted": "Model(-model) Masuk Daftar Putih", + "Modelfile Content": "Konten File Model", + "Models": "Model", + "More": "Lainnya", + "Name": "Nama", + "Name Tag": "Label Nama", + "Name your model": "Beri nama model Anda", + "New Chat": "Obrolan Baru", + "New Password": "Kata Sandi Baru", + "No content to speak": "Tidak ada konten untuk dibicarakan", + "No documents found": "Tidak ada dokumen yang ditemukan", + "No file selected": "Tidak ada file yang dipilih", + "No results found": "Tidak ada hasil yang ditemukan", + "No search query generated": "Tidak ada permintaan pencarian yang dibuat", + "No source available": "Tidak ada sumber yang tersedia", + "No valves to update": "Tidak ada katup untuk diperbarui", + "None": "Tidak ada", + "Not factually correct": "Tidak benar secara faktual", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Catatan: Jika Anda menetapkan skor minimum, pencarian hanya akan mengembalikan dokumen dengan skor yang lebih besar atau sama dengan skor minimum.", + "Notifications": "Pemberitahuan", + "November": "November", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "ID OAuth", + "October": "Oktober", + "Off": "Mati", + "Okay, Let's Go!": "Oke, Ayo Kita Pergi!", + "OLED Dark": "OLED Gelap", + "Ollama": "Ollama", + "Ollama API": "API Ollama", + "Ollama API disabled": "API Ollama dinonaktifkan", + "Ollama API is disabled": "API Ollama dinonaktifkan", + "Ollama Version": "Versi Ollama", + "On": "Aktif", + "Only": "Hanya", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Hanya karakter alfanumerik dan tanda hubung yang diizinkan dalam string perintah.", + "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.": "Ups! Pegangan yang kuat! File Anda masih berada di dalam oven pemrosesan. Kami sedang memasaknya hingga sempurna. Mohon bersabar dan kami akan memberi tahu Anda jika sudah siap.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Sepertinya URL tidak valid. Mohon periksa ulang dan coba lagi.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Ups! Ada kesalahan pada respons sebelumnya. Silakan coba lagi atau hubungi admin.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Anda menggunakan metode yang tidak didukung (hanya untuk frontend). Silakan sajikan WebUI dari backend.", + "Open AI (Dall-E)": "Buka AI (Dall-E)", + "Open new chat": "Buka obrolan baru", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Konfigurasi API OpenAI", + "OpenAI API Key is required.": "Diperlukan Kunci API OpenAI.", + "OpenAI URL/Key required.": "Diperlukan URL/Kunci OpenAI.", + "or": "atau", + "Other": "Lainnya", + "Password": "Kata sandi", + "PDF document (.pdf)": "Dokumen PDF (.pdf)", + "PDF Extract Images (OCR)": "Ekstrak Gambar PDF (OCR)", + "pending": "tertunda", + "Permission denied when accessing media devices": "Izin ditolak saat mengakses perangkat media", + "Permission denied when accessing microphone": "Izin ditolak saat mengakses mikrofon", + "Permission denied when accessing microphone: {{error}}": "Izin ditolak saat mengakses mikrofon: {{error}}", + "Personalization": "Personalisasi", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "Pipeline berhasil dihapus", + "Pipeline downloaded successfully": "Saluran pipa berhasil diunduh", + "Pipelines": "Saluran pipa", + "Pipelines Not Detected": "Saluran Pipa Tidak Terdeteksi", + "Pipelines Valves": "Katup Saluran Pipa", + "Plain text (.txt)": "Teks biasa (.txt)", + "Playground": "Taman bermain", + "Please carefully review the following warnings:": "", + "Positive attitude": "Sikap positif", + "Previous 30 days": "30 hari sebelumnya", + "Previous 7 days": "7 hari sebelumnya", + "Profile Image": "Gambar Profil", + "Prompt": "Permintaan", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Permintaan (mis. Ceritakan sebuah fakta menarik tentang Kekaisaran Romawi)", + "Prompt Content": "Konten yang Diminta", + "Prompt suggestions": "Saran yang diminta", + "Prompts": "Prompt", + "Pull \"{{searchValue}}\" from Ollama.com": "Tarik \"{{searchValue}}\" dari Ollama.com", + "Pull a model from Ollama.com": "Tarik model dari Ollama.com", + "Query Params": "Parameter Kueri", + "RAG Template": "Templat RAG", + "Read Aloud": "Baca dengan Keras", + "Record voice": "Rekam suara", + "Redirecting you to OpenWebUI Community": "Mengarahkan Anda ke Komunitas OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Merujuk diri Anda sebagai \"Pengguna\" (misalnya, \"Pengguna sedang belajar bahasa Spanyol\")", + "Refused when it shouldn't have": "Menolak ketika seharusnya tidak", + "Regenerate": "Regenerasi", + "Release Notes": "Catatan Rilis", + "Remove": "Hapus", + "Remove Model": "Hapus Model", + "Rename": "Ganti nama", + "Repeat Last N": "Ulangi N Terakhir", + "Request Mode": "Mode Permintaan", + "Reranking Model": "Model Pemeringkatan Ulang", + "Reranking model disabled": "Model pemeringkatan ulang dinonaktifkan", + "Reranking model set to \"{{reranking_model}}\"": "Model pemeringkatan diatur ke \"{{reranking_model}}\"", + "Reset": "Atur Ulang", + "Reset Upload Directory": "Setel Ulang Direktori Unggahan", + "Reset Vector Storage": "Setel Ulang Penyimpanan Vektor", + "Response AutoCopy to Clipboard": "Tanggapan Salin Otomatis ke Papan Klip", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Notifikasi respons tidak dapat diaktifkan karena izin situs web telah ditolak. Silakan kunjungi pengaturan browser Anda untuk memberikan akses yang diperlukan.", + "Role": "Peran", + "Rosé Pine": "Pinus Rosé", + "Rosé Pine Dawn": "Rosé Pine Fajar", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Berjalan", + "Save": "Simpan", + "Save & Create": "Simpan & Buat", + "Save & Update": "Simpan & Perbarui", + "Save Tag": "", + "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": "Menyimpan log obrolan secara langsung ke penyimpanan browser Anda tidak lagi didukung. Mohon luangkan waktu sejenak untuk mengunduh dan menghapus log obrolan Anda dengan mengeklik tombol di bawah ini. Jangan khawatir, Anda dapat dengan mudah mengimpor kembali log obrolan Anda ke backend melalui", + "Scan": "Pindai", + "Scan complete!": "Pindai selesai!", + "Scan for documents from {{path}}": "Memindai dokumen dari {{path}}", + "Search": "Cari", + "Search a model": "Mencari model", + "Search Chats": "Cari Obrolan", + "Search Documents": "Cari Dokumen", + "Search Functions": "Fungsi Pencarian", + "Search Models": "Cari Model", + "Search Prompts": "Perintah Pencarian", + "Search Query Generation Prompt": "Permintaan Pembuatan Kueri Pencarian", + "Search Query Generation Prompt Length Threshold": "Ambang Batas Panjang Permintaan Pembuatan Kueri Pencarian", + "Search Result Count": "Jumlah Hasil Pencarian", + "Search Tools": "Alat Pencarian", + "Searched {{count}} sites_one": "Mencari {{count}} situs_satu", + "Searched {{count}} sites_other": "Mencari {{count}} situs_lain", + "Searching \"{{searchQuery}}\"": "Mencari \"{{searchQuery}}\"", + "Searxng Query URL": "URL Kueri Pencarian Searxng", + "See readme.md for instructions": "Lihat readme.md untuk instruksi", + "See what's new": "Lihat apa yang baru", + "Seed": "Benih", + "Select a base model": "Pilih model dasar", + "Select a engine": "Pilih mesin", + "Select a function": "Memilih fungsi", + "Select a mode": "Pilih mode", + "Select a model": "Pilih model", + "Select a pipeline": "Pilih saluran pipa", + "Select a pipeline url": "Pilih url saluran pipa", + "Select a tool": "Pilih alat", + "Select an Ollama instance": "Pilih contoh Ollama", + "Select Documents": "Pilih Dokumen", + "Select model": "Pilih model", + "Select only one model to call": "Pilih hanya satu model untuk dipanggil", + "Selected model(s) do not support image inputs": "Model yang dipilih tidak mendukung input gambar", + "Send": "Kirim", + "Send a Message": "Kirim Pesan", + "Send message": "Kirim pesan", + "September": "September", + "Serper API Key": "Kunci API Serper", + "Serply API Key": "Kunci API Serply", + "Serpstack API Key": "Kunci API Serpstack", + "Server connection verified": "Koneksi server diverifikasi", + "Set as default": "Ditetapkan sebagai default", + "Set Default Model": "Tetapkan Model Default", + "Set embedding model (e.g. {{model}})": "Tetapkan model penyematan (mis. {{model}})", + "Set Image Size": "Mengatur Ukuran Gambar", + "Set reranking model (e.g. {{model}})": "Tetapkan model pemeringkatan ulang (mis. {{model}})", + "Set Steps": "Tetapkan Langkah", + "Set Task Model": "Tetapkan Model Tugas", + "Set Voice": "Mengatur Suara", + "Settings": "Pengaturan", + "Settings saved successfully!": "Pengaturan berhasil disimpan!", + "Settings updated successfully": "Pengaturan berhasil diperbarui", + "Share": "Berbagi", + "Share Chat": "Bagikan Obrolan", + "Share to OpenWebUI Community": "Bagikan ke Komunitas OpenWebUI", + "short-summary": "ringkasan singkat", + "Show": "Tampilkan", + "Show Admin Details in Account Pending Overlay": "Tampilkan Detail Admin di Hamparan Akun Tertunda", + "Show Model": "Tampilkan Model", + "Show shortcuts": "Tampilkan pintasan", + "Show your support!": "Tunjukkan dukungan Anda!", + "Showcased creativity": "Menampilkan kreativitas", + "Sign in": "Masuk", + "Sign Out": "Keluar", + "Sign up": "Daftar", + "Signing in": "Masuk", + "Source": "Sumber", + "Speech recognition error: {{error}}": "Kesalahan pengenalan suara: {{error}}", + "Speech-to-Text Engine": "Mesin Pengenal Ucapan ke Teks", + "Stop Sequence": "Hentikan Urutan", + "STT Model": "Model STT", + "STT Settings": "Pengaturan STT", + "Submit": "Kirim", + "Subtitle (e.g. about the Roman Empire)": "Subtitle (misalnya tentang Kekaisaran Romawi)", + "Success": "Berhasil", + "Successfully updated.": "Berhasil diperbarui.", + "Suggested": "Disarankan", + "Support": "", + "Support this plugin:": "", + "System": "Sistem", + "System Prompt": "Permintaan Sistem", + "Tags": "Tag", + "Tap to interrupt": "Ketuk untuk menyela", + "Tavily API Key": "Kunci API Tavily", + "Tell us more:": "Beri tahu kami lebih lanjut:", + "Temperature": "Suhu", + "Template": "Templat", + "Text Completion": "Penyelesaian Teks", + "Text-to-Speech Engine": "Mesin Teks-ke-Suara", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Terima kasih atas umpan balik Anda!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Nilai yang diberikan haruslah nilai antara 0,0 (0%) dan 1,0 (100%).", + "Theme": "Tema", + "Thinking...": "Berpikir", + "This action cannot be undone. Do you wish to continue?": "Tindakan ini tidak dapat dibatalkan. Apakah Anda ingin melanjutkan?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ini akan memastikan bahwa percakapan Anda yang berharga disimpan dengan aman ke basis data backend. Terima kasih!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Ini adalah fitur eksperimental, mungkin tidak berfungsi seperti yang diharapkan dan dapat berubah sewaktu-waktu.", + "This setting does not sync across browsers or devices.": "Pengaturan ini tidak disinkronkan di seluruh browser atau perangkat.", + "This will delete": "Ini akan menghapus", + "Thorough explanation": "Penjelasan menyeluruh", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tips: Perbarui beberapa slot variabel secara berurutan dengan menekan tombol tab di input obrolan setelah setiap penggantian.", + "Title": "Judul", + "Title (e.g. Tell me a fun fact)": "Judul (misalnya, Ceritakan sebuah fakta menarik)", + "Title Auto-Generation": "Pembuatan Judul Secara Otomatis", + "Title cannot be an empty string.": "Judul tidak boleh berupa string kosong.", + "Title Generation Prompt": "Perintah Pembuatan Judul", + "to": "untuk", + "To access the available model names for downloading,": "Untuk mengakses nama model yang tersedia untuk diunduh,", + "To access the GGUF models available for downloading,": "Untuk mengakses model GGUF yang tersedia untuk diunduh,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Untuk mengakses WebUI, hubungi administrator. Admin dapat mengelola status pengguna dari Panel Admin.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Untuk menambahkan dokumen di sini, unggah dokumen ke ruang kerja \"Dokumen\" terlebih dahulu.", + "to chat input.": "Untuk memasukkan input obrolan.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Untuk memilih filter di sini, tambahkan filter ke ruang kerja \"Fungsi\" terlebih dahulu.", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Untuk memilih perangkat di sini, tambahkan ke ruang kerja \"Alat\" terlebih dahulu.", + "Today": "Hari ini", + "Toggle settings": "Beralih pengaturan", + "Toggle sidebar": "Beralih bilah sisi", + "Tokens To Keep On Context Refresh (num_keep)": "Token Untuk Menyimpan Penyegaran Konteks (num_keep)", + "Tool created successfully": "Alat berhasil dibuat", + "Tool deleted successfully": "Alat berhasil dihapus", + "Tool imported successfully": "Alat berhasil diimpor", + "Tool updated successfully": "Alat berhasil diperbarui", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Alat", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "K atas", + "Top P": "P Atas", + "Trouble accessing Ollama?": "Kesulitan mengakses Ollama?", + "TTS Model": "Model TTS", + "TTS Settings": "Pengaturan TTS", + "TTS Voice": "Suara TTS", + "Type": "Ketik", + "Type Hugging Face Resolve (Download) URL": "Ketik Hugging Face Resolve (Unduh) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Ada masalah saat menyambung ke {{provider}}.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Jenis file tidak dikenal '{{file_type}}'. Tetap lanjutkan dengan mengunggah file.", + "Unpin": "", + "Update": "Memperbarui", + "Update and Copy Link": "Perbarui dan Salin Tautan", + "Update password": "Perbarui kata sandi", + "Updated at": "Diperbarui di", + "Upload": "Unggah", + "Upload a GGUF model": "Unggah model GGUF", + "Upload Files": "Unggah File", + "Upload Pipeline": "Unggah Pipeline", + "Upload Progress": "Kemajuan Unggah", + "URL Mode": "Mode URL", + "Use '#' in the prompt input to load and select your documents.": "Gunakan '#' pada input prompt untuk memuat dan memilih dokumen Anda.", + "Use Gravatar": "Gunakan Gravatar", + "Use Initials": "Gunakan Inisial", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "pengguna", + "User location successfully retrieved.": "Lokasi pengguna berhasil diambil.", + "User Permissions": "Izin Pengguna", + "Users": "Pengguna", + "Utilize": "Memanfaatkan", + "Valid time units:": "Unit waktu yang valid:", + "Valves": "Katup", + "Valves updated": "Katup diperbarui", + "Valves updated successfully": "Katup berhasil diperbarui", + "variable": "variabel", + "variable to have them replaced with clipboard content.": "variabel untuk diganti dengan konten papan klip.", + "Version": "Versi", + "Voice": "Suara", + "Warning": "Peringatan", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Peringatan: Jika Anda memperbarui atau mengubah model penyematan, Anda harus mengimpor ulang semua dokumen.", + "Web": "Web", + "Web API": "API Web", + "Web Loader Settings": "Pengaturan Pemuat Web", + "Web Params": "Parameter Web", + "Web Search": "Pencarian Web", + "Web Search Engine": "Mesin Pencari Web", + "Webhook URL": "URL pengait web", + "WebUI Settings": "Pengaturan WebUI", + "WebUI will make requests to": "WebUI akan membuat permintaan ke", + "What’s New in": "Apa yang Baru di", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Ketika riwayat dimatikan, obrolan baru di browser ini tidak akan muncul di riwayat Anda di perangkat mana pun.", + "Whisper (Local)": "Bisikan (Lokal)", + "Widescreen Mode": "Mode Layar Lebar", + "Workspace": "Ruang Kerja", + "Write a prompt suggestion (e.g. Who are you?)": "Menulis saran cepat (misalnya Siapa kamu?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Tulis ringkasan dalam 50 kata yang merangkum [topik atau kata kunci].", + "Yesterday": "Kemarin", + "You": "Anda", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Anda dapat mempersonalisasi interaksi Anda dengan LLM dengan menambahkan kenangan melalui tombol 'Kelola' di bawah ini, sehingga lebih bermanfaat dan disesuaikan untuk Anda.", + "You cannot clone a base model": "Anda tidak dapat mengkloning model dasar", + "You have no archived conversations.": "Anda tidak memiliki percakapan yang diarsipkan.", + "You have shared this chat": "Anda telah membagikan obrolan ini", + "You're a helpful assistant.": "Anda adalah asisten yang membantu.", + "You're now logged in.": "Anda sekarang sudah masuk.", + "Your account status is currently pending activation.": "Status akun Anda saat ini sedang menunggu aktivasi.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Pengaturan Pemuat Youtube" +} diff --git a/src/lib/i18n/locales/it-IT/translation.json b/src/lib/i18n/locales/it-IT/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..3f27a2067259d642480580fb37dba5fa29b3c5d7 --- /dev/null +++ b/src/lib/i18n/locales/it-IT/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' o '-1' per nessuna scadenza.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(p.e. `sh webui.sh --api`)", + "(latest)": "(ultima)", + "{{ models }}": "{{ modelli }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: non è possibile eliminare un modello di base", + "{{modelName}} is thinking...": "{{modelName}} sta pensando...", + "{{user}}'s Chats": "{{user}} Chat", + "{{webUIName}} Backend Required": "{{webUIName}} Backend richiesto", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Un modello di attività viene utilizzato durante l'esecuzione di attività come la generazione di titoli per chat e query di ricerca Web", + "a user": "un utente", + "About": "Informazioni", + "Account": "Account", + "Account Activation Pending": "", + "Accurate information": "Informazioni accurate", + "Actions": "", + "Active Users": "", + "Add": "Aggiungi", + "Add a model id": "Aggiungere un ID modello", + "Add a short description about what this model does": "Aggiungi una breve descrizione di ciò che fa questo modello", + "Add a short title for this prompt": "Aggiungi un titolo breve per questo prompt", + "Add a tag": "Aggiungi un tag", + "Add custom prompt": "Aggiungi un prompt custom", + "Add Docs": "Aggiungi documenti", + "Add Files": "Aggiungi file", + "Add Memory": "Aggiungi memoria", + "Add message": "Aggiungi messaggio", + "Add Model": "Aggiungi modello", + "Add Tag": "", + "Add Tags": "Aggiungi tag", + "Add User": "Aggiungi utente", + "Adjusting these settings will apply changes universally to all users.": "La modifica di queste impostazioni applicherà le modifiche universalmente a tutti gli utenti.", + "admin": "amministratore", + "Admin": "", + "Admin Panel": "Pannello di amministrazione", + "Admin Settings": "Impostazioni amministratore", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Parametri avanzati", + "Advanced Params": "Parametri avanzati", + "all": "tutti", + "All Documents": "Tutti i documenti", + "All Users": "Tutti gli utenti", + "Allow": "Consenti", + "Allow Chat Deletion": "Consenti l'eliminazione della chat", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "caratteri alfanumerici e trattini", + "Already have an account?": "Hai già un account?", + "an assistant": "un assistente", + "and": "e", + "and create a new shared link.": "e crea un nuovo link condiviso.", + "API Base URL": "URL base API", + "API Key": "Chiave API", + "API Key created.": "Chiave API creata.", + "API keys": "Chiavi API", + "April": "Aprile", + "Archive": "Archivio", + "Archive All Chats": "Archivia tutte le chat", + "Archived Chats": "Chat archiviate", + "are allowed - Activate this command by typing": "sono consentiti - Attiva questo comando digitando", + "Are you sure?": "Sei sicuro?", + "Attach file": "Allega file", + "Attention to detail": "Attenzione ai dettagli", + "Audio": "Audio", + "Audio settings updated successfully": "", + "August": "Agosto", + "Auto-playback response": "Riproduzione automatica della risposta", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "URL base AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "L'URL base AUTOMATIC1111 è obbligatorio.", + "available!": "disponibile!", + "Back": "Indietro", + "Bad Response": "Risposta non valida", + "Banners": "Banner", + "Base Model (From)": "Modello base (da)", + "Batch Size (num_batch)": "", + "before": "prima", + "Being lazy": "Essere pigri", + "Brave Search API Key": "Chiave API di ricerca Brave", + "Bypass SSL verification for Websites": "Aggira la verifica SSL per i siti web", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Annulla", + "Capabilities": "Funzionalità", + "Change Password": "Cambia password", + "Chat": "Chat", + "Chat Background Image": "", + "Chat Bubble UI": "UI bolle chat", + "Chat Controls": "", + "Chat direction": "Direzione chat", + "Chat History": "Cronologia chat", + "Chat History is off for this browser.": "La cronologia chat è disattivata per questo browser.", + "Chats": "Chat", + "Check Again": "Controlla di nuovo", + "Check for updates": "Controlla aggiornamenti", + "Checking for updates...": "Controllo aggiornamenti...", + "Choose a model before saving...": "Scegli un modello prima di salvare...", + "Chunk Overlap": "Sovrapposizione chunk", + "Chunk Params": "Parametri chunk", + "Chunk Size": "Dimensione chunk", + "Citation": "Citazione", + "Clear memory": "", + "Click here for help.": "Clicca qui per aiuto.", + "Click here to": "Clicca qui per", + "Click here to download user import template file.": "", + "Click here to select": "Clicca qui per selezionare", + "Click here to select a csv file.": "Clicca qui per selezionare un file csv.", + "Click here to select a py file.": "", + "Click here to select documents.": "Clicca qui per selezionare i documenti.", + "click here.": "clicca qui.", + "Click on the user role button to change a user's role.": "Clicca sul pulsante del ruolo utente per modificare il ruolo di un utente.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Clone", + "Close": "Chiudi", + "Code formatted successfully": "", + "Collection": "Collezione", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL base ComfyUI", + "ComfyUI Base URL is required.": "L'URL base ComfyUI è obbligatorio.", + "Command": "Comando", + "Concurrent Requests": "Richieste simultanee", + "Confirm": "", + "Confirm Password": "Conferma password", + "Confirm your action": "", + "Connections": "Connessioni", + "Contact Admin for WebUI Access": "", + "Content": "Contenuto", + "Content Extraction": "", + "Context Length": "Lunghezza contesto", + "Continue Response": "Continua risposta", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL della chat condivisa copiato negli appunti!", + "Copy": "Copia", + "Copy last code block": "Copia ultimo blocco di codice", + "Copy last response": "Copia ultima risposta", + "Copy Link": "Copia link", + "Copying to clipboard was successful!": "Copia negli appunti riuscita!", + "Create a model": "Creare un modello", + "Create Account": "Crea account", + "Create new key": "Crea nuova chiave", + "Create new secret key": "Crea nuova chiave segreta", + "Created at": "Creato il", + "Created At": "Creato il", + "Created by": "", + "CSV Import": "", + "Current Model": "Modello corrente", + "Current Password": "Password corrente", + "Custom": "Personalizzato", + "Customize models for a specific purpose": "Personalizza i modelli per uno scopo specifico", + "Dark": "Scuro", + "Dashboard": "", + "Database": "Database", + "December": "Dicembre", + "Default": "Predefinito", + "Default (Automatic1111)": "Predefinito (Automatic1111)", + "Default (SentenceTransformers)": "Predefinito (SentenceTransformers)", + "Default Model": "Modello di default", + "Default model updated": "Modello predefinito aggiornato", + "Default Prompt Suggestions": "Suggerimenti prompt predefiniti", + "Default User Role": "Ruolo utente predefinito", + "delete": "elimina", + "Delete": "Elimina", + "Delete a model": "Elimina un modello", + "Delete All Chats": "Elimina tutte le chat", + "Delete chat": "Elimina chat", + "Delete Chat": "Elimina chat", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "elimina questo link", + "Delete tool?": "", + "Delete User": "Elimina utente", + "Deleted {{deleteModelTag}}": "Eliminato {{deleteModelTag}}", + "Deleted {{name}}": "Eliminato {{name}}", + "Description": "Descrizione", + "Didn't fully follow instructions": "Non ha seguito completamente le istruzioni", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Scopri un modello", + "Discover a prompt": "Scopri un prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Scopri, scarica ed esplora prompt personalizzati", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Scopri, scarica ed esplora i preset del modello", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Visualizza il nome utente invece di Tu nella chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Documento", + "Document Settings": "Impostazioni documento", + "Documentation": "", + "Documents": "Documenti", + "does not make any external connections, and your data stays securely on your locally hosted server.": "non effettua connessioni esterne e i tuoi dati rimangono al sicuro sul tuo server ospitato localmente.", + "Don't Allow": "Non consentire", + "Don't have an account?": "Non hai un account?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Non ti piace lo stile", + "Done": "", + "Download": "Scarica", + "Download canceled": "Scaricamento annullato", + "Download Database": "Scarica database", + "Drop any files here to add to the conversation": "Trascina qui i file da aggiungere alla conversazione", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "ad esempio '30s','10m'. Le unità di tempo valide sono 's', 'm', 'h'.", + "Edit": "Modifica", + "Edit Doc": "Modifica documento", + "Edit Memory": "", + "Edit User": "Modifica utente", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "", + "Embedding Model": "Modello di embedding", + "Embedding Model Engine": "Motore del modello di embedding", + "Embedding model set to \"{{embedding_model}}\"": "Modello di embedding impostato su \"{{embedding_model}}\"", + "Enable Chat History": "Abilita cronologia chat", + "Enable Community Sharing": "Abilita la condivisione della community", + "Enable New Sign Ups": "Abilita nuove iscrizioni", + "Enable Web Search": "Abilita ricerca Web", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Assicurati che il tuo file CSV includa 4 colonne in questo ordine: Nome, Email, Password, Ruolo.", + "Enter {{role}} message here": "Inserisci il messaggio per {{role}} qui", + "Enter a detail about yourself for your LLMs to recall": "Inserisci un dettaglio su di te per che i LLM possano ricordare", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Inserisci la chiave API di Brave Search", + "Enter Chunk Overlap": "Inserisci la sovrapposizione chunk", + "Enter Chunk Size": "Inserisci la dimensione chunk", + "Enter Github Raw URL": "Immettere l'URL grezzo di Github", + "Enter Google PSE API Key": "Inserisci la chiave API PSE di Google", + "Enter Google PSE Engine Id": "Inserisci l'ID motore PSE di Google", + "Enter Image Size (e.g. 512x512)": "Inserisci la dimensione dell'immagine (ad esempio 512x512)", + "Enter language codes": "Inserisci i codici lingua", + "Enter model tag (e.g. {{modelTag}})": "Inserisci il tag del modello (ad esempio {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Inserisci il numero di passaggi (ad esempio 50)", + "Enter Score": "Inserisci il punteggio", + "Enter Searxng Query URL": "Immettere l'URL della query Searxng", + "Enter Serper API Key": "Inserisci la chiave API Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Inserisci la chiave API Serpstack", + "Enter stop sequence": "Inserisci la sequenza di arresto", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Inserisci Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Inserisci URL (ad esempio http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Inserisci URL (ad esempio http://localhost:11434)", + "Enter Your Email": "Inserisci la tua email", + "Enter Your Full Name": "Inserisci il tuo nome completo", + "Enter your message": "", + "Enter Your Password": "Inserisci la tua password", + "Enter Your Role": "Inserisci il tuo ruolo", + "Error": "Errore", + "Experimental": "Sperimentale", + "Export": "Esportazione", + "Export All Chats (All Users)": "Esporta tutte le chat (tutti gli utenti)", + "Export chat (.json)": "", + "Export Chats": "Esporta chat", + "Export Documents Mapping": "Esporta mappatura documenti", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Esporta modelli", + "Export Prompts": "Esporta prompt", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Impossibile creare la chiave API.", + "Failed to read clipboard contents": "Impossibile leggere il contenuto degli appunti", + "Failed to update settings": "", + "February": "Febbraio", + "Feel free to add specific details": "Sentiti libero/a di aggiungere dettagli specifici", + "File": "", + "File Mode": "Modalità file", + "File not found.": "File non trovato.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Rilevato spoofing delle impronte digitali: impossibile utilizzare le iniziali come avatar. Ripristino all'immagine del profilo predefinita.", + "Fluidly stream large external response chunks": "Trasmetti in modo fluido blocchi di risposta esterni di grandi dimensioni", + "Focus chat input": "Metti a fuoco l'input della chat", + "Followed instructions perfectly": "Ha seguito le istruzioni alla perfezione", + "Form": "", + "Format your variables using square brackets like this:": "Formatta le tue variabili usando parentesi quadre come questa:", + "Frequency Penalty": "Penalità di frequenza", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Generale", + "General Settings": "Impostazioni generali", + "Generate Image": "", + "Generating search query": "Generazione di query di ricerca", + "Generation Info": "Informazioni generazione", + "Get up and running with": "", + "Global": "", + "Good Response": "Buona risposta", + "Google PSE API Key": "Chiave API PSE di Google", + "Google PSE Engine Id": "ID motore PSE di Google", + "h:mm a": "h:mm a", + "has no conversations.": "non ha conversazioni.", + "Hello, {{name}}": "Ciao, {{name}}", + "Help": "Aiuto", + "Hide": "Nascondi", + "Hide Model": "", + "How can I help you today?": "Come posso aiutarti oggi?", + "Hybrid Search": "Ricerca ibrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Generazione di immagini (sperimentale)", + "Image Generation Engine": "Motore di generazione immagini", + "Image Settings": "Impostazioni immagine", + "Images": "Immagini", + "Import Chats": "Importa chat", + "Import Documents Mapping": "Importa mappatura documenti", + "Import Functions": "", + "Import Models": "Importazione di modelli", + "Import Prompts": "Importa prompt", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Includi il flag `--api` quando esegui stable-diffusion-webui", + "Info": "Informazioni", + "Input commands": "Comandi di input", + "Install from Github URL": "Eseguire l'installazione dall'URL di Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Interfaccia", + "Invalid Tag": "Tag non valido", + "January": "Gennaio", + "join our Discord for help.": "unisciti al nostro Discord per ricevere aiuto.", + "JSON": "JSON", + "JSON Preview": "Anteprima JSON", + "July": "Luglio", + "June": "Giugno", + "JWT Expiration": "Scadenza JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Mantieni attivo", + "Keyboard shortcuts": "Scorciatoie da tastiera", + "Knowledge": "", + "Language": "Lingua", + "large language models, locally.": "", + "Last Active": "Ultima attività", + "Last Modified": "", + "Light": "Chiaro", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "Gli LLM possono commettere errori. Verifica le informazioni importanti.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Realizzato dalla comunità OpenWebUI", + "Make sure to enclose them with": "Assicurati di racchiuderli con", + "Manage": "", + "Manage Models": "Gestisci modelli", + "Manage Ollama Models": "Gestisci modelli Ollama", + "Manage Pipelines": "Gestire le pipeline", + "Manage Valves": "", + "March": "Marzo", + "Max Tokens (num_predict)": "Numero massimo di gettoni (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "È possibile scaricare un massimo di 3 modelli contemporaneamente. Riprova più tardi.", + "May": "Maggio", + "Memories accessible by LLMs will be shown here.": "I memori accessibili ai LLM saranno mostrati qui.", + "Memory": "Memoria", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "I messaggi inviati dopo la creazione del link non verranno condivisi. Gli utenti con l'URL saranno in grado di visualizzare la chat condivisa.", + "Minimum Score": "Punteggio minimo", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Il modello '{{modelName}}' è stato scaricato con successo.", + "Model '{{modelTag}}' is already in queue for downloading.": "Il modello '{{modelTag}}' è già in coda per il download.", + "Model {{modelId}} not found": "Modello {{modelId}} non trovato", + "Model {{modelName}} is not vision capable": "Il modello {{modelName}} non è in grado di vedere", + "Model {{name}} is now {{status}}": "Il modello {{name}} è ora {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Percorso del filesystem del modello rilevato. Il nome breve del modello è richiesto per l'aggiornamento, impossibile continuare.", + "Model ID": "ID modello", + "Model not selected": "Modello non selezionato", + "Model Params": "Parametri del modello", + "Model updated successfully": "", + "Model Whitelisting": "Whitelisting del modello", + "Model(s) Whitelisted": "Modello/i in whitelist", + "Modelfile Content": "Contenuto del file modello", + "Models": "Modelli", + "More": "Altro", + "Name": "Nome", + "Name Tag": "Nome tag", + "Name your model": "Assegna un nome al tuo modello", + "New Chat": "Nuova chat", + "New Password": "Nuova password", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Nessun risultato trovato", + "No search query generated": "Nessuna query di ricerca generata", + "No source available": "Nessuna fonte disponibile", + "No valves to update": "", + "None": "Nessuno", + "Not factually correct": "Non corretto dal punto di vista fattuale", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: se imposti un punteggio minimo, la ricerca restituirà solo i documenti con un punteggio maggiore o uguale al punteggio minimo.", + "Notifications": "Notifiche desktop", + "November": "Novembre", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Ottobre", + "Off": "Disattivato", + "Okay, Let's Go!": "Ok, andiamo!", + "OLED Dark": "OLED scuro", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API Ollama disabilitata", + "Ollama API is disabled": "", + "Ollama Version": "Versione Ollama", + "On": "Attivato", + "Only": "Solo", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Nella stringa di comando sono consentiti solo caratteri alfanumerici e trattini.", + "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.": "Ops! Aspetta! I tuoi file sono ancora in fase di elaborazione. Li stiamo cucinando alla perfezione. Per favore sii paziente e ti faremo sapere quando saranno pronti.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ops! Sembra che l'URL non sia valido. Si prega di ricontrollare e riprovare.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ops! Stai utilizzando un metodo non supportato (solo frontend). Si prega di servire la WebUI dal backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Apri nuova chat", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Configurazione API OpenAI", + "OpenAI API Key is required.": "La chiave API OpenAI è obbligatoria.", + "OpenAI URL/Key required.": "URL/Chiave OpenAI obbligatori.", + "or": "o", + "Other": "Altro", + "Password": "Password", + "PDF document (.pdf)": "Documento PDF (.pdf)", + "PDF Extract Images (OCR)": "Estrazione immagini PDF (OCR)", + "pending": "in sospeso", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Autorizzazione negata durante l'accesso al microfono: {{error}}", + "Personalization": "Personalizzazione", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Condutture", + "Pipelines Not Detected": "", + "Pipelines Valves": "Valvole per tubazioni", + "Plain text (.txt)": "Testo normale (.txt)", + "Playground": "Terreno di gioco", + "Please carefully review the following warnings:": "", + "Positive attitude": "Attitudine positiva", + "Previous 30 days": "Ultimi 30 giorni", + "Previous 7 days": "Ultimi 7 giorni", + "Profile Image": "Immagine del profilo", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (ad esempio Dimmi un fatto divertente sull'Impero Romano)", + "Prompt Content": "Contenuto del prompt", + "Prompt suggestions": "Suggerimenti prompt", + "Prompts": "Prompt", + "Pull \"{{searchValue}}\" from Ollama.com": "Estrai \"{{searchValue}}\" da Ollama.com", + "Pull a model from Ollama.com": "Estrai un modello da Ollama.com", + "Query Params": "Parametri query", + "RAG Template": "Modello RAG", + "Read Aloud": "Leggi ad alta voce", + "Record voice": "Registra voce", + "Redirecting you to OpenWebUI Community": "Reindirizzamento alla comunità OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Rifiutato quando non avrebbe dovuto", + "Regenerate": "Rigenera", + "Release Notes": "Note di rilascio", + "Remove": "Rimuovi", + "Remove Model": "Rimuovi modello", + "Rename": "Rinomina", + "Repeat Last N": "Ripeti ultimi N", + "Request Mode": "Modalità richiesta", + "Reranking Model": "Modello di riclassificazione", + "Reranking model disabled": "Modello di riclassificazione disabilitato", + "Reranking model set to \"{{reranking_model}}\"": "Modello di riclassificazione impostato su \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Reimposta archivio vettoriale", + "Response AutoCopy to Clipboard": "Copia automatica della risposta negli appunti", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Ruolo", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Salva", + "Save & Create": "Salva e crea", + "Save & Update": "Salva e aggiorna", + "Save Tag": "", + "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": "Il salvataggio dei registri della chat direttamente nell'archivio del browser non è più supportato. Si prega di dedicare un momento per scaricare ed eliminare i registri della chat facendo clic sul pulsante in basso. Non preoccuparti, puoi facilmente reimportare i registri della chat nel backend tramite", + "Scan": "Scansione", + "Scan complete!": "Scansione completata!", + "Scan for documents from {{path}}": "Cerca documenti da {{path}}", + "Search": "Cerca", + "Search a model": "Cerca un modello", + "Search Chats": "Cerca nelle chat", + "Search Documents": "Cerca documenti", + "Search Functions": "", + "Search Models": "Cerca modelli", + "Search Prompts": "Cerca prompt", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Conteggio dei risultati della ricerca", + "Search Tools": "", + "Searched {{count}} sites_one": "Ricercato {{count}} sites_one", + "Searched {{count}} sites_many": "Ricercato {{count}} sites_many", + "Searched {{count}} sites_other": "Ricercato {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng Query URL", + "See readme.md for instructions": "Vedi readme.md per le istruzioni", + "See what's new": "Guarda le novità", + "Seed": "Seme", + "Select a base model": "Selezionare un modello di base", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Seleziona una modalità", + "Select a model": "Seleziona un modello", + "Select a pipeline": "Selezionare una tubazione", + "Select a pipeline url": "Selezionare l'URL di una pipeline", + "Select a tool": "", + "Select an Ollama instance": "Seleziona un'istanza Ollama", + "Select Documents": "", + "Select model": "Seleziona modello", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "I modelli selezionati non supportano l'input di immagini", + "Send": "Invia", + "Send a Message": "Invia un messaggio", + "Send message": "Invia messaggio", + "September": "Settembre", + "Serper API Key": "Chiave API Serper", + "Serply API Key": "", + "Serpstack API Key": "Chiave API Serpstack", + "Server connection verified": "Connessione al server verificata", + "Set as default": "Imposta come predefinito", + "Set Default Model": "Imposta modello predefinito", + "Set embedding model (e.g. {{model}})": "Imposta modello di embedding (ad esempio {{model}})", + "Set Image Size": "Imposta dimensione immagine", + "Set reranking model (e.g. {{model}})": "Imposta modello di riclassificazione (ad esempio {{model}})", + "Set Steps": "Imposta passaggi", + "Set Task Model": "Imposta modello di attività", + "Set Voice": "Imposta voce", + "Settings": "Impostazioni", + "Settings saved successfully!": "Impostazioni salvate con successo!", + "Settings updated successfully": "", + "Share": "Condividi", + "Share Chat": "Condividi chat", + "Share to OpenWebUI Community": "Condividi con la comunità OpenWebUI", + "short-summary": "riassunto-breve", + "Show": "Mostra", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Mostra", + "Show your support!": "", + "Showcased creativity": "Creatività messa in mostra", + "Sign in": "Accedi", + "Sign Out": "Esci", + "Sign up": "Registrati", + "Signing in": "Accesso in corso", + "Source": "Fonte", + "Speech recognition error: {{error}}": "Errore di riconoscimento vocale: {{error}}", + "Speech-to-Text Engine": "Motore da voce a testo", + "Stop Sequence": "Sequenza di arresto", + "STT Model": "", + "STT Settings": "Impostazioni STT", + "Submit": "Invia", + "Subtitle (e.g. about the Roman Empire)": "Sottotitolo (ad esempio sull'Impero Romano)", + "Success": "Successo", + "Successfully updated.": "Aggiornato con successo.", + "Suggested": "Suggerito", + "Support": "", + "Support this plugin:": "", + "System": "Sistema", + "System Prompt": "Prompt di sistema", + "Tags": "Tag", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Raccontaci di più:", + "Temperature": "Temperatura", + "Template": "Modello", + "Text Completion": "Completamento del testo", + "Text-to-Speech Engine": "Motore da testo a voce", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Grazie per il tuo feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Il punteggio dovrebbe essere un valore compreso tra 0.0 (0%) e 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ciò garantisce che le tue preziose conversazioni siano salvate in modo sicuro nel tuo database backend. Grazie!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Questa impostazione non si sincronizza tra browser o dispositivi.", + "This will delete": "", + "Thorough explanation": "Spiegazione dettagliata", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Suggerimento: aggiorna più slot di variabili consecutivamente premendo il tasto tab nell'input della chat dopo ogni sostituzione.", + "Title": "Titolo", + "Title (e.g. Tell me a fun fact)": "Titolo (ad esempio Dimmi un fatto divertente)", + "Title Auto-Generation": "Generazione automatica del titolo", + "Title cannot be an empty string.": "Il titolo non può essere una stringa vuota.", + "Title Generation Prompt": "Prompt di generazione del titolo", + "to": "a", + "To access the available model names for downloading,": "Per accedere ai nomi dei modelli disponibili per il download,", + "To access the GGUF models available for downloading,": "Per accedere ai modelli GGUF disponibili per il download,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "all'input della chat.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Oggi", + "Toggle settings": "Attiva/disattiva impostazioni", + "Toggle sidebar": "Attiva/disattiva barra laterale", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemi di accesso a Ollama?", + "TTS Model": "", + "TTS Settings": "Impostazioni TTS", + "TTS Voice": "", + "Type": "Digitare", + "Type Hugging Face Resolve (Download) URL": "Digita l'URL di Hugging Face Resolve (Download)", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Si è verificato un problema durante la connessione a {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Aggiorna e copia link", + "Update password": "Aggiorna password", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Carica un modello GGUF", + "Upload Files": "Carica file", + "Upload Pipeline": "", + "Upload Progress": "Avanzamento caricamento", + "URL Mode": "Modalità URL", + "Use '#' in the prompt input to load and select your documents.": "Usa '#' nell'input del prompt per caricare e selezionare i tuoi documenti.", + "Use Gravatar": "Usa Gravatar", + "Use Initials": "Usa iniziali", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "utente", + "User location successfully retrieved.": "", + "User Permissions": "Autorizzazioni utente", + "Users": "Utenti", + "Utilize": "Utilizza", + "Valid time units:": "Unità di tempo valide:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variabile", + "variable to have them replaced with clipboard content.": "variabile per farli sostituire con il contenuto degli appunti.", + "Version": "Versione", + "Voice": "", + "Warning": "Avvertimento", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Attenzione: se aggiorni o cambi il tuo modello di embedding, dovrai reimportare tutti i documenti.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Impostazioni del caricatore Web", + "Web Params": "Parametri Web", + "Web Search": "Ricerca sul Web", + "Web Search Engine": "Motore di ricerca Web", + "Webhook URL": "URL webhook", + "WebUI Settings": "Impostazioni WebUI", + "WebUI will make requests to": "WebUI effettuerà richieste a", + "What’s New in": "Novità in", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Quando la cronologia è disattivata, le nuove chat su questo browser non verranno visualizzate nella cronologia su nessuno dei tuoi dispositivi.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Area di lavoro", + "Write a prompt suggestion (e.g. Who are you?)": "Scrivi un suggerimento per il prompt (ad esempio Chi sei?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Scrivi un riassunto in 50 parole che riassume [argomento o parola chiave].", + "Yesterday": "Ieri", + "You": "Tu", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Non è possibile clonare un modello di base", + "You have no archived conversations.": "Non hai conversazioni archiviate.", + "You have shared this chat": "Hai condiviso questa chat", + "You're a helpful assistant.": "Sei un assistente utile.", + "You're now logged in.": "Ora hai effettuato l'accesso.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Impostazioni del caricatore Youtube" +} diff --git a/src/lib/i18n/locales/ja-JP/translation.json b/src/lib/i18n/locales/ja-JP/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..5cd3083ce5835999b44364594fdd16d31f4329c6 --- /dev/null +++ b/src/lib/i18n/locales/ja-JP/translation.json @@ -0,0 +1,713 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' または '-1' で無期限。", + "(Beta)": "(ベータ版)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(例: `sh webui.sh --api`)", + "(latest)": "(最新)", + "{{ models }}": "{{ モデル }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: ベースモデルは削除できません", + "{{modelName}} is thinking...": "{{modelName}} は思考中です...", + "{{user}}'s Chats": "{{user}} のチャット", + "{{webUIName}} Backend Required": "{{webUIName}} バックエンドが必要です", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "タスクモデルは、チャットやWeb検索クエリのタイトルの生成などのタスクを実行するときに使用されます", + "a user": "ユーザー", + "About": "概要", + "Account": "アカウント", + "Account Activation Pending": "", + "Accurate information": "情報の正確性", + "Actions": "", + "Active Users": "", + "Add": "追加", + "Add a model id": "モデル ID を追加する", + "Add a short description about what this model does": "このモデルの機能に関する簡単な説明を追加します", + "Add a short title for this prompt": "このプロンプトの短いタイトルを追加", + "Add a tag": "タグを追加", + "Add custom prompt": "カスタムプロンプトを追加", + "Add Docs": "ドキュメントを追加", + "Add Files": "ファイルを追加", + "Add Memory": "メモリを追加", + "Add message": "メッセージを追加", + "Add Model": "モデルを追加", + "Add Tag": "", + "Add Tags": "タグを追加", + "Add User": "ユーザーを追加", + "Adjusting these settings will apply changes universally to all users.": "これらの設定を調整すると、すべてのユーザーに普遍的に変更が適用されます。", + "admin": "管理者", + "Admin": "", + "Admin Panel": "管理者パネル", + "Admin Settings": "管理者設定", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "詳細パラメーター", + "Advanced Params": "高度なパラメータ", + "all": "すべて", + "All Documents": "全てのドキュメント", + "All Users": "すべてのユーザー", + "Allow": "許可", + "Allow Chat Deletion": "チャットの削除を許可", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "英数字とハイフン", + "Already have an account?": "すでにアカウントをお持ちですか?", + "an assistant": "アシスタント", + "and": "および", + "and create a new shared link.": "し、新しい共有リンクを作成します。", + "API Base URL": "API ベース URL", + "API Key": "API キー", + "API Key created.": "API キーが作成されました。", + "API keys": "API キー", + "April": "4月", + "Archive": "アーカイブ", + "Archive All Chats": "すべてのチャットをアーカイブする", + "Archived Chats": "チャット記録", + "are allowed - Activate this command by typing": "が許可されています - 次のように入力してこのコマンドをアクティブ化します", + "Are you sure?": "よろしいですか?", + "Attach file": "ファイルを添付する", + "Attention to detail": "詳細に注意する", + "Audio": "オーディオ", + "Audio settings updated successfully": "", + "August": "8月", + "Auto-playback response": "応答の自動再生", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 ベース URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 ベース URL が必要です。", + "available!": "利用可能!", + "Back": "戻る", + "Bad Response": "応答が悪い", + "Banners": "バナー", + "Base Model (From)": "ベースモデル(From)", + "Batch Size (num_batch)": "", + "before": "より前", + "Being lazy": "怠惰な", + "Brave Search API Key": "Brave Search APIキー", + "Bypass SSL verification for Websites": "SSL 検証をバイパスする", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "キャンセル", + "Capabilities": "資格", + "Change Password": "パスワードを変更", + "Chat": "チャット", + "Chat Background Image": "", + "Chat Bubble UI": "チャットバブルUI", + "Chat Controls": "", + "Chat direction": "チャットの方向", + "Chat History": "チャット履歴", + "Chat History is off for this browser.": "このブラウザではチャット履歴が無効になっています。", + "Chats": "チャット", + "Check Again": "再確認", + "Check for updates": "アップデートを確認", + "Checking for updates...": "アップデートを確認しています...", + "Choose a model before saving...": "保存する前にモデルを選択してください...", + "Chunk Overlap": "チャンクオーバーラップ", + "Chunk Params": "チャンクパラメーター", + "Chunk Size": "チャンクサイズ", + "Citation": "引用文", + "Clear memory": "", + "Click here for help.": "ヘルプについてはここをクリックしてください。", + "Click here to": "ここをクリックして", + "Click here to download user import template file.": "", + "Click here to select": "選択するにはここをクリックしてください", + "Click here to select a csv file.": "CSVファイルを選択するにはここをクリックしてください。", + "Click here to select a py file.": "", + "Click here to select documents.": "ドキュメントを選択するにはここをクリックしてください。", + "click here.": "ここをクリックしてください。", + "Click on the user role button to change a user's role.": "ユーザーの役割を変更するには、ユーザー役割ボタンをクリックしてください。", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "クローン", + "Close": "閉じる", + "Code formatted successfully": "", + "Collection": "コレクション", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUIベースURL", + "ComfyUI Base URL is required.": "ComfyUIベースURLが必要です。", + "Command": "コマンド", + "Concurrent Requests": "コンカレント要求", + "Confirm": "", + "Confirm Password": "パスワードを確認", + "Confirm your action": "", + "Connections": "接続", + "Contact Admin for WebUI Access": "", + "Content": "コンテンツ", + "Content Extraction": "", + "Context Length": "コンテキストの長さ", + "Continue Response": "続きの応答", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "共有チャットURLをクリップボードにコピーしました!", + "Copy": "コピー", + "Copy last code block": "最後のコードブロックをコピー", + "Copy last response": "最後の応答をコピー", + "Copy Link": "リンクをコピー", + "Copying to clipboard was successful!": "クリップボードへのコピーが成功しました!", + "Create a model": "モデルを作成する", + "Create Account": "アカウントを作成", + "Create new key": "新しいキーを作成", + "Create new secret key": "新しいシークレットキーを作成", + "Created at": "作成日時", + "Created At": "作成日時", + "Created by": "", + "CSV Import": "", + "Current Model": "現在のモデル", + "Current Password": "現在のパスワード", + "Custom": "カスタム", + "Customize models for a specific purpose": "特定の目的に合わせてモデルをカスタマイズする", + "Dark": "ダーク", + "Dashboard": "", + "Database": "データベース", + "December": "12月", + "Default": "デフォルト", + "Default (Automatic1111)": "デフォルト (Automatic1111)", + "Default (SentenceTransformers)": "デフォルト (SentenceTransformers)", + "Default Model": "デフォルトモデル", + "Default model updated": "デフォルトモデルが更新されました", + "Default Prompt Suggestions": "デフォルトのプロンプトの提案", + "Default User Role": "デフォルトのユーザー役割", + "delete": "削除", + "Delete": "削除", + "Delete a model": "モデルを削除", + "Delete All Chats": "すべてのチャットを削除", + "Delete chat": "チャットを削除", + "Delete Chat": "チャットを削除", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "このリンクを削除します", + "Delete tool?": "", + "Delete User": "ユーザーを削除", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} を削除しました", + "Deleted {{name}}": "{{name}}を削除しました", + "Description": "説明", + "Didn't fully follow instructions": "説明に沿って操作していませんでした", + "Disabled": "", + "Discover a function": "", + "Discover a model": "モデルを検出する", + "Discover a prompt": "プロンプトを見つける", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "カスタムプロンプトを見つけて、ダウンロードして、探索", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "モデルプリセットを見つけて、ダウンロードして、探索", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "チャットで「あなた」の代わりにユーザー名を表示", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "ドキュメント", + "Document Settings": "ドキュメント設定", + "Documentation": "", + "Documents": "ドキュメント", + "does not make any external connections, and your data stays securely on your locally hosted server.": "外部接続を行わず、データはローカルでホストされているサーバー上に安全に保持されます。", + "Don't Allow": "許可しない", + "Don't have an account?": "アカウントをお持ちではありませんか?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "デザインが好きでない", + "Done": "", + "Download": "ダウンロードをキャンセルしました", + "Download canceled": "ダウンロードをキャンセルしました", + "Download Database": "データベースをダウンロード", + "Drop any files here to add to the conversation": "会話を追加するには、ここにファイルをドロップしてください", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例: '30秒'、'10分'。有効な時間単位は '秒'、'分'、'時間' です。", + "Edit": "編集", + "Edit Doc": "ドキュメントを編集", + "Edit Memory": "", + "Edit User": "ユーザーを編集", + "ElevenLabs": "", + "Email": "メールアドレス", + "Embedding Batch Size": "", + "Embedding Model": "埋め込みモデル", + "Embedding Model Engine": "埋め込みモデルエンジン", + "Embedding model set to \"{{embedding_model}}\"": "埋め込みモデルを\"{{embedding_model}}\"に設定しました", + "Enable Chat History": "チャット履歴を有効化", + "Enable Community Sharing": "コミュニティ共有の有効化", + "Enable New Sign Ups": "新規登録を有効化", + "Enable Web Search": "Web 検索を有効にする", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "CSVファイルに4つの列が含まれていることを確認してください: Name, Email, Password, Role.", + "Enter {{role}} message here": "{{role}} メッセージをここに入力してください", + "Enter a detail about yourself for your LLMs to recall": "LLM が記憶するために、自分についての詳細を入力してください", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Brave Search APIキーの入力", + "Enter Chunk Overlap": "チャンクオーバーラップを入力してください", + "Enter Chunk Size": "チャンクサイズを入力してください", + "Enter Github Raw URL": "Github Raw URLを入力", + "Enter Google PSE API Key": "Google PSE APIキーの入力", + "Enter Google PSE Engine Id": "Google PSE エンジン ID を入力します。", + "Enter Image Size (e.g. 512x512)": "画像サイズを入力してください (例: 512x512)", + "Enter language codes": "言語コードを入力してください", + "Enter model tag (e.g. {{modelTag}})": "モデルタグを入力してください (例: {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "ステップ数を入力してください (例: 50)", + "Enter Score": "スコアを入力してください", + "Enter Searxng Query URL": "SearxngクエリURLを入力", + "Enter Serper API Key": "Serper APIキーの入力", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Serpstack APIキーの入力", + "Enter stop sequence": "ストップシーケンスを入力してください", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "トップ K を入力してください", + "Enter URL (e.g. http://127.0.0.1:7860/)": "URL を入力してください (例: http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "URL を入力してください (例: http://localhost:11434)", + "Enter Your Email": "メールアドレスを入力してください", + "Enter Your Full Name": "フルネームを入力してください", + "Enter your message": "", + "Enter Your Password": "パスワードを入力してください", + "Enter Your Role": "ロールを入力してください", + "Error": "エラー", + "Experimental": "実験的", + "Export": "輸出", + "Export All Chats (All Users)": "すべてのチャットをエクスポート (すべてのユーザー)", + "Export chat (.json)": "", + "Export Chats": "チャットをエクスポート", + "Export Documents Mapping": "ドキュメントマッピングをエクスポート", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "モデルのエクスポート", + "Export Prompts": "プロンプトをエクスポート", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "APIキーの作成に失敗しました。", + "Failed to read clipboard contents": "クリップボードの内容を読み取れませんでした", + "Failed to update settings": "", + "February": "2月", + "Feel free to add specific details": "詳細を追加してください", + "File": "", + "File Mode": "ファイルモード", + "File not found.": "ファイルが見つかりません。", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "指紋のなりすましが検出されました: イニシャルをアバターとして使用できません。デフォルトのプロファイル画像にデフォルト設定されています。", + "Fluidly stream large external response chunks": "大規模な外部応答チャンクを流動的にストリーミングする", + "Focus chat input": "チャット入力をフォーカス", + "Followed instructions perfectly": "完全に指示に従った", + "Form": "", + "Format your variables using square brackets like this:": "次のように角括弧を使用して変数をフォーマットします。", + "Frequency Penalty": "周波数ペナルティ", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "一般", + "General Settings": "一般設定", + "Generate Image": "", + "Generating search query": "検索クエリの生成", + "Generation Info": "生成情報", + "Get up and running with": "", + "Global": "", + "Good Response": "良い応答", + "Google PSE API Key": "Google PSE APIキー", + "Google PSE Engine Id": "Google PSE エンジン ID", + "h:mm a": "h:mm a", + "has no conversations.": "対話はありません。", + "Hello, {{name}}": "こんにちは、{{name}} さん", + "Help": "ヘルプ", + "Hide": "非表示", + "Hide Model": "", + "How can I help you today?": "今日はどのようにお手伝いしましょうか?", + "Hybrid Search": "ブリッジ検索", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "画像生成 (実験的)", + "Image Generation Engine": "画像生成エンジン", + "Image Settings": "画像設定", + "Images": "画像", + "Import Chats": "チャットをインポート", + "Import Documents Mapping": "ドキュメントマッピングをインポート", + "Import Functions": "", + "Import Models": "モデルのインポート", + "Import Prompts": "プロンプトをインポート", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webuiを実行する際に`--api`フラグを含める", + "Info": "情報", + "Input commands": "入力コマンド", + "Install from Github URL": "Github URLからインストール", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "インターフェース", + "Invalid Tag": "無効なタグ", + "January": "1月", + "join our Discord for help.": "ヘルプについては、Discord に参加してください。", + "JSON": "JSON", + "JSON Preview": "JSON プレビュー", + "July": "7月", + "June": "6月", + "JWT Expiration": "JWT 有効期限", + "JWT Token": "JWT トークン", + "Keep Alive": "キープアライブ", + "Keyboard shortcuts": "キーボードショートカット", + "Knowledge": "", + "Language": "言語", + "large language models, locally.": "", + "Last Active": "最終アクティブ", + "Last Modified": "", + "Light": "ライト", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLM は間違いを犯す可能性があります。重要な情報を検証してください。", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "OpenWebUI コミュニティによって作成", + "Make sure to enclose them with": "必ず次で囲んでください", + "Manage": "", + "Manage Models": "モデルを管理", + "Manage Ollama Models": "Ollama モデルを管理", + "Manage Pipelines": "パイプラインの管理", + "Manage Valves": "", + "March": "3月", + "Max Tokens (num_predict)": "最大トークン数 (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "同時にダウンロードできるモデルは最大 3 つです。後でもう一度お試しください。", + "May": "5月", + "Memories accessible by LLMs will be shown here.": "LLM がアクセスできるメモリはここに表示されます。", + "Memory": "メモリ", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "リンクを作成した後、送信したメッセージは共有されません。URL を持つユーザーは共有チャットを閲覧できます。", + "Minimum Score": "最低スコア", + "Mirostat": "ミロスタット", + "Mirostat Eta": "ミロスタット Eta", + "Mirostat Tau": "ミロスタット Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "モデル '{{modelName}}' が正常にダウンロードされました。", + "Model '{{modelTag}}' is already in queue for downloading.": "モデル '{{modelTag}}' はすでにダウンロード待ち行列に入っています。", + "Model {{modelId}} not found": "モデル {{modelId}} が見つかりません", + "Model {{modelName}} is not vision capable": "モデル {{modelName}} は視覚に対応していません", + "Model {{name}} is now {{status}}": "モデル {{name}} は {{status}} になりました。", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "モデルファイルシステムパスが検出されました。モデルの短縮名が必要です。更新できません。", + "Model ID": "モデルID", + "Model not selected": "モデルが選択されていません", + "Model Params": "モデルパラメータ", + "Model updated successfully": "", + "Model Whitelisting": "モデルホワイトリスト", + "Model(s) Whitelisted": "ホワイトリストに登録されたモデル", + "Modelfile Content": "モデルファイルの内容", + "Models": "モデル", + "More": "もっと見る", + "Name": "名前", + "Name Tag": "名前タグ", + "Name your model": "モデルに名前を付ける", + "New Chat": "新しいチャット", + "New Password": "新しいパスワード", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "結果が見つかりません", + "No search query generated": "検索クエリは生成されません", + "No source available": "使用可能なソースがありません", + "No valves to update": "", + "None": "何一つ", + "Not factually correct": "実事上正しくない", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:最小スコアを設定した場合、検索は最小スコア以上のスコアを持つドキュメントのみを返します。", + "Notifications": "デスクトップ通知", + "November": "11月", + "num_thread (Ollama)": "num_thread(オラマ)", + "OAuth ID": "", + "October": "10月", + "Off": "オフ", + "Okay, Let's Go!": "OK、始めましょう!", + "OLED Dark": "OLED ダーク", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API が無効になっています", + "Ollama API is disabled": "", + "Ollama Version": "Ollama バージョン", + "On": "オン", + "Only": "のみ", + "Only alphanumeric characters and hyphens are allowed in the command string.": "コマンド文字列には英数字とハイフンのみが許可されています。", + "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.": "おっと! しばらくお待ちください! ファイルはまだ処理中です。完璧に仕上げていますので、しばらくお待ちください。準備ができたらお知らせします。", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "おっと! URL が無効なようです。もう一度確認してやり直してください。", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "おっと! サポートされていない方法 (フロントエンドのみ) を使用しています。バックエンドから WebUI を提供してください。", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "新しいチャットを開く", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API 設定", + "OpenAI API Key is required.": "OpenAI API キーが必要です。", + "OpenAI URL/Key required.": "OpenAI URL/Key が必要です。", + "or": "または", + "Other": "その他", + "Password": "パスワード", + "PDF document (.pdf)": "PDF ドキュメント (.pdf)", + "PDF Extract Images (OCR)": "PDF 画像抽出 (OCR)", + "pending": "保留中", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "マイクへのアクセス時に権限が拒否されました: {{error}}", + "Personalization": "個人化", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "パイプライン", + "Pipelines Not Detected": "", + "Pipelines Valves": "パイプラインバルブ", + "Plain text (.txt)": "プレーンテキスト (.txt)", + "Playground": "プレイグラウンド", + "Please carefully review the following warnings:": "", + "Positive attitude": "陽気な態度", + "Previous 30 days": "前の30日間", + "Previous 7 days": "前の7日間", + "Profile Image": "プロフィール画像", + "Prompt": "プロンプト", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "プロンプト(例:ローマ帝国についての楽しい事を教えてください)", + "Prompt Content": "プロンプトの内容", + "Prompt suggestions": "プロンプトの提案", + "Prompts": "プロンプト", + "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com から \"{{searchValue}}\" をプル", + "Pull a model from Ollama.com": "Ollama.com からモデルをプル", + "Query Params": "クエリパラメーター", + "RAG Template": "RAG テンプレート", + "Read Aloud": "読み上げ", + "Record voice": "音声を録音", + "Redirecting you to OpenWebUI Community": "OpenWebUI コミュニティにリダイレクトしています", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "許可されないのに許可されました", + "Regenerate": "再生成", + "Release Notes": "リリースノート", + "Remove": "削除", + "Remove Model": "モデルを削除", + "Rename": "名前を変更", + "Repeat Last N": "最後の N を繰り返す", + "Request Mode": "リクエストモード", + "Reranking Model": "モデルの再ランキング", + "Reranking model disabled": "再ランキングモデルが無効です", + "Reranking model set to \"{{reranking_model}}\"": "再ランキングモデルを \"{{reranking_model}}\" に設定しました", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "ベクトルストレージをリセット", + "Response AutoCopy to Clipboard": "クリップボードへの応答の自動コピー", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "役割", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "保存", + "Save & Create": "保存して作成", + "Save & Update": "保存して更新", + "Save Tag": "", + "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": "チャットログをブラウザのストレージに直接保存する機能はサポートされなくなりました。下のボタンをクリックして、チャットログをダウンロードして削除してください。ご心配なく。チャットログは、次の方法でバックエンドに簡単に再インポートできます。", + "Scan": "スキャン", + "Scan complete!": "スキャン完了!", + "Scan for documents from {{path}}": "{{path}} からドキュメントをスキャン", + "Search": "検索", + "Search a model": "モデルを検索", + "Search Chats": "チャットの検索", + "Search Documents": "ドキュメントを検索", + "Search Functions": "", + "Search Models": "モデル検索", + "Search Prompts": "プロンプトを検索", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "検索結果数", + "Search Tools": "", + "Searched {{count}} sites_other": "{{count}} sites_other検索", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng クエリ URL", + "See readme.md for instructions": "手順については readme.md を参照してください", + "See what's new": "新機能を見る", + "Seed": "シード", + "Select a base model": "基本モデルの選択", + "Select a engine": "", + "Select a function": "", + "Select a mode": "モードを選択", + "Select a model": "モデルを選択", + "Select a pipeline": "パイプラインの選択", + "Select a pipeline url": "パイプラインの URL を選択する", + "Select a tool": "", + "Select an Ollama instance": "Ollama インスタンスを選択", + "Select Documents": "", + "Select model": "モデルを選択", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "一部のモデルは画像入力をサポートしていません", + "Send": "送信", + "Send a Message": "メッセージを送信", + "Send message": "メッセージを送信", + "September": "9月", + "Serper API Key": "Serper APIキー", + "Serply API Key": "", + "Serpstack API Key": "Serpstack APIキー", + "Server connection verified": "サーバー接続が確認されました", + "Set as default": "デフォルトに設定", + "Set Default Model": "デフォルトモデルを設定", + "Set embedding model (e.g. {{model}})": "埋め込みモデルを設定します(例:{{model}})", + "Set Image Size": "画像サイズを設定", + "Set reranking model (e.g. {{model}})": "モデルを設定します(例:{{model}})", + "Set Steps": "ステップを設定", + "Set Task Model": "タスクモデルの設定", + "Set Voice": "音声を設定", + "Settings": "設定", + "Settings saved successfully!": "設定が正常に保存されました!", + "Settings updated successfully": "", + "Share": "共有", + "Share Chat": "チャットを共有", + "Share to OpenWebUI Community": "OpenWebUI コミュニティに共有", + "short-summary": "short-summary", + "Show": "表示", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "表示", + "Show your support!": "", + "Showcased creativity": "創造性を披露", + "Sign in": "サインイン", + "Sign Out": "サインアウト", + "Sign up": "サインアップ", + "Signing in": "サインイン中", + "Source": "ソース", + "Speech recognition error: {{error}}": "音声認識エラー: {{error}}", + "Speech-to-Text Engine": "音声テキスト変換エンジン", + "Stop Sequence": "ストップシーケンス", + "STT Model": "", + "STT Settings": "STT 設定", + "Submit": "送信", + "Subtitle (e.g. about the Roman Empire)": "タイトル (例: ロマ帝国)", + "Success": "成功", + "Successfully updated.": "正常に更新されました。", + "Suggested": "提案", + "Support": "", + "Support this plugin:": "", + "System": "システム", + "System Prompt": "システムプロンプト", + "Tags": "タグ", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "もっと話してください:", + "Temperature": "温度", + "Template": "テンプレート", + "Text Completion": "テキスト補完", + "Text-to-Speech Engine": "テキスト音声変換エンジン", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "ご意見ありがとうございます!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "スコアは0.0(0%)から1.0(100%)の間の値にしてください。", + "Theme": "テーマ", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "これは、貴重な会話がバックエンドデータベースに安全に保存されることを保証します。ありがとうございます!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "この設定は、ブラウザやデバイス間で同期されません。", + "This will delete": "", + "Thorough explanation": "詳細な説明", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ヒント: 各置換後にチャット入力で Tab キーを押すことで、複数の変数スロットを連続して更新できます。", + "Title": "タイトル", + "Title (e.g. Tell me a fun fact)": "タイトル (例: 楽しい事を教えて)", + "Title Auto-Generation": "タイトル自動生成", + "Title cannot be an empty string.": "タイトルは空文字列にできません。", + "Title Generation Prompt": "タイトル生成プロンプト", + "to": "まで", + "To access the available model names for downloading,": "ダウンロード可能なモデル名にアクセスするには、", + "To access the GGUF models available for downloading,": "ダウンロード可能な GGUF モデルにアクセスするには、", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "チャット入力へ。", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "今日", + "Toggle settings": "設定を切り替え", + "Toggle sidebar": "サイドバーを切り替え", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "トップ K", + "Top P": "トップ P", + "Trouble accessing Ollama?": "Ollama へのアクセスに問題がありますか?", + "TTS Model": "", + "TTS Settings": "TTS 設定", + "TTS Voice": "", + "Type": "種類", + "Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (ダウンロード) URL を入力してください", + "Uh-oh! There was an issue connecting to {{provider}}.": "おっと! {{provider}} への接続に問題が発生しました。", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "リンクの更新とコピー", + "Update password": "パスワードを更新", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "GGUF モデルをアップロード", + "Upload Files": "ファイルのアップロード", + "Upload Pipeline": "", + "Upload Progress": "アップロードの進行状況", + "URL Mode": "URL モード", + "Use '#' in the prompt input to load and select your documents.": "プロンプト入力で '#' を使用して、ドキュメントを読み込んで選択します。", + "Use Gravatar": "Gravatar を使用する", + "Use Initials": "初期値を使用する", + "use_mlock (Ollama)": "use_mlock(オラマ)", + "use_mmap (Ollama)": "use_mmap(オラマ)", + "user": "ユーザー", + "User location successfully retrieved.": "", + "User Permissions": "ユーザー権限", + "Users": "ユーザー", + "Utilize": "活用", + "Valid time units:": "有効な時間単位:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "変数", + "variable to have them replaced with clipboard content.": "クリップボードの内容に置き換える変数。", + "Version": "バージョン", + "Voice": "", + "Warning": "警告", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告: 埋め込みモデルを更新または変更した場合は、すべてのドキュメントを再インポートする必要があります。", + "Web": "ウェブ", + "Web API": "", + "Web Loader Settings": "Web 読み込み設定", + "Web Params": "Web パラメータ", + "Web Search": "ウェブ検索", + "Web Search Engine": "ウェブ検索エンジン", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI 設定", + "WebUI will make requests to": "WebUI は次に対してリクエストを行います", + "What’s New in": "新機能", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "履歴が無効になっている場合、このブラウザでの新しいチャットは、どのデバイスの履歴にも表示されません。", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "ワークスペース", + "Write a prompt suggestion (e.g. Who are you?)": "プロンプトの提案を書いてください (例: あなたは誰ですか?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "[トピックまたはキーワード] を要約する 50 語の概要を書いてください。", + "Yesterday": "昨日", + "You": "あなた", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "基本モデルのクローンを作成できない", + "You have no archived conversations.": "これまでにアーカイブされた会話はありません。", + "You have shared this chat": "このチャットを共有しました", + "You're a helpful assistant.": "あなたは役に立つアシスタントです。", + "You're now logged in.": "ログインしました。", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "YouTube", + "Youtube Loader Settings": "Youtubeローダー設定" +} diff --git a/src/lib/i18n/locales/ka-GE/translation.json b/src/lib/i18n/locales/ka-GE/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..741f45edc44078195b11ea9cfbf053ff298321e1 --- /dev/null +++ b/src/lib/i18n/locales/ka-GE/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ან '-1' ვადის გასვლისთვის.", + "(Beta)": "(ბეტა)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(მაგ. `sh webui.sh --api`)", + "(latest)": "(უახლესი)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: თქვენ არ შეგიძლიათ წაშალოთ ბაზის მოდელი", + "{{modelName}} is thinking...": "{{modelName}} ფიქრობს...", + "{{user}}'s Chats": "{{user}}-ის ჩათები", + "{{webUIName}} Backend Required": "{{webUIName}} საჭიროა ბექენდი", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "დავალების მოდელი გამოიყენება ისეთი ამოცანების შესრულებისას, როგორიცაა ჩეთების სათაურების გენერირება და ვებ – ძიების მოთხოვნები", + "a user": "მომხმარებელი", + "About": "შესახებ", + "Account": "ანგარიში", + "Account Activation Pending": "", + "Accurate information": "დიდი ინფორმაცია", + "Actions": "", + "Active Users": "", + "Add": "დამატება", + "Add a model id": "დაამატეთ მოდელის ID", + "Add a short description about what this model does": "დაამატეთ მოკლე აღწერა იმის შესახებ, თუ რას აკეთებს ეს მოდელი", + "Add a short title for this prompt": "დაამატე მოკლე სათაური ამ მოთხოვნისთვის", + "Add a tag": "დაამატე ტეგი", + "Add custom prompt": "პირველადი მოთხოვნის დამატება", + "Add Docs": "დოკუმენტის დამატება", + "Add Files": "ფაილების დამატება", + "Add Memory": "მემორიის დამატება", + "Add message": "შეტყობინების დამატება", + "Add Model": "მოდელის დამატება", + "Add Tag": "", + "Add Tags": "ტეგების დამატება", + "Add User": "მომხმარებლის დამატება", + "Adjusting these settings will apply changes universally to all users.": "ამ პარამეტრების რეგულირება ცვლილებებს უნივერსალურად გამოიყენებს ყველა მომხმარებლისთვის", + "admin": "ადმინისტრატორი", + "Admin": "", + "Admin Panel": "ადმინ პანელი", + "Admin Settings": "ადმინისტრატორის ხელსაწყოები", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "დამატებითი პარამეტრები", + "Advanced Params": "მოწინავე პარამები", + "all": "ყველა", + "All Documents": "ყველა დოკუმენტი", + "All Users": "ყველა მომხმარებელი", + "Allow": "ნების დართვა", + "Allow Chat Deletion": "მიმოწერის წაშლის დაშვება", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "ალფანუმერული სიმბოლოები და დეფისები", + "Already have an account?": "უკვე გაქვს ანგარიში?", + "an assistant": "ასისტენტი", + "and": "და", + "and create a new shared link.": "და შექმენით ახალი გაზიარებული ბმული.", + "API Base URL": "API საბაზისო URL", + "API Key": "API გასაღები", + "API Key created.": "API გასაღები შექმნილია.", + "API keys": "API გასაღები", + "April": "აპრილი", + "Archive": "არქივი", + "Archive All Chats": "არქივი ყველა ჩატი", + "Archived Chats": "ჩატის ისტორიის არქივი", + "are allowed - Activate this command by typing": "დაშვებულია - ბრძანების გასააქტიურებლად აკრიფეთ:", + "Are you sure?": "დარწმუნებული ხარ?", + "Attach file": "ფაილის ჩაწერა", + "Attention to detail": "დეტალური მიმართვა", + "Audio": "ხმოვანი", + "Audio settings updated successfully": "", + "August": "აგვისტო", + "Auto-playback response": "ავტომატური დაკვრის პასუხი", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 საბაზისო მისამართი", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 საბაზისო მისამართი აუცილებელია", + "available!": "ხელმისაწვდომია!", + "Back": "უკან", + "Bad Response": "ხარვეზი", + "Banners": "რეკლამა", + "Base Model (From)": "საბაზო მოდელი (-დან)", + "Batch Size (num_batch)": "", + "before": "ადგილზე", + "Being lazy": "ჩაიტყვევა", + "Brave Search API Key": "Brave Search API გასაღები", + "Bypass SSL verification for Websites": "SSL-ის ვერიფიკაციის გააუქმება ვებსაიტებზე", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "გაუქმება", + "Capabilities": "შესაძლებლობები", + "Change Password": "პაროლის შეცვლა", + "Chat": "მიმოწერა", + "Chat Background Image": "", + "Chat Bubble UI": "ჩატის ბულბი", + "Chat Controls": "", + "Chat direction": "ჩატის მიმართულება", + "Chat History": "მიმოწერის ისტორია", + "Chat History is off for this browser.": "მიმოწერის ისტორია ამ ბრაუზერისთვის გათიშულია", + "Chats": "მიმოწერები", + "Check Again": "თავიდან შემოწმება", + "Check for updates": "განახლებების ძიება", + "Checking for updates...": "მიმდინარეობს განახლებების ძიება...", + "Choose a model before saving...": "აირჩიეთ მოდელი შენახვამდე...", + "Chunk Overlap": "გადახურვა ფრაგმენტულია", + "Chunk Params": "გადახურვის პარამეტრები", + "Chunk Size": "გადახურვის ზომა", + "Citation": "ციტატა", + "Clear memory": "", + "Click here for help.": "დახმარებისთვის, დააკლიკე აქ", + "Click here to": "დააკლიკე აქ", + "Click here to download user import template file.": "", + "Click here to select": "ასარჩევად, დააკლიკე აქ", + "Click here to select a csv file.": "ასარჩევად, დააკლიკე აქ", + "Click here to select a py file.": "", + "Click here to select documents.": "დოკუმენტების ასარჩევად, დააკლიკე აქ", + "click here.": "დააკლიკე აქ", + "Click on the user role button to change a user's role.": "დააკლიკეთ მომხმარებლის როლის ღილაკს რომ შეცვალოთ მომხმარების როლი", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "კლონი", + "Close": "დახურვა", + "Code formatted successfully": "", + "Collection": "ნაკრები", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI საბაზისო URL", + "ComfyUI Base URL is required.": "ComfyUI საბაზისო URL აუცილებელია.", + "Command": "ბრძანება", + "Concurrent Requests": "თანმხლები მოთხოვნები", + "Confirm": "", + "Confirm Password": "პაროლის დამოწმება", + "Confirm your action": "", + "Connections": "კავშირები", + "Contact Admin for WebUI Access": "", + "Content": "კონტენტი", + "Content Extraction": "", + "Context Length": "კონტექსტის სიგრძე", + "Continue Response": "პასუხის გაგრძელება", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "ყავს ჩათის URL-ი კლიპბორდში!", + "Copy": "კოპირება", + "Copy last code block": "ბოლო ბლოკის კოპირება", + "Copy last response": "ბოლო პასუხის კოპირება", + "Copy Link": "კოპირება", + "Copying to clipboard was successful!": "კლავიატურაზე კოპირება წარმატებით დასრულდა", + "Create a model": "შექმენით მოდელი", + "Create Account": "ანგარიშის შექმნა", + "Create new key": "პირადი ღირებულბრის შექმნა", + "Create new secret key": "პირადი ღირებულბრის შექმნა", + "Created at": "შექმნილია", + "Created At": "შექმნილია", + "Created by": "", + "CSV Import": "", + "Current Model": "მიმდინარე მოდელი", + "Current Password": "მიმდინარე პაროლი", + "Custom": "საკუთარი", + "Customize models for a specific purpose": "მოდელების მორგება კონკრეტული მიზნისთვის", + "Dark": "მუქი", + "Dashboard": "", + "Database": "მონაცემთა ბაზა", + "December": "დეკემბერი", + "Default": "დეფოლტი", + "Default (Automatic1111)": "დეფოლტ (Automatic1111)", + "Default (SentenceTransformers)": "დეფოლტ (SentenceTransformers)", + "Default Model": "ნაგულისხმები მოდელი", + "Default model updated": "დეფოლტ მოდელი განახლებულია", + "Default Prompt Suggestions": "დეფოლტ პრომპტი პირველი პირველი", + "Default User Role": "მომხმარებლის დეფოლტ როლი", + "delete": "წაშლა", + "Delete": "წაშლა", + "Delete a model": "მოდელის წაშლა", + "Delete All Chats": "ყველა ჩატის წაშლა", + "Delete chat": "შეტყობინების წაშლა", + "Delete Chat": "შეტყობინების წაშლა", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "ბმულის წაშლა", + "Delete tool?": "", + "Delete User": "მომხმარებლის წაშლა", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} წაშლილია", + "Deleted {{name}}": "Deleted {{name}}", + "Description": "აღწერა", + "Didn't fully follow instructions": "ვერ ყველა ინფორმაციისთვის ვერ ხელახლა ჩაწერე", + "Disabled": "", + "Discover a function": "", + "Discover a model": "გაიგეთ მოდელი", + "Discover a prompt": "აღმოაჩინეთ მოთხოვნა", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "აღმოაჩინეთ, ჩამოტვირთეთ და შეისწავლეთ მორგებული მოთხოვნები", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "აღმოაჩინეთ, ჩამოტვირთეთ და შეისწავლეთ მოდელის წინასწარ პარამეტრები", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "ჩატში აჩვენე მომხმარებლის სახელი თქვენს ნაცვლად", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "დოკუმენტი", + "Document Settings": "დოკუმენტის პარამეტრები", + "Documentation": "", + "Documents": "დოკუმენტები", + "does not make any external connections, and your data stays securely on your locally hosted server.": "არ ამყარებს გარე კავშირებს და თქვენი მონაცემები უსაფრთხოდ რჩება თქვენს ადგილობრივ სერვერზე.", + "Don't Allow": "არ დაუშვა", + "Don't have an account?": "არ გაქვს ანგარიში?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "არ ეთიკურია ფართოდ", + "Done": "", + "Download": "ჩამოტვირთვა გაუქმებულია", + "Download canceled": "ჩამოტვირთვა გაუქმებულია", + "Download Database": "გადმოწერე მონაცემთა ბაზა", + "Drop any files here to add to the conversation": "გადაიტანეთ ფაილები აქ, რათა დაამატოთ ისინი მიმოწერაში", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "მაგალითად, '30წ', '10მ'. მოქმედი დროის ერთეულები: 'წ', 'წთ', 'სთ'.", + "Edit": "რედაქტირება", + "Edit Doc": "დოკუმენტის ედიტირება", + "Edit Memory": "", + "Edit User": "მომხმარებლის ედიტირება", + "ElevenLabs": "", + "Email": "ელ-ფოსტა", + "Embedding Batch Size": "", + "Embedding Model": "ჩასმის ძირითადი პროგრამა", + "Embedding Model Engine": "ჩასმის ძირითადი პროგრამა", + "Embedding model set to \"{{embedding_model}}\"": "ჩასმის ძირითადი პროგრამა ჩართულია \"{{embedding_model}}\"", + "Enable Chat History": "მიმოწერის ისტორიის ჩართვა", + "Enable Community Sharing": "საზოგადოების გაზიარების ჩართვა", + "Enable New Sign Ups": "ახალი რეგისტრაციების ჩართვა", + "Enable Web Search": "ვებ ძიების ჩართვა", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "გთხოვთ, უზრუნველყოთ, რომთქვევის CSV-ფაილი შეიცავს 4 ველი, ჩაწერილი ორივე ველი უდრის პირველი ველით.", + "Enter {{role}} message here": "შეიყვანე {{role}} შეტყობინება აქ", + "Enter a detail about yourself for your LLMs to recall": "შეიყვანე დეტალი ჩემთათვის, რომ ჩვენი LLMs-ს შეიძლოს აღაქვს", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "შეიყვანეთ Brave Search API გასაღები", + "Enter Chunk Overlap": "შეიყვანეთ ნაწილის გადახურვა", + "Enter Chunk Size": "შეიყვანე ბლოკის ზომა", + "Enter Github Raw URL": "შეიყვანეთ Github Raw URL", + "Enter Google PSE API Key": "შეიყვანეთ Google PSE API გასაღები", + "Enter Google PSE Engine Id": "შეიყვანეთ Google PSE ძრავის ID", + "Enter Image Size (e.g. 512x512)": "შეიყვანეთ სურათის ზომა (მაგ. 512x512)", + "Enter language codes": "შეიყვანეთ ენის კოდი", + "Enter model tag (e.g. {{modelTag}})": "შეიყვანეთ მოდელის ტეგი (მაგ. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "შეიყვანეთ ნაბიჯების რაოდენობა (მაგ. 50)", + "Enter Score": "შეიყვანეთ ქულა", + "Enter Searxng Query URL": "შეიყვანეთ Searxng Query URL", + "Enter Serper API Key": "შეიყვანეთ Serper API Key", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "შეიყვანეთ Serpstack API Key", + "Enter stop sequence": "შეიყვანეთ ტოპ თანმიმდევრობა", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "შეიყვანეთ Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "შეიყვანეთ მისამართი (მაგალითად http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "შეიყვანეთ მისამართი (მაგალითად http://localhost:11434)", + "Enter Your Email": "შეიყვანეთ თქვენი ელ-ფოსტა", + "Enter Your Full Name": "შეიყვანეთ თქვენი სრული სახელი", + "Enter your message": "", + "Enter Your Password": "შეიყვანეთ თქვენი პაროლი", + "Enter Your Role": "შეიყვანეთ თქვენი როლი", + "Error": "შეცდომა", + "Experimental": "ექსპერიმენტალური", + "Export": "ექსპორტი", + "Export All Chats (All Users)": "ექსპორტი ყველა ჩათი (ყველა მომხმარებელი)", + "Export chat (.json)": "", + "Export Chats": "მიმოწერის ექსპორტირება", + "Export Documents Mapping": "დოკუმენტების კავშირის ექსპორტი", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "ექსპორტის მოდელები", + "Export Prompts": "მოთხოვნების ექსპორტი", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "API ღილაკის შექმნა ვერ მოხერხდა.", + "Failed to read clipboard contents": "ბუფერში შიგთავსის წაკითხვა ვერ მოხერხდა", + "Failed to update settings": "", + "February": "თებერვალი", + "Feel free to add specific details": "უფასოდ დაამატეთ დეტალები", + "File": "", + "File Mode": "ფაილური რეჟიმი", + "File not found.": "ფაილი ვერ მოიძებნა", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "აღმოჩენილია თითის ანაბეჭდის გაყალბება: ინიციალების გამოყენება ავატარად შეუძლებელია. დეფოლტ პროფილის დეფოლტ სურათი.", + "Fluidly stream large external response chunks": "თხევადი ნაკადი დიდი გარე საპასუხო ნაწილაკების", + "Focus chat input": "ჩეთის შეყვანის ფოკუსი", + "Followed instructions perfectly": "ყველა ინსტრუქცია უზრუნველყოფა", + "Form": "", + "Format your variables using square brackets like this:": "დააფორმატეთ თქვენი ცვლადები კვადრატული ფრჩხილების გამოყენებით:", + "Frequency Penalty": "სიხშირის ჯარიმა", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "ზოგადი", + "General Settings": "ზოგადი პარამეტრები", + "Generate Image": "", + "Generating search query": "საძიებო მოთხოვნის გენერირება", + "Generation Info": "გენერაციის ინფორმაცია", + "Get up and running with": "", + "Global": "", + "Good Response": "დიდი პასუხი", + "Google PSE API Key": "Google PSE API გასაღები", + "Google PSE Engine Id": "Google PSE ძრავის Id", + "h:mm a": "h:mm a", + "has no conversations.": "არა უფლება ჩაწერა", + "Hello, {{name}}": "გამარჯობა, {{name}}", + "Help": "დახმარება", + "Hide": "დამალვა", + "Hide Model": "", + "How can I help you today?": "როგორ შემიძლია დაგეხმარო დღეს?", + "Hybrid Search": "ჰიბრიდური ძებნა", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "სურათების გენერაცია (ექსპერიმენტული)", + "Image Generation Engine": "სურათის გენერაციის ძრავა", + "Image Settings": "სურათის პარამეტრები", + "Images": "სურათები", + "Import Chats": "მიმოწერების იმპორტი", + "Import Documents Mapping": "დოკუმენტების კავშირის იმპორტი", + "Import Functions": "", + "Import Models": "იმპორტის მოდელები", + "Import Prompts": "მოთხოვნების იმპორტი", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "ჩართეთ `--api` დროშა stable-diffusion-webui-ის გაშვებისას", + "Info": "ინფორმაცია", + "Input commands": "შეყვანით ბრძანებებს", + "Install from Github URL": "დააინსტალირეთ Github URL- დან", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "ინტერფეისი", + "Invalid Tag": "არასწორი ტეგი", + "January": "იანვარი", + "join our Discord for help.": "შეუერთდით ჩვენს Discord-ს დახმარებისთვის", + "JSON": "JSON", + "JSON Preview": "JSON გადახედვა", + "July": "ივნისი", + "June": "ივლა", + "JWT Expiration": "JWT-ის ვადა", + "JWT Token": "JWT ტოკენი", + "Keep Alive": "აქტიურად დატოვება", + "Keyboard shortcuts": "კლავიატურის მალსახმობები", + "Knowledge": "", + "Language": "ენა", + "large language models, locally.": "", + "Last Active": "ბოლო აქტიური", + "Last Modified": "", + "Light": "მსუბუქი", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "შესაძლოა LLM-ებმა შეცდომები დაუშვან. გადაამოწმეთ მნიშვნელოვანი ინფორმაცია.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "დამზადებულია OpenWebUI საზოგადოების მიერ", + "Make sure to enclose them with": "დარწმუნდით, რომ დაურთეთ ისინი", + "Manage": "", + "Manage Models": "მოდელების მართვა", + "Manage Ollama Models": "Ollama მოდელების მართვა", + "Manage Pipelines": "მილსადენების მართვა", + "Manage Valves": "", + "March": "მარტივი", + "Max Tokens (num_predict)": "მაქს ტოკენსი (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "მაქსიმუმ 3 მოდელის ჩამოტვირთვა შესაძლებელია ერთდროულად. Გთხოვთ სცადოთ მოგვიანებით.", + "May": "მაი", + "Memories accessible by LLMs will be shown here.": "ლლმ-ს აქვს ხელმისაწვდომი მემორიები აქ იქნება.", + "Memory": "მემორია", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "შეტყობინებები, რომელსაც თქვენ აგზავნით თქვენი ბმულის შექმნის შემდეგ, არ იქნება გაზიარებული. URL– ის მქონე მომხმარებლებს შეეძლებათ ნახონ საერთო ჩატი.", + "Minimum Score": "მინიმალური ქულა", + "Mirostat": "მიროსტატი", + "Mirostat Eta": "მიროსტატი ეტა", + "Mirostat Tau": "მიროსტატი ტაუ", + "MMMM DD, YYYY": "თვე დღე, წელი", + "MMMM DD, YYYY HH:mm": "თვე დღე, წელი HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "მოდელი „{{modelName}}“ წარმატებით ჩამოიტვირთა.", + "Model '{{modelTag}}' is already in queue for downloading.": "მოდელი „{{modelTag}}“ უკვე ჩამოტვირთვის რიგშია.", + "Model {{modelId}} not found": "მოდელი {{modelId}} ვერ მოიძებნა", + "Model {{modelName}} is not vision capable": "Model {{modelName}} is not vision capable", + "Model {{name}} is now {{status}}": "Model {{name}} is now {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "აღმოჩენილია მოდელის ფაილური სისტემის გზა. განახლებისთვის საჭიროა მოდელის მოკლე სახელი, გაგრძელება შეუძლებელია.", + "Model ID": "მოდელის ID", + "Model not selected": "მოდელი არ არის არჩეული", + "Model Params": "მოდელის პარამები", + "Model updated successfully": "", + "Model Whitelisting": "მოდელის თეთრ სიაში შეყვანა", + "Model(s) Whitelisted": "მოდელ(ებ)ი თეთრ სიაშია", + "Modelfile Content": "მოდელური ფაილის კონტენტი", + "Models": "მოდელები", + "More": "ვრცლად", + "Name": "სახელი", + "Name Tag": "სახელის ტეგი", + "Name your model": "დაასახელეთ თქვენი მოდელი", + "New Chat": "ახალი მიმოწერა", + "New Password": "ახალი პაროლი", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "ჩვენ ვერ პოულობით ნაპოვნი ჩაწერები", + "No search query generated": "ძიების მოთხოვნა არ არის გენერირებული", + "No source available": "წყარო არ არის ხელმისაწვდომი", + "No valves to update": "", + "None": "არცერთი", + "Not factually correct": "არ ვეთანხმები პირდაპირ ვერც ვეთანხმები", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "შენიშვნა: თუ თქვენ დააყენებთ მინიმალურ ქულას, ძებნა დააბრუნებს მხოლოდ დოკუმენტებს მინიმალური ქულის მეტი ან ტოლი ქულით.", + "Notifications": "შეტყობინება", + "November": "ნოემბერი", + "num_thread (Ollama)": "num_thread (ოლამა)", + "OAuth ID": "", + "October": "ოქტომბერი", + "Off": "გამორთვა", + "Okay, Let's Go!": "კარგი, წავედით!", + "OLED Dark": "OLED მუქი", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API გამორთულია", + "Ollama API is disabled": "", + "Ollama Version": "Ollama ვერსია", + "On": "ჩართვა", + "Only": "მხოლოდ", + "Only alphanumeric characters and hyphens are allowed in the command string.": "ბრძანების სტრიქონში დაშვებულია მხოლოდ ალფანუმერული სიმბოლოები და დეფისები.", + "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.": "უპს! გამაგრდი! თქვენი ფაილები ჯერ კიდევ დამუშავების ღუმელშია. ჩვენ მათ სრულყოფილებამდე ვამზადებთ. გთხოვთ მოითმინოთ და ჩვენ შეგატყობინებთ, როგორც კი ისინი მზად იქნებიან.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "უი! როგორც ჩანს, მისამართი არასწორია. გთხოვთ, გადაამოწმოთ და ისევ სცადოთ.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "უპს! თქვენ იყენებთ მხარდაუჭერელ მეთოდს (მხოლოდ frontend). გთხოვთ, მოემსახუროთ WebUI-ს ბექენდიდან", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "ახალი მიმოწერის გახსნა", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API პარამეტრები", + "OpenAI API Key is required.": "OpenAI API გასაღები აუცილებელია", + "OpenAI URL/Key required.": "OpenAI URL/Key აუცილებელია", + "or": "ან", + "Other": "სხვა", + "Password": "პაროლი", + "PDF document (.pdf)": "PDF დოკუმენტი (.pdf)", + "PDF Extract Images (OCR)": "PDF იდან ამოღებული სურათები (OCR)", + "pending": "ლოდინის რეჟიმშია", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "ნებართვა უარყოფილია მიკროფონზე წვდომისას: {{error}}", + "Personalization": "პერსონალიზაცია", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "მილსადენები", + "Pipelines Not Detected": "", + "Pipelines Valves": "მილსადენების სარქველები", + "Plain text (.txt)": "ტექსტი (.txt)", + "Playground": "სათამაშო მოედანი", + "Please carefully review the following warnings:": "", + "Positive attitude": "პოზიტიური ანგარიში", + "Previous 30 days": "უკან 30 დღე", + "Previous 7 days": "უკან 7 დღე", + "Profile Image": "პროფილის სურათი", + "Prompt": "პრომპტი", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (მაგ. მითხარი სახალისო ფაქტი რომის იმპერიის შესახებ)", + "Prompt Content": "მოთხოვნის შინაარსი", + "Prompt suggestions": "მოთხოვნის რჩევები", + "Prompts": "მოთხოვნები", + "Pull \"{{searchValue}}\" from Ollama.com": "ჩაიამოვეთ \"{{searchValue}}\" Ollama.com-იდან", + "Pull a model from Ollama.com": "Ollama.com იდან მოდელის გადაწერა ", + "Query Params": "პარამეტრების ძიება", + "RAG Template": "RAG შაბლონი", + "Read Aloud": "ხმის ჩაწერა", + "Record voice": "ხმის ჩაწერა", + "Redirecting you to OpenWebUI Community": "გადამისამართდებით OpenWebUI საზოგადოებაში", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "უარა, როგორც უნდა იყოს", + "Regenerate": "ხელახლა გენერირება", + "Release Notes": "Გამოშვების შენიშვნები", + "Remove": "პოპულარობის რაოდენობა", + "Remove Model": "პოპულარობის რაოდენობა", + "Rename": "პოპულარობის რაოდენობა", + "Repeat Last N": "გაიმეორეთ ბოლო N", + "Request Mode": "მოთხოვნის რეჟიმი", + "Reranking Model": "რექვექტირება", + "Reranking model disabled": "რექვექტირება არაა ჩართული", + "Reranking model set to \"{{reranking_model}}\"": "Reranking model set to \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "ვექტორული მეხსიერების გადატვირთვა", + "Response AutoCopy to Clipboard": "პასუხის ავტომატური კოპირება ბუფერში", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "როლი", + "Rosé Pine": "ვარდისფერი ფიჭვის ხე", + "Rosé Pine Dawn": "ვარდისფერი ფიჭვის გარიჟრაჟი", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "შენახვა", + "Save & Create": "დამახსოვრება და შექმნა", + "Save & Update": "დამახსოვრება და განახლება", + "Save Tag": "", + "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": "ჩეთის ისტორიის შენახვა პირდაპირ თქვენი ბრაუზერის საცავში აღარ არის მხარდაჭერილი. გთხოვთ, დაუთმოთ და წაშალოთ თქვენი ჩატის ჟურნალები ქვემოთ მოცემულ ღილაკზე დაწკაპუნებით. არ ინერვიულოთ, თქვენ შეგიძლიათ მარტივად ხელახლა შემოიტანოთ თქვენი ჩეთის ისტორია ბექენდში", + "Scan": "სკანირება", + "Scan complete!": "სკანირება დასრულდა!", + "Scan for documents from {{path}}": "დოკუმენტების სკანირება {{ path}}-დან", + "Search": "ძიება", + "Search a model": "მოდელის ძიება", + "Search Chats": "ჩატების ძებნა", + "Search Documents": "დოკუმენტების ძიება", + "Search Functions": "", + "Search Models": "საძიებო მოდელები", + "Search Prompts": "მოთხოვნების ძიება", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "ძიების შედეგების რაოდენობა", + "Search Tools": "", + "Searched {{count}} sites_one": "Searched {{count}} sites_one", + "Searched {{count}} sites_other": "Searched {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng Query URL", + "See readme.md for instructions": "იხილეთ readme.md ინსტრუქციებისთვის", + "See what's new": "სიახლეების ნახვა", + "Seed": "სიდი", + "Select a base model": "აირჩიეთ ბაზის მოდელი", + "Select a engine": "", + "Select a function": "", + "Select a mode": "რეჟიმის არჩევა", + "Select a model": "მოდელის არჩევა", + "Select a pipeline": "აირჩიეთ მილსადენი", + "Select a pipeline url": "აირჩიეთ მილსადენის url", + "Select a tool": "", + "Select an Ollama instance": "Ollama ინსტანსის არჩევა", + "Select Documents": "", + "Select model": "მოდელის არჩევა", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "შერჩეული მოდელი (ებ) ი არ უჭერს მხარს გამოსახულების შეყვანას", + "Send": "გაგზავნა", + "Send a Message": "შეტყობინების გაგზავნა", + "Send message": "შეტყობინების გაგზავნა", + "September": "სექტემბერი", + "Serper API Key": "Serper API Key", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API Key", + "Server connection verified": "სერვერთან კავშირი დადასტურებულია", + "Set as default": "დეფოლტად დაყენება", + "Set Default Model": "დეფოლტ მოდელის დაყენება", + "Set embedding model (e.g. {{model}})": "ჩვენება მოდელის დაყენება (მაგ. {{model}})", + "Set Image Size": "სურათის ზომის დაყენება", + "Set reranking model (e.g. {{model}})": "რეტარირება მოდელის დაყენება (მაგ. {{model}})", + "Set Steps": "ნაბიჯების დაყენება", + "Set Task Model": "დააყენეთ სამუშაო მოდელი", + "Set Voice": "ხმის დაყენება", + "Settings": "ხელსაწყოები", + "Settings saved successfully!": "პარამეტრები წარმატებით განახლდა!", + "Settings updated successfully": "", + "Share": "გაზიარება", + "Share Chat": "გაზიარება", + "Share to OpenWebUI Community": "გააზიარე OpenWebUI საზოგადოებაში ", + "short-summary": "მოკლე შინაარსი", + "Show": "ჩვენება", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "მალსახმობების ჩვენება", + "Show your support!": "", + "Showcased creativity": "ჩვენებული ქონება", + "Sign in": "ავტორიზაცია", + "Sign Out": "გასვლა", + "Sign up": "რეგისტრაცია", + "Signing in": "ავტორიზაცია", + "Source": "წყარო", + "Speech recognition error: {{error}}": "მეტყველების ამოცნობის შეცდომა: {{error}}", + "Speech-to-Text Engine": "ხმოვან-ტექსტური ძრავი", + "Stop Sequence": "შეჩერების თანმიმდევრობა", + "STT Model": "", + "STT Settings": "მეტყველების ამოცნობის პარამეტრები", + "Submit": "გაგზავნა", + "Subtitle (e.g. about the Roman Empire)": "სუბტიტრები (მაგ. რომის იმპერიის შესახებ)", + "Success": "წარმატება", + "Successfully updated.": "წარმატებით განახლდა", + "Suggested": "პირდაპირ პოპულარული", + "Support": "", + "Support this plugin:": "", + "System": "სისტემა", + "System Prompt": "სისტემური მოთხოვნა", + "Tags": "ტეგები", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "ჩვენთან დავუკავშირდით", + "Temperature": "ტემპერატურა", + "Template": "შაბლონი", + "Text Completion": "ტექსტის დასრულება", + "Text-to-Speech Engine": "ტექსტურ-ხმოვანი ძრავი", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "მადლობა გამოხმაურებისთვის!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "ქულა 0.0 (0%) და 1.0 (100%) ჩაშენებული უნდა იყოს.", + "Theme": "თემა", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "ეს უზრუნველყოფს, რომ თქვენი ძვირფასი საუბრები უსაფრთხოდ შეინახება თქვენს backend მონაცემთა ბაზაში. Გმადლობთ!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "ეს პარამეტრი არ სინქრონიზდება ბრაუზერებსა და მოწყობილობებში", + "This will delete": "", + "Thorough explanation": "ვრცლად აღწერა", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "რჩევა: განაახლეთ რამდენიმე ცვლადი სლოტი თანმიმდევრულად, ყოველი ჩანაცვლების შემდეგ ჩატის ღილაკზე დაჭერით.", + "Title": "სათაური", + "Title (e.g. Tell me a fun fact)": "სათაური (მაგ. გაიხსნე რაღაც ხარისხი)", + "Title Auto-Generation": "სათაურის ავტო-გენერაცია", + "Title cannot be an empty string.": "სათაური ცარიელი ველი ვერ უნდა იყოს.", + "Title Generation Prompt": "სათაურის გენერაციის მოთხოვნა ", + "to": "ში", + "To access the available model names for downloading,": "ჩამოტვირთვისთვის ხელმისაწვდომი მოდელების სახელებზე წვდომისთვის", + "To access the GGUF models available for downloading,": "ჩასატვირთად ხელმისაწვდომი GGUF მოდელებზე წვდომისთვის", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "ჩატში", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "დღეს", + "Toggle settings": "პარამეტრების გადართვა", + "Toggle sidebar": "გვერდითი ზოლის გადართვა", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "ტოპ K", + "Top P": "ტოპ P", + "Trouble accessing Ollama?": "Ollama-ს ვერ უკავშირდები?", + "TTS Model": "", + "TTS Settings": "TTS პარამეტრები", + "TTS Voice": "", + "Type": "ტიპი", + "Type Hugging Face Resolve (Download) URL": "სცადე გადმოწერო Hugging Face Resolve URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "{{provider}}-თან დაკავშირების პრობლემა წარმოიშვა.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "განახლება და ბმულის კოპირება", + "Update password": "პაროლის განახლება", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "GGUF მოდელის ატვირთვა", + "Upload Files": "ატვირთეთ ფაილები", + "Upload Pipeline": "", + "Upload Progress": "პროგრესის ატვირთვა", + "URL Mode": "URL რეჟიმი", + "Use '#' in the prompt input to load and select your documents.": "პრომტში გამოიყენე '#' რომელიც გაიტანს დოკუმენტებს", + "Use Gravatar": "გამოიყენე Gravatar", + "Use Initials": "გამოიყენე ინიციალები", + "use_mlock (Ollama)": "use_mlock (ოლამა)", + "use_mmap (Ollama)": "use_mmap (ოლამა)", + "user": "მომხმარებელი", + "User location successfully retrieved.": "", + "User Permissions": "მომხმარებლის უფლებები", + "Users": "მომხმარებლები", + "Utilize": "გამოყენება", + "Valid time units:": "მოქმედი დროის ერთეულები", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "ცვლადი", + "variable to have them replaced with clipboard content.": "ცვლადი, რომ შეცვალოს ისინი ბუფერში შიგთავსით.", + "Version": "ვერსია", + "Voice": "", + "Warning": "გაფრთხილება", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "გაფრთხილება: თუ განაახლებთ ან შეცვლით ჩანერგვის მოდელს, მოგიწევთ ყველა დოკუმენტის ხელახლა იმპორტი.", + "Web": "ვები", + "Web API": "", + "Web Loader Settings": "ვების ჩატარების პარამეტრები", + "Web Params": "ვების პარამეტრები", + "Web Search": "ვებ ძებნა", + "Web Search Engine": "ვებ საძიებო სისტემა", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI პარამეტრები", + "WebUI will make requests to": "WebUI გამოგიგზავნით მოთხოვნებს", + "What’s New in": "რა არის ახალი", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "როდესაც ისტორია გამორთულია, ახალი ჩეთები ამ ბრაუზერში არ გამოჩნდება თქვენს ისტორიაში არცერთ მოწყობილობაზე.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "ვულერი", + "Write a prompt suggestion (e.g. Who are you?)": "დაწერეთ მოკლე წინადადება (მაგ. ვინ ხარ?", + "Write a summary in 50 words that summarizes [topic or keyword].": "დაწერეთ რეზიუმე 50 სიტყვით, რომელიც აჯამებს [თემას ან საკვანძო სიტყვას].", + "Yesterday": "აღდგენა", + "You": "ჩემი", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "თქვენ არ შეგიძლიათ ბაზის მოდელის კლონირება", + "You have no archived conversations.": "არ ხართ არქივირებული განხილვები.", + "You have shared this chat": "ამ ჩატის გააგზავნა", + "You're a helpful assistant.": "თქვენ სასარგებლო ასისტენტი ხართ.", + "You're now logged in.": "თქვენ შესული ხართ.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube Loader Settings" +} diff --git a/src/lib/i18n/locales/ko-KR/translation.json b/src/lib/i18n/locales/ko-KR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..68a27508e61298c9e1cc534b636c230146a869b7 --- /dev/null +++ b/src/lib/i18n/locales/ko-KR/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "만료 없음은 '-1', 아니면 's', 'm', 'h', 'd', 'w' 중 하나를 사용하세요.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(예: `sh webui.sh --api`)", + "(latest)": "(latest)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: 기본 모델은 삭제할 수 없습니다.", + "{{modelName}} is thinking...": "{{modelName}} 모델이 생각 중입니다....", + "{{user}}'s Chats": "{{user}}의 채팅", + "{{webUIName}} Backend Required": "{{webUIName}} 백엔드가 필요합니다.", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "작업 모델은 채팅 및 웹 검색 쿼리에 대한 제목 생성 등의 작업 수행 시 사용됩니다.", + "a user": "사용자", + "About": "정보", + "Account": "계정", + "Account Activation Pending": "계정 활성화 보류", + "Accurate information": "정확한 정보", + "Actions": "", + "Active Users": "활성 사용자", + "Add": "추가", + "Add a model id": "모델 ID 추가", + "Add a short description about what this model does": "모델의 기능에 대한 간단한 설명 추가", + "Add a short title for this prompt": "프롬프트에 대한 간단한 제목 추가", + "Add a tag": "태그 추가", + "Add custom prompt": "프롬프트 추가", + "Add Docs": "문서 추가", + "Add Files": "파일 추가", + "Add Memory": "메모리 추가", + "Add message": "메시지 추가", + "Add Model": "모델 추가", + "Add Tag": "", + "Add Tags": "태그 추가", + "Add User": "사용자 추가", + "Adjusting these settings will apply changes universally to all users.": "이 설정을 조정하면 모든 사용자에게 적용됩니다.", + "admin": "관리자", + "Admin": "관리자", + "Admin Panel": "관리자 패널", + "Admin Settings": "관리자 설정", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "관리자는 항상 모든 도구에 접근할 수 있지만, 사용자는 워크스페이스에서 모델마다 도구를 할당받아야 합니다.", + "Advanced Parameters": "고급 파라미터", + "Advanced Params": "고급 파라미터", + "all": "모두", + "All Documents": "모든 문서", + "All Users": "모든 사용자", + "Allow": "허용", + "Allow Chat Deletion": "채팅 삭제 허용", + "Allow non-local voices": "외부 음성 허용", + "Allow User Location": "사용자 위치 활용 허용", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "영문자, 숫자, 하이픈", + "Already have an account?": "이미 계정이 있으신가요?", + "an assistant": "어시스턴트", + "and": "그리고", + "and create a new shared link.": "새로운 공유 링크를 생성합니다.", + "API Base URL": "API 기본 URL", + "API Key": "API 키", + "API Key created.": "API 키가 생성되었습니다.", + "API keys": "API 키", + "April": "4월", + "Archive": "아카이브", + "Archive All Chats": "모든 채팅 아카이브", + "Archived Chats": "아카이브된 채팅", + "are allowed - Activate this command by typing": "허용됩니다. - 이 명령을 활성화하려면 입력하세요.", + "Are you sure?": "확실합니까?", + "Attach file": "파일 첨부", + "Attention to detail": "세부 사항에 대한 주의", + "Audio": "오디오", + "Audio settings updated successfully": "", + "August": "8월", + "Auto-playback response": "응답 자동 재생", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 기본 URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 기본 URL 설정이 필요합니다.", + "available!": "사용 가능!", + "Back": "뒤로가기", + "Bad Response": "잘못된 응답", + "Banners": "배너", + "Base Model (From)": "기본 모델(시작)", + "Batch Size (num_batch)": "배치 크기 (num_batch)", + "before": "이전", + "Being lazy": "게으름 피우기", + "Brave Search API Key": "Brave Search API 키", + "Bypass SSL verification for Websites": "웹 사이트에 대한 SSL 검증 무시: ", + "Call": "콜", + "Call feature is not supported when using Web STT engine": "웹 STT 엔진 사용 시, 콜 기능은 지원되지 않습니다.", + "Camera": "카메라", + "Cancel": "취소", + "Capabilities": "기능", + "Change Password": "비밀번호 변경", + "Chat": "채팅", + "Chat Background Image": "채팅 배경 이미지", + "Chat Bubble UI": "버블형 채팅 UI", + "Chat Controls": "", + "Chat direction": "채팅 방향", + "Chat History": "채팅 기록", + "Chat History is off for this browser.": "브라우저에서 채팅 기록이 꺼져 있습니다.", + "Chats": "채팅", + "Check Again": "다시 확인", + "Check for updates": "업데이트 확인", + "Checking for updates...": "업데이트 확인중...", + "Choose a model before saving...": "저장하기 전에 모델을 선택하세요...", + "Chunk Overlap": "Chunk 오버랩", + "Chunk Params": "Chunk 파라미터", + "Chunk Size": "Chunk 크기", + "Citation": "인용", + "Clear memory": "메모리 초기화", + "Click here for help.": "도움말을 보려면 여기를 클릭하세요.", + "Click here to": "여기를 클릭하면", + "Click here to download user import template file.": "", + "Click here to select": "선택하려면 여기를 클릭하세요.", + "Click here to select a csv file.": "csv 파일을 선택하려면 여기를 클릭하세요.", + "Click here to select a py file.": "py 파일을 선택하려면 여기를 클릭하세요.", + "Click here to select documents.": "문서를 선택하려면 여기를 클릭하세요.", + "click here.": "여기를 클릭하세요.", + "Click on the user role button to change a user's role.": "사용자 역할 버튼을 클릭하여 사용자의 역할을 변경하세요.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "복제", + "Close": "닫기", + "Code formatted successfully": "", + "Collection": "컬렉션", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI 기본 URL", + "ComfyUI Base URL is required.": "ComfyUI 기본 URL이 필요합니다.", + "Command": "명령", + "Concurrent Requests": "동시 요청 수", + "Confirm": "확인", + "Confirm Password": "비밀번호 확인", + "Confirm your action": "액션 확인", + "Connections": "연결", + "Contact Admin for WebUI Access": "WebUI 접속을 위해서는 관리자에게 연락 필요", + "Content": "내용", + "Content Extraction": "", + "Context Length": "내용 길이", + "Continue Response": "대화 계속", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "공유 채팅 URL이 클립보드에 복사되었습니다!", + "Copy": "복사", + "Copy last code block": "마지막 코드 블록 복사", + "Copy last response": "마지막 응답 복사", + "Copy Link": "링크 복사", + "Copying to clipboard was successful!": "클립보드에 복사되었습니다!", + "Create a model": "모델 만들기", + "Create Account": "계정 만들기", + "Create new key": "새 키 만들기", + "Create new secret key": "새 비밀 키 만들기", + "Created at": "생성일", + "Created At": "생성일", + "Created by": "", + "CSV Import": "", + "Current Model": "현재 모델", + "Current Password": "현재 비밀번호", + "Custom": "사용자 정의", + "Customize models for a specific purpose": "특정 목적을 위한 모델 사용자 지정", + "Dark": "Dark", + "Dashboard": "대시보드", + "Database": "데이터베이스", + "December": "12월", + "Default": "기본값", + "Default (Automatic1111)": "기본값 (Automatic1111)", + "Default (SentenceTransformers)": "기본값 (SentenceTransformers)", + "Default Model": "기본 모델", + "Default model updated": "기본 모델이 업데이트되었습니다.", + "Default Prompt Suggestions": "기본 프롬프트 제안", + "Default User Role": "기본 사용자 역할", + "delete": "삭제", + "Delete": "삭제", + "Delete a model": "모델 삭제", + "Delete All Chats": "모든 채팅 삭제", + "Delete chat": "채팅 삭제", + "Delete Chat": "채팅 삭제", + "Delete chat?": "채팅을 삭제하겠습니까?", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "이 링크를 삭제합니다.", + "Delete tool?": "", + "Delete User": "사용자 삭제", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} 삭제됨", + "Deleted {{name}}": "{{name}}을(를) 삭제했습니다.", + "Description": "설명", + "Didn't fully follow instructions": "완전히 지침을 따르지 않음", + "Disabled": "", + "Discover a function": "", + "Discover a model": "모델 검색", + "Discover a prompt": "프롬프트 검색", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "사용자 정의 프롬프트 검색, 다운로드 및 탐색", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "모델 사전 설정 검색, 다운로드 및 탐색", + "Dismissible": "제외가능", + "Display Emoji in Call": "콜(call)에서 이모지 표시", + "Display the username instead of You in the Chat": "채팅에서 '당신' 대신 사용자 이름 표시", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "문서", + "Document Settings": "문서 설정", + "Documentation": "문서 조사", + "Documents": "문서", + "does not make any external connections, and your data stays securely on your locally hosted server.": "어떠한 외부 연결도 하지 않으며, 데이터는 로컬에서 호스팅되는 서버에 안전하게 유지됩니다.", + "Don't Allow": "허용 안 함", + "Don't have an account?": "계정이 없으신가요?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "스타일을 좋아하지 않으세요?", + "Done": "", + "Download": "다운로드", + "Download canceled": "다운로드 취소", + "Download Database": "데이터베이스 다운로드", + "Drop any files here to add to the conversation": "대화에 추가할 파일을 여기에 드롭하세요.", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "예: '30s','10m'. 유효한 시간 단위는 's', 'm', 'h'입니다.", + "Edit": "편집", + "Edit Doc": "문서 편집", + "Edit Memory": "메모리 편집", + "Edit User": "사용자 편집", + "ElevenLabs": "", + "Email": "이메일", + "Embedding Batch Size": "임베딩 배치 크기", + "Embedding Model": "임베딩 모델", + "Embedding Model Engine": "임베딩 모델 엔진", + "Embedding model set to \"{{embedding_model}}\"": "임베딩 모델을 \"{{embedding_model}}\"로 설정함", + "Enable Chat History": "채팅 기록 활성화", + "Enable Community Sharing": "커뮤니티 공유 활성화", + "Enable New Sign Ups": "새 회원가입 활성화", + "Enable Web Search": "웹 검색 활성화", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "CSV 파일에 이름, 이메일, 비밀번호, 역할 4개의 컬럼이 순서대로 포함되어 있는지 확인하세요.", + "Enter {{role}} message here": "여기에 {{role}} 메시지 입력", + "Enter a detail about yourself for your LLMs to recall": "자신에 대한 세부사항을 입력하여 LLM들이 기억할 수 있도록 하세요.", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Brave Search API Key 입력", + "Enter Chunk Overlap": "청크 오버랩 입력", + "Enter Chunk Size": "청크 크기 입력", + "Enter Github Raw URL": "Github Raw URL 입력", + "Enter Google PSE API Key": "Google PSE API 키 입력", + "Enter Google PSE Engine Id": "Google PSE 엔진 ID 입력", + "Enter Image Size (e.g. 512x512)": "이미지 크기 입력(예: 512x512)", + "Enter language codes": "언어 코드 입력", + "Enter model tag (e.g. {{modelTag}})": "모델 태그 입력(예: {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "단계 수 입력(예: 50)", + "Enter Score": "점수 입력", + "Enter Searxng Query URL": "Searxng 쿼리 URL 입력", + "Enter Serper API Key": "Serper API 키 입력", + "Enter Serply API Key": "Serply API 키 입력", + "Enter Serpstack API Key": "Serpstack API 키 입력", + "Enter stop sequence": "중지 시퀀스 입력", + "Enter system prompt": "", + "Enter Tavily API Key": "Tavily API 키 입력", + "Enter Tika Server URL": "", + "Enter Top K": "Top K 입력", + "Enter URL (e.g. http://127.0.0.1:7860/)": "URL 입력(예: http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "URL 입력(예: http://localhost:11434)", + "Enter Your Email": "이메일 입력", + "Enter Your Full Name": "이름 입력", + "Enter your message": "", + "Enter Your Password": "비밀번호 입력", + "Enter Your Role": "역할 입력", + "Error": "오류", + "Experimental": "실험적", + "Export": "내보내기", + "Export All Chats (All Users)": "모든 채팅 내보내기(모든 사용자)", + "Export chat (.json)": "채팅 내보내기(.json)", + "Export Chats": "채팅 내보내기", + "Export Documents Mapping": "문서 매핑 내보내기", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "모델 내보내기", + "Export Prompts": "프롬프트 내보내기", + "Export Tools": "도구 내보내기", + "External Models": "", + "Failed to create API Key.": "API 키 생성에 실패했습니다.", + "Failed to read clipboard contents": "클립보드 내용을 읽는 데 실패하였습니다.", + "Failed to update settings": "설정 업데이트에 실패하였습니다.", + "February": "2월", + "Feel free to add specific details": "자세한 내용을 자유롭게 추가하세요.", + "File": "", + "File Mode": "파일 모드", + "File not found.": "파일을 찾을 수 없습니다.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingerprint spoofing 감지: 이니셜을 아바타로 사용할 수 없습니다. 기본 프로필 이미지로 설정합니다.", + "Fluidly stream large external response chunks": "대규모 외부 응답 청크를 유연하게 스트리밍", + "Focus chat input": "채팅 입력창에 포커스", + "Followed instructions perfectly": "명령을 완벽히 따름", + "Form": "", + "Format your variables using square brackets like this:": "다음과 같이 대괄호를 사용하여 변수를 형식화하세요:", + "Frequency Penalty": "프리퀀시 페널티", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "일반", + "General Settings": "일반 설정", + "Generate Image": "이미지 생성", + "Generating search query": "검색 쿼리 생성", + "Generation Info": "생성 정보", + "Get up and running with": "", + "Global": "", + "Good Response": "좋은 응답", + "Google PSE API Key": "Google PSE API 키", + "Google PSE Engine Id": "Google PSE 엔진 ID", + "h:mm a": "h:mm a", + "has no conversations.": "대화가 없습니다.", + "Hello, {{name}}": "안녕하세요, {{name}}", + "Help": "도움말", + "Hide": "숨기기", + "Hide Model": "", + "How can I help you today?": "오늘 어떻게 도와드릴까요?", + "Hybrid Search": "하이브리드 검색", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "이미지 생성(실험적)", + "Image Generation Engine": "이미지 생성 엔진", + "Image Settings": "이미지 설정", + "Images": "이미지", + "Import Chats": "채팅 가져오기", + "Import Documents Mapping": "문서 매핑 가져오기", + "Import Functions": "", + "Import Models": "모델 가져오기", + "Import Prompts": "프롬프트 가져오기", + "Import Tools": "도구 가져오기", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui를 실행 시 `--api` 플래그 포함 필요", + "Info": "정보", + "Input commands": "입력 명령", + "Install from Github URL": "Github URL에서 설치", + "Instant Auto-Send After Voice Transcription": "음성 변환 후 즉시 자동 전송", + "Interface": "인터페이스", + "Invalid Tag": "잘못된 태그", + "January": "1월", + "join our Discord for help.": "도움말을 보려면 Discord에 가입하세요.", + "JSON": "JSON", + "JSON Preview": "JSON 미리 보기", + "July": "7월", + "June": "6월", + "JWT Expiration": "JWT 만료", + "JWT Token": "JWT 토큰", + "Keep Alive": "계속 유지하기", + "Keyboard shortcuts": "키보드 단축키", + "Knowledge": "지식 기반", + "Language": "언어", + "large language models, locally.": "", + "Last Active": "최근 활동", + "Last Modified": "마지막 수정", + "Light": "Light", + "Listening...": "듣는 중...", + "LLMs can make mistakes. Verify important information.": "LLM은 실수를 할 수 있습니다. 중요한 정보는 확인이 필요합니다.", + "Local Models": "로컬 모델", + "LTR": "LTR", + "Made by OpenWebUI Community": "OpenWebUI 커뮤니티에 의해 개발됨", + "Make sure to enclose them with": "꼭 다음으로 감싸세요:", + "Manage": "관리", + "Manage Models": "모델 관리", + "Manage Ollama Models": "Ollama 모델 관리", + "Manage Pipelines": "파이프라인 관리", + "Manage Valves": "", + "March": "3월", + "Max Tokens (num_predict)": "최대 토큰(num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "최대 3개의 모델을 동시에 다운로드할 수 있습니다. 나중에 다시 시도하세요.", + "May": "5월", + "Memories accessible by LLMs will be shown here.": "LLM에서 액세스할 수 있는 메모리는 여기에 표시됩니다.", + "Memory": "메모리", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "링크 생성 후에 보낸 메시지는 공유되지 않습니다. URL이 있는 사용자는 공유된 채팅을 볼 수 있습니다.", + "Minimum Score": "최소 점수", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' 모델이 성공적으로 다운로드되었습니다.", + "Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' 모델은 이미 다운로드 대기열에 있습니다.", + "Model {{modelId}} not found": "{{modelId}} 모델을 찾을 수 없습니다.", + "Model {{modelName}} is not vision capable": "{{modelName}} 모델은 비전을 사용할 수 없습니다.", + "Model {{name}} is now {{status}}": "{{name}} 모델은 이제 {{status}} 상태입니다.", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "모델 파일 시스템 경로가 감지되었습니다. 업데이트하려면 모델 단축 이름이 필요하며 계속할 수 없습니다.", + "Model ID": "모델 ID", + "Model not selected": "모델이 선택되지 않았습니다.", + "Model Params": "모델 파라미터", + "Model updated successfully": "", + "Model Whitelisting": "허용 모델 명시", + "Model(s) Whitelisted": "허용 모델", + "Modelfile Content": "Modelfile 내용", + "Models": "모델", + "More": "더보기", + "Name": "이름", + "Name Tag": "이름 태그", + "Name your model": "모델 이름 지정", + "New Chat": "새 채팅", + "New Password": "새 비밀번호", + "No content to speak": "", + "No documents found": "문서 없음", + "No file selected": "", + "No results found": "결과 없음", + "No search query generated": "검색어가 생성되지 않았습니다.", + "No source available": "사용 가능한 소스 없음", + "No valves to update": "", + "None": "없음", + "Not factually correct": "사실상 맞지 않음", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "참고: 최소 점수를 설정하면, 검색 결과로 최소 점수 이상의 점수를 가진 문서만 반환합니다.", + "Notifications": "알림", + "November": "11월", + "num_thread (Ollama)": "num_thread (올라마)", + "OAuth ID": "", + "October": "10월", + "Off": "끄기", + "Okay, Let's Go!": "좋아요, 시작합시다!", + "OLED Dark": "OLED Dark", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API 비활성화", + "Ollama API is disabled": "Ollama API 비활성화", + "Ollama Version": "Ollama 버전", + "On": "켜기", + "Only": "오직", + "Only alphanumeric characters and hyphens are allowed in the command string.": "명령어 문자열에는 영문자, 숫자 및 하이픈만 허용됩니다.", + "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.": "이런! 잠시만 기다려 주세요! 파일이 아직 처리 중입니다. 완벽을 위해 준비하고 있습니다. 준비가 되면 알려드리겠습니다.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "이런! URL이 잘못된 것 같습니다. 다시 한번 확인하고 다시 시도해주세요.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "이런! 지원되지 않는 방식(프론트엔드만)을 사용하고 계십니다. 백엔드에서 WebUI를 제공해주세요.", + "Open AI (Dall-E)": "OpenAI(Dall-E)", + "Open new chat": "새 채팅 열기", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API 설정", + "OpenAI API Key is required.": "OpenAI API 키가 필요합니다.", + "OpenAI URL/Key required.": "OpenAI URL/키가 필요합니다.", + "or": "또는", + "Other": "기타", + "Password": "비밀번호", + "PDF document (.pdf)": "PDF 문서(.pdf)", + "PDF Extract Images (OCR)": "PDF 이미지 추출(OCR)", + "pending": "보류 중", + "Permission denied when accessing media devices": "미디어 장치 액세스가 거부되었습니다.", + "Permission denied when accessing microphone": "마이크 액세스가 거부되었습니다.", + "Permission denied when accessing microphone: {{error}}": "마이크 액세스가 거부되었습니다: {{error}}", + "Personalization": "개인화", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "파이프라인", + "Pipelines Not Detected": "", + "Pipelines Valves": "파이프라인 밸브", + "Plain text (.txt)": "일반 텍스트(.txt)", + "Playground": "놀이터", + "Please carefully review the following warnings:": "", + "Positive attitude": "긍정적인 자세", + "Previous 30 days": "이전 30일", + "Previous 7 days": "이전 7일", + "Profile Image": "프로필 이미지", + "Prompt": "프롬프트", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "프롬프트 (예: 로마 황제에 대해 재미있는 사실을 알려주세요)", + "Prompt Content": "프롬프트 내용", + "Prompt suggestions": "프롬프트 제안", + "Prompts": "프롬프트", + "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com에서 \"{{searchValue}}\" 가져오기", + "Pull a model from Ollama.com": "Ollama.com에서 모델 가져오기(pull)", + "Query Params": "쿼리 파라미터", + "RAG Template": "RAG 템플릿", + "Read Aloud": "읽어주기", + "Record voice": "음성 녹음", + "Redirecting you to OpenWebUI Community": "OpenWebUI 커뮤니티로 리디렉션 중", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "스스로를 \"User\" 라고 지칭하세요. (예: \"User is learning Spanish\")", + "Refused when it shouldn't have": "허용되지 않았지만 허용되어야 합니다.", + "Regenerate": "재생성", + "Release Notes": "릴리스 노트", + "Remove": "삭제", + "Remove Model": "모델 삭제", + "Rename": "이름 변경", + "Repeat Last N": "마지막 N 반복", + "Request Mode": "요청 모드", + "Reranking Model": "Reranking 모델", + "Reranking model disabled": "Reranking 모델 비활성화", + "Reranking model set to \"{{reranking_model}}\"": "Reranking 모델을 \"{{reranking_model}}\"로 설정", + "Reset": "초기화", + "Reset Upload Directory": "업로드 디렉토리 초기화", + "Reset Vector Storage": "벡터 스토리지 초기화", + "Response AutoCopy to Clipboard": "응답을 클립보드에 자동 복사", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "역할", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "실행 중", + "Save": "저장", + "Save & Create": "저장 및 생성", + "Save & Update": "저장 및 업데이트", + "Save Tag": "", + "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": "브라우저의 저장소에 채팅 로그를 직접 저장하는 것은 더 이상 지원되지 않습니다. 아래 버튼을 클릭하여 채팅 로그를 다운로드하고 삭제하세요. 걱정 마세요. 백엔드를 통해 채팅 로그를 쉽게 다시 가져올 수 있습니다.", + "Scan": "스캔", + "Scan complete!": "스캔 완료!", + "Scan for documents from {{path}}": "{{path}} 에서 문서 스캔", + "Search": "검색", + "Search a model": "모델 검색", + "Search Chats": "채팅 검색", + "Search Documents": "문서 검색", + "Search Functions": "", + "Search Models": "모델 검색", + "Search Prompts": "프롬프트 검색", + "Search Query Generation Prompt": "검색 쿼리 생성 프롬프트", + "Search Query Generation Prompt Length Threshold": "검색 쿼리 생성 프롬프트 길이 임계치", + "Search Result Count": "검색 결과 수", + "Search Tools": "검색 도구", + "Searched {{count}} sites_one": "sites_one {{count}} 검색됨", + "Searched {{count}} sites_other": "sites_other {{count}} 검색됨", + "Searching \"{{searchQuery}}\"": "\"{{searchQuery}}\" 검색 중", + "Searxng Query URL": "Searxng 쿼리 URL", + "See readme.md for instructions": "설명은 readme.md를 참조하세요.", + "See what's new": "새로운 기능 보기", + "Seed": "시드", + "Select a base model": "기본 모델 선택", + "Select a engine": "엔진 선택", + "Select a function": "", + "Select a mode": "모드 선택", + "Select a model": "모델 선택", + "Select a pipeline": "파이프라인 선택", + "Select a pipeline url": "파이프라인 URL 선택", + "Select a tool": "", + "Select an Ollama instance": "Ollama 인스턴스 선택", + "Select Documents": "문서 선택", + "Select model": "모델 선택", + "Select only one model to call": "콜을 위해서는 모델을 하나만 선택해야 합니다.", + "Selected model(s) do not support image inputs": "선택한 모델은 이미지 입력을 지원하지 않습니다.", + "Send": "보내기", + "Send a Message": "메시지 보내기", + "Send message": "메시지 보내기", + "September": "9월", + "Serper API Key": "Serper API 키", + "Serply API Key": "Serply API 키", + "Serpstack API Key": "Serpstack API 키", + "Server connection verified": "서버 연결 확인됨", + "Set as default": "기본값으로 설정", + "Set Default Model": "기본 모델 설정", + "Set embedding model (e.g. {{model}})": "임베딩 모델 설정 (예: {{model}})", + "Set Image Size": "이미지 크기 설정", + "Set reranking model (e.g. {{model}})": "reranking 모델 설정 (예: {{model}})", + "Set Steps": "단계 설정", + "Set Task Model": "작업 모델 설정", + "Set Voice": "음성 설정", + "Settings": "설정", + "Settings saved successfully!": "설정이 성공적으로 저장되었습니다!", + "Settings updated successfully": "설정이 성공적으로 업데이트되었습니다.", + "Share": "공유", + "Share Chat": "채팅 공유", + "Share to OpenWebUI Community": "OpenWebUI 커뮤니티에 공유", + "short-summary": "간단한 요약", + "Show": "보이기", + "Show Admin Details in Account Pending Overlay": "사용자용 계정 보류 설명창에, 관리자 상세 정보 노출", + "Show Model": "", + "Show shortcuts": "단축키 보기", + "Show your support!": "", + "Showcased creativity": "창의성 발휘", + "Sign in": "로그인", + "Sign Out": "로그아웃", + "Sign up": "가입", + "Signing in": "로그인 중", + "Source": "출처", + "Speech recognition error: {{error}}": "음성 인식 오류: {{error}}", + "Speech-to-Text Engine": "음성-텍스트 변환 엔진", + "Stop Sequence": "중지 시퀀스", + "STT Model": "STT 모델", + "STT Settings": "STT 설정", + "Submit": "제출", + "Subtitle (e.g. about the Roman Empire)": "자막 (예: 로마 황제)", + "Success": "성공", + "Successfully updated.": "성공적으로 업데이트되었습니다.", + "Suggested": "제안", + "Support": "", + "Support this plugin:": "", + "System": "시스템", + "System Prompt": "시스템 프롬프트", + "Tags": "태그", + "Tap to interrupt": "탭하여 중단", + "Tavily API Key": "Tavily API 키", + "Tell us more:": "더 알려주세요:", + "Temperature": "온도", + "Template": "템플릿", + "Text Completion": "텍스트 완성", + "Text-to-Speech Engine": "텍스트-음성 변환 엔진", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "피드백 감사합니다!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "점수는 0.0(0%)에서 1.0(100%) 사이의 값이어야 합니다.", + "Theme": "테마", + "Thinking...": "생각 중...", + "This action cannot be undone. Do you wish to continue?": "이 액션은 되돌릴 수 없습니다. 계속하시겠습니까?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "이렇게 하면 소중한 대화 내용이 백엔드 데이터베이스에 안전하게 저장됩니다. 감사합니다!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "이것은 실험적 기능으로, 예상대로 작동하지 않을 수 있으며 언제든지 변경될 수 있습니다.", + "This setting does not sync across browsers or devices.": "이 설정은 브라우저 또는 장치 간에 동기화되지 않습니다.", + "This will delete": "이것은 다음을 삭제합니다.", + "Thorough explanation": "완전한 설명", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "팁: 각 대체 후 채팅 입력에서 탭 키를 눌러 여러 개의 변수 슬롯을 연속적으로 업데이트하세요.", + "Title": "제목", + "Title (e.g. Tell me a fun fact)": "제목 (예: 재미있는 사실을 알려주세요)", + "Title Auto-Generation": "제목 자동 생성", + "Title cannot be an empty string.": "제목은 빈 문자열일 수 없습니다.", + "Title Generation Prompt": "제목 생성 프롬프트", + "to": "까지", + "To access the available model names for downloading,": "다운로드 가능한 모델명을 확인하려면,", + "To access the GGUF models available for downloading,": "다운로드 가능한 GGUF 모델을 확인하려면,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "WebUI에 접속하려면 관리자에게 문의하십시오. 관리자는 관리자 패널에서 사용자 상태를 관리할 수 있습니다.", + "To add documents here, upload them to the \"Documents\" workspace first.": "여기에 문서를 추가하려면, \"문서\" 워크스페이스에 먼저 업로드하세요.", + "to chat input.": "채팅 입력으로.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "여기서 도구를 선택하려면, \"도구\" 워크스페이스에 먼저 추가하세요.", + "Today": "오늘", + "Toggle settings": "설정 전환", + "Toggle sidebar": "사이드바 전환", + "Tokens To Keep On Context Refresh (num_keep)": "컨텍스트 새로 고침 시 유지할 토큰 수(num_keep)", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "도구", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Ollama에 접근하는 데 문제가 있나요?", + "TTS Model": "TTS 모델", + "TTS Settings": "TTS 설정", + "TTS Voice": "TTS 음성", + "Type": "입력", + "Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (다운로드) URL 입력", + "Uh-oh! There was an issue connecting to {{provider}}.": "앗! {{provider}}에 연결하는 데 문제가 있었습니다.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "업데이트", + "Update and Copy Link": "링크 업데이트 및 복사", + "Update password": "비밀번호 업데이트", + "Updated at": "다음에 업데이트됨", + "Upload": "업로드", + "Upload a GGUF model": "GGUF 모델 업로드", + "Upload Files": "파일 업로드", + "Upload Pipeline": "업로드 파이프라인", + "Upload Progress": "업로드 진행 상황", + "URL Mode": "URL 모드", + "Use '#' in the prompt input to load and select your documents.": "프롬프트 입력에서 '#'를 사용하여 문서를 로드하고 선택하세요.", + "Use Gravatar": "Gravatar 사용", + "Use Initials": "초성 사용", + "use_mlock (Ollama)": "use_mlock (올라마)", + "use_mmap (Ollama)": "use_mmap (올라마)", + "user": "사용자", + "User location successfully retrieved.": "", + "User Permissions": "사용자 권한", + "Users": "사용자", + "Utilize": "활용", + "Valid time units:": "유효 시간 단위:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "변수", + "variable to have them replaced with clipboard content.": "변수를 사용하여 클립보드 내용으로 바꾸세요.", + "Version": "버전", + "Voice": "음성", + "Warning": "경고", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "주의: 기존 임베딩 모델을 변경 또는 업데이트하는 경우, 모든 문서를 다시 가져와야 합니다.", + "Web": "웹", + "Web API": "웹 API", + "Web Loader Settings": "웹 로더 설정", + "Web Params": "웹 파라미터", + "Web Search": "웹 검색", + "Web Search Engine": "웹 검색 엔진", + "Webhook URL": "웹훅 URL", + "WebUI Settings": "WebUI 설정", + "WebUI will make requests to": "WebUI 요청 대상:", + "What’s New in": "새로운 기능:", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "기록 기능이 꺼져 있으면 이 브라우저의 새 채팅이 다른 장치의 채팅 기록에 나타나지 않습니다.", + "Whisper (Local)": "Whisper (로컬)", + "Widescreen Mode": "와이드스크린 모드", + "Workspace": "워크스페이스", + "Write a prompt suggestion (e.g. Who are you?)": "프롬프트 제안 작성 (예: 당신은 누구인가요?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "[주제 또는 키워드]에 대한 50단어 요약문 작성.", + "Yesterday": "어제", + "You": "당신", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "아래 '관리' 버튼으로 메모리를 추가하여 LLM들과의 상호작용을 개인화할 수 있습니다. 이를 통해 더 유용하고 맞춤화된 경험을 제공합니다.", + "You cannot clone a base model": "기본 모델은 복제할 수 없습니다", + "You have no archived conversations.": "채팅을 아카이브한 적이 없습니다.", + "You have shared this chat": "이 채팅을 공유했습니다.", + "You're a helpful assistant.": "당신은 유용한 어시스턴트입니다.", + "You're now logged in.": "로그인되었습니다.", + "Your account status is currently pending activation.": "현재 계정은 아직 활성화되지 않았습니다.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "유튜브", + "Youtube Loader Settings": "유튜브 로더 설정" +} diff --git a/src/lib/i18n/locales/languages.json b/src/lib/i18n/locales/languages.json new file mode 100644 index 0000000000000000000000000000000000000000..018c6d1accb1f0edd7e3e462b9587aae6c5676bc --- /dev/null +++ b/src/lib/i18n/locales/languages.json @@ -0,0 +1,158 @@ +[ + { + "code": "en-US", + "title": "English (US)" + }, + { + "code": "en-GB", + "title": "English (GB)" + }, + { + "code": "ar-BH", + "title": "Arabic (عربي)" + }, + { + "code": "bn-BD", + "title": "Bengali (বাংলা)" + }, + { + "code": "bg-BG", + "title": "Bulgarian (български)" + }, + { + "code": "ca-ES", + "title": "Catalan (català)" + }, + { + "code": "ceb-PH", + "title": "Cebuano (Filipino)" + }, + { + "code": "de-DE", + "title": "German (Deutsch)" + }, + { + "code": "es-ES", + "title": "Spanish (Español)" + }, + { + "code": "fa-IR", + "title": "Persian (فارسی)" + }, + { + "code": "fi-FI", + "title": "Finnish (Suomalainen)" + }, + { + "code": "fr-CA", + "title": "French (Canada)" + }, + { + "code": "fr-FR", + "title": "French (France)" + }, + { + "code": "he-IL", + "title": "Hebrew (עברית)" + }, + { + "code": "hi-IN", + "title": "Hindi (हिंदी)" + }, + { + "code": "hr-HR", + "title": "Croatian (Hrvatski)" + }, + { + "code": "id-ID", + "title": "Indonesian (Bahasa Indonesia)" + }, + { + "code": "it-IT", + "title": "Italian (Italiano)" + }, + { + "code": "ja-JP", + "title": "Japanese (日本語)" + }, + { + "code": "ka-GE", + "title": "Georgian (ქართული)" + }, + { + "code": "ko-KR", + "title": "Korean (한국어)" + }, + { + "code": "lt-LT", + "title": "Lithuanian (Lietuvių)" + }, + { + "code": "nb-NO", + "title": "Norwegian Bokmål (Norway)" + }, + { + "code": "nl-NL", + "title": "Dutch (Netherlands)" + }, + { + "code": "pa-IN", + "title": "Punjabi (India)" + }, + { + "code": "pl-PL", + "title": "Polish (Polski)" + }, + { + "code": "pt-BR", + "title": "Portuguese (Brazil)" + }, + { + "code": "pt-PT", + "title": "Portuguese (Portugal)" + }, + { + "code": "ru-RU", + "title": "Russian (Russia)" + }, + { + "code": "sv-SE", + "title": "Swedish (Svenska)" + }, + { + "code": "sr-RS", + "title": "Serbian (Српски)" + }, + { + "code": "th-TH", + "title": "Thailand (ไทย)" + }, + { + "code": "tr-TR", + "title": "Turkish (Türkçe)" + }, + { + "code": "tk-TW", + "title": "Turkmen (Türkmençe)" + }, + { + "code": "uk-UA", + "title": "Ukrainian (Українська)" + }, + { + "code": "vi-VN", + "title": "Vietnamese (Tiếng Việt)" + }, + { + "code": "zh-CN", + "title": "Chinese (简体中文)" + }, + { + "code": "zh-TW", + "title": "Chinese (繁體中文)" + }, + { + "code": "dg-DG", + "title": "Doge (🐶)" + } +] diff --git a/src/lib/i18n/locales/lt-LT/translation.json b/src/lib/i18n/locales/lt-LT/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..db9f76710affa23aa1bdc79e8f8e47e4cfc92c97 --- /dev/null +++ b/src/lib/i18n/locales/lt-LT/translation.json @@ -0,0 +1,716 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' arba '-1' kad neišteitų iš galiojimo.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(pvz. `sh webui.sh --api`)", + "(latest)": "(naujausias)", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "{{modelName}} mąsto...", + "{{user}}'s Chats": "{{user}} susirašinėjimai", + "{{webUIName}} Backend Required": "{{webUIName}} būtinas serveris", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "naudotojas", + "About": "Apie", + "Account": "Paskyra", + "Account Activation Pending": "", + "Accurate information": "Tiksli informacija", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "Pridėti trumpą šios užklausos pavadinimą", + "Add a tag": "Pridėti žymą", + "Add custom prompt": "Pridėti užklausos šabloną", + "Add Docs": "Pridėti dokumentų", + "Add Files": "Pridėti failus", + "Add Memory": "", + "Add message": "Pridėti žinutę", + "Add Model": "Pridėti modelį", + "Add Tag": "", + "Add Tags": "Pridėti žymas", + "Add User": "Pridėti naudotoją", + "Adjusting these settings will apply changes universally to all users.": "Šių nustatymų pakeitimas bus pritakytas visiems naudotojams.", + "admin": "Administratorius", + "Admin": "", + "Admin Panel": "Administratorių panelė", + "Admin Settings": "Administratorių nustatymai", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Gilieji nustatymai", + "Advanced Params": "", + "all": "visi", + "All Documents": "Visi dokumentai", + "All Users": "Visi naudotojai", + "Allow": "Leisti", + "Allow Chat Deletion": "Leisti pokalbių ištrynimą", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "skaičiai, raidės ir brūkšneliai", + "Already have an account?": "Ar jau turite paskyrą?", + "an assistant": "assistentas", + "and": "ir", + "and create a new shared link.": "sukurti naują dalinimosi nuorodą", + "API Base URL": "API basės nuoroda", + "API Key": "API raktas", + "API Key created.": "API raktas sukurtas", + "API keys": "API raktai", + "April": "Balandis", + "Archive": "Archyvai", + "Archive All Chats": "", + "Archived Chats": "Archyvuoti pokalbiai", + "are allowed - Activate this command by typing": "leistina - aktyvuokite komandą rašydami", + "Are you sure?": "Are esate tikri?", + "Attach file": "Pridėti failą", + "Attention to detail": "Dėmesys detalėms", + "Audio": "Audio įrašas", + "Audio settings updated successfully": "", + "August": "Rugpjūtis", + "Auto-playback response": "Automatinis atsakymo skaitymas", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 bazės nuoroda", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 bazės nuoroda reikalinga.", + "available!": "prieinama!", + "Back": "Atgal", + "Bad Response": "Neteisingas atsakymas", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "prieš", + "Being lazy": "Būvimas tingiu", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "Išvengti SSL patikros puslapiams", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Atšaukti", + "Capabilities": "", + "Change Password": "Keisti slaptažodį", + "Chat": "Pokalbis", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "Pokalbių istorija", + "Chat History is off for this browser.": "Šioje naršyklėje pokalbių istorija išjungta.", + "Chats": "Pokalbiai", + "Check Again": "Patikrinti iš naujo", + "Check for updates": "Patikrinti atnaujinimus", + "Checking for updates...": "Ieškoma atnaujinimų...", + "Choose a model before saving...": "Pasirinkite modelį prieš išsaugant...", + "Chunk Overlap": "Blokų persidengimas", + "Chunk Params": "Blokų nustatymai", + "Chunk Size": "Blokų dydis", + "Citation": "Citata", + "Clear memory": "", + "Click here for help.": "Paspauskite čia dėl pagalbos.", + "Click here to": "Paspauskite čia, kad:", + "Click here to download user import template file.": "", + "Click here to select": "Spauskite čia norėdami pasirinkti", + "Click here to select a csv file.": "Spauskite čia tam, kad pasirinkti csv failą", + "Click here to select a py file.": "", + "Click here to select documents.": "Spauskite čia norėdami pasirinkti dokumentus.", + "click here.": "paspauskite čia.", + "Click on the user role button to change a user's role.": "Paspauskite ant naudotojo rolės mygtuko tam, kad pakeisti naudotojo rolę.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "Uždaryti", + "Code formatted successfully": "", + "Collection": "Kolekcija", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI bazės nuoroda", + "ComfyUI Base URL is required.": "ComfyUI bazės nuoroda privaloma", + "Command": "Command", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "Patvirtinkite slaptažodį", + "Confirm your action": "", + "Connections": "Ryšiai", + "Contact Admin for WebUI Access": "", + "Content": "Turinys", + "Content Extraction": "", + "Context Length": "Konteksto ilgis", + "Continue Response": "Tęsti atsakymą", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Nukopijavote pokalbio nuorodą", + "Copy": "Kopijuoti", + "Copy last code block": "Kopijuoti paskutinį kodo bloką", + "Copy last response": "Kopijuoti paskutinį atsakymą", + "Copy Link": "Kopijuoti nuorodą", + "Copying to clipboard was successful!": "La copie dans le presse-papiers a réussi !", + "Create a model": "", + "Create Account": "Créer un compte", + "Create new key": "Sukurti naują raktą", + "Create new secret key": "Sukurti naują slaptą raktą", + "Created at": "Sukurta", + "Created At": "Sukurta", + "Created by": "", + "CSV Import": "", + "Current Model": "Dabartinis modelis", + "Current Password": "Esamas slaptažodis", + "Custom": "Personalizuota", + "Customize models for a specific purpose": "", + "Dark": "Tamsus", + "Dashboard": "", + "Database": "Duomenų bazė", + "December": "Gruodis", + "Default": "Numatytasis", + "Default (Automatic1111)": "Numatytasis (Automatic1111)", + "Default (SentenceTransformers)": "Numatytasis (SentenceTransformers)", + "Default Model": "", + "Default model updated": "Numatytasis modelis atnaujintas", + "Default Prompt Suggestions": "Numatytieji užklausų pasiūlymai", + "Default User Role": "Numatytoji naudotojo rolė", + "delete": "ištrinti", + "Delete": "ištrinti", + "Delete a model": "Ištrinti modėlį", + "Delete All Chats": "", + "Delete chat": "Išrinti pokalbį", + "Delete Chat": "Ištrinti pokalbį", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "Ištrinti nuorodą", + "Delete tool?": "", + "Delete User": "Ištrinti naudotoją", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} ištrinta", + "Deleted {{name}}": "", + "Description": "Aprašymas", + "Didn't fully follow instructions": "Pilnai nesekė instrukcijų", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "Atrasti užklausas", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Atrasti ir parsisiųsti užklausas", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Atrasti ir parsisiųsti modelių konfigūracija", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Rodyti naudotojo vardą vietoje žodžio Jūs pokalbyje", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokumentas", + "Document Settings": "Dokumento nuostatos", + "Documentation": "", + "Documents": "Dokumentai", + "does not make any external connections, and your data stays securely on your locally hosted server.": "neturi jokių išorinių ryšių ir duomenys lieka serveryje.", + "Don't Allow": "Neleisti", + "Don't have an account?": "Neturite paskyros?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Nepatinka stilius", + "Done": "", + "Download": "Parsisiųsti", + "Download canceled": "Parsisiuntimas atšauktas", + "Download Database": "Parsisiųsti duomenų bazę", + "Drop any files here to add to the conversation": "Įkelkite dokumentus čia, kad juos pridėti į pokalbį", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "pvz. '30s', '10m'. Laiko vienetai yra 's', 'm', 'h'.", + "Edit": "Redaguoti", + "Edit Doc": "Redaguoti dokumentą", + "Edit Memory": "", + "Edit User": "Redaguoti naudotoją", + "ElevenLabs": "", + "Email": "El. paštas", + "Embedding Batch Size": "", + "Embedding Model": "Embedding modelis", + "Embedding Model Engine": "Embedding modelio variklis", + "Embedding model set to \"{{embedding_model}}\"": "Embedding modelis nustatytas kaip\"{{embedding_model}}\"", + "Enable Chat History": "Aktyvuoti pokalbių istoriją", + "Enable Community Sharing": "", + "Enable New Sign Ups": "Aktyvuoti naujas registracijas", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Įsitikinkite, kad CSV failas turi 4 kolonas šiuo eiliškumu: Name, Email, Password, Role.", + "Enter {{role}} message here": "Įveskite {{role}} žinutę čia", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "Įveskite blokų persidengimą", + "Enter Chunk Size": "Įveskite blokų dydį", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "Įveskite paveiksliuko dydį (pvz. 512x512)", + "Enter language codes": "Įveskite kalbos kodus", + "Enter model tag (e.g. {{modelTag}})": "Įveskite modelio žymą (pvz. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Įveskite žingsnių kiekį (pvz. 50)", + "Enter Score": "Įveskite rezultatą", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "Įveskite pabaigos sekvenciją", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Įveskite Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Įveskite nuorodą (pvz. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Įveskite nuorododą (pvz. http://localhost:11434", + "Enter Your Email": "Įveskite el. pašto adresą", + "Enter Your Full Name": "Įveskite vardą bei pavardę", + "Enter your message": "", + "Enter Your Password": "Įveskite slaptažodį", + "Enter Your Role": "Įveskite savo rolę", + "Error": "", + "Experimental": "Eksperimentinis", + "Export": "", + "Export All Chats (All Users)": "Eksportuoti visų naudotojų visus pokalbius", + "Export chat (.json)": "", + "Export Chats": "Eksportuoti pokalbius", + "Export Documents Mapping": "Eksportuoti dokumentų žemėlapį", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "Eksportuoti užklausas", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Nepavyko sukurti API rakto", + "Failed to read clipboard contents": "Nepavyko perskaityti kopijuoklės", + "Failed to update settings": "", + "February": "Vasaris", + "Feel free to add specific details": "Galite pridėti specifinių detalių", + "File": "", + "File Mode": "Dokumentų rėžimas", + "File not found.": "Failas nerastas.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Nepavyko nsutatyti profilio nuotraukos", + "Fluidly stream large external response chunks": "Sklandžiai transliuoti ilgus atsakymus", + "Focus chat input": "Fokusuoti žinutės įvestį", + "Followed instructions perfectly": "Tobulai sekė instrukcijas", + "Form": "", + "Format your variables using square brackets like this:": "Formatuokite kintamuosius su kvadratiniais skliausteliais:", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Bendri", + "General Settings": "Bendri nustatymai", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "Generavimo informacija", + "Get up and running with": "", + "Global": "", + "Good Response": "Geras atsakymas", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "neturi pokalbių", + "Hello, {{name}}": "Sveiki, {{name}}", + "Help": "Pagalba", + "Hide": "Paslėpti", + "Hide Model": "", + "How can I help you today?": "Kuo galėčiau Jums padėti ?", + "Hybrid Search": "Hibridinė paieška", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Vaizdų generavimas (eksperimentinis)", + "Image Generation Engine": "Vaizdų generavimo variklis", + "Image Settings": "Vaizdų nustatymai", + "Images": "Vaizdai", + "Import Chats": "Importuoti pokalbius", + "Import Documents Mapping": "Importuoti dokumentų žemėlapį", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "Importuoti užklausas", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Pridėti `--api` kai vykdomas stable-diffusion-webui", + "Info": "", + "Input commands": "Įvesties komandos", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Sąsaja", + "Invalid Tag": "Neteisinga žyma", + "January": "Sausis", + "join our Discord for help.": "prisijunkite prie mūsų Discord.", + "JSON": "JSON", + "JSON Preview": "", + "July": "liepa", + "June": "birželis", + "JWT Expiration": "JWT išėjimas iš galiojimo", + "JWT Token": "JWT žetonas", + "Keep Alive": "Išlaikyti aktyviu", + "Keyboard shortcuts": "Klaviatūros trumpiniai", + "Knowledge": "", + "Language": "Kalba", + "large language models, locally.": "", + "Last Active": "Paskutinį kartą aktyvus", + "Last Modified": "", + "Light": "Šviesus", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "Dideli kalbos modeliai gali klysti. Patikrinkite atsakymų teisingumą.", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "Sukurta OpenWebUI bendruomenės", + "Make sure to enclose them with": "Užtikrinktie, kad įtraukiate viduje:", + "Manage": "", + "Manage Models": "Tvarkyti modelius", + "Manage Ollama Models": "Tvarkyti Ollama modelius", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "Kovas", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Daugiausiai trys modeliai gali būti parsisiunčiami vienu metu.", + "May": "gegužė", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "Minimalus rezultatas", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' modelis sėkmingai atsisiųstas.", + "Model '{{modelTag}}' is already in queue for downloading.": "Modelis '{{modelTag}}' jau atsisiuntimų eilėje.", + "Model {{modelId}} not found": "Modelis {{modelId}} nerastas", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modelio failų sistemos kelias aptiktas. Reikalingas trumpas modelio pavadinimas atnaujinimui.", + "Model ID": "", + "Model not selected": "Modelis nepasirinktas", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "Modeliu baltasis sąrašas", + "Model(s) Whitelisted": "Modelis baltąjame sąraše", + "Modelfile Content": "Modelio failo turinys", + "Models": "Modeliai", + "More": "Daugiau", + "Name": "Pavadinimas", + "Name Tag": "Žymos pavadinimas", + "Name your model": "", + "New Chat": "Naujas pokalbis", + "New Password": "Naujas slaptažodis", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Rezultatų nerasta", + "No search query generated": "", + "No source available": "Šaltinių nerasta", + "No valves to update": "", + "None": "", + "Not factually correct": "Faktiškai netikslu", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Jei turite minimalų įvertį, paieška gražins tik tą informaciją, kuri viršyje šį įvertį", + "Notifications": "Pranešimai", + "November": "lapkritis", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "spalis", + "Off": "Išjungta", + "Okay, Let's Go!": "Gerai, važiuojam!", + "OLED Dark": "OLED tamsus", + "Ollama": "Ollama", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "Ollama versija", + "On": "Aktyvuota", + "Only": "Tiktais", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Leistinos tik raidės, skaičiai ir brūkšneliai.", + "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.": "Jūsų failai vis dar tvarkomi.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Looks like the URL is invalid. Please double-check and try again.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Naudojate nepalaikomą (front-end) web ui rėžimą. Prašau serviruokite WebUI iš back-end", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Atverti naują pokalbį", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "Open AI API nustatymai", + "OpenAI API Key is required.": "OpenAI API raktas būtinas.", + "OpenAI URL/Key required.": "OpenAI API nuoroda ir raktas būtini", + "or": "arba", + "Other": "Kita", + "Password": "Slaptažodis", + "PDF document (.pdf)": "PDF dokumentas (.pdf)", + "PDF Extract Images (OCR)": "PDF paveikslėlių skaitymas (OCR)", + "pending": "laukiama", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Leidimas naudoti mikrofoną atmestas: {{error}}", + "Personalization": "", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "Grynas tekstas (.txt)", + "Playground": "Eksperimentavimo erdvė", + "Please carefully review the following warnings:": "", + "Positive attitude": "Pozityvus elgesys", + "Previous 30 days": "Paskutinės 30 dienų", + "Previous 7 days": "Paskutinės 7 dienos", + "Profile Image": "Profilio nuotrauka", + "Prompt": "Užklausa", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Užklausa (pvz. supaprastink šį laišką)", + "Prompt Content": "Užklausos turinys", + "Prompt suggestions": "Užklausų pavyzdžiai", + "Prompts": "Užklausos", + "Pull \"{{searchValue}}\" from Ollama.com": "Rasti \"{{searchValue}}\" iš Ollama.com", + "Pull a model from Ollama.com": "Gauti modelį iš Ollama.com", + "Query Params": "Užklausos parametrai", + "RAG Template": "RAG šablonas", + "Read Aloud": "Skaityti garsiai", + "Record voice": "Įrašyti balsą", + "Redirecting you to OpenWebUI Community": "Perkeliam Jus į OpenWebUI bendruomenę", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Atmesta kai neturėtų būti atmesta", + "Regenerate": "Generuoti iš naujo", + "Release Notes": "Naujovės", + "Remove": "Pašalinti", + "Remove Model": "Pašalinti modelį", + "Rename": "Pervadinti", + "Repeat Last N": "Pakartoti paskutinius N", + "Request Mode": "Užklausos rėžimas", + "Reranking Model": "Reranking modelis", + "Reranking model disabled": "Reranking modelis neleidžiamas", + "Reranking model set to \"{{reranking_model}}\"": "Nustatytas rereanking modelis: \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Reinicializuoti vektorių atmintį", + "Response AutoCopy to Clipboard": "Automatiškai nukopijuoti atsakymą", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Rolė", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Išsaugoti", + "Save & Create": "Išsaugoti ir sukurti", + "Save & Update": "Išsaugoti ir atnaujinti", + "Save Tag": "", + "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": "Pokalbių saugojimas naršyklėje nebegalimas.", + "Scan": "Skenuoti", + "Scan complete!": "Skenavimas baigtas!", + "Scan for documents from {{path}}": "Skenuoti dokumentus iš {{path}}", + "Search": "Ieškoti", + "Search a model": "Ieškoti modelio", + "Search Chats": "", + "Search Documents": "Ieškoti dokumentų", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "Ieškoti užklausų", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_few": "", + "Searched {{count}} sites_many": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "Žiūrėti readme.md papildomoms instrukcijoms", + "See what's new": "Žiūrėti naujoves", + "Seed": "Sėkla", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Pasirinkti režimą", + "Select a model": "Pasirinkti modelį", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "Pasirinkti Ollama instanciją", + "Select Documents": "", + "Select model": "Pasirinkti modelį", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "Siųsti žinutę", + "Send message": "Siųsti žinutę", + "September": "rugsėjis", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "Serverio sujungimas patvirtintas", + "Set as default": "Nustatyti numatytąjį", + "Set Default Model": "Nustatyti numatytąjį modelį", + "Set embedding model (e.g. {{model}})": "Nustatyti embedding modelį", + "Set Image Size": "Nustatyti paveikslėlių dydį", + "Set reranking model (e.g. {{model}})": "Nustatyti reranking modelį", + "Set Steps": "Numatyti etapus", + "Set Task Model": "", + "Set Voice": "Numatyti balsą", + "Settings": "Nustatymai", + "Settings saved successfully!": "Parametrai sėkmingai išsaugoti!", + "Settings updated successfully": "", + "Share": "Dalintis", + "Share Chat": "Dalintis pokalbiu", + "Share to OpenWebUI Community": "Dalintis su OpenWebUI bendruomene", + "short-summary": "trumpinys", + "Show": "Rodyti", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Rodyti trumpinius", + "Show your support!": "", + "Showcased creativity": "Kūrybingų užklausų paroda", + "Sign in": "Prisijungti", + "Sign Out": "Atsijungti", + "Sign up": "Sukurti paskyrą", + "Signing in": "Prisijungiama", + "Source": "Šaltinis", + "Speech recognition error: {{error}}": "Balso atpažinimo problema: {{error}}", + "Speech-to-Text Engine": "Balso atpažinimo modelis", + "Stop Sequence": "Baigt sekvenciją", + "STT Model": "", + "STT Settings": "STT nustatymai", + "Submit": "Pateikti", + "Subtitle (e.g. about the Roman Empire)": "Subtitras", + "Success": "Sėkmingai", + "Successfully updated.": "Sėkmingai atnaujinta.", + "Suggested": "Siūloma", + "Support": "", + "Support this plugin:": "", + "System": "Sistema", + "System Prompt": "Sistemos užklausa", + "Tags": "Žymos", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Papasakokite daugiau", + "Temperature": "Temperatūra", + "Template": "Modelis", + "Text Completion": "Teksto pildymas", + "Text-to-Speech Engine": "Balso sintezės modelis", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Ačiū už atsiliepimus", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Rezultatas turėtų būti tarp 0.0 (0%) ir 1.0 (100%)", + "Theme": "Tema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Tai užtikrina, kad Jūsų pokalbiai saugiai saugojami duomenų bazėje. Ačiū!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Šis parametras nesisinchronizuoja su skirtingomis naršyklėmis ir įrankiais.", + "This will delete": "", + "Thorough explanation": "Platus paaiškinimas", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Jei norite pakeisti keletą kintamųjų vieną po kitos, spauskite Tab", + "Title": "Pavadinimas", + "Title (e.g. Tell me a fun fact)": "Pavadinimas", + "Title Auto-Generation": "Automatinis pavadinimų generavimas", + "Title cannot be an empty string.": "Pavadinimas negali būti tuščias", + "Title Generation Prompt": "Pavadinimo generavimo užklausa", + "to": "➡️", + "To access the available model names for downloading,": "Tam, kad prieiti prie galimų parsisiųsti modelių", + "To access the GGUF models available for downloading,": "Tam, kad prieiti prie galimų parsisiųsti GGUF,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "į pokalbio įvestį", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Šiandien", + "Toggle settings": "Atverti/užverti parametrus", + "Toggle sidebar": "Atverti/užverti šoninį meniu", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemos prieinant prie Ollama?", + "TTS Model": "", + "TTS Settings": "TTS parametrai", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "Įveskite Hugging Face Resolve nuorodą", + "Uh-oh! There was an issue connecting to {{provider}}.": "O ne! Prisijungiant prie {{provider}} kilo problema.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Atnaujinti ir kopijuoti nuorodą", + "Update password": "Atnaujinti slaptažodį", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Parsisiųsti GGUF modelį", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "Įkėlimo progresas", + "URL Mode": "URL režimas", + "Use '#' in the prompt input to load and select your documents.": "Naudokite '#' norėdami naudoti dokumentą.", + "Use Gravatar": "Naudoti Gravatar", + "Use Initials": "Naudotojo inicialai", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "naudotojas", + "User location successfully retrieved.": "", + "User Permissions": "Naudotojo leidimai", + "Users": "Naudotojai", + "Utilize": "Naudoti", + "Valid time units:": "Teisingūs laiko vienetai :", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "kintamasis", + "variable to have them replaced with clipboard content.": "kintamoji pakeičiama kopijuoklės turiniu.", + "Version": "Versija", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Jei pakeisite embedding modelį, turėsite reimportuoti visus dokumentus", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Web krovimo nustatymai", + "Web Params": "Web nustatymai", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "Webhook nuoroda", + "WebUI Settings": "WebUI parametrai", + "WebUI will make requests to": "WebUI vykdys užklausas", + "What’s New in": "Kas naujo", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kai istorija išjungta, pokalbiai neatsiras jūsų istorijoje.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "Parašykite užklausą", + "Write a summary in 50 words that summarizes [topic or keyword].": "Parašyk santrumpą trumpesnę nei 50 žodžių šiam tekstui: [tekstas]", + "Yesterday": "Vakar", + "You": "Jūs", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "Jūs neturite archyvuotų pokalbių", + "You have shared this chat": "Pasidalinote šiuo pokalbiu", + "You're a helpful assistant.": "Esi asistentas.", + "You're now logged in.": "Esate prisijungę.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube krovimo nustatymai" +} diff --git a/src/lib/i18n/locales/nb-NO/translation.json b/src/lib/i18n/locales/nb-NO/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..ede136e6c45e177f6627dd4130bd7710964e9af5 --- /dev/null +++ b/src/lib/i18n/locales/nb-NO/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 't', 'd', 'u' eller '-1' for ingen utløp.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(f.eks. `sh webui.sh --api`)", + "(latest)": "(siste)", + "{{ models }}": "{{ modeller }}", + "{{ owner }}: You cannot delete a base model": "{{ eier }}: Du kan ikke slette en grunnmodell", + "{{modelName}} is thinking...": "{{modelName}} tenker...", + "{{user}}'s Chats": "{{user}}'s chatter", + "{{webUIName}} Backend Required": "{{webUIName}} Backend kreves", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "En oppgavemodell brukes når du utfører oppgaver som å generere titler for chatter og websøkeforespørsler", + "a user": "en bruker", + "About": "Om", + "Account": "Konto", + "Account Activation Pending": "", + "Accurate information": "Nøyaktig informasjon", + "Actions": "", + "Active Users": "", + "Add": "Legg til", + "Add a model id": "Legg til en modell-ID", + "Add a short description about what this model does": "Legg til en kort beskrivelse av hva denne modellen gjør", + "Add a short title for this prompt": "Legg til en kort tittel for denne prompten", + "Add a tag": "Legg til en tag", + "Add custom prompt": "Legg til egendefinert prompt", + "Add Docs": "Legg til dokumenter", + "Add Files": "Legg til filer", + "Add Memory": "Legg til minne", + "Add message": "Legg til melding", + "Add Model": "Legg til modell", + "Add Tag": "", + "Add Tags": "Legg til tagger", + "Add User": "Legg til bruker", + "Adjusting these settings will apply changes universally to all users.": "Justering av disse innstillingene vil gjelde universelt for alle brukere.", + "admin": "administrator", + "Admin": "", + "Admin Panel": "Administrasjonspanel", + "Admin Settings": "Administrasjonsinnstillinger", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Avanserte parametere", + "Advanced Params": "Avanserte parametere", + "all": "alle", + "All Documents": "Alle dokumenter", + "All Users": "Alle brukere", + "Allow": "Tillat", + "Allow Chat Deletion": "Tillat sletting av chatter", + "Allow non-local voices": "Tillat ikke-lokale stemmer", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "alfanumeriske tegn og bindestreker", + "Already have an account?": "Har du allerede en konto?", + "an assistant": "en assistent", + "and": "og", + "and create a new shared link.": "og opprett en ny delt lenke.", + "API Base URL": "API Grunn-URL", + "API Key": "API-nøkkel", + "API Key created.": "API-nøkkel opprettet.", + "API keys": "API-nøkler", + "April": "April", + "Archive": "Arkiv", + "Archive All Chats": "Arkiver alle chatter", + "Archived Chats": "Arkiverte chatter", + "are allowed - Activate this command by typing": "er tillatt - Aktiver denne kommandoen ved å skrive", + "Are you sure?": "Er du sikker?", + "Attach file": "Legg ved fil", + "Attention to detail": "Oppmerksomhet på detaljer", + "Audio": "Lyd", + "Audio settings updated successfully": "", + "August": "August", + "Auto-playback response": "Automatisk avspilling av svar", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Grunn-URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Grunn-URL kreves.", + "available!": "tilgjengelig!", + "Back": "Tilbake", + "Bad Response": "Dårlig svar", + "Banners": "Bannere", + "Base Model (From)": "Grunnmodell (Fra)", + "Batch Size (num_batch)": "", + "before": "før", + "Being lazy": "Er lat", + "Brave Search API Key": "Brave Search API-nøkkel", + "Bypass SSL verification for Websites": "Omgå SSL-verifisering for nettsteder", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Avbryt", + "Capabilities": "Muligheter", + "Change Password": "Endre passord", + "Chat": "Chat", + "Chat Background Image": "", + "Chat Bubble UI": "Chat-boble UI", + "Chat Controls": "", + "Chat direction": "Chat-retning", + "Chat History": "Chat-historikk", + "Chat History is off for this browser.": "Chat-historikk er av for denne nettleseren.", + "Chats": "Chatter", + "Check Again": "Sjekk igjen", + "Check for updates": "Sjekk for oppdateringer", + "Checking for updates...": "Sjekker for oppdateringer...", + "Choose a model before saving...": "Velg en modell før du lagrer...", + "Chunk Overlap": "Chunk Overlap", + "Chunk Params": "Chunk-parametere", + "Chunk Size": "Chunk-størrelse", + "Citation": "Sitering", + "Clear memory": "", + "Click here for help.": "Klikk her for hjelp.", + "Click here to": "Klikk her for å", + "Click here to download user import template file.": "", + "Click here to select": "Klikk her for å velge", + "Click here to select a csv file.": "Klikk her for å velge en csv-fil.", + "Click here to select a py file.": "", + "Click here to select documents.": "Klikk her for å velge dokumenter.", + "click here.": "klikk her.", + "Click on the user role button to change a user's role.": "Klikk på brukerrolle-knappen for å endre en brukers rolle.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Klon", + "Close": "Lukk", + "Code formatted successfully": "", + "Collection": "Samling", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Grunn-URL", + "ComfyUI Base URL is required.": "ComfyUI Grunn-URL kreves.", + "Command": "Kommando", + "Concurrent Requests": "Samtidige forespørsler", + "Confirm": "", + "Confirm Password": "Bekreft passord", + "Confirm your action": "", + "Connections": "Tilkoblinger", + "Contact Admin for WebUI Access": "", + "Content": "Innhold", + "Content Extraction": "", + "Context Length": "Kontekstlengde", + "Continue Response": "Fortsett svar", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Kopiert delt chat-URL til utklippstavlen!", + "Copy": "Kopier", + "Copy last code block": "Kopier siste kodeblokk", + "Copy last response": "Kopier siste svar", + "Copy Link": "Kopier lenke", + "Copying to clipboard was successful!": "Kopiering til utklippstavlen var vellykket!", + "Create a model": "Lag en modell", + "Create Account": "Opprett konto", + "Create new key": "Lag ny nøkkel", + "Create new secret key": "Lag ny hemmelig nøkkel", + "Created at": "Opprettet", + "Created At": "Opprettet", + "Created by": "", + "CSV Import": "", + "Current Model": "Nåværende modell", + "Current Password": "Nåværende passord", + "Custom": "Tilpasset", + "Customize models for a specific purpose": "Tilpass modeller for et spesifikt formål", + "Dark": "Mørk", + "Dashboard": "", + "Database": "Database", + "December": "Desember", + "Default": "Standard", + "Default (Automatic1111)": "Standard (Automatic1111)", + "Default (SentenceTransformers)": "Standard (SentenceTransformers)", + "Default Model": "Standardmodell", + "Default model updated": "Standardmodell oppdatert", + "Default Prompt Suggestions": "Standard promptforslag", + "Default User Role": "Standard brukerrolle", + "delete": "slett", + "Delete": "Slett", + "Delete a model": "Slett en modell", + "Delete All Chats": "Slett alle chatter", + "Delete chat": "Slett chat", + "Delete Chat": "Slett chat", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "slett denne lenken", + "Delete tool?": "", + "Delete User": "Slett bruker", + "Deleted {{deleteModelTag}}": "Slettet {{deleteModelTag}}", + "Deleted {{name}}": "Slettet {{name}}", + "Description": "Beskrivelse", + "Didn't fully follow instructions": "Fulgte ikke instruksjonene fullt ut", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Oppdag en modell", + "Discover a prompt": "Oppdag en prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Oppdag, last ned og utforsk egendefinerte prompts", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Oppdag, last ned og utforsk modellforhåndsinnstillinger", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Vis brukernavnet i stedet for Du i chatten", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokument", + "Document Settings": "Dokumentinnstillinger", + "Documentation": "Dokumentasjon", + "Documents": "Dokumenter", + "does not make any external connections, and your data stays securely on your locally hosted server.": "lager ingen eksterne tilkoblinger, og dataene dine forblir trygt på din lokalt hostede server.", + "Don't Allow": "Ikke tillat", + "Don't have an account?": "Har du ikke en konto?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Liker ikke stilen", + "Done": "", + "Download": "Last ned", + "Download canceled": "Nedlasting avbrutt", + "Download Database": "Last ned database", + "Drop any files here to add to the conversation": "Slipp filer her for å legge dem til i samtalen", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "f.eks. '30s','10m'. Gyldige tidsenheter er 's', 'm', 't'.", + "Edit": "Rediger", + "Edit Doc": "Rediger dokument", + "Edit Memory": "", + "Edit User": "Rediger bruker", + "ElevenLabs": "", + "Email": "E-post", + "Embedding Batch Size": "Batch-størrelse for embedding", + "Embedding Model": "Embedding-modell", + "Embedding Model Engine": "Embedding-modellmotor", + "Embedding model set to \"{{embedding_model}}\"": "Embedding-modell satt til \"{{embedding_model}}\"", + "Enable Chat History": "Aktiver chat-historikk", + "Enable Community Sharing": "Aktiver deling i fellesskap", + "Enable New Sign Ups": "Aktiver nye registreringer", + "Enable Web Search": "Aktiver websøk", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Sørg for at CSV-filen din inkluderer 4 kolonner i denne rekkefølgen: Navn, E-post, Passord, Rolle.", + "Enter {{role}} message here": "Skriv inn {{role}} melding her", + "Enter a detail about yourself for your LLMs to recall": "Skriv inn en detalj om deg selv som LLM-ene dine kan huske", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Skriv inn Brave Search API-nøkkel", + "Enter Chunk Overlap": "Skriv inn Chunk Overlap", + "Enter Chunk Size": "Skriv inn Chunk-størrelse", + "Enter Github Raw URL": "Skriv inn Github Raw-URL", + "Enter Google PSE API Key": "Skriv inn Google PSE API-nøkkel", + "Enter Google PSE Engine Id": "Skriv inn Google PSE Motor-ID", + "Enter Image Size (e.g. 512x512)": "Skriv inn bildestørrelse (f.eks. 512x512)", + "Enter language codes": "Skriv inn språkkoder", + "Enter model tag (e.g. {{modelTag}})": "Skriv inn modelltag (f.eks. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Skriv inn antall steg (f.eks. 50)", + "Enter Score": "Skriv inn poengsum", + "Enter Searxng Query URL": "Skriv inn Searxng forespørsels-URL", + "Enter Serper API Key": "Skriv inn Serper API-nøkkel", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Skriv inn Serpstack API-nøkkel", + "Enter stop sequence": "Skriv inn stoppsekvens", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Skriv inn Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Skriv inn URL (f.eks. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Skriv inn URL (f.eks. http://localhost:11434)", + "Enter Your Email": "Skriv inn din e-post", + "Enter Your Full Name": "Skriv inn ditt fulle navn", + "Enter your message": "", + "Enter Your Password": "Skriv inn ditt passord", + "Enter Your Role": "Skriv inn din rolle", + "Error": "Feil", + "Experimental": "Eksperimentell", + "Export": "Eksporter", + "Export All Chats (All Users)": "Eksporter alle chatter (alle brukere)", + "Export chat (.json)": "Eksporter chat (.json)", + "Export Chats": "Eksporter chatter", + "Export Documents Mapping": "Eksporter dokumentkartlegging", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Eksporter modeller", + "Export Prompts": "Eksporter prompts", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Kunne ikke opprette API-nøkkel.", + "Failed to read clipboard contents": "Kunne ikke lese utklippstavleinnhold", + "Failed to update settings": "Kunne ikke oppdatere innstillinger", + "February": "Februar", + "Feel free to add specific details": "Føl deg fri til å legge til spesifikke detaljer", + "File": "", + "File Mode": "Filmodus", + "File not found.": "Fil ikke funnet.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingeravtrykk-spoofing oppdaget: Kan ikke bruke initialer som avatar. Bruker standard profilbilde.", + "Fluidly stream large external response chunks": "Strøm store eksterne svarchunks flytende", + "Focus chat input": "Fokuser chatinput", + "Followed instructions perfectly": "Fulgte instruksjonene perfekt", + "Form": "", + "Format your variables using square brackets like this:": "Formatér variablene dine med hakeparenteser som dette:", + "Frequency Penalty": "Frekvensstraff", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Generelt", + "General Settings": "Generelle innstillinger", + "Generate Image": "", + "Generating search query": "Genererer søkeforespørsel", + "Generation Info": "Generasjonsinfo", + "Get up and running with": "", + "Global": "", + "Good Response": "Godt svar", + "Google PSE API Key": "Google PSE API-nøkkel", + "Google PSE Engine Id": "Google PSE Motor-ID", + "h:mm a": "t:mm a", + "has no conversations.": "har ingen samtaler.", + "Hello, {{name}}": "Hei, {{name}}", + "Help": "Hjelp", + "Hide": "Skjul", + "Hide Model": "", + "How can I help you today?": "Hvordan kan jeg hjelpe deg i dag?", + "Hybrid Search": "Hybrid-søk", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Bildegenerering (Eksperimentell)", + "Image Generation Engine": "Bildegenereringsmotor", + "Image Settings": "Bildeinnstillinger", + "Images": "Bilder", + "Import Chats": "Importer chatter", + "Import Documents Mapping": "Importer dokumentkartlegging", + "Import Functions": "", + "Import Models": "Importer modeller", + "Import Prompts": "Importer prompts", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Inkluder `--api`-flagget når du kjører stable-diffusion-webui", + "Info": "Info", + "Input commands": "Inntast kommandoer", + "Install from Github URL": "Installer fra Github-URL", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Grensesnitt", + "Invalid Tag": "Ugyldig tag", + "January": "Januar", + "join our Discord for help.": "bli med i vår Discord for hjelp.", + "JSON": "JSON", + "JSON Preview": "JSON-forhåndsvisning", + "July": "Juli", + "June": "Juni", + "JWT Expiration": "JWT-utløp", + "JWT Token": "JWT-token", + "Keep Alive": "Hold i live", + "Keyboard shortcuts": "Hurtigtaster", + "Knowledge": "", + "Language": "Språk", + "large language models, locally.": "", + "Last Active": "Sist aktiv", + "Last Modified": "", + "Light": "Lys", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLM-er kan gjøre feil. Verifiser viktig informasjon.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Laget av OpenWebUI-fellesskapet", + "Make sure to enclose them with": "Sørg for å omslutte dem med", + "Manage": "", + "Manage Models": "Administrer modeller", + "Manage Ollama Models": "Administrer Ollama-modeller", + "Manage Pipelines": "Administrer pipelines", + "Manage Valves": "", + "March": "Mars", + "Max Tokens (num_predict)": "Maks antall tokens (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksimalt 3 modeller kan lastes ned samtidig. Vennligst prøv igjen senere.", + "May": "Mai", + "Memories accessible by LLMs will be shown here.": "Minner tilgjengelige for LLM-er vil vises her.", + "Memory": "Minne", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Meldinger du sender etter at du har opprettet lenken din vil ikke bli delt. Brukere med URL-en vil kunne se den delte chatten.", + "Minimum Score": "Minimum poengsum", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Modellen '{{modelName}}' er lastet ned.", + "Model '{{modelTag}}' is already in queue for downloading.": "Modellen '{{modelTag}}' er allerede i nedlastingskøen.", + "Model {{modelId}} not found": "Modellen {{modelId}} ble ikke funnet", + "Model {{modelName}} is not vision capable": "Modellen {{modelName}} er ikke visjonsdyktig", + "Model {{name}} is now {{status}}": "Modellen {{name}} er nå {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modellens filsystemsti oppdaget. Modellens kortnavn er påkrevd for oppdatering, kan ikke fortsette.", + "Model ID": "Modell-ID", + "Model not selected": "Modell ikke valgt", + "Model Params": "Modellparametere", + "Model updated successfully": "", + "Model Whitelisting": "Modell hvitlisting", + "Model(s) Whitelisted": "Modell(er) hvitlistet", + "Modelfile Content": "Modellfilinnhold", + "Models": "Modeller", + "More": "Mer", + "Name": "Navn", + "Name Tag": "Navnetag", + "Name your model": "Gi modellen din et navn", + "New Chat": "Ny chat", + "New Password": "Nytt passord", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Ingen resultater funnet", + "No search query generated": "Ingen søkeforespørsel generert", + "No source available": "Ingen kilde tilgjengelig", + "No valves to update": "", + "None": "Ingen", + "Not factually correct": "Ikke faktuelt korrekt", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Merk: Hvis du setter en minimums poengsum, vil søket kun returnere dokumenter med en poengsum som er større enn eller lik minimums poengsummen.", + "Notifications": "Varsler", + "November": "November", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Oktober", + "Off": "Av", + "Okay, Let's Go!": "Ok, la oss gå!", + "OLED Dark": "OLED mørk", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API deaktivert", + "Ollama API is disabled": "", + "Ollama Version": "Ollama versjon", + "On": "På", + "Only": "Kun", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Kun alfanumeriske tegn og bindestreker er tillatt i kommandostrengen.", + "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.": "Oops! Hold deg fast! Filene dine er fortsatt i prosesseringsovnen. Vi tilbereder dem til perfeksjon. Vennligst vær tålmodig, vi gir beskjed når de er klare.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Ser ut som URL-en er ugyldig. Vennligst dobbeltsjekk og prøv igjen.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Du bruker en ikke-støttet metode (kun frontend). Vennligst server WebUI fra backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Åpne ny chat", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API-konfigurasjon", + "OpenAI API Key is required.": "OpenAI API-nøkkel kreves.", + "OpenAI URL/Key required.": "OpenAI URL/nøkkel kreves.", + "or": "eller", + "Other": "Annet", + "Password": "Passord", + "PDF document (.pdf)": "PDF-dokument (.pdf)", + "PDF Extract Images (OCR)": "PDF-ekstraktbilder (OCR)", + "pending": "avventer", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Tillatelse nektet ved tilgang til mikrofon: {{error}}", + "Personalization": "Personalisering", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "", + "Pipelines Valves": "Pipeline-ventiler", + "Plain text (.txt)": "Ren tekst (.txt)", + "Playground": "Lekeplass", + "Please carefully review the following warnings:": "", + "Positive attitude": "Positiv holdning", + "Previous 30 days": "Forrige 30 dager", + "Previous 7 days": "Forrige 7 dager", + "Profile Image": "Profilbilde", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (f.eks. Fortell meg en morsom fakta om Romerriket)", + "Prompt Content": "Prompt-innhold", + "Prompt suggestions": "Promptforslag", + "Prompts": "Prompter", + "Pull \"{{searchValue}}\" from Ollama.com": "Trekk \"{{searchValue}}\" fra Ollama.com", + "Pull a model from Ollama.com": "Trekk en modell fra Ollama.com", + "Query Params": "Forespørselsparametere", + "RAG Template": "RAG-mal", + "Read Aloud": "Les høyt", + "Record voice": "Ta opp stemme", + "Redirecting you to OpenWebUI Community": "Omdirigerer deg til OpenWebUI-fellesskapet", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Avvist når det ikke skulle ha vært det", + "Regenerate": "Regenerer", + "Release Notes": "Utgivelsesnotater", + "Remove": "Fjern", + "Remove Model": "Fjern modell", + "Rename": "Gi nytt navn", + "Repeat Last N": "Gjenta siste N", + "Request Mode": "Forespørselsmodus", + "Reranking Model": "Reranking-modell", + "Reranking model disabled": "Reranking-modell deaktivert", + "Reranking model set to \"{{reranking_model}}\"": "Reranking-modell satt til \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Tilbakestill vektorlagring", + "Response AutoCopy to Clipboard": "Respons auto-kopi til utklippstavle", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Rolle", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Lagre", + "Save & Create": "Lagre og opprett", + "Save & Update": "Lagre og oppdater", + "Save Tag": "", + "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": "Lagring av chatlogger direkte til nettleserens lagring støttes ikke lenger. Vennligst ta et øyeblikk for å laste ned og slette chatloggene dine ved å klikke på knappen nedenfor. Ikke bekymre deg, du kan enkelt re-importere chatloggene dine til backend via", + "Scan": "Skann", + "Scan complete!": "Skanning fullført!", + "Scan for documents from {{path}}": "Skann etter dokumenter fra {{path}}", + "Search": "Søk", + "Search a model": "Søk en modell", + "Search Chats": "Søk chatter", + "Search Documents": "Søk dokumenter", + "Search Functions": "", + "Search Models": "Søk modeller", + "Search Prompts": "Søk prompter", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Antall søkeresultater", + "Search Tools": "", + "Searched {{count}} sites_one": "Søkte på {{count}} side", + "Searched {{count}} sites_other": "Søkte på {{count}} sider", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng forespørsels-URL", + "See readme.md for instructions": "Se readme.md for instruksjoner", + "See what's new": "Se hva som er nytt", + "Seed": "Seed", + "Select a base model": "Velg en grunnmodell", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Velg en modus", + "Select a model": "Velg en modell", + "Select a pipeline": "Velg en pipeline", + "Select a pipeline url": "Velg en pipeline-URL", + "Select a tool": "", + "Select an Ollama instance": "Velg en Ollama-instans", + "Select Documents": "", + "Select model": "Velg modell", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Valgte modell(er) støtter ikke bildeforslag", + "Send": "Send", + "Send a Message": "Send en melding", + "Send message": "Send melding", + "September": "September", + "Serper API Key": "Serper API-nøkkel", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API-nøkkel", + "Server connection verified": "Servertilkobling bekreftet", + "Set as default": "Sett som standard", + "Set Default Model": "Sett standardmodell", + "Set embedding model (e.g. {{model}})": "Sett embedding-modell (f.eks. {{model}})", + "Set Image Size": "Sett bildestørrelse", + "Set reranking model (e.g. {{model}})": "Sett reranking-modell (f.eks. {{model}})", + "Set Steps": "Sett steg", + "Set Task Model": "Sett oppgavemodell", + "Set Voice": "Sett stemme", + "Settings": "Innstillinger", + "Settings saved successfully!": "Innstillinger lagret!", + "Settings updated successfully": "Innstillinger oppdatert", + "Share": "Del", + "Share Chat": "Del chat", + "Share to OpenWebUI Community": "Del med OpenWebUI-fellesskapet", + "short-summary": "kort sammendrag", + "Show": "Vis", + "Show Admin Details in Account Pending Overlay": "Vis administratordetaljer i ventende kontooverlay", + "Show Model": "", + "Show shortcuts": "Vis snarveier", + "Show your support!": "", + "Showcased creativity": "Vist frem kreativitet", + "Sign in": "Logg inn", + "Sign Out": "Logg ut", + "Sign up": "Registrer deg", + "Signing in": "Logger inn", + "Source": "Kilde", + "Speech recognition error: {{error}}": "Feil ved talegjenkjenning: {{error}}", + "Speech-to-Text Engine": "Tale-til-tekst-motor", + "Stop Sequence": "Stoppsekvens", + "STT Model": "", + "STT Settings": "STT-innstillinger", + "Submit": "Send inn", + "Subtitle (e.g. about the Roman Empire)": "Undertittel (f.eks. om Romerriket)", + "Success": "Suksess", + "Successfully updated.": "Oppdatert.", + "Suggested": "Foreslått", + "Support": "", + "Support this plugin:": "", + "System": "System", + "System Prompt": "Systemprompt", + "Tags": "Tagger", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Fortell oss mer:", + "Temperature": "Temperatur", + "Template": "Mal", + "Text Completion": "Tekstfullføring", + "Text-to-Speech Engine": "Tekst-til-tale-motor", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Takk for tilbakemeldingen!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Poengsummen skal være en verdi mellom 0,0 (0%) og 1,0 (100%).", + "Theme": "Tema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dette sikrer at dine verdifulle samtaler er trygt lagret i backend-databasen din. Takk!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Denne innstillingen synkroniseres ikke mellom nettlesere eller enheter.", + "This will delete": "", + "Thorough explanation": "Grundig forklaring", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tips: Oppdater flere variabelplasser etter hverandre ved å trykke på tab-tasten i chatinputen etter hver erstatning.", + "Title": "Tittel", + "Title (e.g. Tell me a fun fact)": "Tittel (f.eks. Fortell meg en morsom fakta)", + "Title Auto-Generation": "Automatisk tittelgenerering", + "Title cannot be an empty string.": "Tittelen kan ikke være en tom streng.", + "Title Generation Prompt": "Tittelgenereringsprompt", + "to": "til", + "To access the available model names for downloading,": "For å få tilgang til tilgjengelige modelnavn for nedlasting,", + "To access the GGUF models available for downloading,": "For å få tilgang til GGUF-modellene som er tilgjengelige for nedlasting,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "til chatinput.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "I dag", + "Toggle settings": "Veksle innstillinger", + "Toggle sidebar": "Veksle sidefelt", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemer med tilgang til Ollama?", + "TTS Model": "", + "TTS Settings": "TTS-innstillinger", + "TTS Voice": "", + "Type": "Type", + "Type Hugging Face Resolve (Download) URL": "Skriv inn Hugging Face Resolve (nedlasting) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "Oops! Det oppsto et problem med tilkoblingen til {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Oppdater og kopier lenke", + "Update password": "Oppdater passord", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Last opp en GGUF-modell", + "Upload Files": "Last opp filer", + "Upload Pipeline": "", + "Upload Progress": "Opplastingsfremdrift", + "URL Mode": "URL-modus", + "Use '#' in the prompt input to load and select your documents.": "Bruk '#' i prompt-input for å laste og velge dokumentene dine.", + "Use Gravatar": "Bruk Gravatar", + "Use Initials": "Bruk initialer", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "bruker", + "User location successfully retrieved.": "", + "User Permissions": "Brukertillatelser", + "Users": "Brukere", + "Utilize": "Utnytt", + "Valid time units:": "Gyldige tidsenheter:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variabel", + "variable to have them replaced with clipboard content.": "variabel for å få dem erstattet med utklippstavleinnhold.", + "Version": "Versjon", + "Voice": "", + "Warning": "Advarsel", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Advarsel: Hvis du oppdaterer eller endrer embedding-modellen din, må du re-importere alle dokumenter.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Web-lasterinnstillinger", + "Web Params": "Web-parametere", + "Web Search": "Websøk", + "Web Search Engine": "Websøkemotor", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI innstillinger", + "WebUI will make requests to": "WebUI vil gjøre forespørsler til", + "What’s New in": "Hva er nytt i", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Når historikken er slått av, vil nye chatter på denne nettleseren ikke vises i historikken din på noen av enhetene dine.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Arbeidsområde", + "Write a prompt suggestion (e.g. Who are you?)": "Skriv et promptforslag (f.eks. Hvem er du?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Skriv et sammendrag på 50 ord som oppsummerer [emne eller nøkkelord].", + "Yesterday": "I går", + "You": "Du", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Du kan ikke klone en grunnmodell", + "You have no archived conversations.": "Du har ingen arkiverte samtaler.", + "You have shared this chat": "Du har delt denne chatten", + "You're a helpful assistant.": "Du er en hjelpsom assistent.", + "You're now logged in.": "Du er nå logget inn.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube-lasterinnstillinger" +} diff --git a/src/lib/i18n/locales/nl-NL/translation.json b/src/lib/i18n/locales/nl-NL/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..f5d169bdf469613a4ee7cb3f5de14b3781b3e111 --- /dev/null +++ b/src/lib/i18n/locales/nl-NL/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' of '-1' for geen vervaldatum.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", + "(latest)": "(nieuwste)", + "{{ models }}": "{{ modellen }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: U kunt een basismodel niet verwijderen", + "{{modelName}} is thinking...": "{{modelName}} is aan het denken...", + "{{user}}'s Chats": "{{user}}'s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Backend Verlpicht", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Een taakmodel wordt gebruikt bij het uitvoeren van taken zoals het genereren van titels voor chats en zoekopdrachten op internet", + "a user": "een gebruiker", + "About": "Over", + "Account": "Account", + "Account Activation Pending": "", + "Accurate information": "Accurate informatie", + "Actions": "", + "Active Users": "", + "Add": "Toevoegen", + "Add a model id": "Een model-id toevoegen", + "Add a short description about what this model does": "Voeg een korte beschrijving toe over wat dit model doet", + "Add a short title for this prompt": "Voeg een korte titel toe voor deze prompt", + "Add a tag": "Voeg een tag toe", + "Add custom prompt": "Voeg een aangepaste prompt toe", + "Add Docs": "Voeg Docs toe", + "Add Files": "Voege Bestanden toe", + "Add Memory": "Voeg Geheugen toe", + "Add message": "Voeg bericht toe", + "Add Model": "Voeg Model toe", + "Add Tag": "", + "Add Tags": "voeg tags toe", + "Add User": "Voeg Gebruiker toe", + "Adjusting these settings will apply changes universally to all users.": "Het aanpassen van deze instellingen zal universeel worden toegepast op alle gebruikers.", + "admin": "admin", + "Admin": "", + "Admin Panel": "Administratieve Paneel", + "Admin Settings": "Administratieve Settings", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Geavanceerde Parameters", + "Advanced Params": "Geavanceerde parameters", + "all": "alle", + "All Documents": "Alle Documenten", + "All Users": "Alle Gebruikers", + "Allow": "Toestaan", + "Allow Chat Deletion": "Sta Chat Verwijdering toe", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "alfanumerieke karakters en streepjes", + "Already have an account?": "Heb je al een account?", + "an assistant": "een assistent", + "and": "en", + "and create a new shared link.": "en maak een nieuwe gedeelde link.", + "API Base URL": "API Base URL", + "API Key": "API Key", + "API Key created.": "API Key gemaakt.", + "API keys": "API keys", + "April": "April", + "Archive": "Archief", + "Archive All Chats": "Archiveer alle chats", + "Archived Chats": "chatrecord", + "are allowed - Activate this command by typing": "zijn toegestaan - Activeer deze commando door te typen", + "Are you sure?": "Zeker weten?", + "Attach file": "Voeg een bestand toe", + "Attention to detail": "Attention to detail", + "Audio": "Audio", + "Audio settings updated successfully": "", + "August": "Augustus", + "Auto-playback response": "Automatisch afspelen van antwoord", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Base URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Basis URL is verplicht", + "available!": "beschikbaar!", + "Back": "Terug", + "Bad Response": "Ongeldig antwoord", + "Banners": "Banners", + "Base Model (From)": "Basismodel (vanaf)", + "Batch Size (num_batch)": "", + "before": "voor", + "Being lazy": "Lustig zijn", + "Brave Search API Key": "Brave Search API-sleutel", + "Bypass SSL verification for Websites": "SSL-verificatie omzeilen voor websites", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Annuleren", + "Capabilities": "Mogelijkheden", + "Change Password": "Wijzig Wachtwoord", + "Chat": "Chat", + "Chat Background Image": "", + "Chat Bubble UI": "Chat Bubble UI", + "Chat Controls": "", + "Chat direction": "Chat Richting", + "Chat History": "Chat Geschiedenis", + "Chat History is off for this browser.": "Chat Geschiedenis is uitgeschakeld voor deze browser.", + "Chats": "Chats", + "Check Again": "Controleer Opnieuw", + "Check for updates": "Controleer op updates", + "Checking for updates...": "Controleren op updates...", + "Choose a model before saving...": "Kies een model voordat je opslaat...", + "Chunk Overlap": "Chunk Overlap", + "Chunk Params": "Chunk Params", + "Chunk Size": "Chunk Grootte", + "Citation": "Citaat", + "Clear memory": "", + "Click here for help.": "Klik hier voor hulp.", + "Click here to": "Klik hier om", + "Click here to download user import template file.": "", + "Click here to select": "Klik hier om te selecteren", + "Click here to select a csv file.": "Klik hier om een csv file te selecteren.", + "Click here to select a py file.": "", + "Click here to select documents.": "Klik hier om documenten te selecteren", + "click here.": "klik hier.", + "Click on the user role button to change a user's role.": "Klik op de gebruikersrol knop om de rol van een gebruiker te wijzigen.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Kloon", + "Close": "Sluiten", + "Code formatted successfully": "", + "Collection": "Verzameling", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "ComfyUI Base URL is required.", + "Command": "Commando", + "Concurrent Requests": "Gelijktijdige verzoeken", + "Confirm": "", + "Confirm Password": "Bevestig Wachtwoord", + "Confirm your action": "", + "Connections": "Verbindingen", + "Contact Admin for WebUI Access": "", + "Content": "Inhoud", + "Content Extraction": "", + "Context Length": "Context Lengte", + "Continue Response": "Doorgaan met Antwoord", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL van gedeelde gesprekspagina gekopieerd naar klembord!", + "Copy": "Kopieer", + "Copy last code block": "Kopieer laatste code blok", + "Copy last response": "Kopieer laatste antwoord", + "Copy Link": "Kopieer Link", + "Copying to clipboard was successful!": "Kopiëren naar klembord was succesvol!", + "Create a model": "Een model maken", + "Create Account": "Maak Account", + "Create new key": "Maak nieuwe sleutel", + "Create new secret key": "Maak nieuwe geheim sleutel", + "Created at": "Gemaakt op", + "Created At": "Gemaakt op", + "Created by": "", + "CSV Import": "", + "Current Model": "Huidig Model", + "Current Password": "Huidig Wachtwoord", + "Custom": "Aangepast", + "Customize models for a specific purpose": "Modellen aanpassen voor een specifiek doel", + "Dark": "Donker", + "Dashboard": "", + "Database": "Database", + "December": "December", + "Default": "Standaard", + "Default (Automatic1111)": "Standaard (Automatic1111)", + "Default (SentenceTransformers)": "Standaard (SentenceTransformers)", + "Default Model": "Standaard model", + "Default model updated": "Standaard model bijgewerkt", + "Default Prompt Suggestions": "Standaard Prompt Suggesties", + "Default User Role": "Standaard Gebruikersrol", + "delete": "verwijderen", + "Delete": "Verwijderen", + "Delete a model": "Verwijder een model", + "Delete All Chats": "Verwijder alle chats", + "Delete chat": "Verwijder chat", + "Delete Chat": "Verwijder Chat", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "verwijder deze link", + "Delete tool?": "", + "Delete User": "Verwijder Gebruiker", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} is verwijderd", + "Deleted {{name}}": "{{name}} verwijderd", + "Description": "Beschrijving", + "Didn't fully follow instructions": "Ik heb niet alle instructies volgt", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Ontdek een model", + "Discover a prompt": "Ontdek een prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Ontdek, download en verken aangepaste prompts", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Ontdek, download en verken model presets", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Toon de gebruikersnaam in plaats van Jij in de Chat", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Document", + "Document Settings": "Document Instellingen", + "Documentation": "", + "Documents": "Documenten", + "does not make any external connections, and your data stays securely on your locally hosted server.": "maakt geen externe verbindingen, en je gegevens blijven veilig op je lokaal gehoste server.", + "Don't Allow": "Niet Toestaan", + "Don't have an account?": "Heb je geen account?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Je vindt het stijl niet?", + "Done": "", + "Download": "Download", + "Download canceled": "Download geannuleerd", + "Download Database": "Download Database", + "Drop any files here to add to the conversation": "Sleep bestanden hier om toe te voegen aan het gesprek", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "bijv. '30s', '10m'. Geldige tijdseenheden zijn 's', 'm', 'h'.", + "Edit": "Wijzig", + "Edit Doc": "Wijzig Doc", + "Edit Memory": "", + "Edit User": "Wijzig Gebruiker", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "", + "Embedding Model": "Embedding Model", + "Embedding Model Engine": "Embedding Model Engine", + "Embedding model set to \"{{embedding_model}}\"": "Embedding model ingesteld op \"{{embedding_model}}\"", + "Enable Chat History": "Schakel Chat Geschiedenis in", + "Enable Community Sharing": "Delen via de community inschakelen", + "Enable New Sign Ups": "Schakel Nieuwe Registraties in", + "Enable Web Search": "Zoeken op het web inschakelen", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Zorg ervoor dat uw CSV-bestand de volgende vier kolommen in deze volgorde bevat: Naam, E-mail, Wachtwoord, Rol.", + "Enter {{role}} message here": "Voeg {{role}} bericht hier toe", + "Enter a detail about yourself for your LLMs to recall": "Voer een detail over jezelf in voor je LLMs om het her te onthouden", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Voer de Brave Search API-sleutel in", + "Enter Chunk Overlap": "Voeg Chunk Overlap toe", + "Enter Chunk Size": "Voeg Chunk Size toe", + "Enter Github Raw URL": "Voer de Github Raw-URL in", + "Enter Google PSE API Key": "Voer de Google PSE API-sleutel in", + "Enter Google PSE Engine Id": "Voer Google PSE Engine-ID in", + "Enter Image Size (e.g. 512x512)": "Voeg afbeelding formaat toe (Bijv. 512x512)", + "Enter language codes": "Voeg taal codes toe", + "Enter model tag (e.g. {{modelTag}})": "Voeg model tag toe (Bijv. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Voeg aantal stappen toe (Bijv. 50)", + "Enter Score": "Voeg score toe", + "Enter Searxng Query URL": "Voer de URL van de Searxng-query in", + "Enter Serper API Key": "Voer de Serper API-sleutel in", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Voer de Serpstack API-sleutel in", + "Enter stop sequence": "Zet stop sequentie", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Voeg Top K toe", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Zet URL (Bijv. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Zet URL (Bijv. http://localhost:11434)", + "Enter Your Email": "Voer je Email in", + "Enter Your Full Name": "Voer je Volledige Naam in", + "Enter your message": "", + "Enter Your Password": "Voer je Wachtwoord in", + "Enter Your Role": "Voer je Rol in", + "Error": "Fout", + "Experimental": "Experimenteel", + "Export": "Exporteren", + "Export All Chats (All Users)": "Exporteer Alle Chats (Alle Gebruikers)", + "Export chat (.json)": "", + "Export Chats": "Exporteer Chats", + "Export Documents Mapping": "Exporteer Documenten Mapping", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Modellen exporteren", + "Export Prompts": "Exporteer Prompts", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Kan API Key niet aanmaken.", + "Failed to read clipboard contents": "Kan klembord inhoud niet lezen", + "Failed to update settings": "", + "February": "Februarij", + "Feel free to add specific details": "Voeg specifieke details toe", + "File": "", + "File Mode": "Bestandsmodus", + "File not found.": "Bestand niet gevonden.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Vingerafdruk spoofing gedetecteerd: kan initialen niet gebruiken als avatar. Standaardprofielafbeelding wordt gebruikt.", + "Fluidly stream large external response chunks": "Stream vloeiend grote externe responsbrokken", + "Focus chat input": "Focus chat input", + "Followed instructions perfectly": "Volgde instructies perfect", + "Form": "", + "Format your variables using square brackets like this:": "Formatteer je variabelen met vierkante haken zoals dit:", + "Frequency Penalty": "Frequentie Straf", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Algemeen", + "General Settings": "Algemene Instellingen", + "Generate Image": "", + "Generating search query": "Zoekopdracht genereren", + "Generation Info": "Generatie Info", + "Get up and running with": "", + "Global": "", + "Good Response": "Goede Antwoord", + "Google PSE API Key": "Google PSE API-sleutel", + "Google PSE Engine Id": "Google PSE-engine-ID", + "h:mm a": "h:mm a", + "has no conversations.": "heeft geen gesprekken.", + "Hello, {{name}}": "Hallo, {{name}}", + "Help": "Help", + "Hide": "Verberg", + "Hide Model": "", + "How can I help you today?": "Hoe kan ik je vandaag helpen?", + "Hybrid Search": "Hybride Zoeken", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Afbeelding Generatie (Experimenteel)", + "Image Generation Engine": "Afbeelding Generatie Engine", + "Image Settings": "Afbeelding Instellingen", + "Images": "Afbeeldingen", + "Import Chats": "Importeer Chats", + "Import Documents Mapping": "Importeer Documenten Mapping", + "Import Functions": "", + "Import Models": "Modellen importeren", + "Import Prompts": "Importeer Prompts", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Voeg `--api` vlag toe bij het uitvoeren van stable-diffusion-webui", + "Info": "Info", + "Input commands": "Voer commando's in", + "Install from Github URL": "Installeren vanaf Github-URL", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Interface", + "Invalid Tag": "Ongeldige Tag", + "January": "Januari", + "join our Discord for help.": "join onze Discord voor hulp.", + "JSON": "JSON", + "JSON Preview": "JSON-voorbeeld", + "July": "Juli", + "June": "Juni", + "JWT Expiration": "JWT Expiration", + "JWT Token": "JWT Token", + "Keep Alive": "Houd Actief", + "Keyboard shortcuts": "Toetsenbord snelkoppelingen", + "Knowledge": "", + "Language": "Taal", + "large language models, locally.": "", + "Last Active": "Laatst Actief", + "Last Modified": "", + "Light": "Licht", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMs kunnen fouten maken. Verifieer belangrijke informatie.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Gemaakt door OpenWebUI Community", + "Make sure to enclose them with": "Zorg ervoor dat je ze omringt met", + "Manage": "", + "Manage Models": "Beheer Modellen", + "Manage Ollama Models": "Beheer Ollama Modellen", + "Manage Pipelines": "Pijplijnen beheren", + "Manage Valves": "", + "March": "Maart", + "Max Tokens (num_predict)": "Max Tokens (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maximaal 3 modellen kunnen tegelijkertijd worden gedownload. Probeer het later opnieuw.", + "May": "Mei", + "Memories accessible by LLMs will be shown here.": "Geheugen toegankelijk voor LLMs wordt hier getoond.", + "Memory": "Geheugen", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Berichten die u verzendt nadat u uw link hebt gemaakt, worden niet gedeeld. Gebruikers met de URL kunnen de gedeelde chat bekijken.", + "Minimum Score": "Minimale Score", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' is succesvol gedownload.", + "Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' staat al in de wachtrij voor downloaden.", + "Model {{modelId}} not found": "Model {{modelId}} niet gevonden", + "Model {{modelName}} is not vision capable": "Model {{modelName}} is niet geschikt voor visie", + "Model {{name}} is now {{status}}": "Model {{name}} is nu {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model filesystem path gedetecteerd. Model shortname is vereist voor update, kan niet doorgaan.", + "Model ID": "Model-ID", + "Model not selected": "Model niet geselecteerd", + "Model Params": "Model Params", + "Model updated successfully": "", + "Model Whitelisting": "Model Whitelisting", + "Model(s) Whitelisted": "Model(len) zijn ge-whitelist", + "Modelfile Content": "Modelfile Inhoud", + "Models": "Modellen", + "More": "Meer", + "Name": "Naam", + "Name Tag": "Naam Tag", + "Name your model": "Geef uw model een naam", + "New Chat": "Nieuwe Chat", + "New Password": "Nieuw Wachtwoord", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Geen resultaten gevonden", + "No search query generated": "Geen zoekopdracht gegenereerd", + "No source available": "Geen bron beschikbaar", + "No valves to update": "", + "None": "Geen", + "Not factually correct": "Feitelijk niet juist", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Opmerking: Als u een minimumscore instelt, levert de zoekopdracht alleen documenten op met een score groter dan of gelijk aan de minimumscore.", + "Notifications": "Desktop Notificaties", + "November": "November", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Oktober", + "Off": "Uit", + "Okay, Let's Go!": "Okay, Laten we gaan!", + "OLED Dark": "OLED Donker", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API uitgeschakeld", + "Ollama API is disabled": "", + "Ollama Version": "Ollama Versie", + "On": "Aan", + "Only": "Alleen", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Alleen alfanumerieke karakters en streepjes zijn toegestaan in de commando string.", + "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.": "Oops! Houd vast! Je bestanden zijn nog steeds in de verwerkingsoven. We zijn ze aan het bereiden tot perfectie. Wees geduldig en we laten je weten wanneer ze klaar zijn.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Oops! Het lijkt erop dat de URL ongeldig is. Controleer het nogmaals en probeer opnieuw.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Oops! Je gebruikt een niet-ondersteunde methode (alleen frontend). Serveer de WebUI vanuit de backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Open nieuwe chat", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API Config", + "OpenAI API Key is required.": "OpenAI API Sleutel is verplicht", + "OpenAI URL/Key required.": "OpenAI URL/Sleutel vereist.", + "or": "of", + "Other": "Andere", + "Password": "Wachtwoord", + "PDF document (.pdf)": "PDF document (.pdf)", + "PDF Extract Images (OCR)": "PDF Extract Afbeeldingen (OCR)", + "pending": "wachtend", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Toestemming geweigerd bij toegang tot microfoon: {{error}}", + "Personalization": "Personalisatie", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Pijpleidingen", + "Pipelines Not Detected": "", + "Pipelines Valves": "Pijpleidingen Kleppen", + "Plain text (.txt)": "Platte tekst (.txt)", + "Playground": "Speeltuin", + "Please carefully review the following warnings:": "", + "Positive attitude": "Positieve positie", + "Previous 30 days": "Vorige 30 dagen", + "Previous 7 days": "Vorige 7 dagen", + "Profile Image": "Profielafbeelding", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (bijvoorbeeld: vertel me een leuke gebeurtenis over het Romeinse eeuw)", + "Prompt Content": "Prompt Inhoud", + "Prompt suggestions": "Prompt suggesties", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Haal \"{{searchValue}}\" uit Ollama.com", + "Pull a model from Ollama.com": "Haal een model van Ollama.com", + "Query Params": "Query Params", + "RAG Template": "RAG Template", + "Read Aloud": "Voorlezen", + "Record voice": "Neem stem op", + "Redirecting you to OpenWebUI Community": "Je wordt doorgestuurd naar OpenWebUI Community", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Geweigerd terwijl het niet had moeten", + "Regenerate": "Regenereren", + "Release Notes": "Release Notes", + "Remove": "Verwijderen", + "Remove Model": "Verwijder Model", + "Rename": "Hervatten", + "Repeat Last N": "Herhaal Laatste N", + "Request Mode": "Request Modus", + "Reranking Model": "Reranking Model", + "Reranking model disabled": "Reranking model uitgeschakeld", + "Reranking model set to \"{{reranking_model}}\"": "Reranking model ingesteld op \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Reset Vector Opslag", + "Response AutoCopy to Clipboard": "Antwoord Automatisch Kopiëren naar Klembord", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Rol", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Opslaan", + "Save & Create": "Opslaan & Creëren", + "Save & Update": "Opslaan & Bijwerken", + "Save Tag": "", + "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": "Chat logs direct opslaan in de opslag van je browser wordt niet langer ondersteund. Neem even de tijd om je chat logs te downloaden en te verwijderen door op de knop hieronder te klikken. Maak je geen zorgen, je kunt je chat logs eenvoudig opnieuw importeren naar de backend via", + "Scan": "Scan", + "Scan complete!": "Scan voltooid!", + "Scan for documents from {{path}}": "Scan voor documenten van {{path}}", + "Search": "Zoeken", + "Search a model": "Zoek een model", + "Search Chats": "Chats zoeken", + "Search Documents": "Zoek Documenten", + "Search Functions": "", + "Search Models": "Modellen zoeken", + "Search Prompts": "Zoek Prompts", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Aantal zoekresultaten", + "Search Tools": "", + "Searched {{count}} sites_one": "Gezocht op {{count}} sites_one", + "Searched {{count}} sites_other": "Gezocht op {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng Query URL", + "See readme.md for instructions": "Zie readme.md voor instructies", + "See what's new": "Zie wat er nieuw is", + "Seed": "Seed", + "Select a base model": "Selecteer een basismodel", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Selecteer een modus", + "Select a model": "Selecteer een model", + "Select a pipeline": "Selecteer een pijplijn", + "Select a pipeline url": "Selecteer een pijplijn-URL", + "Select a tool": "", + "Select an Ollama instance": "Selecteer een Ollama instantie", + "Select Documents": "", + "Select model": "Selecteer een model", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Geselecteerde modellen ondersteunen geen beeldinvoer", + "Send": "Verzenden", + "Send a Message": "Stuur een Bericht", + "Send message": "Stuur bericht", + "September": "September", + "Serper API Key": "Serper API-sleutel", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API-sleutel", + "Server connection verified": "Server verbinding geverifieerd", + "Set as default": "Stel in als standaard", + "Set Default Model": "Stel Standaard Model in", + "Set embedding model (e.g. {{model}})": "Stel embedding model in (bv. {{model}})", + "Set Image Size": "Stel Afbeelding Grootte in", + "Set reranking model (e.g. {{model}})": "Stel reranking model in (bv. {{model}})", + "Set Steps": "Stel Stappen in", + "Set Task Model": "Taakmodel instellen", + "Set Voice": "Stel Stem in", + "Settings": "Instellingen", + "Settings saved successfully!": "Instellingen succesvol opgeslagen!", + "Settings updated successfully": "", + "Share": "Deel Chat", + "Share Chat": "Deel Chat", + "Share to OpenWebUI Community": "Deel naar OpenWebUI Community", + "short-summary": "korte-samenvatting", + "Show": "Toon", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Toon snelkoppelingen", + "Show your support!": "", + "Showcased creativity": "Tooncase creativiteit", + "Sign in": "Inloggen", + "Sign Out": "Uitloggen", + "Sign up": "Registreren", + "Signing in": "Aanmelden", + "Source": "Bron", + "Speech recognition error: {{error}}": "Spraakherkenning fout: {{error}}", + "Speech-to-Text Engine": "Spraak-naar-tekst Engine", + "Stop Sequence": "Stop Sequentie", + "STT Model": "", + "STT Settings": "STT Instellingen", + "Submit": "Verzenden", + "Subtitle (e.g. about the Roman Empire)": "Ondertitel (bijv. over de Romeinse Empire)", + "Success": "Succes", + "Successfully updated.": "Succesvol bijgewerkt.", + "Suggested": "Suggestie", + "Support": "", + "Support this plugin:": "", + "System": "Systeem", + "System Prompt": "Systeem Prompt", + "Tags": "Tags", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Vertel ons meer:", + "Temperature": "Temperatuur", + "Template": "Template", + "Text Completion": "Tekst Aanvulling", + "Text-to-Speech Engine": "Tekst-naar-Spraak Engine", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Bedankt voor uw feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Het score moet een waarde zijn tussen 0.0 (0%) en 1.0 (100%).", + "Theme": "Thema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Dit zorgt ervoor dat je waardevolle gesprekken veilig worden opgeslagen in je backend database. Dank je wel!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Deze instelling wordt niet gesynchroniseerd tussen browsers of apparaten.", + "This will delete": "", + "Thorough explanation": "Gevorderde uitleg", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tip: Werk meerdere variabele slots achtereenvolgens bij door op de tab-toets te drukken in de chat input na elke vervanging.", + "Title": "Titel", + "Title (e.g. Tell me a fun fact)": "Titel (bv. Vertel me een leuke gebeurtenis)", + "Title Auto-Generation": "Titel Auto-Generatie", + "Title cannot be an empty string.": "Titel kan niet leeg zijn.", + "Title Generation Prompt": "Titel Generatie Prompt", + "to": "naar", + "To access the available model names for downloading,": "Om de beschikbare modelnamen voor downloaden te openen,", + "To access the GGUF models available for downloading,": "Om toegang te krijgen tot de GGUF modellen die beschikbaar zijn voor downloaden,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "naar chat input.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Vandaag", + "Toggle settings": "Wissel instellingen", + "Toggle sidebar": "Wissel sidebar", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemen met toegang tot Ollama?", + "TTS Model": "", + "TTS Settings": "TTS instellingen", + "TTS Voice": "", + "Type": "Type", + "Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Er was een probleem met verbinden met {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Update en Kopieer Link", + "Update password": "Wijzig wachtwoord", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Upload een GGUF model", + "Upload Files": "Bestanden uploaden", + "Upload Pipeline": "", + "Upload Progress": "Upload Voortgang", + "URL Mode": "URL Modus", + "Use '#' in the prompt input to load and select your documents.": "Gebruik '#' in de prompt input om je documenten te laden en te selecteren.", + "Use Gravatar": "Gebruik Gravatar", + "Use Initials": "Gebruik Initials", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "user", + "User location successfully retrieved.": "", + "User Permissions": "Gebruikers Rechten", + "Users": "Gebruikers", + "Utilize": "Utilize", + "Valid time units:": "Geldige tijdseenheden:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variabele", + "variable to have them replaced with clipboard content.": "variabele om ze te laten vervangen door klembord inhoud.", + "Version": "Versie", + "Voice": "", + "Warning": "Waarschuwing", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Warning: Als je de embedding model bijwerkt of wijzigt, moet je alle documenten opnieuw importeren.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Web Loader instellingen", + "Web Params": "Web Params", + "Web Search": "Zoeken op het web", + "Web Search Engine": "Zoekmachine op het web", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI Instellingen", + "WebUI will make requests to": "WebUI zal verzoeken doen naar", + "What’s New in": "Wat is nieuw in", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Wanneer geschiedenis is uitgeschakeld, zullen nieuwe chats op deze browser niet verschijnen in je geschiedenis op een van je apparaten.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Werkruimte", + "Write a prompt suggestion (e.g. Who are you?)": "Schrijf een prompt suggestie (bijv. Wie ben je?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Schrijf een samenvatting in 50 woorden die [onderwerp of trefwoord] samenvat.", + "Yesterday": "gisteren", + "You": "U", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "U kunt een basismodel niet klonen", + "You have no archived conversations.": "U heeft geen gearchiveerde gesprekken.", + "You have shared this chat": "U heeft dit gesprek gedeeld", + "You're a helpful assistant.": "Jij bent een behulpzame assistent.", + "You're now logged in.": "Je bent nu ingelogd.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube-laderinstellingen" +} diff --git a/src/lib/i18n/locales/pa-IN/translation.json b/src/lib/i18n/locales/pa-IN/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..21eff4890af92db061725f8d8d4647e6610962ae --- /dev/null +++ b/src/lib/i18n/locales/pa-IN/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'ਸ', 'ਮ', 'ਘੰ', 'ਦ', 'ਹਫ਼ਤਾ' ਜਾਂ '-1' ਬਿਨਾ ਮਿਆਦ ਦੇ।", + "(Beta)": "(ਬੀਟਾ)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(ਉਦਾਹਰਣ ਦੇ ਤੌਰ ਤੇ `sh webui.sh --api`)", + "(latest)": "(ਤਾਜ਼ਾ)", + "{{ models }}": "{{ ਮਾਡਲ }}", + "{{ owner }}: You cannot delete a base model": "{{ ਮਾਲਕ }}: ਤੁਸੀਂ ਬੇਸ ਮਾਡਲ ਨੂੰ ਮਿਟਾ ਨਹੀਂ ਸਕਦੇ", + "{{modelName}} is thinking...": "{{modelName}} ਸੋਚ ਰਿਹਾ ਹੈ...", + "{{user}}'s Chats": "{{user}} ਦੀਆਂ ਗੱਲਾਂ", + "{{webUIName}} Backend Required": "{{webUIName}} ਬੈਕਐਂਡ ਲੋੜੀਂਦਾ ਹੈ", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "ਚੈਟਾਂ ਅਤੇ ਵੈੱਬ ਖੋਜ ਪੁੱਛਗਿੱਛਾਂ ਵਾਸਤੇ ਸਿਰਲੇਖ ਤਿਆਰ ਕਰਨ ਵਰਗੇ ਕਾਰਜ ਾਂ ਨੂੰ ਕਰਦੇ ਸਮੇਂ ਇੱਕ ਕਾਰਜ ਮਾਡਲ ਦੀ ਵਰਤੋਂ ਕੀਤੀ ਜਾਂਦੀ ਹੈ", + "a user": "ਇੱਕ ਉਪਭੋਗਤਾ", + "About": "ਬਾਰੇ", + "Account": "ਖਾਤਾ", + "Account Activation Pending": "", + "Accurate information": "ਸਹੀ ਜਾਣਕਾਰੀ", + "Actions": "", + "Active Users": "", + "Add": "ਸ਼ਾਮਲ ਕਰੋ", + "Add a model id": "ਇੱਕ ਮਾਡਲ ID ਸ਼ਾਮਲ ਕਰੋ", + "Add a short description about what this model does": "ਇਸ ਬਾਰੇ ਇੱਕ ਸੰਖੇਪ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਕਿ ਇਹ ਮਾਡਲ ਕੀ ਕਰਦਾ ਹੈ", + "Add a short title for this prompt": "ਇਸ ਪ੍ਰੰਪਟ ਲਈ ਇੱਕ ਛੋਟਾ ਸਿਰਲੇਖ ਸ਼ਾਮਲ ਕਰੋ", + "Add a tag": "ਇੱਕ ਟੈਗ ਸ਼ਾਮਲ ਕਰੋ", + "Add custom prompt": "ਕਸਟਮ ਪ੍ਰੰਪਟ ਸ਼ਾਮਲ ਕਰੋ", + "Add Docs": "ਡਾਕੂਮੈਂਟ ਸ਼ਾਮਲ ਕਰੋ", + "Add Files": "ਫਾਈਲਾਂ ਸ਼ਾਮਲ ਕਰੋ", + "Add Memory": "ਮਿਹਾਨ ਸ਼ਾਮਲ ਕਰੋ", + "Add message": "ਸੁਨੇਹਾ ਸ਼ਾਮਲ ਕਰੋ", + "Add Model": "ਮਾਡਲ ਸ਼ਾਮਲ ਕਰੋ", + "Add Tag": "", + "Add Tags": "ਟੈਗ ਸ਼ਾਮਲ ਕਰੋ", + "Add User": "ਉਪਭੋਗਤਾ ਸ਼ਾਮਲ ਕਰੋ", + "Adjusting these settings will apply changes universally to all users.": "ਇਹ ਸੈਟਿੰਗਾਂ ਨੂੰ ਠੀਕ ਕਰਨ ਨਾਲ ਸਾਰੇ ਉਪਭੋਗਤਾਵਾਂ ਲਈ ਬਦਲਾਅ ਲਾਗੂ ਹੋਣਗੇ।", + "admin": "ਪ੍ਰਬੰਧਕ", + "Admin": "", + "Admin Panel": "ਪ੍ਰਬੰਧਕ ਪੈਨਲ", + "Admin Settings": "ਪ੍ਰਬੰਧਕ ਸੈਟਿੰਗਾਂ", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "ਉੱਚ ਸਤਰ ਦੇ ਪੈਰਾਮੀਟਰ", + "Advanced Params": "ਐਡਵਾਂਸਡ ਪਰਮਜ਼", + "all": "ਸਾਰੇ", + "All Documents": "ਸਾਰੇ ਡਾਕੂਮੈਂਟ", + "All Users": "ਸਾਰੇ ਉਪਭੋਗਤਾ", + "Allow": "ਅਨੁਮਤੀ", + "Allow Chat Deletion": "ਗੱਲਬਾਤ ਮਿਟਾਉਣ ਦੀ ਆਗਿਆ ਦਿਓ", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "ਅਲਫ਼ਾਨਯੂਮੈਰਿਕ ਅੱਖਰ ਅਤੇ ਹਾਈਫਨ", + "Already have an account?": "ਪਹਿਲਾਂ ਹੀ ਖਾਤਾ ਹੈ?", + "an assistant": "ਇੱਕ ਸਹਾਇਕ", + "and": "ਅਤੇ", + "and create a new shared link.": "ਅਤੇ ਇੱਕ ਨਵਾਂ ਸਾਂਝਾ ਲਿੰਕ ਬਣਾਓ।", + "API Base URL": "API ਬੇਸ URL", + "API Key": "API ਕੁੰਜੀ", + "API Key created.": "API ਕੁੰਜੀ ਬਣਾਈ ਗਈ।", + "API keys": "API ਕੁੰਜੀਆਂ", + "April": "ਅਪ੍ਰੈਲ", + "Archive": "ਆਰਕਾਈਵ", + "Archive All Chats": "ਸਾਰੀਆਂ ਚੈਟਾਂ ਨੂੰ ਆਰਕਾਈਵ ਕਰੋ", + "Archived Chats": "ਆਰਕਾਈਵ ਕੀਤੀਆਂ ਗੱਲਾਂ", + "are allowed - Activate this command by typing": "ਅਨੁਮਤ ਹਨ - ਇਸ ਕਮਾਂਡ ਨੂੰ ਟਾਈਪ ਕਰਕੇ ਸਰਗਰਮ ਕਰੋ", + "Are you sure?": "ਕੀ ਤੁਸੀਂ ਯਕੀਨਨ ਹੋ?", + "Attach file": "ਫਾਈਲ ਜੋੜੋ", + "Attention to detail": "ਵੇਰਵੇ 'ਤੇ ਧਿਆਨ", + "Audio": "ਆਡੀਓ", + "Audio settings updated successfully": "", + "August": "ਅਗਸਤ", + "Auto-playback response": "ਆਟੋ-ਪਲੇਬੈਕ ਜਵਾਬ", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 ਬੇਸ URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 ਬੇਸ URL ਦੀ ਲੋੜ ਹੈ।", + "available!": "ਉਪਲਬਧ ਹੈ!", + "Back": "ਵਾਪਸ", + "Bad Response": "ਖਰਾਬ ਜਵਾਬ", + "Banners": "ਬੈਨਰ", + "Base Model (From)": "ਬੇਸ ਮਾਡਲ (ਤੋਂ)", + "Batch Size (num_batch)": "", + "before": "ਪਹਿਲਾਂ", + "Being lazy": "ਆਲਸੀ ਹੋਣਾ", + "Brave Search API Key": "ਬਹਾਦਰ ਖੋਜ API ਕੁੰਜੀ", + "Bypass SSL verification for Websites": "ਵੈਬਸਾਈਟਾਂ ਲਈ SSL ਪ੍ਰਮਾਣਿਕਤਾ ਨੂੰ ਬਾਈਪਾਸ ਕਰੋ", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "ਰੱਦ ਕਰੋ", + "Capabilities": "ਸਮਰੱਥਾਵਾਂ", + "Change Password": "ਪਾਸਵਰਡ ਬਦਲੋ", + "Chat": "ਗੱਲਬਾਤ", + "Chat Background Image": "", + "Chat Bubble UI": "ਗੱਲਬਾਤ ਬਬਲ UI", + "Chat Controls": "", + "Chat direction": "ਗੱਲਬਾਤ ਡਿਰੈਕਟਨ", + "Chat History": "ਗੱਲਬਾਤ ਦਾ ਇਤਿਹਾਸ", + "Chat History is off for this browser.": "ਇਸ ਬ੍ਰਾਊਜ਼ਰ ਲਈ ਗੱਲਬਾਤ ਦਾ ਇਤਿਹਾਸ ਬੰਦ ਹੈ।", + "Chats": "ਗੱਲਾਂ", + "Check Again": "ਮੁੜ ਜਾਂਚ ਕਰੋ", + "Check for updates": "ਅੱਪਡੇਟ ਲਈ ਜਾਂਚ ਕਰੋ", + "Checking for updates...": "ਅੱਪਡੇਟ ਲਈ ਜਾਂਚ ਰਿਹਾ ਹੈ...", + "Choose a model before saving...": "ਸੰਭਾਲਣ ਤੋਂ ਪਹਿਲਾਂ ਇੱਕ ਮਾਡਲ ਚੁਣੋ...", + "Chunk Overlap": "ਚੰਕ ਓਵਰਲੈਪ", + "Chunk Params": "ਚੰਕ ਪੈਰਾਮੀਟਰ", + "Chunk Size": "ਚੰਕ ਆਕਾਰ", + "Citation": "ਹਵਾਲਾ", + "Clear memory": "", + "Click here for help.": "ਮਦਦ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ।", + "Click here to": "ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ", + "Click here to download user import template file.": "", + "Click here to select": "ਚੁਣਨ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ", + "Click here to select a csv file.": "CSV ਫਾਈਲ ਚੁਣਨ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ।", + "Click here to select a py file.": "", + "Click here to select documents.": "ਡਾਕੂਮੈਂਟ ਚੁਣਨ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ।", + "click here.": "ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ।", + "Click on the user role button to change a user's role.": "ਉਪਭੋਗਤਾ ਦੀ ਭੂਮਿਕਾ ਬਦਲਣ ਲਈ ਉਪਭੋਗਤਾ ਭੂਮਿਕਾ ਬਟਨ 'ਤੇ ਕਲਿੱਕ ਕਰੋ।", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "ਕਲੋਨ", + "Close": "ਬੰਦ ਕਰੋ", + "Code formatted successfully": "", + "Collection": "ਸੰਗ੍ਰਹਿ", + "ComfyUI": "ਕੰਫੀਯੂਆਈ", + "ComfyUI Base URL": "ਕੰਫੀਯੂਆਈ ਬੇਸ URL", + "ComfyUI Base URL is required.": "ਕੰਫੀਯੂਆਈ ਬੇਸ URL ਦੀ ਲੋੜ ਹੈ।", + "Command": "ਕਮਾਂਡ", + "Concurrent Requests": "ਸਮਕਾਲੀ ਬੇਨਤੀਆਂ", + "Confirm": "", + "Confirm Password": "ਪਾਸਵਰਡ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ", + "Confirm your action": "", + "Connections": "ਕਨੈਕਸ਼ਨ", + "Contact Admin for WebUI Access": "", + "Content": "ਸਮੱਗਰੀ", + "Content Extraction": "", + "Context Length": "ਸੰਦਰਭ ਲੰਬਾਈ", + "Continue Response": "ਜਵਾਬ ਜਾਰੀ ਰੱਖੋ", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "ਸਾਂਝੇ ਕੀਤੇ ਗੱਲਬਾਤ URL ਨੂੰ ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕਰ ਦਿੱਤਾ!", + "Copy": "ਕਾਪੀ ਕਰੋ", + "Copy last code block": "ਆਖਰੀ ਕੋਡ ਬਲਾਕ ਨੂੰ ਕਾਪੀ ਕਰੋ", + "Copy last response": "ਆਖਰੀ ਜਵਾਬ ਨੂੰ ਕਾਪੀ ਕਰੋ", + "Copy Link": "ਲਿੰਕ ਕਾਪੀ ਕਰੋ", + "Copying to clipboard was successful!": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕਰਨਾ ਸਫਲ ਰਿਹਾ!", + "Create a model": "ਇੱਕ ਮਾਡਲ ਬਣਾਓ", + "Create Account": "ਖਾਤਾ ਬਣਾਓ", + "Create new key": "ਨਵੀਂ ਕੁੰਜੀ ਬਣਾਓ", + "Create new secret key": "ਨਵੀਂ ਗੁਪਤ ਕੁੰਜੀ ਬਣਾਓ", + "Created at": "ਤੇ ਬਣਾਇਆ ਗਿਆ", + "Created At": "ਤੇ ਬਣਾਇਆ ਗਿਆ", + "Created by": "", + "CSV Import": "", + "Current Model": "ਮੌਜੂਦਾ ਮਾਡਲ", + "Current Password": "ਮੌਜੂਦਾ ਪਾਸਵਰਡ", + "Custom": "ਕਸਟਮ", + "Customize models for a specific purpose": "ਕਿਸੇ ਖਾਸ ਉਦੇਸ਼ ਲਈ ਮਾਡਲਾਂ ਨੂੰ ਅਨੁਕੂਲਿਤ ਕਰੋ", + "Dark": "ਗੂੜ੍ਹਾ", + "Dashboard": "", + "Database": "ਡਾਟਾਬੇਸ", + "December": "ਦਸੰਬਰ", + "Default": "ਮੂਲ", + "Default (Automatic1111)": "ਮੂਲ (Automatic1111)", + "Default (SentenceTransformers)": "ਮੂਲ (ਸੈਂਟੈਂਸਟ੍ਰਾਂਸਫਾਰਮਰਸ)", + "Default Model": "ਡਿਫਾਲਟ ਮਾਡਲ", + "Default model updated": "ਮੂਲ ਮਾਡਲ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ", + "Default Prompt Suggestions": "ਮੂਲ ਪ੍ਰੰਪਟ ਸੁਝਾਅ", + "Default User Role": "ਮੂਲ ਉਪਭੋਗਤਾ ਭੂਮਿਕਾ", + "delete": "ਮਿਟਾਓ", + "Delete": "ਮਿਟਾਓ", + "Delete a model": "ਇੱਕ ਮਾਡਲ ਮਿਟਾਓ", + "Delete All Chats": "ਸਾਰੀਆਂ ਚੈਟਾਂ ਨੂੰ ਮਿਟਾਓ", + "Delete chat": "ਗੱਲਬਾਤ ਮਿਟਾਓ", + "Delete Chat": "ਗੱਲਬਾਤ ਮਿਟਾਓ", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "ਇਸ ਲਿੰਕ ਨੂੰ ਮਿਟਾਓ", + "Delete tool?": "", + "Delete User": "ਉਪਭੋਗਤਾ ਮਿਟਾਓ", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} ਮਿਟਾਇਆ ਗਿਆ", + "Deleted {{name}}": "ਮਿਟਾ ਦਿੱਤਾ ਗਿਆ {{name}}", + "Description": "ਵਰਣਨਾ", + "Didn't fully follow instructions": "ਹਦਾਇਤਾਂ ਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਫਾਲੋ ਨਹੀਂ ਕੀਤਾ", + "Disabled": "", + "Discover a function": "", + "Discover a model": "ਇੱਕ ਮਾਡਲ ਲੱਭੋ", + "Discover a prompt": "ਇੱਕ ਪ੍ਰੰਪਟ ਖੋਜੋ", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "ਕਸਟਮ ਪ੍ਰੰਪਟਾਂ ਨੂੰ ਖੋਜੋ, ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਪੜਚੋਲ ਕਰੋ", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "ਮਾਡਲ ਪ੍ਰੀਸੈਟਾਂ ਨੂੰ ਖੋਜੋ, ਡਾਊਨਲੋਡ ਕਰੋ ਅਤੇ ਪੜਚੋਲ ਕਰੋ", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "ਗੱਲਬਾਤ 'ਚ ਤੁਹਾਡੇ ਸਥਾਨ 'ਤੇ ਉਪਭੋਗਤਾ ਨਾਮ ਦਿਖਾਓ", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "ਡਾਕੂਮੈਂਟ", + "Document Settings": "ਡਾਕੂਮੈਂਟ ਸੈਟਿੰਗਾਂ", + "Documentation": "", + "Documents": "ਡਾਕੂਮੈਂਟ", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ਕੋਈ ਬਾਹਰੀ ਕਨੈਕਸ਼ਨ ਨਹੀਂ ਬਣਾਉਂਦਾ, ਅਤੇ ਤੁਹਾਡਾ ਡਾਟਾ ਤੁਹਾਡੇ ਸਥਾਨਕ ਸਰਵਰ 'ਤੇ ਸੁਰੱਖਿਅਤ ਰਹਿੰਦਾ ਹੈ।", + "Don't Allow": "ਆਗਿਆ ਨਾ ਦਿਓ", + "Don't have an account?": "ਖਾਤਾ ਨਹੀਂ ਹੈ?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "ਸਟਾਈਲ ਪਸੰਦ ਨਹੀਂ ਹੈ", + "Done": "", + "Download": "ਡਾਊਨਲੋਡ", + "Download canceled": "ਡਾਊਨਲੋਡ ਰੱਦ ਕੀਤਾ ਗਿਆ", + "Download Database": "ਡਾਟਾਬੇਸ ਡਾਊਨਲੋਡ ਕਰੋ", + "Drop any files here to add to the conversation": "ਗੱਲਬਾਤ ਵਿੱਚ ਸ਼ਾਮਲ ਕਰਨ ਲਈ ਕੋਈ ਵੀ ਫਾਈਲ ਇੱਥੇ ਛੱਡੋ", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "ਉਦਾਹਰਣ ਲਈ '30ਸ','10ਮਿ'. ਸਹੀ ਸਮਾਂ ਇਕਾਈਆਂ ਹਨ 'ਸ', 'ਮ', 'ਘੰ'.", + "Edit": "ਸੰਪਾਦਨ ਕਰੋ", + "Edit Doc": "ਡਾਕੂਮੈਂਟ ਸੰਪਾਦਨ ਕਰੋ", + "Edit Memory": "", + "Edit User": "ਉਪਭੋਗਤਾ ਸੰਪਾਦਨ ਕਰੋ", + "ElevenLabs": "", + "Email": "ਈਮੇਲ", + "Embedding Batch Size": "", + "Embedding Model": "ਐਮਬੈੱਡਿੰਗ ਮਾਡਲ", + "Embedding Model Engine": "ਐਮਬੈੱਡਿੰਗ ਮਾਡਲ ਇੰਜਣ", + "Embedding model set to \"{{embedding_model}}\"": "ਐਮਬੈੱਡਿੰਗ ਮਾਡਲ ਨੂੰ \"{{embedding_model}}\" 'ਤੇ ਸੈੱਟ ਕੀਤਾ ਗਿਆ", + "Enable Chat History": "ਗੱਲਬਾਤ ਦਾ ਇਤਿਹਾਸ ਯੋਗ ਕਰੋ", + "Enable Community Sharing": "ਕਮਿਊਨਿਟੀ ਸ਼ੇਅਰਿੰਗ ਨੂੰ ਸਮਰੱਥ ਕਰੋ", + "Enable New Sign Ups": "ਨਵੇਂ ਸਾਈਨ ਅਪ ਯੋਗ ਕਰੋ", + "Enable Web Search": "ਵੈੱਬ ਖੋਜ ਨੂੰ ਸਮਰੱਥ ਕਰੋ", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "ਸੁਨਿਸ਼ਚਿਤ ਕਰੋ ਕਿ ਤੁਹਾਡੀ CSV ਫਾਈਲ ਵਿੱਚ ਇਸ ਕ੍ਰਮ ਵਿੱਚ 4 ਕਾਲਮ ਹਨ: ਨਾਮ, ਈਮੇਲ, ਪਾਸਵਰਡ, ਭੂਮਿਕਾ।", + "Enter {{role}} message here": "{{role}} ਸੁਨੇਹਾ ਇੱਥੇ ਦਰਜ ਕਰੋ", + "Enter a detail about yourself for your LLMs to recall": "ਤੁਹਾਡੇ LLMs ਨੂੰ ਸੁਨੇਹਾ ਕਰਨ ਲਈ ਸੁਨੇਹਾ ਇੱਥੇ ਦਰਜ ਕਰੋ", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "ਬਹਾਦਰ ਖੋਜ API ਕੁੰਜੀ ਦਾਖਲ ਕਰੋ", + "Enter Chunk Overlap": "ਚੰਕ ਓਵਰਲੈਪ ਦਰਜ ਕਰੋ", + "Enter Chunk Size": "ਚੰਕ ਆਕਾਰ ਦਰਜ ਕਰੋ", + "Enter Github Raw URL": "Github ਕੱਚਾ URL ਦਾਖਲ ਕਰੋ", + "Enter Google PSE API Key": "Google PSE API ਕੁੰਜੀ ਦਾਖਲ ਕਰੋ", + "Enter Google PSE Engine Id": "Google PSE ਇੰਜਣ ID ਦਾਖਲ ਕਰੋ", + "Enter Image Size (e.g. 512x512)": "ਚਿੱਤਰ ਆਕਾਰ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ 512x512)", + "Enter language codes": "ਭਾਸ਼ਾ ਕੋਡ ਦਰਜ ਕਰੋ", + "Enter model tag (e.g. {{modelTag}})": "ਮਾਡਲ ਟੈਗ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "ਕਦਮਾਂ ਦੀ ਗਿਣਤੀ ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ 50)", + "Enter Score": "ਸਕੋਰ ਦਰਜ ਕਰੋ", + "Enter Searxng Query URL": "Searxng Query URL ਦਾਖਲ ਕਰੋ", + "Enter Serper API Key": "Serper API ਕੁੰਜੀ ਦਾਖਲ ਕਰੋ", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Serpstack API ਕੁੰਜੀ ਦਾਖਲ ਕਰੋ", + "Enter stop sequence": "ਰੋਕਣ ਦਾ ਕ੍ਰਮ ਦਰਜ ਕਰੋ", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "ਸਿਖਰ K ਦਰਜ ਕਰੋ", + "Enter URL (e.g. http://127.0.0.1:7860/)": "URL ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "URL ਦਰਜ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ http://localhost:11434)", + "Enter Your Email": "ਆਪਣੀ ਈਮੇਲ ਦਰਜ ਕਰੋ", + "Enter Your Full Name": "ਆਪਣਾ ਪੂਰਾ ਨਾਮ ਦਰਜ ਕਰੋ", + "Enter your message": "", + "Enter Your Password": "ਆਪਣਾ ਪਾਸਵਰਡ ਦਰਜ ਕਰੋ", + "Enter Your Role": "ਆਪਣੀ ਭੂਮਿਕਾ ਦਰਜ ਕਰੋ", + "Error": "ਗਲਤੀ", + "Experimental": "ਪਰਮਾਣੂਕ੍ਰਿਤ", + "Export": "ਨਿਰਯਾਤ", + "Export All Chats (All Users)": "ਸਾਰੀਆਂ ਗੱਲਾਂ ਨਿਰਯਾਤ ਕਰੋ (ਸਾਰੇ ਉਪਭੋਗਤਾ)", + "Export chat (.json)": "", + "Export Chats": "ਗੱਲਾਂ ਨਿਰਯਾਤ ਕਰੋ", + "Export Documents Mapping": "ਡਾਕੂਮੈਂਟ ਮੈਪਿੰਗ ਨਿਰਯਾਤ ਕਰੋ", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "ਨਿਰਯਾਤ ਮਾਡਲ", + "Export Prompts": "ਪ੍ਰੰਪਟ ਨਿਰਯਾਤ ਕਰੋ", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "API ਕੁੰਜੀ ਬਣਾਉਣ ਵਿੱਚ ਅਸਫਲ।", + "Failed to read clipboard contents": "ਕਲਿੱਪਬੋਰਡ ਸਮੱਗਰੀ ਪੜ੍ਹਣ ਵਿੱਚ ਅਸਫਲ", + "Failed to update settings": "", + "February": "ਫਰਵਰੀ", + "Feel free to add specific details": "ਖੁੱਲ੍ਹੇ ਦਿਲ ਨਾਲ ਖਾਸ ਵੇਰਵੇ ਸ਼ਾਮਲ ਕਰੋ", + "File": "", + "File Mode": "ਫਾਈਲ ਮੋਡ", + "File not found.": "ਫਾਈਲ ਨਹੀਂ ਮਿਲੀ।", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ਫਿੰਗਰਪ੍ਰਿੰਟ ਸਪੂਫਿੰਗ ਪਾਈ ਗਈ: ਅਵਤਾਰ ਵਜੋਂ ਸ਼ੁਰੂਆਤੀ ਅੱਖਰ ਵਰਤਣ ਵਿੱਚ ਅਸਮਰੱਥ। ਮੂਲ ਪ੍ਰੋਫਾਈਲ ਚਿੱਤਰ 'ਤੇ ਡਿਫਾਲਟ।", + "Fluidly stream large external response chunks": "ਵੱਡੇ ਬਾਹਰੀ ਜਵਾਬ ਚੰਕਾਂ ਨੂੰ ਸਹੀ ਢੰਗ ਨਾਲ ਸਟ੍ਰੀਮ ਕਰੋ", + "Focus chat input": "ਗੱਲਬਾਤ ਇਨਪੁਟ 'ਤੇ ਧਿਆਨ ਦਿਓ", + "Followed instructions perfectly": "ਹਦਾਇਤਾਂ ਨੂੰ ਬਿਲਕੁਲ ਫਾਲੋ ਕੀਤਾ", + "Form": "", + "Format your variables using square brackets like this:": "ਤੁਹਾਡੀਆਂ ਵੈਰੀਏਬਲਾਂ ਨੂੰ ਇਸ ਤਰ੍ਹਾਂ ਵਰਤੋਂ: [ ]", + "Frequency Penalty": "ਬਾਰੰਬਾਰਤਾ ਜੁਰਮਾਨਾ", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "ਆਮ", + "General Settings": "ਆਮ ਸੈਟਿੰਗਾਂ", + "Generate Image": "", + "Generating search query": "ਖੋਜ ਪੁੱਛਗਿੱਛ ਤਿਆਰ ਕਰਨਾ", + "Generation Info": "ਜਨਰੇਸ਼ਨ ਜਾਣਕਾਰੀ", + "Get up and running with": "", + "Global": "", + "Good Response": "ਵਧੀਆ ਜਵਾਬ", + "Google PSE API Key": "Google PSE API ਕੁੰਜੀ", + "Google PSE Engine Id": "ਗੂਗਲ PSE ਇੰਜਣ ID", + "h:mm a": "ਹ:ਮਿੰਟ ਪੂਃ", + "has no conversations.": "ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ ਹੈ।", + "Hello, {{name}}": "ਸਤ ਸ੍ਰੀ ਅਕਾਲ, {{name}}", + "Help": "ਮਦਦ", + "Hide": "ਲੁਕਾਓ", + "Hide Model": "", + "How can I help you today?": "ਮੈਂ ਅੱਜ ਤੁਹਾਡੀ ਕਿਵੇਂ ਮਦਦ ਕਰ ਸਕਦਾ ਹਾਂ?", + "Hybrid Search": "ਹਾਈਬ੍ਰਿਡ ਖੋਜ", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "ਚਿੱਤਰ ਜਨਰੇਸ਼ਨ (ਪਰਮਾਣੂਕ੍ਰਿਤ)", + "Image Generation Engine": "ਚਿੱਤਰ ਜਨਰੇਸ਼ਨ ਇੰਜਣ", + "Image Settings": "ਚਿੱਤਰ ਸੈਟਿੰਗਾਂ", + "Images": "ਚਿੱਤਰ", + "Import Chats": "ਗੱਲਾਂ ਆਯਾਤ ਕਰੋ", + "Import Documents Mapping": "ਡਾਕੂਮੈਂਟ ਮੈਪਿੰਗ ਆਯਾਤ ਕਰੋ", + "Import Functions": "", + "Import Models": "ਮਾਡਲ ਆਯਾਤ ਕਰੋ", + "Import Prompts": "ਪ੍ਰੰਪਟ ਆਯਾਤ ਕਰੋ", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "ਸਟੇਬਲ-ਡਿਫਿਊਸ਼ਨ-ਵੈਬਯੂਆਈ ਚਲਾਉਣ ਸਮੇਂ `--api` ਝੰਡਾ ਸ਼ਾਮਲ ਕਰੋ", + "Info": "ਜਾਣਕਾਰੀ", + "Input commands": "ਇਨਪੁਟ ਕਮਾਂਡਾਂ", + "Install from Github URL": "Github URL ਤੋਂ ਇੰਸਟਾਲ ਕਰੋ", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "ਇੰਟਰਫੇਸ", + "Invalid Tag": "ਗਲਤ ਟੈਗ", + "January": "ਜਨਵਰੀ", + "join our Discord for help.": "ਮਦਦ ਲਈ ਸਾਡੇ ਡਿਸਕੋਰਡ ਵਿੱਚ ਸ਼ਾਮਲ ਹੋਵੋ।", + "JSON": "JSON", + "JSON Preview": "JSON ਪੂਰਵ-ਦਰਸ਼ਨ", + "July": "ਜੁਲਾਈ", + "June": "ਜੂਨ", + "JWT Expiration": "JWT ਮਿਆਦ ਖਤਮ", + "JWT Token": "JWT ਟੋਕਨ", + "Keep Alive": "ਜੀਵਿਤ ਰੱਖੋ", + "Keyboard shortcuts": "ਕੀਬੋਰਡ ਸ਼ਾਰਟਕਟ", + "Knowledge": "", + "Language": "ਭਾਸ਼ਾ", + "large language models, locally.": "", + "Last Active": "ਆਖਰੀ ਸਰਗਰਮ", + "Last Modified": "", + "Light": "ਹਲਕਾ", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMs ਗਲਤੀਆਂ ਕਰ ਸਕਦੇ ਹਨ। ਮਹੱਤਵਪੂਰਨ ਜਾਣਕਾਰੀ ਦੀ ਪੁਸ਼ਟੀ ਕਰੋ।", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "ਓਪਨਵੈਬਯੂਆਈ ਕਮਿਊਨਿਟੀ ਦੁਆਰਾ ਬਣਾਇਆ ਗਿਆ", + "Make sure to enclose them with": "ਸੁਨਿਸ਼ਚਿਤ ਕਰੋ ਕਿ ਉਨ੍ਹਾਂ ਨੂੰ ਘੇਰੋ", + "Manage": "", + "Manage Models": "ਮਾਡਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ", + "Manage Ollama Models": "ਓਲਾਮਾ ਮਾਡਲਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ", + "Manage Pipelines": "ਪਾਈਪਲਾਈਨਾਂ ਦਾ ਪ੍ਰਬੰਧਨ ਕਰੋ", + "Manage Valves": "", + "March": "ਮਾਰਚ", + "Max Tokens (num_predict)": "ਮੈਕਸ ਟੋਕਨ (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "ਇੱਕ ਸਮੇਂ ਵਿੱਚ ਵੱਧ ਤੋਂ ਵੱਧ 3 ਮਾਡਲ ਡਾਊਨਲੋਡ ਕੀਤੇ ਜਾ ਸਕਦੇ ਹਨ। ਕਿਰਪਾ ਕਰਕੇ ਬਾਅਦ ਵਿੱਚ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", + "May": "ਮਈ", + "Memories accessible by LLMs will be shown here.": "LLMs ਲਈ ਸਮਰੱਥ ਕਾਰਨ ਇੱਕ ਸੂਚਨਾ ਨੂੰ ਸ਼ਾਮਲ ਕੀਤਾ ਗਿਆ ਹੈ।", + "Memory": "ਮੀਮਰ", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "ਤੁਹਾਡਾ ਲਿੰਕ ਬਣਾਉਣ ਤੋਂ ਬਾਅਦ ਤੁਹਾਡੇ ਵੱਲੋਂ ਭੇਜੇ ਗਏ ਸੁਨੇਹੇ ਸਾਂਝੇ ਨਹੀਂ ਕੀਤੇ ਜਾਣਗੇ। URL ਵਾਲੇ ਉਪਭੋਗਤਾ ਸਾਂਝੀ ਚੈਟ ਨੂੰ ਵੇਖ ਸਕਣਗੇ।", + "Minimum Score": "ਘੱਟੋ-ਘੱਟ ਸਕੋਰ", + "Mirostat": "ਮਿਰੋਸਟੈਟ", + "Mirostat Eta": "ਮਿਰੋਸਟੈਟ ਈਟਾ", + "Mirostat Tau": "ਮਿਰੋਸਟੈਟ ਟਾਉ", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "ਮਾਡਲ '{{modelName}}' ਸਫਲਤਾਪੂਰਵਕ ਡਾਊਨਲੋਡ ਕੀਤਾ ਗਿਆ ਹੈ।", + "Model '{{modelTag}}' is already in queue for downloading.": "ਮਾਡਲ '{{modelTag}}' ਪਹਿਲਾਂ ਹੀ ਡਾਊਨਲੋਡ ਲਈ ਕਤਾਰ ਵਿੱਚ ਹੈ।", + "Model {{modelId}} not found": "ਮਾਡਲ {{modelId}} ਨਹੀਂ ਮਿਲਿਆ", + "Model {{modelName}} is not vision capable": "ਮਾਡਲ {{modelName}} ਦ੍ਰਿਸ਼ਟੀ ਸਮਰੱਥ ਨਹੀਂ ਹੈ", + "Model {{name}} is now {{status}}": "ਮਾਡਲ {{name}} ਹੁਣ {{status}} ਹੈ", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "ਮਾਡਲ ਫਾਈਲਸਿਸਟਮ ਪੱਥ ਪਾਇਆ ਗਿਆ। ਅੱਪਡੇਟ ਲਈ ਮਾਡਲ ਸ਼ੌਰਟਨੇਮ ਦੀ ਲੋੜ ਹੈ, ਜਾਰੀ ਨਹੀਂ ਰੱਖ ਸਕਦੇ।", + "Model ID": "ਮਾਡਲ ID", + "Model not selected": "ਮਾਡਲ ਚੁਣਿਆ ਨਹੀਂ ਗਿਆ", + "Model Params": "ਮਾਡਲ ਪਰਮਜ਼", + "Model updated successfully": "", + "Model Whitelisting": "ਮਾਡਲ ਵ੍ਹਾਈਟਲਿਸਟਿੰਗ", + "Model(s) Whitelisted": "ਮਾਡਲ(ਜ਼) ਵ੍ਹਾਈਟਲਿਸਟ ਕੀਤਾ ਗਿਆ", + "Modelfile Content": "ਮਾਡਲਫਾਈਲ ਸਮੱਗਰੀ", + "Models": "ਮਾਡਲ", + "More": "ਹੋਰ", + "Name": "ਨਾਮ", + "Name Tag": "ਨਾਮ ਟੈਗ", + "Name your model": "ਆਪਣੇ ਮਾਡਲ ਦਾ ਨਾਮ ਦੱਸੋ", + "New Chat": "ਨਵੀਂ ਗੱਲਬਾਤ", + "New Password": "ਨਵਾਂ ਪਾਸਵਰਡ", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "ਕੋਈ ਨਤੀਜੇ ਨਹੀਂ ਮਿਲੇ", + "No search query generated": "ਕੋਈ ਖੋਜ ਪੁੱਛਗਿੱਛ ਤਿਆਰ ਨਹੀਂ ਕੀਤੀ ਗਈ", + "No source available": "ਕੋਈ ਸਰੋਤ ਉਪਲਬਧ ਨਹੀਂ", + "No valves to update": "", + "None": "ਕੋਈ ਨਹੀਂ", + "Not factually correct": "ਤੱਥਕ ਰੂਪ ਵਿੱਚ ਸਹੀ ਨਹੀਂ", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "ਨੋਟ: ਜੇ ਤੁਸੀਂ ਘੱਟੋ-ਘੱਟ ਸਕੋਰ ਸੈੱਟ ਕਰਦੇ ਹੋ, ਤਾਂ ਖੋਜ ਸਿਰਫ਼ ਉਹੀ ਡਾਕੂਮੈਂਟ ਵਾਪਸ ਕਰੇਗੀ ਜਿਨ੍ਹਾਂ ਦਾ ਸਕੋਰ ਘੱਟੋ-ਘੱਟ ਸਕੋਰ ਦੇ ਬਰਾਬਰ ਜਾਂ ਵੱਧ ਹੋਵੇ।", + "Notifications": "ਸੂਚਨਾਵਾਂ", + "November": "ਨਵੰਬਰ", + "num_thread (Ollama)": "num_thread (ਓਲਾਮਾ)", + "OAuth ID": "", + "October": "ਅਕਤੂਬਰ", + "Off": "ਬੰਦ", + "Okay, Let's Go!": "ਠੀਕ ਹੈ, ਚੱਲੋ ਚੱਲੀਏ!", + "OLED Dark": "OLED ਗੂੜ੍ਹਾ", + "Ollama": "ਓਲਾਮਾ", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API ਅਸਮਰੱਥ", + "Ollama API is disabled": "", + "Ollama Version": "ਓਲਾਮਾ ਵਰਜਨ", + "On": "ਚਾਲੂ", + "Only": "ਸਿਰਫ਼", + "Only alphanumeric characters and hyphens are allowed in the command string.": "ਕਮਾਂਡ ਸਤਰ ਵਿੱਚ ਸਿਰਫ਼ ਅਲਫ਼ਾਨਯੂਮੈਰਿਕ ਅੱਖਰ ਅਤੇ ਹਾਈਫਨ ਦੀ ਆਗਿਆ ਹੈ।", + "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.": "ਓਹੋ! ਥੋੜਾ ਸਬਰ ਕਰੋ! ਤੁਹਾਡੀਆਂ ਫਾਈਲਾਂ ਅਜੇ ਵੀ ਪ੍ਰਕਿਰਿਆ ਵਿੱਚ ਹਨ। ਅਸੀਂ ਉਨ੍ਹਾਂ ਨੂੰ ਪੂਰੀ ਤਰ੍ਹਾਂ ਤਿਆਰ ਕਰ ਰਹੇ ਹਾਂ। ਕਿਰਪਾ ਕਰਕੇ ਧੀਰਜ ਰੱਖੋ ਅਤੇ ਅਸੀਂ ਤੁਹਾਨੂੰ ਦੱਸਾਂਗੇ ਜਦੋਂ ਉਹ ਤਿਆਰ ਹੋ ਜਾਣਗੇ।", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "ਓਹੋ! ਲੱਗਦਾ ਹੈ ਕਿ URL ਗਲਤ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਜਾਂਚ ਕਰੋ ਅਤੇ ਮੁੜ ਕੋਸ਼ਿਸ਼ ਕਰੋ।", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "ਓਹੋ! ਤੁਸੀਂ ਇੱਕ ਅਣਸਮਰਥਿਤ ਢੰਗ ਵਰਤ ਰਹੇ ਹੋ (ਸਿਰਫ਼ ਫਰੰਟਐਂਡ)। ਕਿਰਪਾ ਕਰਕੇ ਵੈਬਯੂਆਈ ਨੂੰ ਬੈਕਐਂਡ ਤੋਂ ਸਰਵ ਕਰੋ।", + "Open AI (Dall-E)": "ਓਪਨ ਏਆਈ (ਡਾਲ-ਈ)", + "Open new chat": "ਨਵੀਂ ਗੱਲਬਾਤ ਖੋਲ੍ਹੋ", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "ਓਪਨਏਆਈ", + "OpenAI API": "ਓਪਨਏਆਈ API", + "OpenAI API Config": "ਓਪਨਏਆਈ API ਕਨਫਿਗ", + "OpenAI API Key is required.": "ਓਪਨਏਆਈ API ਕੁੰਜੀ ਦੀ ਲੋੜ ਹੈ।", + "OpenAI URL/Key required.": "ਓਪਨਏਆਈ URL/ਕੁੰਜੀ ਦੀ ਲੋੜ ਹੈ।", + "or": "ਜਾਂ", + "Other": "ਹੋਰ", + "Password": "ਪਾਸਵਰਡ", + "PDF document (.pdf)": "PDF ਡਾਕੂਮੈਂਟ (.pdf)", + "PDF Extract Images (OCR)": "PDF ਚਿੱਤਰ ਕੱਢੋ (OCR)", + "pending": "ਬਕਾਇਆ", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "ਮਾਈਕ੍ਰੋਫ਼ੋਨ ਤੱਕ ਪਹੁੰਚਣ ਸਮੇਂ ਆਗਿਆ ਰੱਦ ਕੀਤੀ ਗਈ: {{error}}", + "Personalization": "ਪਰਸੋਨਲਿਸ਼ਮ", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "ਪਾਈਪਲਾਈਨਾਂ", + "Pipelines Not Detected": "", + "Pipelines Valves": "ਪਾਈਪਲਾਈਨਾਂ ਵਾਲਵ", + "Plain text (.txt)": "ਸਧਾਰਨ ਪਾਠ (.txt)", + "Playground": "ਖੇਡ ਦਾ ਮੈਦਾਨ", + "Please carefully review the following warnings:": "", + "Positive attitude": "ਸਕਾਰਾਤਮਕ ਰਵੱਈਆ", + "Previous 30 days": "ਪਿਛਲੇ 30 ਦਿਨ", + "Previous 7 days": "ਪਿਛਲੇ 7 ਦਿਨ", + "Profile Image": "ਪ੍ਰੋਫਾਈਲ ਚਿੱਤਰ", + "Prompt": "ਪ੍ਰੰਪਟ", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "ਪ੍ਰੰਪਟ (ਉਦਾਹਰਣ ਲਈ ਮੈਨੂੰ ਰੋਮਨ ਸਾਮਰਾਜ ਬਾਰੇ ਇੱਕ ਮਜ਼ੇਦਾਰ ਤੱਥ ਦੱਸੋ)", + "Prompt Content": "ਪ੍ਰੰਪਟ ਸਮੱਗਰੀ", + "Prompt suggestions": "ਪ੍ਰੰਪਟ ਸੁਝਾਅ", + "Prompts": "ਪ੍ਰੰਪਟ", + "Pull \"{{searchValue}}\" from Ollama.com": "ਓਲਾਮਾ.ਕਾਮ ਤੋਂ \"{{searchValue}}\" ਖਿੱਚੋ", + "Pull a model from Ollama.com": "ਓਲਾਮਾ.ਕਾਮ ਤੋਂ ਇੱਕ ਮਾਡਲ ਖਿੱਚੋ", + "Query Params": "ਪ੍ਰਸ਼ਨ ਪੈਰਾਮੀਟਰ", + "RAG Template": "RAG ਟੈਮਪਲੇਟ", + "Read Aloud": "ਜੋਰ ਨਾਲ ਪੜ੍ਹੋ", + "Record voice": "ਆਵਾਜ਼ ਰਿਕਾਰਡ ਕਰੋ", + "Redirecting you to OpenWebUI Community": "ਤੁਹਾਨੂੰ ਓਪਨਵੈਬਯੂਆਈ ਕਮਿਊਨਿਟੀ ਵੱਲ ਰੀਡਾਇਰੈਕਟ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "ਜਦੋਂ ਇਹ ਨਹੀਂ ਹੋਣਾ ਚਾਹੀਦਾ ਸੀ ਤਾਂ ਇਨਕਾਰ ਕੀਤਾ", + "Regenerate": "ਮੁੜ ਬਣਾਓ", + "Release Notes": "ਰਿਲੀਜ਼ ਨੋਟਸ", + "Remove": "ਹਟਾਓ", + "Remove Model": "ਮਾਡਲ ਹਟਾਓ", + "Rename": "ਨਾਮ ਬਦਲੋ", + "Repeat Last N": "ਆਖਰੀ N ਨੂੰ ਦੁਹਰਾਓ", + "Request Mode": "ਬੇਨਤੀ ਮੋਡ", + "Reranking Model": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ", + "Reranking model disabled": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ ਅਯੋਗ ਕੀਤਾ ਗਿਆ", + "Reranking model set to \"{{reranking_model}}\"": "ਮਾਡਲ ਮੁੜ ਰੈਂਕਿੰਗ ਨੂੰ \"{{reranking_model}}\" 'ਤੇ ਸੈੱਟ ਕੀਤਾ ਗਿਆ", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "ਵੈਕਟਰ ਸਟੋਰੇਜ ਨੂੰ ਰੀਸੈਟ ਕਰੋ", + "Response AutoCopy to Clipboard": "ਜਵਾਬ ਆਟੋ ਕਾਪੀ ਕਲਿੱਪਬੋਰਡ 'ਤੇ", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "ਭੂਮਿਕਾ", + "Rosé Pine": "ਰੋਜ਼ ਪਾਈਨ", + "Rosé Pine Dawn": "ਰੋਜ਼ ਪਾਈਨ ਡਾਨ", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "ਸੰਭਾਲੋ", + "Save & Create": "ਸੰਭਾਲੋ ਅਤੇ ਬਣਾਓ", + "Save & Update": "ਸੰਭਾਲੋ ਅਤੇ ਅੱਪਡੇਟ ਕਰੋ", + "Save Tag": "", + "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": "ਤੁਹਾਡੇ ਬ੍ਰਾਊਜ਼ਰ ਦੇ ਸਟੋਰੇਜ ਵਿੱਚ ਸਿੱਧੇ ਗੱਲਬਾਤ ਲੌਗ ਸੰਭਾਲਣਾ ਹੁਣ ਸਮਰਥਿਤ ਨਹੀਂ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਹੇਠਾਂ ਦਿੱਤੇ ਬਟਨ 'ਤੇ ਕਲਿੱਕ ਕਰਕੇ ਆਪਣੇ ਗੱਲਬਾਤ ਲੌਗ ਡਾਊਨਲੋਡ ਅਤੇ ਮਿਟਾਉਣ ਲਈ ਕੁਝ ਸਮਾਂ ਲਓ। ਚਿੰਤਾ ਨਾ ਕਰੋ, ਤੁਸੀਂ ਆਪਣੇ ਗੱਲਬਾਤ ਲੌਗ ਨੂੰ ਬੈਕਐਂਡ ਵਿੱਚ ਆਸਾਨੀ ਨਾਲ ਮੁੜ ਆਯਾਤ ਕਰ ਸਕਦੇ ਹੋ", + "Scan": "ਸਕੈਨ ਕਰੋ", + "Scan complete!": "ਸਕੈਨ ਪੂਰਾ!", + "Scan for documents from {{path}}": "{{path}} ਤੋਂ ਡਾਕੂਮੈਂਟਾਂ ਲਈ ਸਕੈਨ ਕਰੋ", + "Search": "ਖੋਜ", + "Search a model": "ਇੱਕ ਮਾਡਲ ਖੋਜੋ", + "Search Chats": "ਖੋਜ ਚੈਟਾਂ", + "Search Documents": "ਡਾਕੂਮੈਂਟ ਖੋਜੋ", + "Search Functions": "", + "Search Models": "ਖੋਜ ਮਾਡਲ", + "Search Prompts": "ਪ੍ਰੰਪਟ ਖੋਜੋ", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "ਖੋਜ ਨਤੀਜੇ ਦੀ ਗਿਣਤੀ", + "Search Tools": "", + "Searched {{count}} sites_one": "ਖੋਜਿਆ {{count}} sites_one", + "Searched {{count}} sites_other": "ਖੋਜਿਆ {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Searxng Query URL", + "See readme.md for instructions": "ਹਦਾਇਤਾਂ ਲਈ readme.md ਵੇਖੋ", + "See what's new": "ਨਵਾਂ ਕੀ ਹੈ ਵੇਖੋ", + "Seed": "ਬੀਜ", + "Select a base model": "ਆਧਾਰ ਮਾਡਲ ਚੁਣੋ", + "Select a engine": "", + "Select a function": "", + "Select a mode": "ਇੱਕ ਮੋਡ ਚੁਣੋ", + "Select a model": "ਇੱਕ ਮਾਡਲ ਚੁਣੋ", + "Select a pipeline": "ਪਾਈਪਲਾਈਨ ਚੁਣੋ", + "Select a pipeline url": "ਪਾਈਪਲਾਈਨ URL ਚੁਣੋ", + "Select a tool": "", + "Select an Ollama instance": "ਇੱਕ ਓਲਾਮਾ ਇੰਸਟੈਂਸ ਚੁਣੋ", + "Select Documents": "", + "Select model": "ਮਾਡਲ ਚੁਣੋ", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "ਚੁਣੇ ਗਏ ਮਾਡਲ(ਆਂ) ਚਿੱਤਰ ਇਨਪੁੱਟਾਂ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਕਰਦੇ", + "Send": "ਭੇਜੋ", + "Send a Message": "ਇੱਕ ਸੁਨੇਹਾ ਭੇਜੋ", + "Send message": "ਸੁਨੇਹਾ ਭੇਜੋ", + "September": "ਸਤੰਬਰ", + "Serper API Key": "Serper API ਕੁੰਜੀ", + "Serply API Key": "", + "Serpstack API Key": "Serpstack API ਕੁੰਜੀ", + "Server connection verified": "ਸਰਵਰ ਕਨੈਕਸ਼ਨ ਦੀ ਪੁਸ਼ਟੀ ਕੀਤੀ ਗਈ", + "Set as default": "ਮੂਲ ਵਜੋਂ ਸੈੱਟ ਕਰੋ", + "Set Default Model": "ਮੂਲ ਮਾਡਲ ਸੈੱਟ ਕਰੋ", + "Set embedding model (e.g. {{model}})": "ਐਮਬੈੱਡਿੰਗ ਮਾਡਲ ਸੈੱਟ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ {{model}})", + "Set Image Size": "ਚਿੱਤਰ ਆਕਾਰ ਸੈੱਟ ਕਰੋ", + "Set reranking model (e.g. {{model}})": "ਮੁੜ ਰੈਂਕਿੰਗ ਮਾਡਲ ਸੈੱਟ ਕਰੋ (ਉਦਾਹਰਣ ਲਈ {{model}})", + "Set Steps": "ਕਦਮ ਸੈੱਟ ਕਰੋ", + "Set Task Model": "ਟਾਸਕ ਮਾਡਲ ਸੈੱਟ ਕਰੋ", + "Set Voice": "ਆਵਾਜ਼ ਸੈੱਟ ਕਰੋ", + "Settings": "ਸੈਟਿੰਗਾਂ", + "Settings saved successfully!": "ਸੈਟਿੰਗਾਂ ਸਫਲਤਾਪੂਰਵਕ ਸੰਭਾਲੀਆਂ ਗਈਆਂ!", + "Settings updated successfully": "", + "Share": "ਸਾਂਝਾ ਕਰੋ", + "Share Chat": "ਗੱਲਬਾਤ ਸਾਂਝੀ ਕਰੋ", + "Share to OpenWebUI Community": "ਓਪਨਵੈਬਯੂਆਈ ਕਮਿਊਨਿਟੀ ਨਾਲ ਸਾਂਝਾ ਕਰੋ", + "short-summary": "ਛੋਟੀ-ਸੰਖੇਪ", + "Show": "ਦਿਖਾਓ", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "ਸ਼ਾਰਟਕਟ ਦਿਖਾਓ", + "Show your support!": "", + "Showcased creativity": "ਸਿਰਜਣਾਤਮਕਤਾ ਦਿਖਾਈ", + "Sign in": "ਸਾਈਨ ਇਨ ਕਰੋ", + "Sign Out": "ਸਾਈਨ ਆਊਟ ਕਰੋ", + "Sign up": "ਰਜਿਸਟਰ ਕਰੋ", + "Signing in": "ਸਾਈਨ ਇਨ ਕਰ ਰਿਹਾ ਹੈ", + "Source": "ਸਰੋਤ", + "Speech recognition error: {{error}}": "ਬੋਲ ਪਛਾਣ ਗਲਤੀ: {{error}}", + "Speech-to-Text Engine": "ਬੋਲ-ਤੋਂ-ਪਾਠ ਇੰਜਣ", + "Stop Sequence": "ਰੋਕੋ ਕ੍ਰਮ", + "STT Model": "", + "STT Settings": "STT ਸੈਟਿੰਗਾਂ", + "Submit": "ਜਮ੍ਹਾਂ ਕਰੋ", + "Subtitle (e.g. about the Roman Empire)": "ਉਪਸਿਰਲੇਖ (ਉਦਾਹਰਣ ਲਈ ਰੋਮਨ ਸਾਮਰਾਜ ਬਾਰੇ)", + "Success": "ਸਫਲਤਾ", + "Successfully updated.": "ਸਫਲਤਾਪੂਰਵਕ ਅੱਪਡੇਟ ਕੀਤਾ ਗਿਆ।", + "Suggested": "ਸੁਝਾਇਆ ਗਿਆ", + "Support": "", + "Support this plugin:": "", + "System": "ਸਿਸਟਮ", + "System Prompt": "ਸਿਸਟਮ ਪ੍ਰੰਪਟ", + "Tags": "ਟੈਗ", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "ਸਾਨੂੰ ਹੋਰ ਦੱਸੋ:", + "Temperature": "ਤਾਪਮਾਨ", + "Template": "ਟੈਮਪਲੇਟ", + "Text Completion": "ਪਾਠ ਪੂਰਨਤਾ", + "Text-to-Speech Engine": "ਪਾਠ-ਤੋਂ-ਬੋਲ ਇੰਜਣ", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "ਤੁਹਾਡੇ ਫੀਡਬੈਕ ਲਈ ਧੰਨਵਾਦ!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "ਸਕੋਰ 0.0 (0%) ਅਤੇ 1.0 (100%) ਦੇ ਵਿਚਕਾਰ ਹੋਣਾ ਚਾਹੀਦਾ ਹੈ।", + "Theme": "ਥੀਮ", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "ਇਹ ਯਕੀਨੀ ਬਣਾਉਂਦਾ ਹੈ ਕਿ ਤੁਹਾਡੀਆਂ ਕੀਮਤੀ ਗੱਲਾਂ ਤੁਹਾਡੇ ਬੈਕਐਂਡ ਡਾਟਾਬੇਸ ਵਿੱਚ ਸੁਰੱਖਿਅਤ ਤੌਰ 'ਤੇ ਸੰਭਾਲੀਆਂ ਗਈਆਂ ਹਨ। ਧੰਨਵਾਦ!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "ਇਹ ਸੈਟਿੰਗ ਬ੍ਰਾਊਜ਼ਰ ਜਾਂ ਡਿਵਾਈਸਾਂ ਵਿੱਚ ਸਿੰਕ ਨਹੀਂ ਹੁੰਦੀ।", + "This will delete": "", + "Thorough explanation": "ਵਿਸਥਾਰ ਨਾਲ ਵਿਆਖਿਆ", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "ਸਲਾਹ: ਹਰ ਬਦਲਾਅ ਦੇ ਬਾਅਦ ਗੱਲਬਾਤ ਇਨਪੁਟ ਵਿੱਚ ਟੈਬ ਕੀ ਦਬਾ ਕੇ ਲਗਾਤਾਰ ਕਈ ਵੈਰੀਏਬਲ ਸਲਾਟਾਂ ਨੂੰ ਅੱਪਡੇਟ ਕਰੋ।", + "Title": "ਸਿਰਲੇਖ", + "Title (e.g. Tell me a fun fact)": "ਸਿਰਲੇਖ (ਉਦਾਹਰਣ ਲਈ ਮੈਨੂੰ ਇੱਕ ਮਜ਼ੇਦਾਰ ਤੱਥ ਦੱਸੋ)", + "Title Auto-Generation": "ਸਿਰਲੇਖ ਆਟੋ-ਜਨਰੇਸ਼ਨ", + "Title cannot be an empty string.": "ਸਿਰਲੇਖ ਖਾਲੀ ਸਤਰ ਨਹੀਂ ਹੋ ਸਕਦਾ।", + "Title Generation Prompt": "ਸਿਰਲੇਖ ਜਨਰੇਸ਼ਨ ਪ੍ਰੰਪਟ", + "to": "ਨੂੰ", + "To access the available model names for downloading,": "ਡਾਊਨਲੋਡ ਕਰਨ ਲਈ ਉਪਲਬਧ ਮਾਡਲ ਨਾਮਾਂ ਤੱਕ ਪਹੁੰਚਣ ਲਈ,", + "To access the GGUF models available for downloading,": "ਡਾਊਨਲੋਡ ਕਰਨ ਲਈ ਉਪਲਬਧ GGUF ਮਾਡਲਾਂ ਤੱਕ ਪਹੁੰਚਣ ਲਈ,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "ਗੱਲਬਾਤ ਇਨਪੁਟ ਲਈ।", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "ਅੱਜ", + "Toggle settings": "ਸੈਟਿੰਗਾਂ ਟੌਗਲ ਕਰੋ", + "Toggle sidebar": "ਸਾਈਡਬਾਰ ਟੌਗਲ ਕਰੋ", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "ਸਿਖਰ K", + "Top P": "ਸਿਖਰ P", + "Trouble accessing Ollama?": "ਓਲਾਮਾ ਤੱਕ ਪਹੁੰਚਣ ਵਿੱਚ ਮੁਸ਼ਕਲ?", + "TTS Model": "", + "TTS Settings": "TTS ਸੈਟਿੰਗਾਂ", + "TTS Voice": "", + "Type": "ਕਿਸਮ", + "Type Hugging Face Resolve (Download) URL": "Hugging Face Resolve (ਡਾਊਨਲੋਡ) URL ਟਾਈਪ ਕਰੋ", + "Uh-oh! There was an issue connecting to {{provider}}.": "ਓਹੋ! {{provider}} ਨਾਲ ਕਨੈਕਟ ਕਰਨ ਵਿੱਚ ਸਮੱਸਿਆ ਆਈ।", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "ਅੱਪਡੇਟ ਕਰੋ ਅਤੇ ਲਿੰਕ ਕਾਪੀ ਕਰੋ", + "Update password": "ਪਾਸਵਰਡ ਅੱਪਡੇਟ ਕਰੋ", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "ਇੱਕ GGUF ਮਾਡਲ ਅਪਲੋਡ ਕਰੋ", + "Upload Files": "ਫਾਇਲਾਂ ਅੱਪਲੋਡ ਕਰੋ", + "Upload Pipeline": "", + "Upload Progress": "ਅਪਲੋਡ ਪ੍ਰਗਤੀ", + "URL Mode": "URL ਮੋਡ", + "Use '#' in the prompt input to load and select your documents.": "ਆਪਣੇ ਡਾਕੂਮੈਂਟ ਲੋਡ ਅਤੇ ਚੁਣਨ ਲਈ ਪ੍ਰੰਪਟ ਇਨਪੁਟ ਵਿੱਚ '#' ਵਰਤੋ।", + "Use Gravatar": "ਗ੍ਰਾਵਾਟਾਰ ਵਰਤੋ", + "Use Initials": "ਸ਼ੁਰੂਆਤੀ ਅੱਖਰ ਵਰਤੋ", + "use_mlock (Ollama)": "use_mlock (ਓਲਾਮਾ)", + "use_mmap (Ollama)": "use_mmap (ਓਲਾਮਾ)", + "user": "ਉਪਭੋਗਤਾ", + "User location successfully retrieved.": "", + "User Permissions": "ਉਪਭੋਗਤਾ ਅਧਿਕਾਰ", + "Users": "ਉਪਭੋਗਤਾ", + "Utilize": "ਵਰਤੋਂ", + "Valid time units:": "ਵੈਧ ਸਮਾਂ ਇਕਾਈਆਂ:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "ਵੈਰੀਏਬਲ", + "variable to have them replaced with clipboard content.": "ਕਲਿੱਪਬੋਰਡ ਸਮੱਗਰੀ ਨਾਲ ਬਦਲਣ ਲਈ ਵੈਰੀਏਬਲ।", + "Version": "ਵਰਜਨ", + "Voice": "", + "Warning": "ਚੇਤਾਵਨੀ", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "ਚੇਤਾਵਨੀ: ਜੇ ਤੁਸੀਂ ਆਪਣਾ ਐਮਬੈੱਡਿੰਗ ਮਾਡਲ ਅੱਪਡੇਟ ਜਾਂ ਬਦਲਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਨੂੰ ਸਾਰੇ ਡਾਕੂਮੈਂਟ ਮੁੜ ਆਯਾਤ ਕਰਨ ਦੀ ਲੋੜ ਹੋਵੇਗੀ।", + "Web": "ਵੈਬ", + "Web API": "", + "Web Loader Settings": "ਵੈਬ ਲੋਡਰ ਸੈਟਿੰਗਾਂ", + "Web Params": "ਵੈਬ ਪੈਰਾਮੀਟਰ", + "Web Search": "ਵੈੱਬ ਖੋਜ", + "Web Search Engine": "ਵੈੱਬ ਖੋਜ ਇੰਜਣ", + "Webhook URL": "ਵੈਬਹੁੱਕ URL", + "WebUI Settings": "ਵੈਬਯੂਆਈ ਸੈਟਿੰਗਾਂ", + "WebUI will make requests to": "ਵੈਬਯੂਆਈ ਬੇਨਤੀਆਂ ਕਰੇਗਾ", + "What’s New in": "ਨਵਾਂ ਕੀ ਹੈ", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "ਜਦੋਂ ਇਤਿਹਾਸ ਬੰਦ ਹੁੰਦਾ ਹੈ, ਤਾਂ ਇਸ ਬ੍ਰਾਊਜ਼ਰ 'ਤੇ ਨਵੀਆਂ ਗੱਲਾਂ ਤੁਹਾਡੇ ਕਿਸੇ ਵੀ ਜੰਤਰ 'ਤੇ ਤੁਹਾਡੇ ਇਤਿਹਾਸ ਵਿੱਚ ਨਹੀਂ ਆਉਣਗੀਆਂ।", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "ਕਾਰਜਸਥਲ", + "Write a prompt suggestion (e.g. Who are you?)": "ਇੱਕ ਪ੍ਰੰਪਟ ਸੁਝਾਅ ਲਿਖੋ (ਉਦਾਹਰਣ ਲਈ ਤੁਸੀਂ ਕੌਣ ਹੋ?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "50 ਸ਼ਬਦਾਂ ਵਿੱਚ ਇੱਕ ਸੰਖੇਪ ਲਿਖੋ ਜੋ [ਵਿਸ਼ਾ ਜਾਂ ਕੁੰਜੀ ਸ਼ਬਦ] ਨੂੰ ਸੰਖੇਪ ਕਰਦਾ ਹੈ।", + "Yesterday": "ਕੱਲ੍ਹ", + "You": "ਤੁਸੀਂ", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "ਤੁਸੀਂ ਆਧਾਰ ਮਾਡਲ ਨੂੰ ਕਲੋਨ ਨਹੀਂ ਕਰ ਸਕਦੇ", + "You have no archived conversations.": "ਤੁਹਾਡੇ ਕੋਲ ਕੋਈ ਆਰਕਾਈਵ ਕੀਤੀਆਂ ਗੱਲਾਂ ਨਹੀਂ ਹਨ।", + "You have shared this chat": "ਤੁਸੀਂ ਇਹ ਗੱਲਬਾਤ ਸਾਂਝੀ ਕੀਤੀ ਹੈ", + "You're a helpful assistant.": "ਤੁਸੀਂ ਇੱਕ ਮਦਦਗਾਰ ਸਹਾਇਕ ਹੋ।", + "You're now logged in.": "ਤੁਸੀਂ ਹੁਣ ਲੌਗ ਇਨ ਹੋ ਗਏ ਹੋ।", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "ਯੂਟਿਊਬ", + "Youtube Loader Settings": "ਯੂਟਿਊਬ ਲੋਡਰ ਸੈਟਿੰਗਾਂ" +} diff --git a/src/lib/i18n/locales/pl-PL/translation.json b/src/lib/i18n/locales/pl-PL/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..3c82a97d25ad2aa61db97ab67435fc7835483c1a --- /dev/null +++ b/src/lib/i18n/locales/pl-PL/translation.json @@ -0,0 +1,716 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' lub '-1' dla bez wygaśnięcia.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(np. `sh webui.sh --api`)", + "(latest)": "(najnowszy)", + "{{ models }}": "{{ modele }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Nie można usunąć modelu podstawowego", + "{{modelName}} is thinking...": "{{modelName}} myśli...", + "{{user}}'s Chats": "{{user}} - czaty", + "{{webUIName}} Backend Required": "Backend {{webUIName}} wymagane", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Model zadań jest używany podczas wykonywania zadań, takich jak generowanie tytułów czatów i zapytań wyszukiwania w Internecie", + "a user": "użytkownik", + "About": "O nas", + "Account": "Konto", + "Account Activation Pending": "", + "Accurate information": "Dokładna informacja", + "Actions": "", + "Active Users": "", + "Add": "Dodaj", + "Add a model id": "Dodawanie identyfikatora modelu", + "Add a short description about what this model does": "Dodaj krótki opis działania tego modelu", + "Add a short title for this prompt": "Dodaj krótki tytuł tego polecenia", + "Add a tag": "Dodaj tag", + "Add custom prompt": "Dodaj własne polecenie", + "Add Docs": "Dodaj dokumenty", + "Add Files": "Dodaj pliki", + "Add Memory": "Dodaj pamięć", + "Add message": "Dodaj wiadomość", + "Add Model": "Dodaj model", + "Add Tag": "", + "Add Tags": "Dodaj tagi", + "Add User": "Dodaj użytkownika", + "Adjusting these settings will apply changes universally to all users.": "Dostosowanie tych ustawień spowoduje zastosowanie zmian uniwersalnie do wszystkich użytkowników.", + "admin": "admin", + "Admin": "", + "Admin Panel": "Panel administracyjny", + "Admin Settings": "Ustawienia administratora", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Zaawansowane parametry", + "Advanced Params": "Zaawansowane parametry", + "all": "wszyscy", + "All Documents": "Wszystkie dokumenty", + "All Users": "Wszyscy użytkownicy", + "Allow": "Pozwól", + "Allow Chat Deletion": "Pozwól na usuwanie czatu", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "znaki alfanumeryczne i myślniki", + "Already have an account?": "Masz już konto?", + "an assistant": "asystent", + "and": "i", + "and create a new shared link.": "i utwórz nowy udostępniony link", + "API Base URL": "Podstawowy adres URL interfejsu API", + "API Key": "Klucz API", + "API Key created.": "Klucz API utworzony.", + "API keys": "Klucze API", + "April": "Kwiecień", + "Archive": "Archiwum", + "Archive All Chats": "Archiwizuj wszystkie czaty", + "Archived Chats": "Zarchiwizowane czaty", + "are allowed - Activate this command by typing": "są dozwolone - Aktywuj to polecenie, wpisując", + "Are you sure?": "Jesteś pewien?", + "Attach file": "Dołącz plik", + "Attention to detail": "Dbałość o szczegóły", + "Audio": "Dźwięk", + "Audio settings updated successfully": "", + "August": "Sierpień", + "Auto-playback response": "Odtwarzanie automatyczne odpowiedzi", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Podstawowy adres URL AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "Podstawowy adres URL AUTOMATIC1111 jest wymagany.", + "available!": "dostępny!", + "Back": "Wstecz", + "Bad Response": "Zła odpowiedź", + "Banners": "Banery", + "Base Model (From)": "Model podstawowy (od)", + "Batch Size (num_batch)": "", + "before": "przed", + "Being lazy": "Jest leniwy", + "Brave Search API Key": "Klucz API wyszukiwania Brave", + "Bypass SSL verification for Websites": "Pomiń weryfikację SSL dla stron webowych", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Anuluj", + "Capabilities": "Możliwości", + "Change Password": "Zmień hasło", + "Chat": "Czat", + "Chat Background Image": "", + "Chat Bubble UI": "Bąbelki czatu", + "Chat Controls": "", + "Chat direction": "Kierunek czatu", + "Chat History": "Historia czatu", + "Chat History is off for this browser.": "Historia czatu jest wyłączona dla tej przeglądarki.", + "Chats": "Czaty", + "Check Again": "Sprawdź ponownie", + "Check for updates": "Sprawdź aktualizacje", + "Checking for updates...": "Sprawdzanie aktualizacji...", + "Choose a model before saving...": "Wybierz model przed zapisaniem...", + "Chunk Overlap": "Zachodzenie bloku", + "Chunk Params": "Parametry bloku", + "Chunk Size": "Rozmiar bloku", + "Citation": "Cytat", + "Clear memory": "", + "Click here for help.": "Kliknij tutaj, aby uzyskać pomoc.", + "Click here to": "Kliknij tutaj, żeby", + "Click here to download user import template file.": "", + "Click here to select": "Kliknij tutaj, aby wybrać", + "Click here to select a csv file.": "Kliknij tutaj, żeby wybrać plik CSV", + "Click here to select a py file.": "", + "Click here to select documents.": "Kliknij tutaj, aby wybrać dokumenty.", + "click here.": "kliknij tutaj.", + "Click on the user role button to change a user's role.": "Kliknij przycisk roli użytkownika, aby zmienić rolę użytkownika.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Klon", + "Close": "Zamknij", + "Code formatted successfully": "", + "Collection": "Kolekcja", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "Bazowy URL ComfyUI", + "ComfyUI Base URL is required.": "Bazowy URL ComfyUI jest wymagany.", + "Command": "Polecenie", + "Concurrent Requests": "Równoczesne żądania", + "Confirm": "", + "Confirm Password": "Potwierdź hasło", + "Confirm your action": "", + "Connections": "Połączenia", + "Contact Admin for WebUI Access": "", + "Content": "Zawartość", + "Content Extraction": "", + "Context Length": "Długość kontekstu", + "Continue Response": "Kontynuuj odpowiedź", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Skopiowano URL czatu do schowka!", + "Copy": "Kopiuj", + "Copy last code block": "Skopiuj ostatni blok kodu", + "Copy last response": "Skopiuj ostatnią odpowiedź", + "Copy Link": "Kopiuj link", + "Copying to clipboard was successful!": "Kopiowanie do schowka zakończone powodzeniem!", + "Create a model": "Tworzenie modelu", + "Create Account": "Utwórz konto", + "Create new key": "Utwórz nowy klucz", + "Create new secret key": "Utwórz nowy klucz bezpieczeństwa", + "Created at": "Utworzono o", + "Created At": "Utworzono o", + "Created by": "", + "CSV Import": "", + "Current Model": "Bieżący model", + "Current Password": "Bieżące hasło", + "Custom": "Niestandardowy", + "Customize models for a specific purpose": "Dostosowywanie modeli do określonego celu", + "Dark": "Ciemny", + "Dashboard": "", + "Database": "Baza danych", + "December": "Grudzień", + "Default": "Domyślny", + "Default (Automatic1111)": "Domyślny (Automatic1111)", + "Default (SentenceTransformers)": "Domyślny (SentenceTransformers)", + "Default Model": "Model domyślny", + "Default model updated": "Domyślny model zaktualizowany", + "Default Prompt Suggestions": "Domyślne sugestie promptów", + "Default User Role": "Domyślna rola użytkownika", + "delete": "usuń", + "Delete": "Usuń", + "Delete a model": "Usuń model", + "Delete All Chats": "Usuń wszystkie czaty", + "Delete chat": "Usuń czat", + "Delete Chat": "Usuń czat", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "usuń ten link", + "Delete tool?": "", + "Delete User": "Usuń użytkownika", + "Deleted {{deleteModelTag}}": "Usunięto {{deleteModelTag}}", + "Deleted {{name}}": "Usunięto {{name}}", + "Description": "Opis", + "Didn't fully follow instructions": "Nie postępował zgodnie z instrukcjami", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Odkryj model", + "Discover a prompt": "Odkryj prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Odkryj, pobierz i eksploruj niestandardowe prompty", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Odkryj, pobierz i eksploruj ustawienia modeli", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Wyświetl nazwę użytkownika zamiast Ty w czacie", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokument", + "Document Settings": "Ustawienia dokumentu", + "Documentation": "", + "Documents": "Dokumenty", + "does not make any external connections, and your data stays securely on your locally hosted server.": "nie nawiązuje żadnych zewnętrznych połączeń, a Twoje dane pozostają bezpiecznie na Twoim lokalnie hostowanym serwerze.", + "Don't Allow": "Nie zezwalaj", + "Don't have an account?": "Nie masz konta?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Nie podobał mi się styl", + "Done": "", + "Download": "Pobieranie", + "Download canceled": "Pobieranie przerwane", + "Download Database": "Pobierz bazę danych", + "Drop any files here to add to the conversation": "Upuść pliki tutaj, aby dodać do rozmowy", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "np. '30s', '10m'. Poprawne jednostki czasu to 's', 'm', 'h'.", + "Edit": "Edytuj", + "Edit Doc": "Edytuj dokument", + "Edit Memory": "", + "Edit User": "Edytuj użytkownika", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "", + "Embedding Model": "Model osadzania", + "Embedding Model Engine": "Silnik modelu osadzania", + "Embedding model set to \"{{embedding_model}}\"": "Model osadzania ustawiono na \"{{embedding_model}}\"", + "Enable Chat History": "Włącz historię czatu", + "Enable Community Sharing": "Włączanie udostępniania społecznościowego", + "Enable New Sign Ups": "Włącz nowe rejestracje", + "Enable Web Search": "Włączanie wyszukiwania w Internecie", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Upewnij się, że twój plik CSV zawiera 4 kolumny w następującym porządku: Nazwa, Email, Hasło, Rola.", + "Enter {{role}} message here": "Wprowadź wiadomość {{role}} tutaj", + "Enter a detail about yourself for your LLMs to recall": "Wprowadź szczegóły o sobie, aby LLMs mogli pamiętać", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Wprowadź klucz API Brave Search", + "Enter Chunk Overlap": "Wprowadź zakchodzenie bloku", + "Enter Chunk Size": "Wprowadź rozmiar bloku", + "Enter Github Raw URL": "Wprowadź nieprzetworzony adres URL usługi Github", + "Enter Google PSE API Key": "Wprowadź klucz API Google PSE", + "Enter Google PSE Engine Id": "Wprowadź identyfikator aparatu Google PSE", + "Enter Image Size (e.g. 512x512)": "Wprowadź rozmiar obrazu (np. 512x512)", + "Enter language codes": "Wprowadź kody języków", + "Enter model tag (e.g. {{modelTag}})": "Wprowadź tag modelu (np. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Wprowadź liczbę kroków (np. 50)", + "Enter Score": "Wprowadź wynik", + "Enter Searxng Query URL": "Wprowadź adres URL zapytania Searxng", + "Enter Serper API Key": "Wprowadź klucz API Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Wprowadź klucz API Serpstack", + "Enter stop sequence": "Wprowadź sekwencję zatrzymania", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Wprowadź Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Wprowadź adres URL (np. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Wprowadź adres URL (np. http://localhost:11434/)", + "Enter Your Email": "Wprowadź swój adres email", + "Enter Your Full Name": "Wprowadź swoje imię i nazwisko", + "Enter your message": "", + "Enter Your Password": "Wprowadź swoje hasło", + "Enter Your Role": "Wprowadź swoją rolę", + "Error": "Błąd", + "Experimental": "Eksperymentalne", + "Export": "Eksport", + "Export All Chats (All Users)": "Eksportuj wszystkie czaty (wszyscy użytkownicy)", + "Export chat (.json)": "", + "Export Chats": "Eksportuj czaty", + "Export Documents Mapping": "Eksportuj mapowanie dokumentów", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Eksportuj modele", + "Export Prompts": "Eksportuj prompty", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Nie udało się utworzyć klucza API.", + "Failed to read clipboard contents": "Nie udało się odczytać zawartości schowka", + "Failed to update settings": "", + "February": "Luty", + "Feel free to add specific details": "Podaj inne szczegóły", + "File": "", + "File Mode": "Tryb pliku", + "File not found.": "Plik nie został znaleziony.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Wykryto podszywanie się pod odcisk palca: Nie można używać inicjałów jako awatara. Przechodzenie do domyślnego obrazu profilowego.", + "Fluidly stream large external response chunks": "Płynnie przesyłaj strumieniowo duże fragmenty odpowiedzi zewnętrznych", + "Focus chat input": "Skoncentruj na czacie", + "Followed instructions perfectly": "Postępował z idealnie według instrukcji", + "Form": "", + "Format your variables using square brackets like this:": "Formatuj swoje zmienne, używając nawiasów kwadratowych, np.", + "Frequency Penalty": "Kara za częstotliwość", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Ogólne", + "General Settings": "Ogólne ustawienia", + "Generate Image": "", + "Generating search query": "Generowanie zapytania", + "Generation Info": "Informacja o generacji", + "Get up and running with": "", + "Global": "", + "Good Response": "Dobra odpowiedź", + "Google PSE API Key": "Klucz API Google PSE", + "Google PSE Engine Id": "Identyfikator silnika Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "nie ma rozmów.", + "Hello, {{name}}": "Witaj, {{name}}", + "Help": "Pomoc", + "Hide": "Ukryj", + "Hide Model": "", + "How can I help you today?": "Jak mogę Ci dzisiaj pomóc?", + "Hybrid Search": "Szukanie hybrydowe", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Generowanie obrazu (eksperymentalne)", + "Image Generation Engine": "Silnik generowania obrazu", + "Image Settings": "Ustawienia obrazu", + "Images": "Obrazy", + "Import Chats": "Importuj czaty", + "Import Documents Mapping": "Importuj mapowanie dokumentów", + "Import Functions": "", + "Import Models": "Importowanie modeli", + "Import Prompts": "Importuj prompty", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Dołącz flagę `--api` podczas uruchamiania stable-diffusion-webui", + "Info": "Informacji", + "Input commands": "Wprowadź komendy", + "Install from Github URL": "Instalowanie z adresu URL usługi Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Interfejs", + "Invalid Tag": "Nieprawidłowy tag", + "January": "Styczeń", + "join our Discord for help.": "Dołącz do naszego Discorda po pomoc.", + "JSON": "JSON", + "JSON Preview": "JSON (wersja zapoznawcza)", + "July": "Lipiec", + "June": "Czerwiec", + "JWT Expiration": "Wygaśnięcie JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Zachowaj łączność", + "Keyboard shortcuts": "Skróty klawiszowe", + "Knowledge": "", + "Language": "Język", + "large language models, locally.": "", + "Last Active": "Ostatnio aktywny", + "Last Modified": "", + "Light": "Jasny", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMy mogą popełniać błędy. Zweryfikuj ważne informacje.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Stworzone przez społeczność OpenWebUI", + "Make sure to enclose them with": "Upewnij się, że są one zamknięte w", + "Manage": "", + "Manage Models": "Zarządzaj modelami", + "Manage Ollama Models": "Zarządzaj modelami Ollama", + "Manage Pipelines": "Zarządzanie potokami", + "Manage Valves": "", + "March": "Marzec", + "Max Tokens (num_predict)": "Maksymalna liczba żetonów (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Maksymalnie 3 modele można pobierać jednocześnie. Spróbuj ponownie później.", + "May": "Maj", + "Memories accessible by LLMs will be shown here.": "Pamięci używane przez LLM będą tutaj widoczne.", + "Memory": "Pamięć", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Wiadomości wysyłane po utworzeniu linku nie będą udostępniane. Użytkownicy z adresem URL będą mogli wyświetlić udostępniony czat.", + "Minimum Score": "Minimalny wynik", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Model '{{modelName}}' został pomyślnie pobrany.", + "Model '{{modelTag}}' is already in queue for downloading.": "Model '{{modelTag}}' jest już w kolejce do pobrania.", + "Model {{modelId}} not found": "Model {{modelId}} nie został znaleziony", + "Model {{modelName}} is not vision capable": "Model {{modelName}} nie jest w stanie zobaczyć", + "Model {{name}} is now {{status}}": "Model {{name}} to teraz {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Wykryto ścieżkę systemu plików modelu. Wymagana jest krótka nazwa modelu do aktualizacji, nie można kontynuować.", + "Model ID": "Identyfikator modelu", + "Model not selected": "Model nie został wybrany", + "Model Params": "Parametry modelu", + "Model updated successfully": "", + "Model Whitelisting": "Whitelisting modelu", + "Model(s) Whitelisted": "Model(e) dodane do listy białej", + "Modelfile Content": "Zawartość pliku modelu", + "Models": "Modele", + "More": "Więcej", + "Name": "Nazwa", + "Name Tag": "Etykieta nazwy", + "Name your model": "Nazwij swój model", + "New Chat": "Nowy czat", + "New Password": "Nowe hasło", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Nie znaleziono rezultatów", + "No search query generated": "Nie wygenerowano zapytania wyszukiwania", + "No source available": "Źródło nie dostępne", + "No valves to update": "", + "None": "Żaden", + "Not factually correct": "Nie zgodne z faktami", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Uwaga: Jeśli ustawisz minimalny wynik, szukanie zwróci jedynie dokumenty z wynikiem większym lub równym minimalnemu.", + "Notifications": "Powiadomienia", + "November": "Listopad", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Październik", + "Off": "Wyłączony", + "Okay, Let's Go!": "Okej, zaczynamy!", + "OLED Dark": "Ciemny OLED", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Interfejs API Ollama wyłączony", + "Ollama API is disabled": "", + "Ollama Version": "Wersja Ollama", + "On": "Włączony", + "Only": "Tylko", + "Only alphanumeric characters and hyphens are allowed in the command string.": "W poleceniu dozwolone są tylko znaki alfanumeryczne i myślniki.", + "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.": "Ups! Trzymaj się! Twoje pliki są wciąż w procesie obróbki. Gotujemy je do perfekcji. Prosimy o cierpliwość, poinformujemy Cię, gdy będą gotowe.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Ups! Wygląda na to, że URL jest nieprawidłowy. Sprawdź jeszcze raz i spróbuj ponownie.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Ups! Używasz nieobsługiwanej metody (tylko interfejs front-end). Proszę obsłużyć interfejs WebUI z poziomu backendu.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Otwórz nowy czat", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "Konfiguracja OpenAI API", + "OpenAI API Key is required.": "Klucz API OpenAI jest wymagany.", + "OpenAI URL/Key required.": "URL/Klucz OpenAI jest wymagany.", + "or": "lub", + "Other": "Inne", + "Password": "Hasło", + "PDF document (.pdf)": "Dokument PDF (.pdf)", + "PDF Extract Images (OCR)": "PDF Wyodrębnij obrazy (OCR)", + "pending": "oczekujące", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Odmowa dostępu do mikrofonu: {{error}}", + "Personalization": "Personalizacja", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Rurociągów", + "Pipelines Not Detected": "", + "Pipelines Valves": "Rurociągi Zawory", + "Plain text (.txt)": "Zwykły tekst (.txt)", + "Playground": "Plac zabaw", + "Please carefully review the following warnings:": "", + "Positive attitude": "Pozytywne podejście", + "Previous 30 days": "Poprzednie 30 dni", + "Previous 7 days": "Poprzednie 7 dni", + "Profile Image": "Obrazek profilowy", + "Prompt": "Promopt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (np. powiedz mi zabawny fakt o Imperium Rzymskim", + "Prompt Content": "Zawartość prompta", + "Prompt suggestions": "Sugestie prompta", + "Prompts": "Prompty", + "Pull \"{{searchValue}}\" from Ollama.com": "Pobierz \"{{searchValue}}\" z Ollama.com", + "Pull a model from Ollama.com": "Pobierz model z Ollama.com", + "Query Params": "Parametry zapytania", + "RAG Template": "Szablon RAG", + "Read Aloud": "Czytaj na głos", + "Record voice": "Nagraj głos", + "Redirecting you to OpenWebUI Community": "Przekierowujemy Cię do społeczności OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Odmówił, kiedy nie powinien", + "Regenerate": "Generuj ponownie", + "Release Notes": "Notatki wydania", + "Remove": "Usuń", + "Remove Model": "Usuń model", + "Rename": "ZMień nazwę", + "Repeat Last N": "Powtórz ostatnie N", + "Request Mode": "Tryb żądania", + "Reranking Model": "Zmiana rankingu modelu", + "Reranking model disabled": "Zmiana rankingu modelu zablokowana", + "Reranking model set to \"{{reranking_model}}\"": "Zmiana rankingu modelu ustawiona na \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Resetuj przechowywanie wektorów", + "Response AutoCopy to Clipboard": "Automatyczne kopiowanie odpowiedzi do schowka", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Rola", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RLT", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Zapisz", + "Save & Create": "Zapisz i utwórz", + "Save & Update": "Zapisz i zaktualizuj", + "Save Tag": "", + "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": "Bezpośrednie zapisywanie dzienników czatu w pamięci przeglądarki nie jest już obsługiwane. Prosimy o pobranie i usunięcie dzienników czatu, klikając poniższy przycisk. Nie martw się, możesz łatwo ponownie zaimportować dzienniki czatu do backendu za pomocą", + "Scan": "Skanuj", + "Scan complete!": "Skanowanie zakończone!", + "Scan for documents from {{path}}": "Skanuj dokumenty z {{path}}", + "Search": "Szukaj", + "Search a model": "Szukaj modelu", + "Search Chats": "Szukaj w czatach", + "Search Documents": "Szukaj dokumentów", + "Search Functions": "", + "Search Models": "Szukaj modeli", + "Search Prompts": "Szukaj promptów", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Liczba wyników wyszukiwania", + "Search Tools": "", + "Searched {{count}} sites_one": "Wyszukiwano {{count}} sites_one", + "Searched {{count}} sites_few": "Wyszukiwano {{count}} sites_few", + "Searched {{count}} sites_many": "Wyszukiwano {{count}} sites_many", + "Searched {{count}} sites_other": "Wyszukiwano {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "Adres URL zapytania Searxng", + "See readme.md for instructions": "Zajrzyj do readme.md po instrukcje", + "See what's new": "Zobacz co nowego", + "Seed": "Seed", + "Select a base model": "Wybieranie modelu bazowego", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Wybierz tryb", + "Select a model": "Wybierz model", + "Select a pipeline": "Wybieranie potoku", + "Select a pipeline url": "Wybieranie adresu URL potoku", + "Select a tool": "", + "Select an Ollama instance": "Wybierz instancję Ollama", + "Select Documents": "", + "Select model": "Wybierz model", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Wybrane modele nie obsługują danych wejściowych obrazu", + "Send": "Wyślij", + "Send a Message": "Wyślij Wiadomość", + "Send message": "Wyślij wiadomość", + "September": "Wrzesień", + "Serper API Key": "Klucz API Serper", + "Serply API Key": "", + "Serpstack API Key": "Klucz API Serpstack", + "Server connection verified": "Połączenie z serwerem zweryfikowane", + "Set as default": "Ustaw jako domyślne", + "Set Default Model": "Ustaw domyślny model", + "Set embedding model (e.g. {{model}})": "Ustaw model osadzania (e.g. {{model}})", + "Set Image Size": "Ustaw rozmiar obrazu", + "Set reranking model (e.g. {{model}})": "Ustaw zmianę rankingu modelu (e.g. {{model}})", + "Set Steps": "Ustaw kroki", + "Set Task Model": "Ustawianie modelu zadań", + "Set Voice": "Ustaw głos", + "Settings": "Ustawienia", + "Settings saved successfully!": "Ustawienia zapisane pomyślnie!", + "Settings updated successfully": "", + "Share": "Udostępnij", + "Share Chat": "Udostępnij czat", + "Share to OpenWebUI Community": "Dziel się z społecznością OpenWebUI", + "short-summary": "Krótkie podsumowanie", + "Show": "Pokaż", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Pokaż skróty", + "Show your support!": "", + "Showcased creativity": "Pokaz kreatywności", + "Sign in": "Zaloguj się", + "Sign Out": "Wyloguj się", + "Sign up": "Zarejestruj się", + "Signing in": "Zalogowanie", + "Source": "Źródło", + "Speech recognition error: {{error}}": "Błąd rozpoznawania mowy: {{error}}", + "Speech-to-Text Engine": "Silnik mowy na tekst", + "Stop Sequence": "Zatrzymaj sekwencję", + "STT Model": "", + "STT Settings": "Ustawienia STT", + "Submit": "Zatwierdź", + "Subtitle (e.g. about the Roman Empire)": "Podtytuł (np. o Imperium Rzymskim)", + "Success": "Sukces", + "Successfully updated.": "Pomyślnie zaktualizowano.", + "Suggested": "Sugerowane", + "Support": "", + "Support this plugin:": "", + "System": "System", + "System Prompt": "Prompt systemowy", + "Tags": "Tagi", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Powiedz nam więcej", + "Temperature": "Temperatura", + "Template": "Szablon", + "Text Completion": "Uzupełnienie tekstu", + "Text-to-Speech Engine": "Silnik tekstu na mowę", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Dzięki za informację zwrotną!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Wynik powinien być wartością pomiędzy 0.0 (0%) a 1.0 (100%).", + "Theme": "Motyw", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "To zapewnia, że Twoje cenne rozmowy są bezpiecznie zapisywane w bazie danych backendowej. Dziękujemy!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "To ustawienie nie synchronizuje się między przeglądarkami ani urządzeniami.", + "This will delete": "", + "Thorough explanation": "Dokładne wyjaśnienie", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Porada: Aktualizuj wiele zmiennych kolejno, naciskając klawisz tabulatora w polu wprowadzania czatu po każdej zmianie.", + "Title": "Tytuł", + "Title (e.g. Tell me a fun fact)": "Tytuł (np. Powiedz mi jakiś zabawny fakt)", + "Title Auto-Generation": "Automatyczne generowanie tytułu", + "Title cannot be an empty string.": "Tytuł nie może być pusty", + "Title Generation Prompt": "Prompt generowania tytułu", + "to": "do", + "To access the available model names for downloading,": "Aby uzyskać dostęp do dostępnych nazw modeli do pobrania,", + "To access the GGUF models available for downloading,": "Aby uzyskać dostęp do dostępnych modeli GGUF do pobrania,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "do pola wprowadzania czatu.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Dzisiaj", + "Toggle settings": "Przełącz ustawienia", + "Toggle sidebar": "Przełącz panel boczny", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Najlepsze K", + "Top P": "Najlepsze P", + "Trouble accessing Ollama?": "Problemy z dostępem do Ollama?", + "TTS Model": "", + "TTS Settings": "Ustawienia TTS", + "TTS Voice": "", + "Type": "Typ", + "Type Hugging Face Resolve (Download) URL": "Wprowadź adres URL do pobrania z Hugging Face", + "Uh-oh! There was an issue connecting to {{provider}}.": "O nie! Wystąpił problem z połączeniem z {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Uaktualnij i skopiuj link", + "Update password": "Aktualizacja hasła", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Prześlij model GGUF", + "Upload Files": "Prześlij pliki", + "Upload Pipeline": "", + "Upload Progress": "Postęp przesyłania", + "URL Mode": "Tryb adresu URL", + "Use '#' in the prompt input to load and select your documents.": "Użyj '#' w polu wprowadzania polecenia, aby załadować i wybrać swoje dokumenty.", + "Use Gravatar": "Użyj Gravatara", + "Use Initials": "Użyj inicjałów", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "użytkownik", + "User location successfully retrieved.": "", + "User Permissions": "Uprawnienia użytkownika", + "Users": "Użytkownicy", + "Utilize": "Wykorzystaj", + "Valid time units:": "Poprawne jednostki czasu:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "zmienna", + "variable to have them replaced with clipboard content.": "zmienna która zostanie zastąpiona zawartością schowka.", + "Version": "Wersja", + "Voice": "", + "Warning": "Ostrzeżenie", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Uwaga: Jeśli uaktualnisz lub zmienisz model osadzania, będziesz musiał ponownie zaimportować wszystkie dokumenty.", + "Web": "Sieć", + "Web API": "", + "Web Loader Settings": "Ustawienia pobierania z sieci", + "Web Params": "Parametry sieci", + "Web Search": "Wyszukiwarka w Internecie", + "Web Search Engine": "Wyszukiwarka internetowa", + "Webhook URL": "URL webhook", + "WebUI Settings": "Ustawienia interfejsu WebUI", + "WebUI will make requests to": "Interfejs sieciowy będzie wysyłał żądania do", + "What’s New in": "Co nowego w", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Kiedy historia jest wyłączona, nowe czaty na tej przeglądarce nie będą widoczne w historii na żadnym z twoich urządzeń.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Obszar roboczy", + "Write a prompt suggestion (e.g. Who are you?)": "Napisz sugestię do polecenia (np. Kim jesteś?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Napisz podsumowanie w 50 słowach, które podsumowuje [temat lub słowo kluczowe].", + "Yesterday": "Wczoraj", + "You": "Ty", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Nie można sklonować modelu podstawowego", + "You have no archived conversations.": "Nie masz zarchiwizowanych rozmów.", + "You have shared this chat": "Udostępniłeś ten czat", + "You're a helpful assistant.": "Jesteś pomocnym asystentem.", + "You're now logged in.": "Jesteś teraz zalogowany.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Ustawienia pobierania z Youtube" +} diff --git a/src/lib/i18n/locales/pt-BR/translation.json b/src/lib/i18n/locales/pt-BR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..f188bf01fdc8bc29626ab6ec8890f27e44eee606 --- /dev/null +++ b/src/lib/i18n/locales/pt-BR/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ou '-1' para não expirar.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)", + "(latest)": "(mais recente)", + "{{ models }}": "{{ modelos }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Não é possível excluir um modelo base", + "{{modelName}} is thinking...": "{{modelName}} está pensando...", + "{{user}}'s Chats": "{{user}}'s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Um modelo de tarefa é usado ao executar tarefas como a geração de títulos para bate-papos e consultas de pesquisa na Web", + "a user": "um usuário", + "About": "Sobre", + "Account": "Conta", + "Account Activation Pending": "Ativação de Conta Pendente", + "Accurate information": "Informações precisas", + "Actions": "", + "Active Users": "Usuários Ativos", + "Add": "Adicionar", + "Add a model id": "Adicionar um ID de modelo", + "Add a short description about what this model does": "Adicione uma breve descrição sobre o que este modelo faz", + "Add a short title for this prompt": "Adicione um título curto para este prompt", + "Add a tag": "Adicionar uma tag", + "Add custom prompt": "Adicionar prompt personalizado", + "Add Docs": "Adicionar Documentos", + "Add Files": "Adicionar Arquivos", + "Add Memory": "Adicionar Memória", + "Add message": "Adicionar mensagem", + "Add Model": "Adicionar Modelo", + "Add Tag": "", + "Add Tags": "adicionar Tags", + "Add User": "Adicionar Usuário", + "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": "Administrador", + "Admin Panel": "Painel do Administrador", + "Admin Settings": "Configurações do Administrador", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Adminstradores têm acesso a todas as ferramentas o tempo todo; os usuários precisam de ferramentas atribuídas por modelo no espaço de trabalho.", + "Advanced Parameters": "Parâmetros Avançados", + "Advanced Params": "Parâmetros Avançados", + "all": "todos", + "All Documents": "Todos os Documentos", + "All Users": "Todos os Usuários", + "Allow": "Permitir", + "Allow Chat Deletion": "Permitir Exclusão de Bate-papo", + "Allow non-local voices": "Permitir vozes não locais", + "Allow User Location": "Permitir Localização do Usuário", + "Allow Voice Interruption in Call": "Permitir Interrupção de Voz na Chamada", + "alphanumeric characters and hyphens": "caracteres alfanuméricos e hífens", + "Already have an account?": "Já tem uma conta?", + "an assistant": "um assistente", + "and": "e", + "and create a new shared link.": "e criar um novo link compartilhado.", + "API Base URL": "URL Base da API", + "API Key": "Chave da API", + "API Key created.": "Chave da API criada.", + "API keys": "Chaves da API", + "April": "Abril", + "Archive": "Arquivo", + "Archive All Chats": "Arquivar todos os bate-papos", + "Archived Chats": "Bate-papos arquivados", + "are allowed - Activate this command by typing": "são permitidos - Ative este comando digitando", + "Are you sure?": "Tem certeza?", + "Attach file": "Anexar arquivo", + "Attention to detail": "Detalhado", + "Audio": "Áudio", + "Audio settings updated successfully": "Configurações de áudio atualizadas com sucesso", + "August": "Agosto", + "Auto-playback response": "Reprodução automática da resposta", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "URL Base do AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "A URL Base do AUTOMATIC1111 é obrigatória.", + "available!": "disponível!", + "Back": "Voltar", + "Bad Response": "Resposta ruim", + "Banners": "Banners", + "Base Model (From)": "Modelo Base (De)", + "Batch Size (num_batch)": "Tamanho do Lote (num_batch)", + "before": "antes", + "Being lazy": "Ser preguiçoso", + "Brave Search API Key": "Chave da API de pesquisa do Brave", + "Bypass SSL verification for Websites": "Ignorar verificação SSL para sites", + "Call": "Chamada", + "Call feature is not supported when using Web STT engine": "Chamada não é suportada ao usar o mecanismo Web STT", + "Camera": "Camera", + "Cancel": "Cancelar", + "Capabilities": "Capacidades", + "Change Password": "Alterar Senha", + "Chat": "Bate-papo", + "Chat Background Image": "Image de Fundo do Bate-papo", + "Chat Bubble UI": "UI de Bala de Bate-papo", + "Chat Controls": "Controles de Bate-papo", + "Chat direction": "Direção do 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", + "Citation": "Citação", + "Clear memory": "Limpar memória", + "Click here for help.": "Clique aqui para obter ajuda.", + "Click here to": "Clique aqui para", + "Click here to download user import template file.": "Clique aqui para baixar o arquivo de modelo de importação de usuário.", + "Click here to select": "Clique aqui para selecionar", + "Click here to select a csv file.": "Clique aqui para selecionar um arquivo csv.", + "Click here to select a py file.": "Clique aqui para selecionar um arquivo py.", + "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.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Permissão de gravação da área de transferência negada. Verifique as configurações do seu navegador para conceder o acesso necessário.", + "Clone": "Clone", + "Close": "Fechar", + "Code formatted successfully": "Código formatado com sucesso", + "Collection": "Coleção", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL Base do ComfyUI", + "ComfyUI Base URL is required.": "A URL Base do ComfyUI é obrigatória.", + "Command": "Comando", + "Concurrent Requests": "Solicitações simultâneas", + "Confirm": "Confirmar", + "Confirm Password": "Confirmar Senha", + "Confirm your action": "Confirmar sua ação", + "Connections": "Conexões", + "Contact Admin for WebUI Access": "Contate o Administrador para Acesso ao WebUI", + "Content": "Conteúdo", + "Content Extraction": "Extração de Conteúdo", + "Context Length": "Comprimento do Contexto", + "Continue Response": "Continuar resposta", + "Continue with {{provider}}": "Continuar com {{provider}}", + "Controls": "Controles", + "Copied shared chat URL to clipboard!": "URL de bate-papo compartilhado copiada com sucesso!", + "Copy": "Copiar", + "Copy last code block": "Copiar último bloco de código", + "Copy last response": "Copiar última resposta", + "Copy Link": "Copiar link", + "Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!", + "Create a model": "Criar um modelo", + "Create Account": "Criar Conta", + "Create new key": "Criar nova chave", + "Create new secret key": "Criar nova chave secreta", + "Created at": "Criado em", + "Created At": "Criado Em", + "Created by": "Criado por", + "CSV Import": "Importação CSV", + "Current Model": "Modelo Atual", + "Current Password": "Senha Atual", + "Custom": "Personalizado", + "Customize models for a specific purpose": "Personalizar modelos para uma finalidade específica", + "Dark": "Escuro", + "Dashboard": "Dashboard", + "Database": "Banco de dados", + "December": "Dezembro", + "Default": "Padrão", + "Default (Automatic1111)": "Padrão (Automatic1111)", + "Default (SentenceTransformers)": "Padrão (SentenceTransformers)", + "Default Model": "Modelo padrão", + "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": "Excluir", + "Delete a model": "Excluir um modelo", + "Delete All Chats": "Excluir todos os bate-papos", + "Delete chat": "Excluir bate-papo", + "Delete Chat": "Excluir Bate-papo", + "Delete chat?": "Excluir bate-papo?", + "Delete Doc": "", + "Delete function?": "Excluir função?", + "Delete prompt?": "Excluir prompt?", + "delete this link": "excluir este link", + "Delete tool?": "Deletar ferramenta?", + "Delete User": "Excluir Usuário", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} excluído", + "Deleted {{name}}": "Excluído {{nome}}", + "Description": "Descrição", + "Didn't fully follow instructions": "Não seguiu instruções com precisão", + "Disabled": "Desativado", + "Discover a function": "Descobrir uma função", + "Discover a model": "Descubra um modelo", + "Discover a prompt": "Descobrir um prompt", + "Discover a tool": "Descobrir uma ferramenta", + "Discover, download, and explore custom functions": "Descobrir, baixar e explorar funções personalizadas", + "Discover, download, and explore custom prompts": "Descobrir, baixar e explorar prompts personalizados", + "Discover, download, and explore custom tools": "Descobrir, baixar e explorar ferramentas personalizadas", + "Discover, download, and explore model presets": "Descobrir, baixar e explorar predefinições de modelo", + "Dismissible": "Dismissível", + "Display Emoji in Call": "Exibir Emoji na Chamada", + "Display the username instead of You in the Chat": "Exibir o nome de usuário em vez de Você no Bate-papo", + "Do not install functions from sources you do not fully trust.": "Não instale funções de fontes que você não confia totalmente.", + "Do not install tools from sources you do not fully trust.": "Não instale ferramentas de fontes que você não confia totalmente.", + "Document": "Documento", + "Document Settings": "Configurações de Documento", + "Documentation": "Documentação", + "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?", + "don't install random functions from sources you don't trust.": "não instale funções aleatórias de fontes que você não confia.", + "don't install random tools from sources you don't trust.": "não instale ferramentas aleatórias de fontes que você não confia.", + "Don't like the style": "Não gosta do estilo", + "Done": "Feito", + "Download": "Baixar", + "Download canceled": "Download cancelado", + "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": "Editar", + "Edit Doc": "Editar Documento", + "Edit Memory": "Editar Memória", + "Edit User": "Editar Usuário", + "ElevenLabs": "", + "Email": "E-mail", + "Embedding Batch Size": "Tamanho do Lote de Embedding", + "Embedding Model": "Modelo de Embedding", + "Embedding Model Engine": "Motor de Modelo de Embedding", + "Embedding model set to \"{{embedding_model}}\"": "Modelo de Embedding definido como \"{{embedding_model}}\"", + "Enable Chat History": "Ativar Histórico de Bate-papo", + "Enable Community Sharing": "Habilitar o compartilhamento da comunidade", + "Enable New Sign Ups": "Ativar Novas Inscrições", + "Enable Web Search": "Habilitar a Pesquisa na Web", + "Enabled": "Habilitado", + "Engine": "Motor", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Garanta que seu arquivo CSV inclua 4 colunas nesta ordem: Nome, E-mail, Senha, Função.", + "Enter {{role}} message here": "Digite a mensagem de {{role}} aqui", + "Enter a detail about yourself for your LLMs to recall": "Digite um detalhe sobre você para que seus LLMs possam lembrar", + "Enter api auth string (e.g. username:password)": "Digite a string de autenticação da API (por exemplo, nome de usuário:senha)", + "Enter Brave Search API Key": "Insira a chave da API do Brave Search", + "Enter Chunk Overlap": "Digite a Sobreposição de Fragmento", + "Enter Chunk Size": "Digite o Tamanho do Fragmento", + "Enter Github Raw URL": "Insira a URL bruta do Github", + "Enter Google PSE API Key": "Insira a chave da API PSE do Google", + "Enter Google PSE Engine Id": "Digite o ID do mecanismo PSE do Google", + "Enter Image Size (e.g. 512x512)": "Digite o Tamanho da Imagem (por exemplo, 512x512)", + "Enter language codes": "Digite os códigos de idioma", + "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 Score": "Digite a Pontuação", + "Enter Searxng Query URL": "Insira a URL de consulta do Searxng", + "Enter Serper API Key": "Digite a chave da API do Serper", + "Enter Serply API Key": "Digite a chave da API do Serply", + "Enter Serpstack API Key": "Digite a chave da API Serpstack", + "Enter stop sequence": "Digite a sequência de parada", + "Enter system prompt": "Digite o prompt do sistema", + "Enter Tavily API Key": "Digite a chave da API Tavily", + "Enter Tika Server URL": "Digite a URL do Servidor Tika", + "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 URL (e.g. http://localhost:11434)": "Digite a URL (por exemplo, http://localhost:11434)", + "Enter Your Email": "Digite seu E-mail", + "Enter Your Full Name": "Digite seu Nome Completo", + "Enter your message": "Digite sua mensagem", + "Enter Your Password": "Digite sua Senha", + "Enter Your Role": "Digite sua Função", + "Error": "Erro", + "Experimental": "Experimental", + "Export": "Exportação", + "Export All Chats (All Users)": "Exportar Todos os Bate-papos (Todos os Usuários)", + "Export chat (.json)": "Exportar chat (.json)", + "Export Chats": "Exportar Bate-papos", + "Export Documents Mapping": "Exportar Mapeamento de Documentos", + "Export Functions": "Exportar Funções", + "Export LiteLLM config.yaml": "Exportar config.yaml do LiteLLM", + "Export Models": "Modelos de Exportação", + "Export Prompts": "Exportar Prompts", + "Export Tools": "Exportar Ferramentas", + "External Models": "Modelos Externos", + "Failed to create API Key.": "Falha ao criar a Chave da API.", + "Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência", + "Failed to update settings": "Falha ao atualizar as configurações", + "February": "Fevereiro", + "Feel free to add specific details": "Sinta-se à vontade para adicionar detalhes específicos", + "File": "Arquivo", + "File Mode": "Modo de Arquivo", + "File not found.": "Arquivo não encontrado.", + "Files": "", + "Filter is now globally disabled": "O filtro agora está globalmente desativado", + "Filter is now globally enabled": "O filtro agora está globalmente ativado", + "Filters": "Filtros", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Impostação de impressão digital detectada: Não é possível usar iniciais como avatar. Padronizando para imagem de perfil padrão.", + "Fluidly stream large external response chunks": "Transmita com fluidez grandes blocos de resposta externa", + "Focus chat input": "Focar entrada de bate-papo", + "Followed instructions perfectly": "Seguiu instruções perfeitamente", + "Form": "Formulário", + "Format your variables using square brackets like this:": "Formate suas variáveis usando colchetes como este:", + "Frequency Penalty": "Penalidade de Frequência", + "Function created successfully": "Função criada com sucesso", + "Function deleted successfully": "Função deletada com sucesso", + "Function Description (e.g. A filter to remove profanity from text)": "Descrição da Função (por exemplo, Um filtro para remover palavrões do texto)", + "Function ID (e.g. my_filter)": "ID da Função (por exemplo, meu_filtro)", + "Function is now globally disabled": "A função agora está globalmente desativada", + "Function is now globally enabled": "A função agora está globalmente ativada", + "Function Name (e.g. My Filter)": "Nome da Função (por exemplo, Meu Filtro)", + "Function updated successfully": "Função atualizada com sucesso", + "Functions": "Funções", + "Functions allow arbitrary code execution": "Funções permitem a execução de código arbitrário", + "Functions allow arbitrary code execution.": "Funções permitem a execução de código arbitrário.", + "Functions imported successfully": "Funções importadas com sucesso", + "General": "Geral", + "General Settings": "Configurações Gerais", + "Generate Image": "Gerar imagem", + "Generating search query": "Gerando consulta de pesquisa", + "Generation Info": "Informações de Geração", + "Get up and running with": "Comece a trabalhar com", + "Global": "Global", + "Good Response": "Boa Resposta", + "Google PSE API Key": "Chave de API PSE do Google", + "Google PSE Engine Id": "ID do mecanismo PSE do Google", + "h:mm a": "h:mm a", + "has no conversations.": "não possui bate-papos.", + "Hello, {{name}}": "Olá, {{name}}", + "Help": "Ajuda", + "Hide": "Ocultar", + "Hide Model": "Esconder modelo", + "How can I help you today?": "Como posso ajudá-lo hoje?", + "Hybrid Search": "Pesquisa Híbrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "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 Functions": "Importar Funções", + "Import Models": "Modelos de Importação", + "Import Prompts": "Importar Prompts", + "Import Tools": "Importar Ferramentas", + "Include `--api-auth` flag when running stable-diffusion-webui": "Inclua a flag `--api-auth` ao executar stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Inclua a flag `--api` ao executar stable-diffusion-webui", + "Info": "Informação", + "Input commands": "Comandos de entrada", + "Install from Github URL": "Instalar a partir do URL do Github", + "Instant Auto-Send After Voice Transcription": "Autoenvio Instantâneo Após Transcrição de Voz", + "Interface": "Interface", + "Invalid Tag": "Etiqueta Inválida", + "January": "Janeiro", + "join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.", + "JSON": "JSON", + "JSON Preview": "Visualização JSON", + "July": "Julho", + "June": "Junho", + "JWT Expiration": "Expiração JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Manter Vivo", + "Keyboard shortcuts": "Atalhos de teclado", + "Knowledge": "Conhecimento", + "Language": "Idioma", + "large language models, locally.": "modelos largos de linguagem, localmente", + "Last Active": "Ativo por último", + "Last Modified": "Ultima Modificação", + "Light": "Claro", + "Listening...": "Escutando...", + "LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", + "Local Models": "Modelos Locais", + "LTR": "LTR", + "Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI", + "Make sure to enclose them with": "Certifique-se de colocá-los entre", + "Manage": "Gerenciar", + "Manage Models": "Gerenciar Modelos", + "Manage Ollama Models": "Gerenciar Modelos Ollama", + "Manage Pipelines": "Gerenciar Pipelines", + "Manage Valves": "Gerenciar Válvulas", + "March": "Março", + "Max Tokens (num_predict)": "Fichas máximas (num_predict)", + "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.", + "May": "Maio", + "Memories accessible by LLMs will be shown here.": "Memórias acessíveis por LLMs serão mostradas aqui.", + "Memory": "Memória", + "Memory added successfully": "Memória adicionada com sucesso", + "Memory cleared successfully": "Memória limpa com sucesso", + "Memory deleted successfully": "Memória excluída com sucesso", + "Memory updated successfully": "Memória atualizada com sucesso", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens que você enviar após criar seu link não serão compartilhadas. Os usuários com o URL poderão visualizar o bate-papo compartilhado.", + "Minimum Score": "Pontuação Mínima", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD/MM/YYYY", + "MMMM DD, YYYY HH:mm": "DD/MM/YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "DD/MM/YYYY hh:mm:ss A", + "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}} is not vision capable": "O modelo {{modelName}} não é capaz de visão", + "Model {{name}} is now {{status}}": "O modelo {{name}} agora é {{status}}", + "Model created successfully!": "Modelo criado com sucesso!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Otkrivena putanja datoteke modela. Skraćeno ime modela je potrebno za ažuriranje, ne može se nastaviti.", + "Model ID": "ID do modelo", + "Model not selected": "Modelo não selecionado", + "Model Params": "Params Modelo", + "Model updated successfully": "Modelo atualizado com sucesso", + "Model Whitelisting": "Lista de Permissões de Modelo", + "Model(s) Whitelisted": "Modelo(s) na Lista de Permissões", + "Modelfile Content": "Conteúdo do Arquivo de Modelo", + "Models": "Modelos", + "More": "Mais", + "Name": "Nome", + "Name Tag": "Nome da Tag", + "Name your model": "Nomeie seu modelo", + "New Chat": "Novo Bate-papo", + "New Password": "Nova Senha", + "No content to speak": "Nenhum conteudo para falar", + "No documents found": "Nenhuma documento encontrado", + "No file selected": "Nenhum arquivo selecionado", + "No results found": "Nenhum resultado encontrado", + "No search query generated": "Nenhuma consulta de pesquisa gerada", + "No source available": "Nenhuma fonte disponível", + "No valves to update": "Nenhuma válvula para atualizar", + "None": "Nenhum", + "Not factually correct": "Não é correto em termos factuais", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Se você definir uma pontuação mínima, a pesquisa só retornará documentos com uma pontuação maior ou igual à pontuação mínima.", + "Notifications": "Notificações da Área de Trabalho", + "November": "Novembro", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth ID", + "October": "Outubro", + "Off": "Desligado", + "Okay, Let's Go!": "Ok, Vamos Lá!", + "OLED Dark": "OLED Escuro", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API Ollama desativada", + "Ollama API is disabled": "", + "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! There was an error in the previous response. Please try again or contact admin.": "", + "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 AI (Dall-E)": "OpenAI (Dall-E)", + "Open new chat": "Abrir novo bate-papo", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "A versão do Open WebUI (v{{OPEN_WEBUI_VERSION}}) é inferior à versão necessária (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Configuração da API OpenAI", + "OpenAI API Key is required.": "A Chave da API OpenAI é obrigatória.", + "OpenAI URL/Key required.": "URL/Chave da API OpenAI é necessária.", + "or": "ou", + "Other": "Outro", + "Password": "Senha", + "PDF document (.pdf)": "Documento PDF (.pdf)", + "PDF Extract Images (OCR)": "Extrair Imagens de PDF (OCR)", + "pending": "pendente", + "Permission denied when accessing media devices": "Permissão negada ao acessar dispositivos de mídia", + "Permission denied when accessing microphone": "Permissão negada ao acessar o microfone", + "Permission denied when accessing microphone: {{error}}": "Permissão negada ao acessar o microfone: {{error}}", + "Personalization": "Personalização", + "Pin": "Fixar", + "Pinned": "Fixada", + "Pipeline deleted successfully": "Pipeline excluída com sucesso", + "Pipeline downloaded successfully": "Pipeline baixada com sucesso", + "Pipelines": "Pipelines", + "Pipelines Not Detected": "Pipelines não detectado", + "Pipelines Valves": "Válvulas de Dutos", + "Plain text (.txt)": "Texto sem formatação (.txt)", + "Playground": "Parque infantil", + "Please carefully review the following warnings:": "Por favor, revise cuidadosamente os seguintes avisos:", + "Positive attitude": "Atitude Positiva", + "Previous 30 days": "Últimos 30 dias", + "Previous 7 days": "Últimos 7 dias", + "Profile Image": "Imagem de Perfil", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (ex.: Dê-me um fatídico sobre o Império Romano)", + "Prompt Content": "Conteúdo do Prompt", + "Prompt suggestions": "Sugestões de Prompt", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Extrair \"{{searchValue}}\" do Ollama.com", + "Pull a model from Ollama.com": "Extrair um modelo do Ollama.com", + "Query Params": "Parâmetros de Consulta", + "RAG Template": "Modelo RAG", + "Read Aloud": "Ler em Voz Alta", + "Record voice": "Gravar voz", + "Redirecting you to OpenWebUI Community": "Redirecionando você para a Comunidade OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Recusado quando não deveria", + "Regenerate": "Regenerar", + "Release Notes": "Notas de Lançamento", + "Remove": "Remover", + "Remove Model": "Remover Modelo", + "Rename": "Renomear", + "Repeat Last N": "Repetir Últimos N", + "Request Mode": "Modo de Solicitação", + "Reranking Model": "Modelo de Reranking", + "Reranking model disabled": "Modelo de Reranking desativado", + "Reranking model set to \"{{reranking_model}}\"": "Modelo de Reranking definido como \"{{reranking_model}}\"", + "Reset": "Resetar", + "Reset Upload Directory": "Resetar Diretório de Upload", + "Reset Vector Storage": "Redefinir Armazenamento de Vetor", + "Response AutoCopy to Clipboard": "Cópia Automática da Resposta para a Área de Transferência", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Notificações de resposta não podem ser ativadas, pois as permissões do site foram negadas. Visite as configurações do seu navegador para conceder o acesso necessário.", + "Role": "Função", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "Execute Llama 2, Code Llama e outros modelos. Personalize e crie o seu próprio.", + "Running": "Executando", + "Save": "Salvar", + "Save & Create": "Salvar e Criar", + "Save & Update": "Salvar e Atualizar", + "Save Tag": "", + "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 a model": "Pesquisar um modelo", + "Search Chats": "Pesquisar bate-papos", + "Search Documents": "Pesquisar Documentos", + "Search Functions": "Pesquisar Funções", + "Search Models": "Pesquisar Modelos", + "Search Prompts": "Pesquisar Prompts", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "Limite de comprimento do prompt de geração de consulta de pesquisa", + "Search Result Count": "Contagem de resultados de pesquisa", + "Search Tools": "Ferramentas de Pesquisa", + "Searched {{count}} sites_one": "Pesquisado {{count}} sites_one", + "Searched {{count}} sites_many": "Pesquisado {{count}} sites_many", + "Searched {{count}} sites_other": "Pesquisado {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "Pesquisando \"{{searchQuery}}\"", + "Searxng Query URL": "URL de consulta Searxng", + "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 base model": "Selecione um modelo base", + "Select a engine": "Selecione um motor", + "Select a function": "Selecione uma função", + "Select a mode": "Selecione um modo", + "Select a model": "Selecione um modelo", + "Select a pipeline": "Selecione um pipeline", + "Select a pipeline url": "Selecione uma URL de pipeline", + "Select a tool": "Selecione uma ferramenta", + "Select an Ollama instance": "Selecione uma instância Ollama", + "Select Documents": "Selecione Documentos", + "Select model": "Selecione um modelo", + "Select only one model to call": "Selecione apenas um modelo para chamar", + "Selected model(s) do not support image inputs": "O(s) modelo(s) selecionado(s) não suporta(m) entrada(s) de imagem", + "Send": "Enviar", + "Send a Message": "Enviar uma Mensagem", + "Send message": "Enviar mensagem", + "September": "Setembro", + "Serper API Key": "Chave de API Serper", + "Serply API Key": "Chave de API Serply", + "Serpstack API Key": "Chave de API Serpstack", + "Server connection verified": "Conexão com o servidor verificada", + "Set as default": "Definir como padrão", + "Set Default Model": "Definir Modelo Padrão", + "Set embedding model (e.g. {{model}})": "Definir modelo de vetorização (ex.: {{model}})", + "Set Image Size": "Definir Tamanho da Imagem", + "Set reranking model (e.g. {{model}})": "Definir modelo de reranking (ex.: {{model}})", + "Set Steps": "Definir Etapas", + "Set Task Model": "Definir modelo de tarefa", + "Set Voice": "Definir Voz", + "Settings": "Configurações", + "Settings saved successfully!": "Configurações salvas com sucesso!", + "Settings updated successfully": "Configurações atualizadas com sucesso", + "Share": "Compartilhar", + "Share Chat": "Compartilhar Bate-papo", + "Share to OpenWebUI Community": "Compartilhar com a Comunidade OpenWebUI", + "short-summary": "resumo-curto", + "Show": "Mostrar", + "Show Admin Details in Account Pending Overlay": "Mostrar Detalhes do Administrador na Sobreposição de Conta Pendente", + "Show Model": "Mostrar Modelo", + "Show shortcuts": "Mostrar", + "Show your support!": "Mostre seu apoio!", + "Showcased creativity": "Criatividade Exibida", + "Sign in": "Entrar", + "Sign Out": "Sair", + "Sign up": "Inscrever-se", + "Signing in": "Entrando", + "Source": "Fonte", + "Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}", + "Speech-to-Text Engine": "Mecanismo de Fala para Texto", + "Stop Sequence": "Sequência de Parada", + "STT Model": "Modelo STT", + "STT Settings": "Configurações STT", + "Submit": "Enviar", + "Subtitle (e.g. about the Roman Empire)": "Subtítulo (ex.: sobre o Império Romano)", + "Success": "Sucesso", + "Successfully updated.": "Atualizado com sucesso.", + "Suggested": "Sugerido", + "Support": "Suporte", + "Support this plugin:": "Suporte esse plugin:", + "System": "Sistema", + "System Prompt": "Prompt do Sistema", + "Tags": "Tags", + "Tap to interrupt": "Toque para interromper", + "Tavily API Key": "Chave da API Tavily", + "Tell us more:": "Dê-nos mais:", + "Temperature": "Temperatura", + "Template": "Modelo", + "Text Completion": "Complemento de Texto", + "Text-to-Speech Engine": "Mecanismo de Texto para Fala", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Obrigado pelo seu feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Os desenvolvedores por trás deste plugin são voluntários apaixonados da comunidade. Se você achar este plugin útil, considere contribuir para o seu desenvolvimento.", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "O score deve ser um valor entre 0.0 (0%) e 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "Pensando...", + "This action cannot be undone. Do you wish to continue?": "Essa ação não pode ser desfeita. Deseja continuar?", + "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 is an experimental feature, it may not function as expected and is subject to change at any time.": "Essa é uma funcionalidade experimental, pode não funcionar como esperado e está sujeita a alterações a qualquer momento.", + "This setting does not sync across browsers or devices.": "Esta configuração não sincroniza entre navegadores ou dispositivos.", + "This will delete": "Isso irá apagar", + "Thorough explanation": "Explicação Completa", + "Tika": "Tika", + "Tika Server URL required.": "Url do Servidor Tika é obrigatória.", + "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 (e.g. Tell me a fun fact)": "Título (ex.: Dê-me uma curiosidade)", + "Title Auto-Generation": "Geração Automática de Título", + "Title cannot be an empty string.": "Título não pode ser uma string vazia.", + "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 access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para acessar o WebUI, entre em contato com o administrador. Os administradores podem gerenciar os status dos usuários no Painel de Administração.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Para adicionar documentos aqui, carregue-os primeiro para o espaço de trabalho \"Documentos\".", + "to chat input.": "para a entrada de bate-papo.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Para selecionar filtros aqui, adicione-os primeiro ao espaço de trabalho \"Funções\".", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Para selecionar toolkits aqui, adicione-os primeiro ao espaço de trabalho \"Ferramentas\".", + "Today": "Hoje", + "Toggle settings": "Alternar configurações", + "Toggle sidebar": "Alternar barra lateral", + "Tokens To Keep On Context Refresh (num_keep)": "Tokens a Manter na Atualização de Contexto (num_keep)", + "Tool created successfully": "Ferramenta criada com sucesso", + "Tool deleted successfully": "Ferramenta excluída com sucesso", + "Tool imported successfully": "Ferramenta importada com sucesso", + "Tool updated successfully": "Ferramenta atualizada com sucesso", + "Toolkit Description (e.g. A toolkit for performing various operations)": "Descrição do Toolkit (por exemplo, Um toolkit para realizar várias operações)", + "Toolkit ID (e.g. my_toolkit)": "Identificação do Toolkit (por exemplo, meu_toolkit)", + "Toolkit Name (e.g. My ToolKit)": "Nome do Toolkit (por exemplo, Meu Toolkit)", + "Tools": "Ferramentas", + "Tools are a function calling system with arbitrary code execution": "Ferramentas são um sistema de chamada de função com execução de código arbitrário", + "Tools have a function calling system that allows arbitrary code execution": "Ferramentas têm um sistema de chamada de função que permite a execução de código arbitrário", + "Tools have a function calling system that allows arbitrary code execution.": "Ferramentas têm um sistema de chamada de função que permite a execução de código arbitrário.", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemas para acessar o Ollama?", + "TTS Model": "Modelo TTS", + "TTS Settings": "Configurações TTS", + "TTS Voice": "Voz TTS", + "Type": "Tipo", + "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}}.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "tipo de arquivo desconhecido '{{file_type}}'. Prosseguindo com o upload do arquivo de qualquer maneira.", + "Unpin": "Desfixar", + "Update": "Atualizar", + "Update and Copy Link": "Atualizar e Copiar Link", + "Update password": "Atualizar senha", + "Updated at": "Atualizado em", + "Upload": "Upload", + "Upload a GGUF model": "Upload de modelo GGUF", + "Upload Files": "Upload de Arquivos", + "Upload Pipeline": "Upload de Pipeline", + "Upload Progress": "Progresso de Upload", + "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", + "Use Initials": "Usar Iniciais", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "usuário", + "User location successfully retrieved.": "Localização do usuário recuperada com sucesso.", + "User Permissions": "Permissões do Usuário", + "Users": "Usuários", + "Utilize": "Utilizar", + "Valid time units:": "Unidades de tempo válidas:", + "Valves": "Válvulas", + "Valves updated": "Válvulas atualizadas", + "Valves updated successfully": "Válvulas atualizadas com sucesso", + "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", + "Voice": "Voz", + "Warning": "Aviso", + "Warning:": "Aviso:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Aviso: Se você atualizar ou alterar seu modelo de incorporação, você precisará reimportar todos os documentos.", + "Web": "Web", + "Web API": "Web API", + "Web Loader Settings": "Configurações do Carregador da Web", + "Web Params": "Parâmetros da Web", + "Web Search": "Pesquisa na Web", + "Web Search Engine": "Mecanismo de Busca na Web", + "Webhook URL": "URL do Webhook", + "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)", + "Widescreen Mode": "Modo de Tela Larga", + "Workspace": "Espaço de trabalho", + "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].", + "Yesterday": "Ontem", + "You": "Você", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Você pode personalizar suas interações com LLMs adicionando memórias através do botão 'Gerenciar' abaixo, tornando-as mais úteis e adaptadas a você.", + "You cannot clone a base model": "Não é possível clonar um modelo base", + "You have no archived conversations.": "Você não tem conversas arquivadas.", + "You have shared this chat": "Você compartilhou esta conversa", + "You're a helpful assistant.": "Você é um assistente útil.", + "You're now logged in.": "Você está conectado agora.", + "Your account status is currently pending activation.": "Sua conta está atualmente pendente de ativação.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Sua contribuição inteira irá diretamente para o desenvolvedor do plugin; Open WebUI não cobra nenhuma porcentagem. No entanto, a plataforma de financiamento escolhida pode ter suas próprias taxas.", + "Youtube": "Youtube", + "Youtube Loader Settings": "Configurações do carregador do Youtube" +} diff --git a/src/lib/i18n/locales/pt-PT/translation.json b/src/lib/i18n/locales/pt-PT/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..5ec62b31cb086b336193f7085802b3192223b3e6 --- /dev/null +++ b/src/lib/i18n/locales/pt-PT/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ou '-1' para nenhuma expiração.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(por exemplo, `sh webui.sh --api`)", + "(latest)": "(mais recente)", + "{{ models }}": "{{ modelos }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Não é possível excluir um modelo base", + "{{modelName}} is thinking...": "{{modelName}} está a pensar...", + "{{user}}'s Chats": "{{user}}'s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Backend Necessário", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Um modelo de tarefa é usado ao executar tarefas como gerar títulos para bate-papos e consultas de pesquisa na Web", + "a user": "um utilizador", + "About": "Acerca de", + "Account": "Conta", + "Account Activation Pending": "Ativação da Conta Pendente", + "Accurate information": "Informações precisas", + "Actions": "", + "Active Users": "Utilizadores Ativos", + "Add": "Adicionar", + "Add a model id": "Adicionar um ID de modelo", + "Add a short description about what this model does": "Adicione uma breve descrição sobre o que este modelo faz", + "Add a short title for this prompt": "Adicione um título curto para este prompt", + "Add a tag": "Adicionar uma tag", + "Add custom prompt": "Adicionar um prompt curto", + "Add Docs": "Adicionar Documentos", + "Add Files": "Adicionar Ficheiros", + "Add Memory": "Adicionar memória", + "Add message": "Adicionar mensagem", + "Add Model": "Adicionar modelo", + "Add Tag": "", + "Add Tags": "adicionar tags", + "Add User": "Adicionar Utilizador", + "Adjusting these settings will apply changes universally to all users.": "Ajustar essas configurações aplicará alterações universalmente a todos os utilizadores.", + "admin": "administrador", + "Admin": "Admin", + "Admin Panel": "Painel do Administrador", + "Admin Settings": "Configurações do Administrador", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Parâmetros Avançados", + "Advanced Params": "Params Avançados", + "all": "todos", + "All Documents": "Todos os Documentos", + "All Users": "Todos os utilizadores", + "Allow": "Permitir", + "Allow Chat Deletion": "Permitir Exclusão de Conversa", + "Allow non-local voices": "Permitir vozes não locais", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "caracteres alfanuméricos e hífens", + "Already have an account?": "Já tem uma conta?", + "an assistant": "um assistente", + "and": "e", + "and create a new shared link.": "e criar um novo link partilhado.", + "API Base URL": "URL Base da API", + "API Key": "Chave da API", + "API Key created.": "Chave da API criada.", + "API keys": "Chaves da API", + "April": "Abril", + "Archive": "Arquivo", + "Archive All Chats": "Arquivar todos os chats", + "Archived Chats": "Conversas arquivadas", + "are allowed - Activate this command by typing": "são permitidos - Ative este comando escrevendo", + "Are you sure?": "Tem a certeza?", + "Attach file": "Anexar ficheiro", + "Attention to detail": "Detalhado", + "Audio": "Áudio", + "Audio settings updated successfully": "", + "August": "Agosto", + "Auto-playback response": "Reprodução automática da resposta", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "URL Base do AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "O URL Base do AUTOMATIC1111 é obrigatório.", + "available!": "disponível!", + "Back": "Voltar", + "Bad Response": "Resposta má", + "Banners": "Estandartes", + "Base Model (From)": "Modelo Base (De)", + "Batch Size (num_batch)": "", + "before": "antes", + "Being lazy": "Ser preguiçoso", + "Brave Search API Key": "Chave da API de Pesquisa Brave", + "Bypass SSL verification for Websites": "Ignorar verificação SSL para sites", + "Call": "Chamar", + "Call feature is not supported when using Web STT engine": "A funcionalide de Chamar não é suportada quando usa um motor Web STT", + "Camera": "Camera", + "Cancel": "Cancelar", + "Capabilities": "Capacidades", + "Change Password": "Alterar Senha", + "Chat": "Conversa", + "Chat Background Image": "", + "Chat Bubble UI": "Bolha UI da Conversa", + "Chat Controls": "", + "Chat direction": "Direção da Conversa", + "Chat History": "Histórico da Conversa", + "Chat History is off for this browser.": "O histórico da conversa está desativado para este navegador.", + "Chats": "Conversas", + "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 guardar...", + "Chunk Overlap": "Sobreposição de Fragmento", + "Chunk Params": "Parâmetros de Fragmento", + "Chunk Size": "Tamanho do Fragmento", + "Citation": "Citação", + "Clear memory": "Limpar memória", + "Click here for help.": "Clique aqui para obter ajuda.", + "Click here to": "Clique aqui para", + "Click here to download user import template file.": "", + "Click here to select": "Clique aqui para selecionar", + "Click here to select a csv file.": "Clique aqui para selecionar um ficheiro csv.", + "Click here to select a py file.": "Clique aqui para selecionar um ficheiro py", + "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 utilizador para alterar a função de um utilizador.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Clonar", + "Close": "Fechar", + "Code formatted successfully": "", + "Collection": "Coleção", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL Base do ComfyUI", + "ComfyUI Base URL is required.": "O URL Base do ComfyUI é obrigatório.", + "Command": "Comando", + "Concurrent Requests": "Solicitações simultâneas", + "Confirm": "", + "Confirm Password": "Confirmar Senha", + "Confirm your action": "", + "Connections": "Conexões", + "Contact Admin for WebUI Access": "Contatar Admin para acesso ao WebUI", + "Content": "Conteúdo", + "Content Extraction": "", + "Context Length": "Comprimento do Contexto", + "Continue Response": "Continuar resposta", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "URL de Conversa partilhado copiada com sucesso!", + "Copy": "Copiar", + "Copy last code block": "Copiar último bloco de código", + "Copy last response": "Copiar última resposta", + "Copy Link": "Copiar link", + "Copying to clipboard was successful!": "Cópia para a área de transferência bem-sucedida!", + "Create a model": "Criar um modelo", + "Create Account": "Criar Conta", + "Create new key": "Criar nova chave", + "Create new secret key": "Criar nova chave secreta", + "Created at": "Criado em", + "Created At": "Criado em", + "Created by": "", + "CSV Import": "", + "Current Model": "Modelo Atual", + "Current Password": "Senha Atual", + "Custom": "Personalizado", + "Customize models for a specific purpose": "Personalizar modelos para uma finalidade específica", + "Dark": "Escuro", + "Dashboard": "Painel", + "Database": "Base de dados", + "December": "Dezembro", + "Default": "Padrão", + "Default (Automatic1111)": "Padrão (Automatic1111)", + "Default (SentenceTransformers)": "Padrão (SentenceTransformers)", + "Default Model": "Modelo padrão", + "Default model updated": "Modelo padrão atualizado", + "Default Prompt Suggestions": "Sugestões de Prompt Padrão", + "Default User Role": "Função de Utilizador Padrão", + "delete": "apagar", + "Delete": "Apagar", + "Delete a model": "Apagar um modelo", + "Delete All Chats": "Apagar todas as conversas", + "Delete chat": "Apagar conversa", + "Delete Chat": "Apagar Conversa", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "apagar este link", + "Delete tool?": "", + "Delete User": "Apagar Utilizador", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} apagado", + "Deleted {{name}}": "Apagado {{name}}", + "Description": "Descrição", + "Didn't fully follow instructions": "Não seguiu instruções com precisão", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Descubra um modelo", + "Discover a prompt": "Descobrir um prompt", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Descubra, descarregue e explore prompts personalizados", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Descubra, descarregue e explore predefinições de modelo", + "Dismissible": "Dispensável", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Exibir o nome de utilizador em vez de Você na Conversa", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Documento", + "Document Settings": "Configurações de Documento", + "Documentation": "Documentação", + "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 os seus dados permanecem seguros no seu servidor alojado localmente.", + "Don't Allow": "Não Permitir", + "Don't have an account?": "Não tem uma conta?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Não gosta do estilo", + "Done": "", + "Download": "Descarregar", + "Download canceled": "Download cancelado", + "Download Database": "Descarregar Base de Dados", + "Drop any files here to add to the conversation": "Largue os ficheiros 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": "Editar", + "Edit Doc": "Editar Documento", + "Edit Memory": "", + "Edit User": "Editar Utilizador", + "ElevenLabs": "", + "Email": "E-mail", + "Embedding Batch Size": "Tamanho do Lote do Embedding", + "Embedding Model": "Modelo de Embedding", + "Embedding Model Engine": "Motor de Modelo de Embedding", + "Embedding model set to \"{{embedding_model}}\"": "Modelo de Embedding definido como \"{{embedding_model}}\"", + "Enable Chat History": "Ativar Histórico de Conversas", + "Enable Community Sharing": "Active a Partilha da Comunidade", + "Enable New Sign Ups": "Ativar Novas Inscrições", + "Enable Web Search": "Ativar pesquisa na Web", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Confirme que o seu ficheiro CSV inclui 4 colunas nesta ordem: Nome, E-mail, Senha, Função.", + "Enter {{role}} message here": "Escreva a mensagem de {{role}} aqui", + "Enter a detail about yourself for your LLMs to recall": "Escreva um detalhe sobre você para que os seus LLMs possam lembrar-se", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Escreva a chave da API do Brave Search", + "Enter Chunk Overlap": "Escreva a Sobreposição de Fragmento", + "Enter Chunk Size": "Escreva o Tamanho do Fragmento", + "Enter Github Raw URL": "Escreva o URL cru do Github", + "Enter Google PSE API Key": "Escreva a chave da API PSE do Google", + "Enter Google PSE Engine Id": "Escreva o ID do mecanismo PSE do Google", + "Enter Image Size (e.g. 512x512)": "Escreva o Tamanho da Imagem (por exemplo, 512x512)", + "Enter language codes": "Escreva os códigos de idioma", + "Enter model tag (e.g. {{modelTag}})": "Escreva a tag do modelo (por exemplo, {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Escreva o Número de Etapas (por exemplo, 50)", + "Enter Score": "Escreva a Pontuação", + "Enter Searxng Query URL": "Escreva o URL da Pesquisa Searxng", + "Enter Serper API Key": "Escreva a chave da API Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Escreva a chave da API Serpstack", + "Enter stop sequence": "Escreva a sequência de paragem", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Escreva o Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Escreva o URL (por exemplo, http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Escreva o URL (por exemplo, http://localhost:11434)", + "Enter Your Email": "Escreva o seu E-mail", + "Enter Your Full Name": "Escreva o seu Nome Completo", + "Enter your message": "", + "Enter Your Password": "Escreva a sua Senha", + "Enter Your Role": "Escreva a sua Função", + "Error": "Erro", + "Experimental": "Experimental", + "Export": "Exportar", + "Export All Chats (All Users)": "Exportar Todas as Conversas (Todos os Utilizadores)", + "Export chat (.json)": "Exportar Conversa (.json)", + "Export Chats": "Exportar Conversas", + "Export Documents Mapping": "Exportar Mapeamento de Documentos", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Modelos de Exportação", + "Export Prompts": "Exportar Prompts", + "Export Tools": "", + "External Models": "Modelos Externos", + "Failed to create API Key.": "Falha ao criar a Chave da API.", + "Failed to read clipboard contents": "Falha ao ler o conteúdo da área de transferência", + "Failed to update settings": "Falha ao atualizar as definições", + "February": "Fevereiro", + "Feel free to add specific details": "Sinta-se à vontade para adicionar detalhes específicos", + "File": "", + "File Mode": "Modo de Ficheiro", + "File not found.": "Ficheiro não encontrado.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Detectada falsificação da impressão digital: Não é possível usar iniciais como avatar. A usar a imagem de perfil padrão.", + "Fluidly stream large external response chunks": "Transmita com fluidez grandes blocos de resposta externa", + "Focus chat input": "Focar na conversa", + "Followed instructions perfectly": "Seguiu instruções perfeitamente", + "Form": "", + "Format your variables using square brackets like this:": "Formate as suas variáveis usando parenteses rectos como este:", + "Frequency Penalty": "Penalidade de Frequência", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Geral", + "General Settings": "Configurações Gerais", + "Generate Image": "Gerar imagem", + "Generating search query": "A gerar a consulta da pesquisa", + "Generation Info": "Informações de Geração", + "Get up and running with": "", + "Global": "", + "Good Response": "Boa Resposta", + "Google PSE API Key": "Chave da API PSE do Google", + "Google PSE Engine Id": "ID do mecanismo PSE do Google", + "h:mm a": "h:mm a", + "has no conversations.": "não possui conversas.", + "Hello, {{name}}": "Olá, {{name}}", + "Help": "Ajuda", + "Hide": "Ocultar", + "Hide Model": "", + "How can I help you today?": "Como posso ajudá-lo hoje?", + "Hybrid Search": "Pesquisa Híbrida", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Geração de Imagens (Experimental)", + "Image Generation Engine": "Mecanismo de Geração de Imagens", + "Image Settings": "Configurações da Imagem", + "Images": "Imagens", + "Import Chats": "Importar Conversas", + "Import Documents Mapping": "Importar Mapeamento de Documentos", + "Import Functions": "", + "Import Models": "Importar Modelos", + "Import Prompts": "Importar Prompts", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Inclua a flag `--api` ao executar stable-diffusion-webui", + "Info": "Informação", + "Input commands": "Comandos de entrada", + "Install from Github URL": "Instalar a partir do URL do Github", + "Instant Auto-Send After Voice Transcription": "Enviar automaticamente depois da transcrição da voz", + "Interface": "Interface", + "Invalid Tag": "Etiqueta Inválida", + "January": "Janeiro", + "join our Discord for help.": "junte-se ao nosso Discord para obter ajuda.", + "JSON": "JSON", + "JSON Preview": "Pré-visualização JSON", + "July": "Julho", + "June": "Junho", + "JWT Expiration": "Expiração JWT", + "JWT Token": "Token JWT", + "Keep Alive": "Manter Vivo", + "Keyboard shortcuts": "Atalhos de teclado", + "Knowledge": "Conhecimento", + "Language": "Idioma", + "large language models, locally.": "", + "Last Active": "Último Ativo", + "Last Modified": "", + "Light": "Claro", + "Listening...": "A escutar...", + "LLMs can make mistakes. Verify important information.": "LLMs podem cometer erros. Verifique informações importantes.", + "Local Models": "Modelos Locais", + "LTR": "LTR", + "Made by OpenWebUI Community": "Feito pela Comunidade OpenWebUI", + "Make sure to enclose them with": "Certifique-se de colocá-los entre", + "Manage": "Gerir", + "Manage Models": "Gerir Modelos", + "Manage Ollama Models": "Gerir Modelos Ollama", + "Manage Pipelines": "Gerir pipelines", + "Manage Valves": "", + "March": "Março", + "Max Tokens (num_predict)": "Máx Tokens (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "O máximo de 3 modelos podem ser descarregados simultaneamente. Tente novamente mais tarde.", + "May": "Maio", + "Memories accessible by LLMs will be shown here.": "Memórias acessíveis por LLMs serão mostradas aqui.", + "Memory": "Memória", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Mensagens que você enviar após criar o seu link não serão partilhadas. Os utilizadores com o URL poderão visualizar a conversa partilhada.", + "Minimum Score": "Mínimo de Pontuação", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD/MM/YYYY", + "MMMM DD, YYYY HH:mm": "DD/MM/YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "O modelo '{{modelName}}' foi descarregado com sucesso.", + "Model '{{modelTag}}' is already in queue for downloading.": "O modelo '{{modelTag}}' já está na fila para descarregar.", + "Model {{modelId}} not found": "Modelo {{modelId}} não foi encontrado", + "Model {{modelName}} is not vision capable": "O modelo {{modelName}} não é capaz de visão", + "Model {{name}} is now {{status}}": "Modelo {{name}} agora é {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Dtectado caminho do sistema de ficheiros do modelo. É necessário o nome curto do modelo para atualização, não é possível continuar.", + "Model ID": "ID do modelo", + "Model not selected": "Modelo não selecionado", + "Model Params": "Params Modelo", + "Model updated successfully": "", + "Model Whitelisting": "Lista de Permissões do Modelo", + "Model(s) Whitelisted": "Modelo(s) na Lista de Permissões", + "Modelfile Content": "Conteúdo do Ficheiro do Modelo", + "Models": "Modelos", + "More": "Mais", + "Name": "Nome", + "Name Tag": "Etiqueta de Nome", + "Name your model": "Atribua um nome ao seu modelo", + "New Chat": "Nova Conversa", + "New Password": "Nova Senha", + "No content to speak": "", + "No documents found": "Não foram encontrados documentos", + "No file selected": "", + "No results found": "Não foram encontrados resultados", + "No search query generated": "Não foi gerada nenhuma consulta de pesquisa", + "No source available": "Nenhuma fonte disponível", + "No valves to update": "", + "None": "Nenhum", + "Not factually correct": "Não é correto em termos factuais", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Nota: Se você definir uma pontuação mínima, a pesquisa só retornará documentos com uma pontuação maior ou igual à pontuação mínima.", + "Notifications": "Notificações da Área de Trabalho", + "November": "Novembro", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Outubro", + "Off": "Desligado", + "Okay, Let's Go!": "Ok, Vamos Lá!", + "OLED Dark": "OLED Escuro", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API do Ollama desativada", + "Ollama API is disabled": "A API do Ollama está desactivada", + "Ollama Version": "Versão do Ollama", + "On": "Ligado", + "Only": "Apenas", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Apenas 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.": "Epá! Segura-te! Os teus ficheiros ainda estão no forno de processamento. Estamos a cozinhá-los 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.": "Epá! Parece que o URL é inválido. Verifique novamente e tente outra vez.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Epá! Você está a usar um método não suportado (somente frontend). Por favor, sirva o WebUI a partir do backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Abrir nova conversa", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Configuração da API OpenAI", + "OpenAI API Key is required.": "A Chave da API OpenAI é obrigatória.", + "OpenAI URL/Key required.": "URL/Chave da API OpenAI é necessária.", + "or": "ou", + "Other": "Outro", + "Password": "Senha", + "PDF document (.pdf)": "Documento PDF (.pdf)", + "PDF Extract Images (OCR)": "Extrair Imagens de PDF (OCR)", + "pending": "pendente", + "Permission denied when accessing media devices": "A permissão foi negada ao aceder aos dispositivos de media", + "Permission denied when accessing microphone": "A permissão foi negada ao aceder ao microfone", + "Permission denied when accessing microphone: {{error}}": "A permissão foi negada ao aceder o microfone: {{error}}", + "Personalization": "Personalização", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Condutas", + "Pipelines Not Detected": "", + "Pipelines Valves": "Válvulas de Condutas", + "Plain text (.txt)": "Texto sem formatação (.txt)", + "Playground": "Recreio", + "Please carefully review the following warnings:": "", + "Positive attitude": "Atitude Positiva", + "Previous 30 days": "Últimos 30 dias", + "Previous 7 days": "Últimos 7 dias", + "Profile Image": "Imagem de Perfil", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (ex.: Dê-me um facto divertido sobre o Império Romano)", + "Prompt Content": "Conteúdo do Prompt", + "Prompt suggestions": "Sugestões de Prompt", + "Prompts": "Prompts", + "Pull \"{{searchValue}}\" from Ollama.com": "Puxar \"{{searchValue}}\" do Ollama.com", + "Pull a model from Ollama.com": "Puxar um modelo do Ollama.com", + "Query Params": "Parâmetros de Consulta", + "RAG Template": "Modelo RAG", + "Read Aloud": "Ler em Voz Alta", + "Record voice": "Gravar voz", + "Redirecting you to OpenWebUI Community": "Redirecionando-o para a Comunidade OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Refera-se a si próprio como \"User\" (por exemplo, \"User está a aprender Espanhol\")", + "Refused when it shouldn't have": "Recusado quando não deveria", + "Regenerate": "Regenerar", + "Release Notes": "Notas de Lançamento", + "Remove": "Remover", + "Remove Model": "Remover Modelo", + "Rename": "Renomear", + "Repeat Last N": "Repetir Últimos N", + "Request Mode": "Modo de Pedido", + "Reranking Model": "Modelo de Reranking", + "Reranking model disabled": "Modelo de Reranking desativado", + "Reranking model set to \"{{reranking_model}}\"": "Modelo de Reranking definido como \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "Limpar Pasta de Carregamento", + "Reset Vector Storage": "Redefinir Armazenamento de Vetor", + "Response AutoCopy to Clipboard": "Cópia Automática da Resposta para a Área de Transferência", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Função", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "A correr", + "Save": "Guardar", + "Save & Create": "Guardar e Criar", + "Save & Update": "Guardar e Atualizar", + "Save Tag": "", + "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": "Guardar o registo das conversas diretamente no armazenamento do seu navegador já não é suportado. Reserve um momento para descarregar e eliminar os seus registos de conversas clicando no botão abaixo. Não se preocupe, você pode facilmente reimportar os seus registos de conversas 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 a model": "Pesquisar um modelo", + "Search Chats": "Pesquisar Conversas", + "Search Documents": "Pesquisar Documentos", + "Search Functions": "", + "Search Models": "Modelos de pesquisa", + "Search Prompts": "Pesquisar Prompts", + "Search Query Generation Prompt": "Prompt de geração de consulta de pesquisa", + "Search Query Generation Prompt Length Threshold": "Limite de comprimento do prompt de geração de consulta de pesquisa", + "Search Result Count": "Contagem de resultados da pesquisa", + "Search Tools": "", + "Searched {{count}} sites_one": "Pesquisado {{count}} sites_one", + "Searched {{count}} sites_many": "Pesquisado {{count}} sites_many", + "Searched {{count}} sites_other": "Pesquisado {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "URL de consulta Searxng", + "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 base model": "Selecione um modelo base", + "Select a engine": "Selecione um motor", + "Select a function": "", + "Select a mode": "Selecione um modo", + "Select a model": "Selecione um modelo", + "Select a pipeline": "Selecione um pipeline", + "Select a pipeline url": "Selecione um URL de pipeline", + "Select a tool": "", + "Select an Ollama instance": "Selecione uma instância Ollama", + "Select Documents": "", + "Select model": "Selecione o modelo", + "Select only one model to call": "Selecione apenas um modelo para a chamada", + "Selected model(s) do not support image inputs": "O(s) modelo(s) selecionado(s) não suporta(m) entradas de imagem", + "Send": "Enviar", + "Send a Message": "Enviar uma Mensagem", + "Send message": "Enviar mensagem", + "September": "Setembro", + "Serper API Key": "Chave API Serper", + "Serply API Key": "", + "Serpstack API Key": "Chave da API Serpstack", + "Server connection verified": "Conexão com o servidor verificada", + "Set as default": "Definir como padrão", + "Set Default Model": "Definir Modelo Padrão", + "Set embedding model (e.g. {{model}})": "Definir modelo de vetorização (ex.: {{model}})", + "Set Image Size": "Definir Tamanho da Imagem", + "Set reranking model (e.g. {{model}})": "Definir modelo de reranking (ex.: {{model}})", + "Set Steps": "Definir Etapas", + "Set Task Model": "Definir modelo de tarefa", + "Set Voice": "Definir Voz", + "Settings": "Configurações", + "Settings saved successfully!": "Configurações guardadas com sucesso!", + "Settings updated successfully": "Configurações atualizadas com sucesso", + "Share": "Partilhar", + "Share Chat": "Partilhar Conversa", + "Share to OpenWebUI Community": "Partilhar com a Comunidade OpenWebUI", + "short-summary": "resumo-curto", + "Show": "Mostrar", + "Show Admin Details in Account Pending Overlay": "Mostrar Detalhes do Administrador na sobreposição de Conta Pendente", + "Show Model": "", + "Show shortcuts": "Mostrar atalhos", + "Show your support!": "", + "Showcased creativity": "Criatividade Exibida", + "Sign in": "Entrar", + "Sign Out": "Sair", + "Sign up": "Inscrever-se", + "Signing in": "A entrar", + "Source": "Fonte", + "Speech recognition error: {{error}}": "Erro de reconhecimento de fala: {{error}}", + "Speech-to-Text Engine": "Motor de Fala para Texto", + "Stop Sequence": "Sequência de Paragem", + "STT Model": "Modelo STT", + "STT Settings": "Configurações STT", + "Submit": "Enviar", + "Subtitle (e.g. about the Roman Empire)": "Subtítulo (ex.: sobre o Império Romano)", + "Success": "Sucesso", + "Successfully updated.": "Atualizado com sucesso.", + "Suggested": "Sugerido", + "Support": "", + "Support this plugin:": "", + "System": "Sistema", + "System Prompt": "Prompt do Sistema", + "Tags": "Etiquetas", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Diga-nos mais:", + "Temperature": "Temperatura", + "Template": "Modelo", + "Text Completion": "Conclusão de Texto", + "Text-to-Speech Engine": "Motor de Texto para Fala", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Obrigado pelo seu feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "A pontuação deve ser um valor entre 0.0 (0%) e 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "A pensar...", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Isto garante que suas conversas valiosas sejam guardadas com segurança na sua base de dados de backend. Obrigado!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Isto é um recurso experimental, pode não funcionar conforme o esperado e está sujeito a alterações a qualquer momento.", + "This setting does not sync across browsers or devices.": "Esta configuração não sincroniza entre navegadores ou dispositivos.", + "This will delete": "", + "Thorough explanation": "Explicação Minuciosa", + "Tika": "", + "Tika Server URL required.": "", + "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 da conversa após cada substituição.", + "Title": "Título", + "Title (e.g. Tell me a fun fact)": "Título (ex.: Diz-me um facto divertido)", + "Title Auto-Generation": "Geração Automática de Título", + "Title cannot be an empty string.": "Título não pode ser uma string vazia.", + "Title Generation Prompt": "Prompt de Geração de Título", + "to": "para", + "To access the available model names for downloading,": "Para aceder aos nomes de modelo disponíveis para descarregar,", + "To access the GGUF models available for downloading,": "Para aceder aos modelos GGUF disponíveis para descarregar,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Para aceder ao WebUI, entre em contato com o administrador. Os administradores podem gerir o status dos utilizadores no Painel de Administração.", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "para a entrada da conversa.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Hoje", + "Toggle settings": "Alternar configurações", + "Toggle sidebar": "Alternar barra lateral", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Problemas a aceder ao Ollama?", + "TTS Model": "Modelo TTS", + "TTS Settings": "Configurações TTS", + "TTS Voice": "Voz TTS", + "Type": "Tipo", + "Type Hugging Face Resolve (Download) URL": "Escreva o URL do Hugging Face Resolve (Descarregar)", + "Uh-oh! There was an issue connecting to {{provider}}.": "Uh-oh! Houve um problema ao conectar a {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Atualizar e Copiar Link", + "Update password": "Atualizar senha", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Carregar um modelo GGUF", + "Upload Files": "Carregar ficheiros", + "Upload Pipeline": "Carregar Pipeline", + "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 os seus documentos.", + "Use Gravatar": "Usar Gravatar", + "Use Initials": "Usar Iniciais", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "utilizador", + "User location successfully retrieved.": "", + "User Permissions": "Permissões do Utilizador", + "Users": "Utilizadores", + "Utilize": "Utilizar", + "Valid time units:": "Unidades de tempo válidas:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "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", + "Voice": "", + "Warning": "Aviso", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Aviso: Se você atualizar ou alterar o seu modelo de vetorização, você tem de reimportar todos os documentos.", + "Web": "Web", + "Web API": "Web API", + "Web Loader Settings": "Configurações do Carregador da Web", + "Web Params": "Parâmetros da Web", + "Web Search": "Pesquisa na Web", + "Web Search Engine": "Motor de Pesquisa Web", + "Webhook URL": "URL do Webhook", + "WebUI Settings": "Configurações WebUI", + "WebUI will make requests to": "WebUI fará pedidos a", + "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, novas conversas neste navegador não aparecerão em seu histórico em nenhum dos seus dispositivos.", + "Whisper (Local)": "Whisper (Local)", + "Widescreen Mode": "Modo Widescreen", + "Workspace": "Espaço de Trabalho", + "Write a prompt suggestion (e.g. Who are you?)": "Escreva uma sugestão de prompt (por exemplo, Quem és tu?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Escreva um resumo em 50 palavras que resuma [tópico ou palavra-chave].", + "Yesterday": "Ontem", + "You": "Você", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Você pode personalizar as suas interações com LLMs adicionando memórias através do botão ‘Gerir’ abaixo, tornando-as mais úteis e personalizadas para você.", + "You cannot clone a base model": "Não é possível clonar um modelo base", + "You have no archived conversations.": "Você não tem conversas arquivadas.", + "You have shared this chat": "Você partilhou esta conversa", + "You're a helpful assistant.": "Você é um assistente útil.", + "You're now logged in.": "Você agora está conectado.", + "Your account status is currently pending activation.": "O status da sua conta está atualmente com a ativação pendente.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Configurações do Carregador do Youtube" +} diff --git a/src/lib/i18n/locales/ru-RU/translation.json b/src/lib/i18n/locales/ru-RU/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..8aaba9a502fed9a1315f8dfc15ba26771c353a22 --- /dev/null +++ b/src/lib/i18n/locales/ru-RU/translation.json @@ -0,0 +1,716 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' или '-1' для не истечение.", + "(Beta)": "(бета)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(например: `sh webui.sh --api`)", + "(latest)": "(последний)", + "{{ models }}": "{{ модели }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Вы не можете удалить базовую модель", + "{{modelName}} is thinking...": "{{modelName}} думает...", + "{{user}}'s Chats": "{{user}} чаты", + "{{webUIName}} Backend Required": "{{webUIName}} бэкенд требуемый", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Модель задач используется при выполнении таких задач, как генерация заголовков для чатов и поисковых запросов в Интернете", + "a user": "пользователь", + "About": "Об", + "Account": "Аккаунт", + "Account Activation Pending": "", + "Accurate information": "Точная информация", + "Actions": "", + "Active Users": "", + "Add": "Добавить", + "Add a model id": "Добавление идентификатора модели", + "Add a short description about what this model does": "Добавьте краткое описание того, что делает эта модель", + "Add a short title for this prompt": "Добавьте краткий заголовок для этого ввода", + "Add a tag": "Добавьте тэг", + "Add custom prompt": "Добавьте пользовательский ввод", + "Add Docs": "Добавьте документы", + "Add Files": "Добавьте файлы", + "Add Memory": "Добавьте память", + "Add message": "Добавьте сообщение", + "Add Model": "Добавьте модель", + "Add Tag": "", + "Add Tags": "Добавьте тэгы", + "Add User": "Добавьте пользователя", + "Adjusting these settings will apply changes universally to all users.": "Регулирующий этих настроек приведет к изменениям для все пользователей.", + "admin": "админ", + "Admin": "", + "Admin Panel": "Панель админ", + "Admin Settings": "Настройки админ", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Расширенные Параметры", + "Advanced Params": "Расширенные параметры", + "all": "всё", + "All Documents": "Все документы", + "All Users": "Все пользователи", + "Allow": "Разрешить", + "Allow Chat Deletion": "Дозволять удаление чат", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "буквенно цифровые символы и дефисы", + "Already have an account?": "у вас уже есть аккаунт?", + "an assistant": "ассистент", + "and": "и", + "and create a new shared link.": "и создайте новый общий ссылку.", + "API Base URL": "Базовый адрес API", + "API Key": "Ключ API", + "API Key created.": "Ключ API создан.", + "API keys": "Ключи API", + "April": "Апрель", + "Archive": "Архив", + "Archive All Chats": "Архивировать все чаты", + "Archived Chats": "запис на чат", + "are allowed - Activate this command by typing": "разрешено - активируйте эту команду вводом", + "Are you sure?": "Вы уверены?", + "Attach file": "Прикрепить файл", + "Attention to detail": "детализированный", + "Audio": "Аудио", + "Audio settings updated successfully": "", + "August": "Август", + "Auto-playback response": "Автоматическое воспроизведение ответа", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Базовый адрес URL AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Необходима базовый адрес URL.", + "available!": "доступный!", + "Back": "Назад", + "Bad Response": "Недопустимый ответ", + "Banners": "Баннеры", + "Base Model (From)": "Базовая модель (от)", + "Batch Size (num_batch)": "", + "before": "до", + "Being lazy": "ленивый", + "Brave Search API Key": "Ключ API поиска Brave", + "Bypass SSL verification for Websites": "Обход SSL-проверки для веб-сайтов", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Аннулировать", + "Capabilities": "Возможности", + "Change Password": "Изменить пароль", + "Chat": "Чат", + "Chat Background Image": "", + "Chat Bubble UI": "Bubble UI чат", + "Chat Controls": "", + "Chat direction": "Направление чат", + "Chat History": "История чат", + "Chat History is off for this browser.": "История чат отключен для этого браузера.", + "Chats": "Чаты", + "Check Again": "Перепроверять", + "Check for updates": "Проверить обновления", + "Checking for updates...": "Проверка обновлений...", + "Choose a model before saving...": "Выберите модель перед сохранением...", + "Chunk Overlap": "Перекрытие фрагментов", + "Chunk Params": "Параметры фрагментов", + "Chunk Size": "Размер фрагмента", + "Citation": "Цитата", + "Clear memory": "", + "Click here for help.": "Нажмите здесь для помощи.", + "Click here to": "Нажмите здесь чтобы", + "Click here to download user import template file.": "", + "Click here to select": "Нажмите тут чтобы выберите", + "Click here to select a csv file.": "Нажмите здесь чтобы выбрать файл csv.", + "Click here to select a py file.": "", + "Click here to select documents.": "Нажмите здесь чтобы выберите документы.", + "click here.": "нажмите здесь.", + "Click on the user role button to change a user's role.": "Нажмите кнопку роли пользователя чтобы изменить роль пользователя.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Клон", + "Close": "Закрывать", + "Code formatted successfully": "", + "Collection": "Коллекция", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "Базовый адрес URL ComfyUI", + "ComfyUI Base URL is required.": "ComfyUI Необходима базовый адрес URL.", + "Command": "Команда", + "Concurrent Requests": "Одновременные запросы", + "Confirm": "", + "Confirm Password": "Подтвердите пароль", + "Confirm your action": "", + "Connections": "Соединение", + "Contact Admin for WebUI Access": "", + "Content": "Содержание", + "Content Extraction": "", + "Context Length": "Длина контексту", + "Continue Response": "Продолжить ответ", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Копирование общей ссылки чат в буфер обмена!", + "Copy": "Копировать", + "Copy last code block": "Копировать последний блок кода", + "Copy last response": "Копировать последний ответ", + "Copy Link": "Копировать ссылку", + "Copying to clipboard was successful!": "Копирование в буфер обмена прошло успешно!", + "Create a model": "Создание модели", + "Create Account": "Создать аккаунт", + "Create new key": "Создать новый ключ", + "Create new secret key": "Создать новый секретный ключ", + "Created at": "Создано в", + "Created At": "Создано в", + "Created by": "", + "CSV Import": "", + "Current Model": "Текущая модель", + "Current Password": "Текущий пароль", + "Custom": "Пользовательский", + "Customize models for a specific purpose": "Настройка моделей для конкретных целей", + "Dark": "Тёмный", + "Dashboard": "", + "Database": "База данных", + "December": "Декабрь", + "Default": "По умолчанию", + "Default (Automatic1111)": "По умолчанию (Automatic1111)", + "Default (SentenceTransformers)": "По умолчанию (SentenceTransformers)", + "Default Model": "Модель по умолчанию", + "Default model updated": "Модель по умолчанию обновлена", + "Default Prompt Suggestions": "Предложения промтов по умолчанию", + "Default User Role": "Роль пользователя по умолчанию", + "delete": "удалить", + "Delete": "Удалить", + "Delete a model": "Удалить модель", + "Delete All Chats": "Удалить все чаты", + "Delete chat": "Удалить чат", + "Delete Chat": "Удалить чат", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "удалить эту ссылку", + "Delete tool?": "", + "Delete User": "Удалить пользователя", + "Deleted {{deleteModelTag}}": "Удалено {{deleteModelTag}}", + "Deleted {{name}}": "Удалено {{name}}", + "Description": "Описание", + "Didn't fully follow instructions": "Не полностью следул инструкциям", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Откройте для себя модель", + "Discover a prompt": "Найти промт", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Находите, загружайте и исследуйте настраиваемые промты", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Находите, загружайте и исследуйте предустановки модели", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Отображать имя пользователя вместо 'Вы' в чате", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Документ", + "Document Settings": "Настройки документа", + "Documentation": "", + "Documents": "Документы", + "does not make any external connections, and your data stays securely on your locally hosted server.": "не устанавливает никаких внешних соединений, и ваши данные остаются безопасно на вашем локальном сервере.", + "Don't Allow": "Не разрешать", + "Don't have an account?": "у вас не есть аккаунт?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Не нравится стиль", + "Done": "", + "Download": "Загрузить", + "Download canceled": "Загрузка отменена", + "Download Database": "Загрузить базу данных", + "Drop any files here to add to the conversation": "Перетащите сюда файлы, чтобы добавить их в разговор", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "например, '30с','10м'. Допустимые единицы времени: 'с', 'м', 'ч'.", + "Edit": "Редактировать", + "Edit Doc": "Редактировать документ", + "Edit Memory": "", + "Edit User": "Редактировать пользователя", + "ElevenLabs": "", + "Email": "Электронная почта", + "Embedding Batch Size": "", + "Embedding Model": "Модель эмбеддинга", + "Embedding Model Engine": "Модель эмбеддинга", + "Embedding model set to \"{{embedding_model}}\"": "Эмбеддинг-модель установлена в \"{{embedding_model}}\"", + "Enable Chat History": "Включить историю чата", + "Enable Community Sharing": "Включить общий доступ к сообществу", + "Enable New Sign Ups": "Разрешить новые регистрации", + "Enable Web Search": "Включить поиск в Интернете", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Убедитесь, что ваш CSV-файл включает в себя 4 столбца в следующем порядке: Имя, Электронная почта, Пароль, Роль.", + "Enter {{role}} message here": "Введите сообщение {{role}} здесь", + "Enter a detail about yourself for your LLMs to recall": "Введите детали о себе, чтобы LLMs могли запомнить", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Введите ключ API поиска Brave", + "Enter Chunk Overlap": "Введите перекрытие фрагмента", + "Enter Chunk Size": "Введите размер фрагмента", + "Enter Github Raw URL": "Введите необработанный URL-адрес Github", + "Enter Google PSE API Key": "Введите ключ API Google PSE", + "Enter Google PSE Engine Id": "Введите идентификатор движка Google PSE", + "Enter Image Size (e.g. 512x512)": "Введите размер изображения (например, 512x512)", + "Enter language codes": "Введите коды языков", + "Enter model tag (e.g. {{modelTag}})": "Введите тег модели (например, {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Введите количество шагов (например, 50)", + "Enter Score": "Введите оценку", + "Enter Searxng Query URL": "Введите URL-адрес запроса Searxng", + "Enter Serper API Key": "Введите ключ API Serper", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Введите ключ API Serpstack", + "Enter stop sequence": "Введите последовательность остановки", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Введите Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Введите URL-адрес (например, http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Введите URL-адрес (например, http://localhost:11434)", + "Enter Your Email": "Введите вашу электронную почту", + "Enter Your Full Name": "Введите ваше полное имя", + "Enter your message": "", + "Enter Your Password": "Введите ваш пароль", + "Enter Your Role": "Введите вашу роль", + "Error": "Ошибка", + "Experimental": "Экспериментальное", + "Export": "Экспорт", + "Export All Chats (All Users)": "Экспортировать все чаты (все пользователи)", + "Export chat (.json)": "", + "Export Chats": "Экспортировать чаты", + "Export Documents Mapping": "Экспортировать отображение документов", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Экспорт моделей", + "Export Prompts": "Экспортировать промты", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Не удалось создать ключ API.", + "Failed to read clipboard contents": "Не удалось прочитать содержимое буфера обмена", + "Failed to update settings": "", + "February": "Февраль", + "Feel free to add specific details": "Feel free to add specific details", + "File": "", + "File Mode": "Режим файла", + "File not found.": "Файл не найден.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Определение подделки отпечатка: Невозможно использовать инициалы в качестве аватара. По умолчанию используется изображение профиля по умолчанию.", + "Fluidly stream large external response chunks": "Плавная потоковая передача больших фрагментов внешних ответов", + "Focus chat input": "Фокус ввода чата", + "Followed instructions perfectly": "Учитывая инструкции идеально", + "Form": "", + "Format your variables using square brackets like this:": "Форматируйте ваши переменные, используя квадратные скобки, как здесь:", + "Frequency Penalty": "Штраф за частоту", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Общее", + "General Settings": "Общие настройки", + "Generate Image": "", + "Generating search query": "Генерация поискового запроса", + "Generation Info": "Информация о генерации", + "Get up and running with": "", + "Global": "", + "Good Response": "Хороший ответ", + "Google PSE API Key": "Ключ API Google PSE", + "Google PSE Engine Id": "Идентификатор движка Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "не имеет разговоров.", + "Hello, {{name}}": "Привет, {{name}}", + "Help": "Помощь", + "Hide": "Скрыть", + "Hide Model": "", + "How can I help you today?": "Чем я могу помочь вам сегодня?", + "Hybrid Search": "Гибридная поисковая система", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Генерация изображений (Экспериментально)", + "Image Generation Engine": "Механизм генерации изображений", + "Image Settings": "Настройки изображения", + "Images": "Изображения", + "Import Chats": "Импорт чатов", + "Import Documents Mapping": "Импорт сопоставления документов", + "Import Functions": "", + "Import Models": "Импорт моделей", + "Import Prompts": "Импорт подсказок", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Добавьте флаг `--api` при запуске stable-diffusion-webui", + "Info": "Информация", + "Input commands": "Введите команды", + "Install from Github URL": "Установка с URL-адреса Github", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Интерфейс", + "Invalid Tag": "Недопустимый тег", + "January": "Январь", + "join our Discord for help.": "присоединяйтесь к нашему Discord для помощи.", + "JSON": "JSON", + "JSON Preview": "Предварительный просмотр JSON", + "July": "Июль", + "June": "Июнь", + "JWT Expiration": "Истечение срока JWT", + "JWT Token": "Токен JWT", + "Keep Alive": "Поддерживать активность", + "Keyboard shortcuts": "Горячие клавиши", + "Knowledge": "", + "Language": "Язык", + "large language models, locally.": "", + "Last Active": "Последний активный", + "Last Modified": "", + "Light": "Светлый", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "LLMs могут допускать ошибки. Проверяйте важную информацию.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Сделано сообществом OpenWebUI", + "Make sure to enclose them with": "Убедитесь, что они заключены в", + "Manage": "", + "Manage Models": "Управление моделями", + "Manage Ollama Models": "Управление моделями Ollama", + "Manage Pipelines": "Управление конвейерами", + "Manage Valves": "", + "March": "Март", + "Max Tokens (num_predict)": "Максимальное количество жетонов (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимальное количество моделей для загрузки одновременно - 3. Пожалуйста, попробуйте позже.", + "May": "Май", + "Memories accessible by LLMs will be shown here.": "Мемории, доступные LLMs, будут отображаться здесь.", + "Memory": "Мемория", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Сообщения, которые вы отправляете после создания ссылки, не будут распространяться. Пользователи с URL смогут просматривать общий чат.", + "Minimum Score": "Минимальный балл", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD MMMM YYYY г.", + "MMMM DD, YYYY HH:mm": "DD MMMM YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Модель '{{modelName}}' успешно загружена.", + "Model '{{modelTag}}' is already in queue for downloading.": "Модель '{{modelTag}}' уже находится в очереди на загрузку.", + "Model {{modelId}} not found": "Модель {{modelId}} не найдена", + "Model {{modelName}} is not vision capable": "Модель {{modelName}} не поддерживает зрение", + "Model {{name}} is now {{status}}": "Модель {{name}} теперь {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Модель файловой системы обнаружена. Требуется имя тега модели для обновления, не удается продолжить.", + "Model ID": "Идентификатор модели", + "Model not selected": "Модель не выбрана", + "Model Params": "Параметры модели", + "Model updated successfully": "", + "Model Whitelisting": "Включение модели в белый список", + "Model(s) Whitelisted": "Модель(и) включены в белый список", + "Modelfile Content": "Содержимое файла модели", + "Models": "Модели", + "More": "Более", + "Name": "Имя", + "Name Tag": "Имя тега", + "Name your model": "Присвойте модели имя", + "New Chat": "Новый чат", + "New Password": "Новый пароль", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Результатов не найдено", + "No search query generated": "Поисковый запрос не сгенерирован", + "No source available": "Нет доступных источников", + "No valves to update": "", + "None": "Никакой", + "Not factually correct": "Не фактически правильно", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Обратите внимание: Если вы установите минимальный балл, поиск будет возвращать только документы с баллом больше или равным минимальному баллу.", + "Notifications": "Уведомления на рабочем столе", + "November": "Ноябрь", + "num_thread (Ollama)": "num_thread (Оллама)", + "OAuth ID": "", + "October": "Октябрь", + "Off": "Выключено.", + "Okay, Let's Go!": "Давайте начнём!", + "OLED Dark": "OLED темная", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API отключен", + "Ollama API is disabled": "", + "Ollama Version": "Версия Ollama", + "On": "Включено.", + "Only": "Только", + "Only alphanumeric characters and hyphens are allowed in the command string.": "В строке команды разрешено использовать только буквенно-цифровые символы и дефисы.", + "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.": "Упс! Зажмите пояса! Ваши файлы все еще в процессе обработки. Мы готовим их до идеального состояния. Пожалуйста, будьте терпеливы, и мы сообщим вам, когда они будут готовы.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Похоже, что URL-адрес недействителен. Пожалуйста, перепроверьте и попробуйте еще раз.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Вы используете неподдерживаемый метод (только фронтенд). Пожалуйста, обслуживайте веб-интерфейс из бэкенда.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Открыть новый чат", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "Open AI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Конфигурация API OpenAI", + "OpenAI API Key is required.": "Требуется ключ API OpenAI.", + "OpenAI URL/Key required.": "Требуется URL-адрес API OpenAI или ключ API.", + "or": "или", + "Other": "Прочее", + "Password": "Пароль", + "PDF document (.pdf)": "PDF-документ (.pdf)", + "PDF Extract Images (OCR)": "Извлечение изображений из PDF (OCR)", + "pending": "ожидание", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Отказано в доступе к микрофону: {{error}}", + "Personalization": "Персонализация", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Трубопроводов", + "Pipelines Not Detected": "", + "Pipelines Valves": "Трубопроводы Клапаны", + "Plain text (.txt)": "Текст в формате .txt", + "Playground": "Площадка", + "Please carefully review the following warnings:": "", + "Positive attitude": "Позитивная атмосфера", + "Previous 30 days": "Предыдущие 30 дней", + "Previous 7 days": "Предыдущие 7 дней", + "Profile Image": "Изображение профиля", + "Prompt": "Промпт", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Промпт (например. Расскажи мне интересную факт о Римской империи)", + "Prompt Content": "Содержание промпта", + "Prompt suggestions": "Предложения промптов", + "Prompts": "Промпты", + "Pull \"{{searchValue}}\" from Ollama.com": "Загрузить модель из Ollama.com", + "Pull a model from Ollama.com": "Загрузить модель с Ollama.com", + "Query Params": "Параметры запроса", + "RAG Template": "Шаблон RAG", + "Read Aloud": "Прочитать вслух", + "Record voice": "Записать голос", + "Redirecting you to OpenWebUI Community": "Перенаправляем вас в сообщество OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Отказано в доступе, когда это не должно было произойти.", + "Regenerate": "Перезаписать", + "Release Notes": "Примечания к выпуску", + "Remove": "Удалить", + "Remove Model": "Удалить модель", + "Rename": "Переименовать", + "Repeat Last N": "Повторить последние N", + "Request Mode": "Режим запроса", + "Reranking Model": "Reranking модель", + "Reranking model disabled": "Модель реранжирования отключена", + "Reranking model set to \"{{reranking_model}}\"": "Модель реранжирования установлена на \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Сбросить векторное хранилище", + "Response AutoCopy to Clipboard": "Автоматическое копирование ответа в буфер обмена", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Роль", + "Rosé Pine": "Розовое сосновое дерево", + "Rosé Pine Dawn": "Розовое сосновое дерево рассвет", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Сохранить", + "Save & Create": "Сохранить и создать", + "Save & Update": "Сохранить и обновить", + "Save Tag": "", + "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": "Прямое сохранение журналов чата в хранилище вашего браузера больше не поддерживается. Пожалуйста, потратьте минуту, чтобы скачать и удалить ваши журналы чата, нажав на кнопку ниже. Не волнуйтесь, вы легко сможете повторно импортировать свои журналы чата в бэкенд через", + "Scan": "Сканировать", + "Scan complete!": "Сканирование завершено!", + "Scan for documents from {{path}}": "Сканирование документов из {{path}}", + "Search": "Поиск", + "Search a model": "Поиск модели", + "Search Chats": "Поиск в чатах", + "Search Documents": "Поиск документов", + "Search Functions": "", + "Search Models": "Поиск моделей", + "Search Prompts": "Поиск промтов", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Количество результатов поиска", + "Search Tools": "", + "Searched {{count}} sites_one": "Поиск {{count}} sites_one", + "Searched {{count}} sites_few": "Поиск {{count}} sites_few", + "Searched {{count}} sites_many": "Поиск {{count}} sites_many", + "Searched {{count}} sites_other": "Поиск {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "URL-адрес запроса Searxng", + "See readme.md for instructions": "Смотрите readme.md для инструкций", + "See what's new": "Посмотреть, что нового", + "Seed": "Сид", + "Select a base model": "Выбор базовой модели", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Выберите режим", + "Select a model": "Выберите модель", + "Select a pipeline": "Выбор конвейера", + "Select a pipeline url": "Выберите URL-адрес конвейера", + "Select a tool": "", + "Select an Ollama instance": "Выберите экземпляр Ollama", + "Select Documents": "", + "Select model": "Выберите модель", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Выбранные модели не поддерживают ввод изображений", + "Send": "Отправить", + "Send a Message": "Отправить сообщение", + "Send message": "Отправить сообщение", + "September": "Сентябрь", + "Serper API Key": "Ключ API Serper", + "Serply API Key": "", + "Serpstack API Key": "Ключ API Serpstack", + "Server connection verified": "Соединение с сервером проверено", + "Set as default": "Установить по умолчанию", + "Set Default Model": "Установить модель по умолчанию", + "Set embedding model (e.g. {{model}})": "Установить модель эмбеддинга (например. {{model}})", + "Set Image Size": "Установить размер изображения", + "Set reranking model (e.g. {{model}})": "Установить модель реранжирования (например. {{model}})", + "Set Steps": "Установить шаги", + "Set Task Model": "Задать модель задачи", + "Set Voice": "Установить голос", + "Settings": "Настройки", + "Settings saved successfully!": "Настройки успешно сохранены!", + "Settings updated successfully": "", + "Share": "Поделиться", + "Share Chat": "Поделиться чатом", + "Share to OpenWebUI Community": "Поделиться с сообществом OpenWebUI", + "short-summary": "краткое описание", + "Show": "Показать", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Показать клавиатурные сокращения", + "Show your support!": "", + "Showcased creativity": "Показать творчество", + "Sign in": "Войти", + "Sign Out": "Выход", + "Sign up": "зарегистрировать", + "Signing in": "Вход в систему", + "Source": "Источник", + "Speech recognition error: {{error}}": "Ошибка распознавания речи: {{error}}", + "Speech-to-Text Engine": "Система распознавания речи", + "Stop Sequence": "Последовательность остановки", + "STT Model": "", + "STT Settings": "Настройки распознавания речи", + "Submit": "Отправить", + "Subtitle (e.g. about the Roman Empire)": "Подзаголовок (например. о Римской империи)", + "Success": "Успех", + "Successfully updated.": "Успешно обновлено.", + "Suggested": "Предложено", + "Support": "", + "Support this plugin:": "", + "System": "Система", + "System Prompt": "Системный промпт", + "Tags": "Теги", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Пожалуйста, расскажите нам больше:", + "Temperature": "Температура", + "Template": "Шаблон", + "Text Completion": "Завершение текста", + "Text-to-Speech Engine": "Система синтеза речи", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Спасибо за ваше мнение!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Оценка должна быть значением между 0,0 (0%) и 1,0 (100%).", + "Theme": "Тема", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Это обеспечивает сохранение ваших ценных разговоров в безопасной базе данных на вашем сервере. Спасибо!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Эта настройка не синхронизируется между браузерами или устройствами.", + "This will delete": "", + "Thorough explanation": "Повнимательнее", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Совет: Обновляйте несколько переменных подряд, нажимая клавишу Tab в поле ввода чата после каждой замены.", + "Title": "Заголовок", + "Title (e.g. Tell me a fun fact)": "Заголовок (например. Расскажи мне интересную факт)", + "Title Auto-Generation": "Автогенерация заголовка", + "Title cannot be an empty string.": "Заголовок не может быть пустой строкой.", + "Title Generation Prompt": "Промпт для генерации заголовка", + "to": "в", + "To access the available model names for downloading,": "Чтобы получить доступ к доступным для загрузки именам моделей,", + "To access the GGUF models available for downloading,": "Чтобы получить доступ к моделям GGUF, доступным для загрузки,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "в чате.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Сегодня", + "Toggle settings": "Переключить настройки", + "Toggle sidebar": "Переключить боковую панель", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Проблемы с доступом к Ollama?", + "TTS Model": "", + "TTS Settings": "Настройки TTS", + "TTS Voice": "", + "Type": "Тип", + "Type Hugging Face Resolve (Download) URL": "Введите URL-адрес Hugging Face Resolve (загрузки)", + "Uh-oh! There was an issue connecting to {{provider}}.": "Упс! Возникла проблема подключения к {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Обновить и скопировать ссылку", + "Update password": "Обновить пароль", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Загрузить модель GGUF", + "Upload Files": "Загрузка файлов", + "Upload Pipeline": "", + "Upload Progress": "Прогресс загрузки", + "URL Mode": "Режим URL", + "Use '#' in the prompt input to load and select your documents.": "Используйте '#' в поле ввода промпта для загрузки и выбора ваших документов.", + "Use Gravatar": "Использовать Gravatar", + "Use Initials": "Использовать инициалы", + "use_mlock (Ollama)": "use_mlock (Оллама)", + "use_mmap (Ollama)": "use_mmap (Оллама)", + "user": "пользователь", + "User location successfully retrieved.": "", + "User Permissions": "Права пользователя", + "Users": "Пользователи", + "Utilize": "Использовать", + "Valid time units:": "Допустимые единицы времени:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "переменная", + "variable to have them replaced with clipboard content.": "переменная, чтобы их заменить содержимым буфера обмена.", + "Version": "Версия", + "Voice": "", + "Warning": "Предупреждение", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Предупреждение: Если вы обновите или измените модель эмбеддинга, вам нужно будет повторно импортировать все документы.", + "Web": "Веб", + "Web API": "", + "Web Loader Settings": "Настройки загрузчика Web", + "Web Params": "Параметры Web", + "Web Search": "Веб-поиск", + "Web Search Engine": "Поисковая система", + "Webhook URL": "URL-адрес веб-хука", + "WebUI Settings": "Настройки WebUI", + "WebUI will make requests to": "WebUI будет отправлять запросы на", + "What’s New in": "Что нового в", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Когда история отключена, новые чаты в этом браузере не будут отображаться в вашей истории на любом из ваших устройств.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Рабочая область", + "Write a prompt suggestion (e.g. Who are you?)": "Напишите предложение промпта (например, Кто вы?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Напишите резюме в 50 словах, которое кратко описывает [тему или ключевое слово].", + "Yesterday": "Вчера", + "You": "Вы", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Клонировать базовую модель невозможно", + "You have no archived conversations.": "У вас нет архивированных бесед.", + "You have shared this chat": "Вы поделились этим чатом", + "You're a helpful assistant.": "Вы полезный ассистент.", + "You're now logged in.": "Вы вошли в систему.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Ютуб", + "Youtube Loader Settings": "Настройки загрузчика YouTube" +} diff --git a/src/lib/i18n/locales/sr-RS/translation.json b/src/lib/i18n/locales/sr-RS/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..ee061b7fe17351b9179f17ddd23954d97fd32dd0 --- /dev/null +++ b/src/lib/i18n/locales/sr-RS/translation.json @@ -0,0 +1,715 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "„s“, „m“, „h“, „d“, „w“ или „-1“ за без истека.", + "(Beta)": "(бета)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(нпр. `sh webui.sh --api`)", + "(latest)": "(најновије)", + "{{ models }}": "{{ модели }}", + "{{ owner }}: You cannot delete a base model": "{{ оwнер }}: Не можете избрисати основни модел", + "{{modelName}} is thinking...": "{{modelName}} размишља...", + "{{user}}'s Chats": "Ћаскања корисника {{user}}", + "{{webUIName}} Backend Required": "Захтева се {{webUIName}} позадинац", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Модел задатка се користи приликом извршавања задатака као што су генерисање наслова за ћаскања и упите за Веб претрагу", + "a user": "корисник", + "About": "О нама", + "Account": "Налог", + "Account Activation Pending": "", + "Accurate information": "Прецизне информације", + "Actions": "", + "Active Users": "", + "Add": "Додај", + "Add a model id": "Додавање ИД-а модела", + "Add a short description about what this model does": "Додавање кратког описа о томе шта овај модел ради", + "Add a short title for this prompt": "Додај кратак наслов за овај упит", + "Add a tag": "Додај ознаку", + "Add custom prompt": "Додај прилагођен упит", + "Add Docs": "Додај документе", + "Add Files": "Додај датотеке", + "Add Memory": "Додај меморију", + "Add message": "Додај поруку", + "Add Model": "Додај модел", + "Add Tag": "", + "Add Tags": "Додај ознаке", + "Add User": "Додај корисника", + "Adjusting these settings will apply changes universally to all users.": "Прилагођавање ових подешавања ће применити промене на све кориснике.", + "admin": "админ", + "Admin": "", + "Admin Panel": "Админ табла", + "Admin Settings": "Админ подешавања", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "Напредни параметри", + "Advanced Params": "Напредни парамови", + "all": "сви", + "All Documents": "Сви документи", + "All Users": "Сви корисници", + "Allow": "Дозволи", + "Allow Chat Deletion": "Дозволи брисање ћаскања", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "алфанумерички знакови и цртице", + "Already have an account?": "Већ имате налог?", + "an assistant": "помоћник", + "and": "и", + "and create a new shared link.": "и направи нову дељену везу.", + "API Base URL": "Основна адреса API-ја", + "API Key": "API кључ", + "API Key created.": "API кључ направљен.", + "API keys": "API кључеви", + "April": "Април", + "Archive": "Архива", + "Archive All Chats": "Архивирај све ћаскања", + "Archived Chats": "Архивирана ћаскања", + "are allowed - Activate this command by typing": "су дозвољени - Покрените ову наредбу уношењем", + "Are you sure?": "Да ли сте сигурни?", + "Attach file": "Приложи датотеку", + "Attention to detail": "Пажња на детаље", + "Audio": "Звук", + "Audio settings updated successfully": "", + "August": "Август", + "Auto-playback response": "Самостално пуштање одговора", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Основна адреса за AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "Потребна је основна адреса за AUTOMATIC1111.", + "available!": "доступно!", + "Back": "Назад", + "Bad Response": "Лош одговор", + "Banners": "Барјаке", + "Base Model (From)": "Основни модел (од)", + "Batch Size (num_batch)": "", + "before": "пре", + "Being lazy": "Бити лењ", + "Brave Search API Key": "Апи кључ за храбру претрагу", + "Bypass SSL verification for Websites": "Заобиђи SSL потврђивање за веб странице", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "Откажи", + "Capabilities": "Могућности", + "Change Password": "Промени лозинку", + "Chat": "Ћаскање", + "Chat Background Image": "", + "Chat Bubble UI": "Интерфејс балона ћаскања", + "Chat Controls": "", + "Chat direction": "Смер ћаскања", + "Chat History": "Историја ћаскања", + "Chat History is off for this browser.": "Историја ћаскања је искључена за овај прегледач.", + "Chats": "Ћаскања", + "Check Again": "Провери поново", + "Check for updates": "Потражи ажурирања", + "Checking for updates...": "Траже се ажурирања...", + "Choose a model before saving...": "Изабери модел пре чувања...", + "Chunk Overlap": "Преклапање делова", + "Chunk Params": "Параметри делова", + "Chunk Size": "Величина дела", + "Citation": "Цитат", + "Clear memory": "", + "Click here for help.": "Кликните овде за помоћ.", + "Click here to": "Кликните овде да", + "Click here to download user import template file.": "", + "Click here to select": "Кликните овде да изаберете", + "Click here to select a csv file.": "Кликните овде да изаберете csv датотеку.", + "Click here to select a py file.": "", + "Click here to select documents.": "Кликните овде да изаберете документе.", + "click here.": "кликните овде.", + "Click on the user role button to change a user's role.": "Кликните на дугме за улогу корисника да промените улогу корисника.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Клон", + "Close": "Затвори", + "Code formatted successfully": "", + "Collection": "Колекција", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "Основна адреса за ComfyUI", + "ComfyUI Base URL is required.": "Потребна је основна адреса за ComfyUI.", + "Command": "Наредба", + "Concurrent Requests": "Упоредни захтеви", + "Confirm": "", + "Confirm Password": "Потврди лозинку", + "Confirm your action": "", + "Connections": "Везе", + "Contact Admin for WebUI Access": "", + "Content": "Садржај", + "Content Extraction": "", + "Context Length": "Дужина контекста", + "Continue Response": "Настави одговор", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Адреса дељеног ћаскања ископирана у оставу!", + "Copy": "Копирај", + "Copy last code block": "Копирај последњи блок кода", + "Copy last response": "Копирај последњи одговор", + "Copy Link": "Копирај везу", + "Copying to clipboard was successful!": "Успешно копирање у оставу!", + "Create a model": "Креирање модела", + "Create Account": "Направи налог", + "Create new key": "Направи нови кључ", + "Create new secret key": "Направи нови тајни кључ", + "Created at": "Направљено у", + "Created At": "Направљено у", + "Created by": "", + "CSV Import": "", + "Current Model": "Тренутни модел", + "Current Password": "Тренутна лозинка", + "Custom": "Прилагођено", + "Customize models for a specific purpose": "Прилагођавање модела у одређене сврхе", + "Dark": "Тамна", + "Dashboard": "", + "Database": "База података", + "December": "Децембар", + "Default": "Подразумевано", + "Default (Automatic1111)": "Подразумевано (Automatic1111)", + "Default (SentenceTransformers)": "Подразумевано (SentenceTransformers)", + "Default Model": "Подразумевани модел", + "Default model updated": "Подразумевани модел ажуриран", + "Default Prompt Suggestions": "Подразумевани предлози упита", + "Default User Role": "Подразумевана улога корисника", + "delete": "обриши", + "Delete": "Обриши", + "Delete a model": "Обриши модел", + "Delete All Chats": "Избриши сва ћаскања", + "Delete chat": "Обриши ћаскање", + "Delete Chat": "Обриши ћаскање", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "обриши ову везу", + "Delete tool?": "", + "Delete User": "Обриши корисника", + "Deleted {{deleteModelTag}}": "Обрисано {{deleteModelTag}}", + "Deleted {{name}}": "Избрисано {{наме}}", + "Description": "Опис", + "Didn't fully follow instructions": "Упутства нису праћена у потпуности", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Откријте модел", + "Discover a prompt": "Откриј упит", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Откријте, преузмите и истражите прилагођене упите", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Откријте, преузмите и истражите образце модела", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "Прикажи корисничко име уместо Ти у чату", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Документ", + "Document Settings": "Подешавања документа", + "Documentation": "", + "Documents": "Документи", + "does not make any external connections, and your data stays securely on your locally hosted server.": "не отвара никакве спољне везе и ваши подаци остају сигурно на вашем локално хостованом серверу.", + "Don't Allow": "Не дозволи", + "Don't have an account?": "Немате налог?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Не свиђа ми се стил", + "Done": "", + "Download": "Преузми", + "Download canceled": "Преузимање отказано", + "Download Database": "Преузми базу података", + "Drop any files here to add to the conversation": "Убаците било које датотеке овде да их додате у разговор", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "нпр. '30s', '10m'. Важеће временске јединице су 's', 'm', 'h'.", + "Edit": "Уреди", + "Edit Doc": "Уреди документ", + "Edit Memory": "", + "Edit User": "Уреди корисника", + "ElevenLabs": "", + "Email": "Е-пошта", + "Embedding Batch Size": "", + "Embedding Model": "Модел уградње", + "Embedding Model Engine": "Мотор модела уградње", + "Embedding model set to \"{{embedding_model}}\"": "Модел уградње подешен на \"{{embedding_model}}\"", + "Enable Chat History": "Омогући историју ћаскања", + "Enable Community Sharing": "Омогући дељење заједнице", + "Enable New Sign Ups": "Омогући нове пријаве", + "Enable Web Search": "Омогући Wеб претрагу", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Уверите се да ваша CSV датотека укључује 4 колоне у овом редоследу: Име, Е-пошта, Лозинка, Улога.", + "Enter {{role}} message here": "Унесите {{role}} поруку овде", + "Enter a detail about yourself for your LLMs to recall": "Унесите детаље за себе да ће LLMs преузимати", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Унесите БРАВЕ Сеарцх АПИ кључ", + "Enter Chunk Overlap": "Унесите преклапање делова", + "Enter Chunk Size": "Унесите величину дела", + "Enter Github Raw URL": "Унесите Гитхуб Раw УРЛ адресу", + "Enter Google PSE API Key": "Унесите Гоогле ПСЕ АПИ кључ", + "Enter Google PSE Engine Id": "Унесите Гоогле ПСЕ ИД машине", + "Enter Image Size (e.g. 512x512)": "Унесите величину слике (нпр. 512x512)", + "Enter language codes": "Унесите кодове језика", + "Enter model tag (e.g. {{modelTag}})": "Унесите ознаку модела (нпр. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Унесите број корака (нпр. 50)", + "Enter Score": "Унесите резултат", + "Enter Searxng Query URL": "Унесите УРЛ адресу Сеарxнг упита", + "Enter Serper API Key": "Унесите Серпер АПИ кључ", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "Унесите Серпстацк АПИ кључ", + "Enter stop sequence": "Унесите секвенцу заустављања", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Унесите Топ К", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Унесите адресу (нпр. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Унесите адресу (нпр. http://localhost:11434)", + "Enter Your Email": "Унесите вашу е-пошту", + "Enter Your Full Name": "Унесите ваше име и презиме", + "Enter your message": "", + "Enter Your Password": "Унесите вашу лозинку", + "Enter Your Role": "Унесите вашу улогу", + "Error": "Грешка", + "Experimental": "Експериментално", + "Export": "Извоз", + "Export All Chats (All Users)": "Извези сва ћаскања (сви корисници)", + "Export chat (.json)": "", + "Export Chats": "Извези ћаскања", + "Export Documents Mapping": "Извези мапирање докумената", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Извези моделе", + "Export Prompts": "Извези упите", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "Неуспешно стварање API кључа.", + "Failed to read clipboard contents": "Неуспешно читање садржаја оставе", + "Failed to update settings": "", + "February": "Фебруар", + "Feel free to add specific details": "Слободно додајте специфичне детаље", + "File": "", + "File Mode": "Режим датотеке", + "File not found.": "Датотека није пронађена.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Откривено лажно представљање отиска прста: Немогуће је користити иницијале као аватар. Прелазак на подразумевану профилну слику.", + "Fluidly stream large external response chunks": "Течно стримујте велике спољне делове одговора", + "Focus chat input": "Усредсредите унос ћаскања", + "Followed instructions perfectly": "Упутства су савршено праћена", + "Form": "", + "Format your variables using square brackets like this:": "Форматирајте ваше променљиве користећи угластe заграде овако:", + "Frequency Penalty": "Фреквентна казна", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Опште", + "General Settings": "Општа подешавања", + "Generate Image": "", + "Generating search query": "Генерисање упита претраге", + "Generation Info": "Информације о стварању", + "Get up and running with": "", + "Global": "", + "Good Response": "Добар одговор", + "Google PSE API Key": "Гоогле ПСЕ АПИ кључ", + "Google PSE Engine Id": "Гоогле ПСЕ ИД мотора", + "h:mm a": "h:mm a", + "has no conversations.": "нема разговора.", + "Hello, {{name}}": "Здраво, {{name}}", + "Help": "Помоћ", + "Hide": "Сакриј", + "Hide Model": "", + "How can I help you today?": "Како могу да вам помогнем данас?", + "Hybrid Search": "Хибридна претрага", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Стварање слика (експериментално)", + "Image Generation Engine": "Мотор за стварање слика", + "Image Settings": "Подешавања слике", + "Images": "Слике", + "Import Chats": "Увези ћаскања", + "Import Documents Mapping": "Увези мапирање докумената", + "Import Functions": "", + "Import Models": "Увези моделе", + "Import Prompts": "Увези упите", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Укључи `--api` заставицу при покретању stable-diffusion-webui", + "Info": "Инфо", + "Input commands": "Унеси наредбе", + "Install from Github URL": "Инсталирај из Гитхуб УРЛ адресе", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "Изглед", + "Invalid Tag": "Неисправна ознака", + "January": "Јануар", + "join our Discord for help.": "придружите се нашем Дискорду за помоћ.", + "JSON": "JSON", + "JSON Preview": "ЈСОН Преглед", + "July": "Јул", + "June": "Јун", + "JWT Expiration": "Истек JWT-а", + "JWT Token": "JWT жетон", + "Keep Alive": "Одржи трајање", + "Keyboard shortcuts": "Пречице на тастатури", + "Knowledge": "", + "Language": "Језик", + "large language models, locally.": "", + "Last Active": "Последња активност", + "Last Modified": "", + "Light": "Светла", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "ВЈМ-ови (LLM-ови) могу правити грешке. Проверите важне податке.", + "Local Models": "", + "LTR": "ЛНД", + "Made by OpenWebUI Community": "Израдила OpenWebUI заједница", + "Make sure to enclose them with": "Уверите се да их затворите са", + "Manage": "", + "Manage Models": "Управљај моделима", + "Manage Ollama Models": "Управљај Ollama моделима", + "Manage Pipelines": "Управљање цевоводима", + "Manage Valves": "", + "March": "Март", + "Max Tokens (num_predict)": "Маx Токенс (нум_предицт)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Највише 3 модела могу бити преузета истовремено. Покушајте поново касније.", + "May": "Мај", + "Memories accessible by LLMs will be shown here.": "Памћења које ће бити појављена од овог LLM-а ће бити приказана овде.", + "Memory": "Памћење", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Поруке које пошаљете након стварања ваше везе неће бити подељене. Корисници са URL-ом ће моћи да виде дељено ћаскање.", + "Minimum Score": "Најмањи резултат", + "Mirostat": "Миростат", + "Mirostat Eta": "Миростат Ета", + "Mirostat Tau": "Миростат Тау", + "MMMM DD, YYYY": "ММММ ДД, ГГГГ", + "MMMM DD, YYYY HH:mm": "ММММ ДД, ГГГГ ЧЧ:мм", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Модел „{{modelName}}“ је успешно преузет.", + "Model '{{modelTag}}' is already in queue for downloading.": "Модел „{{modelTag}}“ је већ у реду за преузимање.", + "Model {{modelId}} not found": "Модел {{modelId}} није пронађен", + "Model {{modelName}} is not vision capable": "Модел {{моделНаме}} није способан за вид", + "Model {{name}} is now {{status}}": "Модел {{наме}} је сада {{статус}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Откривена путања система датотека модела. За ажурирање је потребан кратак назив модела, не може се наставити.", + "Model ID": "ИД модела", + "Model not selected": "Модел није изабран", + "Model Params": "Модел Парамс", + "Model updated successfully": "", + "Model Whitelisting": "Бели списак модела", + "Model(s) Whitelisted": "Модел(и) на белом списку", + "Modelfile Content": "Садржај модел-датотеке", + "Models": "Модели", + "More": "Више", + "Name": "Име", + "Name Tag": "Назив ознаке", + "Name your model": "Наведи свој модел", + "New Chat": "Ново ћаскање", + "New Password": "Нова лозинка", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "Нема резултата", + "No search query generated": "Није генерисан упит за претрагу", + "No source available": "Нема доступног извора", + "No valves to update": "", + "None": "Нико", + "Not factually correct": "Није чињенично тачно", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Напомена: ако подесите најмањи резултат, претрага ће вратити само документе са резултатом већим или једнаким најмањем резултату.", + "Notifications": "Обавештења", + "November": "Новембар", + "num_thread (Ollama)": "нум _тхреад (Оллама)", + "OAuth ID": "", + "October": "Октобар", + "Off": "Искључено", + "Okay, Let's Go!": "У реду, хајде да кренемо!", + "OLED Dark": "OLED тамна", + "Ollama": "Ollama", + "Ollama API": "Оллама АПИ", + "Ollama API disabled": "Оллама АПИ онемогућен", + "Ollama API is disabled": "", + "Ollama Version": "Издање Ollama-е", + "On": "Укључено", + "Only": "Само", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Само алфанумерички знакови и цртице су дозвољени у низу наредби.", + "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.": "Упс! Само тренутак! Ваше датотеке се још обрађују. Припремамо их до савршенства. Молимо вас за стрпљење и обавестићемо вас када буду спремне.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Изгледа да је адреса неважећа. Молимо вас да проверите и покушате поново.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Користите неподржани метод (само фронтенд). Молимо вас да покренете WebUI са бекенда.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Покрени ново ћаскање", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "Подешавање OpenAI API-ја", + "OpenAI API Key is required.": "Потребан је OpenAI API кључ.", + "OpenAI URL/Key required.": "Потребан је OpenAI URL/кључ.", + "or": "или", + "Other": "Остало", + "Password": "Лозинка", + "PDF document (.pdf)": "PDF документ (.pdf)", + "PDF Extract Images (OCR)": "Извлачење PDF слика (OCR)", + "pending": "на чекању", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "Приступ микрофону је одбијен: {{error}}", + "Personalization": "Прилагођавање", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Цевоводи", + "Pipelines Not Detected": "", + "Pipelines Valves": "Вентили за цевоводе", + "Plain text (.txt)": "Обичан текст (.txt)", + "Playground": "Игралиште", + "Please carefully review the following warnings:": "", + "Positive attitude": "Позитиван став", + "Previous 30 days": "Претходних 30 дана", + "Previous 7 days": "Претходних 7 дана", + "Profile Image": "Слика профила", + "Prompt": "Упит", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Упит (нпр. „реци ми занимљивост о Римском царству“)", + "Prompt Content": "Садржај упита", + "Prompt suggestions": "Предлози упита", + "Prompts": "Упити", + "Pull \"{{searchValue}}\" from Ollama.com": "Повуците \"{{searchValue}}\" са Ollama.com", + "Pull a model from Ollama.com": "Повуците модел са Ollama.com", + "Query Params": "Параметри упита", + "RAG Template": "RAG шаблон", + "Read Aloud": "Прочитај наглас", + "Record voice": "Сними глас", + "Redirecting you to OpenWebUI Community": "Преусмеравање на OpenWebUI заједницу", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "Одбијено када није требало", + "Regenerate": "Регенериши", + "Release Notes": "Напомене о издању", + "Remove": "Уклони", + "Remove Model": "Уклони модел", + "Rename": "Преименуј", + "Repeat Last N": "Понови последњих N", + "Request Mode": "Режим захтева", + "Reranking Model": "Модел поновног рангирања", + "Reranking model disabled": "Модел поновног рангирања онемогућен", + "Reranking model set to \"{{reranking_model}}\"": "Модел поновног рангирања подешен на \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "Ресетуј складиште вектора", + "Response AutoCopy to Clipboard": "Самостално копирање одговора у оставу", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Улога", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "ДНЛ", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "Сачувај", + "Save & Create": "Сачувај и направи", + "Save & Update": "Сачувај и ажурирај", + "Save Tag": "", + "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": "Чување ћаскања директно у складиште вашег прегледача више није подржано. Одвојите тренутак да преузмете и избришете ваша ћаскања кликом на дугме испод. Не брините, можете лако поново увезти ваша ћаскања у бекенд кроз", + "Scan": "Скенирај", + "Scan complete!": "Скенирање завршено!", + "Scan for documents from {{path}}": "Скенирај документе из {{path}}", + "Search": "Претражи", + "Search a model": "Претражи модел", + "Search Chats": "Претражи ћаскања", + "Search Documents": "Претражи документе", + "Search Functions": "", + "Search Models": "Модели претраге", + "Search Prompts": "Претражи упите", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "Број резултата претраге", + "Search Tools": "", + "Searched {{count}} sites_one": "Претражио {{цоунт}} ситес_оне", + "Searched {{count}} sites_few": "Претражио {{цоунт}} ситес_феw", + "Searched {{count}} sites_other": "Претражио {{цоунт}} ситес_отхер", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "УРЛ адреса Сеарxнг упита", + "See readme.md for instructions": "Погледај readme.md за упутства", + "See what's new": "Погледај шта је ново", + "Seed": "Семе", + "Select a base model": "Избор основног модела", + "Select a engine": "", + "Select a function": "", + "Select a mode": "Изабери режим", + "Select a model": "Изабери модел", + "Select a pipeline": "Избор цевовода", + "Select a pipeline url": "Избор урл адресе цевовода", + "Select a tool": "", + "Select an Ollama instance": "Изабери Ollama инстанцу", + "Select Documents": "", + "Select model": "Изабери модел", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "Изабрани модели не подржавају уносе слика", + "Send": "Пошаљи", + "Send a Message": "Пошаљи поруку", + "Send message": "Пошаљи поруку", + "September": "Септембар", + "Serper API Key": "Серпер АПИ кључ", + "Serply API Key": "", + "Serpstack API Key": "Серпстацк АПИ кључ", + "Server connection verified": "Веза са сервером потврђена", + "Set as default": "Подеси као подразумевано", + "Set Default Model": "Подеси као подразумевани модел", + "Set embedding model (e.g. {{model}})": "Подеси модел уградње (нпр. {{model}})", + "Set Image Size": "Подеси величину слике", + "Set reranking model (e.g. {{model}})": "Подеси модел поновног рангирања (нпр. {{model}})", + "Set Steps": "Подеси кораке", + "Set Task Model": "Постављање модела задатка", + "Set Voice": "Подеси глас", + "Settings": "Подешавања", + "Settings saved successfully!": "Подешавања успешно сачувана!", + "Settings updated successfully": "", + "Share": "Подели", + "Share Chat": "Подели ћаскање", + "Share to OpenWebUI Community": "Подели са OpenWebUI заједницом", + "short-summary": "кратак сажетак", + "Show": "Прикажи", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "Прикажи пречице", + "Show your support!": "", + "Showcased creativity": "Приказана креативност", + "Sign in": "Пријави се", + "Sign Out": "Одјави се", + "Sign up": "Региструј се", + "Signing in": "Пријављивање", + "Source": "Извор", + "Speech recognition error: {{error}}": "Грешка у препознавању говора: {{error}}", + "Speech-to-Text Engine": "Мотор за говор у текст", + "Stop Sequence": "Секвенца заустављања", + "STT Model": "", + "STT Settings": "STT подешавања", + "Submit": "Пошаљи", + "Subtitle (e.g. about the Roman Empire)": "Поднаслов (нпр. о Римском царству)", + "Success": "Успех", + "Successfully updated.": "Успешно ажурирано.", + "Suggested": "Предложено", + "Support": "", + "Support this plugin:": "", + "System": "Систем", + "System Prompt": "Системски упит", + "Tags": "Ознаке", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Реците нам више:", + "Temperature": "Температура", + "Template": "Шаблон", + "Text Completion": "Допуна текста", + "Text-to-Speech Engine": "Мотор за текст у говор", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Хвала на вашем коментару!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Резултат треба да буде вредност између 0.0 (0%) и 1.0 (100%).", + "Theme": "Тема", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Ово осигурава да су ваши вредни разговори безбедно сачувани у вашој бекенд бази података. Хвала вам!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "Ово подешавање се не усклађује преко прегледача или уређаја.", + "This will delete": "", + "Thorough explanation": "Детаљно објашњење", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Савет: ажурирајте више променљивих слотова узастопно притиском на тастер Таб у уносу ћаскања након сваке замене.", + "Title": "Наслов", + "Title (e.g. Tell me a fun fact)": "Наслов (нпр. „реци ми занимљивост“)", + "Title Auto-Generation": "Самостално стварање наслова", + "Title cannot be an empty string.": "Наслов не може бити празан низ.", + "Title Generation Prompt": "Упит за стварање наслова", + "to": "до", + "To access the available model names for downloading,": "Да бисте приступили доступним именима модела за преузимање,", + "To access the GGUF models available for downloading,": "Да бисте приступили GGUF моделима доступним за преузимање,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "у унос ћаскања.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "Данас", + "Toggle settings": "Пребаци подешавања", + "Toggle sidebar": "Пребаци бочну траку", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Топ К", + "Top P": "Топ П", + "Trouble accessing Ollama?": "Проблеми са приступом Ollama-и?", + "TTS Model": "", + "TTS Settings": "TTS подешавања", + "TTS Voice": "", + "Type": "Тип", + "Type Hugging Face Resolve (Download) URL": "Унесите Hugging Face Resolve (Download) адресу", + "Uh-oh! There was an issue connecting to {{provider}}.": "Упс! Дошло је до проблема при повезивању са {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Ажурирај и копирај везу", + "Update password": "Ажурирај лозинку", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Отпреми GGUF модел", + "Upload Files": "Отпремање датотека", + "Upload Pipeline": "", + "Upload Progress": "Напредак отпремања", + "URL Mode": "Режим адресе", + "Use '#' in the prompt input to load and select your documents.": "Користи '#' у уносу упита да учитате и изаберете ваше документе.", + "Use Gravatar": "Користи Граватар", + "Use Initials": "Користи иницијале", + "use_mlock (Ollama)": "усе _млоцк (Оллама)", + "use_mmap (Ollama)": "усе _ммап (Оллама)", + "user": "корисник", + "User location successfully retrieved.": "", + "User Permissions": "Овлашћења корисника", + "Users": "Корисници", + "Utilize": "Искористи", + "Valid time units:": "Важеће временске јединице:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "променљива", + "variable to have them replaced with clipboard content.": "променљива за замену са садржајем оставе.", + "Version": "Издање", + "Voice": "", + "Warning": "Упозорење", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Упозорење: ако ажурирате или промените ваш модел уградње, мораћете поново да увезете све документе.", + "Web": "Веб", + "Web API": "", + "Web Loader Settings": "Подешавања веб учитавача", + "Web Params": "Веб параметри", + "Web Search": "Wеб претрага", + "Web Search Engine": "Wеб претраживач", + "Webhook URL": "Адреса веб-куке", + "WebUI Settings": "Подешавања веб интерфејса", + "WebUI will make requests to": "Веб интерфејс ће слати захтеве на", + "What’s New in": "Шта је ново у", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Када је историја искључена, нова ћаскања у овом прегледачу неће се појавити у вашој историји на било ком вашем уређају.", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "Радни простор", + "Write a prompt suggestion (e.g. Who are you?)": "Напишите предлог упита (нпр. „ко си ти?“)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Напишите сажетак у 50 речи који резимира [тему или кључну реч].", + "Yesterday": "Јуче", + "You": "Ти", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "Не можеш клонирати основни модел", + "You have no archived conversations.": "Немате архивиране разговоре.", + "You have shared this chat": "Поделили сте ово ћаскање", + "You're a helpful assistant.": "Ти си користан помоћник.", + "You're now logged in.": "Сада сте пријављени.", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Јутјуб", + "Youtube Loader Settings": "Подешавања Јутјуб учитавача" +} diff --git a/src/lib/i18n/locales/sv-SE/translation.json b/src/lib/i18n/locales/sv-SE/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..23dde641ba9359488cdeff78fd82fb53df48ef02 --- /dev/null +++ b/src/lib/i18n/locales/sv-SE/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' eller '-1' för inget utgångsdatum", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(t.ex. `sh webui.sh --api`)", + "(latest)": "(senaste)", + "{{ models }}": "{{ modeller }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Du kan inte ta bort en basmodell", + "{{modelName}} is thinking...": "{{modelName}} tänker...", + "{{user}}'s Chats": "{{user}}s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Backend krävs", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "En uppgiftsmodell används när du utför uppgifter som att generera titlar för chattar och webbsökningsfrågor", + "a user": "en användare", + "About": "Om", + "Account": "Konto", + "Account Activation Pending": "Kontoaktivering väntar", + "Accurate information": "Exakt information", + "Actions": "", + "Active Users": "Aktiva användare", + "Add": "Lägg till", + "Add a model id": "Lägga till ett modell-ID", + "Add a short description about what this model does": "Lägg till en kort beskrivning av vad den här modellen gör", + "Add a short title for this prompt": "Lägg till en kort titel för denna instruktion", + "Add a tag": "Lägg till en tagg", + "Add custom prompt": "Lägg till en anpassad instruktion", + "Add Docs": "Lägg till dokument", + "Add Files": "Lägg till filer", + "Add Memory": "Lägg till minne", + "Add message": "Lägg till meddelande", + "Add Model": "Lägg till modell", + "Add Tag": "", + "Add Tags": "Lägg till taggar", + "Add User": "Lägg till användare", + "Adjusting these settings will apply changes universally to all users.": "Justering av dessa inställningar kommer att tillämpa ändringar universellt för alla användare.", + "admin": "administratör", + "Admin": "Admin", + "Admin Panel": "Administrationspanel", + "Admin Settings": "Administratörsinställningar", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Administratörer har tillgång till alla verktyg hela tiden, medan användare behöver verktyg som tilldelas per modell i arbetsytan.", + "Advanced Parameters": "Avancerade parametrar", + "Advanced Params": "Avancerade parametrar", + "all": "alla", + "All Documents": "Alla dokument", + "All Users": "Alla användare", + "Allow": "Tillåt", + "Allow Chat Deletion": "Tillåt chattborttagning", + "Allow non-local voices": "Tillåt icke-lokala röster", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "alfanumeriska tecken och bindestreck", + "Already have an account?": "Har du redan ett konto?", + "an assistant": "en assistent", + "and": "och", + "and create a new shared link.": "och skapa en ny delad länk.", + "API Base URL": "API-bas-URL", + "API Key": "API-nyckel", + "API Key created.": "API-nyckel skapad.", + "API keys": "API-nycklar", + "April": "april", + "Archive": "Arkiv", + "Archive All Chats": "Arkivera alla chattar", + "Archived Chats": "Arkiverade chattar", + "are allowed - Activate this command by typing": "är tillåtna - Aktivera detta kommando genom att skriva", + "Are you sure?": "Är du säker?", + "Attach file": "Bifoga fil", + "Attention to detail": "Detaljerad uppmärksamhet", + "Audio": "Ljud", + "Audio settings updated successfully": "", + "August": "augusti", + "Auto-playback response": "Automatisk uppspelning", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 bas-URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 bas-URL krävs.", + "available!": "tillgänglig!", + "Back": "Tillbaka", + "Bad Response": "Felaktig respons", + "Banners": "Banners", + "Base Model (From)": "Basmodell (Från)", + "Batch Size (num_batch)": "Batchstorlek (num_batch)", + "before": "för", + "Being lazy": "Lägg till", + "Brave Search API Key": "API-nyckel för Brave Search", + "Bypass SSL verification for Websites": "Kringgå SSL-verifiering för webbplatser", + "Call": "Samtal", + "Call feature is not supported when using Web STT engine": "Samtalsfunktionen är inte kompatibel med Web Tal-till-text motor", + "Camera": "Kamera", + "Cancel": "Avbryt", + "Capabilities": "Kapaciteter", + "Change Password": "Ändra lösenord", + "Chat": "Chatt", + "Chat Background Image": "", + "Chat Bubble UI": "Chatbubblar UI", + "Chat Controls": "", + "Chat direction": "Chattriktning", + "Chat History": "Chatthistorik", + "Chat History is off for this browser.": "Chatthistoriken är avstängd för denna webbläsare.", + "Chats": "Chattar", + "Check Again": "Kontrollera igen", + "Check for updates": "Sök efter uppdateringar", + "Checking for updates...": "Söker efter uppdateringar...", + "Choose a model before saving...": "Välj en modell innan du sparar...", + "Chunk Overlap": "Överlappning", + "Chunk Params": "Chunk-parametrar", + "Chunk Size": "Chunk-storlek", + "Citation": "Citat", + "Clear memory": "Rensa minnet", + "Click here for help.": "Klicka här för hjälp.", + "Click here to": "Klicka här för att", + "Click here to download user import template file.": "", + "Click here to select": "Klicka här för att välja", + "Click here to select a csv file.": "Klicka här för att välja en csv-fil.", + "Click here to select a py file.": "Klicka här för att välja en python-fil.", + "Click here to select documents.": "Klicka här för att välja dokument.", + "click here.": "klicka här.", + "Click on the user role button to change a user's role.": "Klicka på knappen för användarroll för att ändra en användares roll.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "Klon", + "Close": "Stäng", + "Code formatted successfully": "", + "Collection": "Samling", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "ComfyUI Base URL krävs.", + "Command": "Kommando", + "Concurrent Requests": "Parallella anrop", + "Confirm": "", + "Confirm Password": "Bekräfta lösenord", + "Confirm your action": "", + "Connections": "Anslutningar", + "Contact Admin for WebUI Access": "Kontakta administratören för att få åtkomst till WebUI", + "Content": "Innehåll", + "Content Extraction": "", + "Context Length": "Kontextlängd", + "Continue Response": "Fortsätt svar", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "Kopierad delad chatt-URL till urklipp!", + "Copy": "Kopiera", + "Copy last code block": "Kopiera sista kodblock", + "Copy last response": "Kopiera sista svar", + "Copy Link": "Kopiera länk", + "Copying to clipboard was successful!": "Kopiering till urklipp lyckades!", + "Create a model": "Skapa en modell", + "Create Account": "Skapa konto", + "Create new key": "Skapa ny nyckel", + "Create new secret key": "Skapa ny hemlig nyckel", + "Created at": "Skapad", + "Created At": "Skapad", + "Created by": "", + "CSV Import": "", + "Current Model": "Aktuell modell", + "Current Password": "Nuvarande lösenord", + "Custom": "Anpassad", + "Customize models for a specific purpose": "Anpassa modeller för ett specifikt syfte", + "Dark": "Mörk", + "Dashboard": "Instrumentpanel", + "Database": "Databas", + "December": "december", + "Default": "Standard", + "Default (Automatic1111)": "Standard (Automatic1111)", + "Default (SentenceTransformers)": "Standard (SentenceTransformers)", + "Default Model": "Standardmodell", + "Default model updated": "Standardmodell uppdaterad", + "Default Prompt Suggestions": "Standardinstruktionsförslag", + "Default User Role": "Standardanvändarroll", + "delete": "radera", + "Delete": "Radera", + "Delete a model": "Ta bort en modell", + "Delete All Chats": "Ta bort alla chattar", + "Delete chat": "Radera chatt", + "Delete Chat": "Radera chatt", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "radera denna länk", + "Delete tool?": "", + "Delete User": "Radera användare", + "Deleted {{deleteModelTag}}": "Raderad {{deleteModelTag}}", + "Deleted {{name}}": "Borttagen {{name}}", + "Description": "Beskrivning", + "Didn't fully follow instructions": "Följde inte instruktionerna", + "Disabled": "", + "Discover a function": "", + "Discover a model": "Upptäck en modell", + "Discover a prompt": "Upptäck en instruktion", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "Upptäck, ladda ner och utforska anpassade instruktioner", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "Upptäck, ladda ner och utforska modellförinställningar", + "Dismissible": "Kan stängas", + "Display Emoji in Call": "Visa Emoji under samtal", + "Display the username instead of You in the Chat": "Visa användarnamnet istället för du i chatten", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Dokument", + "Document Settings": "Dokumentinställningar", + "Documentation": "Dokumentation", + "Documents": "Dokument", + "does not make any external connections, and your data stays securely on your locally hosted server.": "gör inga externa anslutningar, och dina data förblir säkra på din lokalt värdade server.", + "Don't Allow": "Tillåt inte", + "Don't have an account?": "Har du inget konto?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Tycker inte om utseendet", + "Done": "", + "Download": "Ladda ner", + "Download canceled": "Nedladdning avbruten", + "Download Database": "Ladda ner databas", + "Drop any files here to add to the conversation": "Släpp filer här för att lägga till i samtalet", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "t.ex. '30s', '10m'. Giltiga tidsenheter är 's', 'm', 'h'.", + "Edit": "Redigera", + "Edit Doc": "Redigera dokument", + "Edit Memory": "", + "Edit User": "Redigera användare", + "ElevenLabs": "", + "Email": "E-post", + "Embedding Batch Size": "Batchstorlek för inbäddning", + "Embedding Model": "Inbäddningsmodell", + "Embedding Model Engine": "Motor för inbäddningsmodell", + "Embedding model set to \"{{embedding_model}}\"": "Inbäddningsmodell inställd på \"{{embedding_model}}\"", + "Enable Chat History": "Aktivera chatthistorik", + "Enable Community Sharing": "Aktivera community-delning", + "Enable New Sign Ups": "Aktivera nya registreringar", + "Enable Web Search": "Aktivera webbsökning", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Se till att din CSV-fil innehåller fyra kolumner i denna ordning: Name, Email, Password, Role.", + "Enter {{role}} message here": "Skriv {{role}} meddelande här", + "Enter a detail about yourself for your LLMs to recall": "Skriv en detalj om dig själv för att dina LLMs ska komma ihåg", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "Ange API-nyckel för Brave Search", + "Enter Chunk Overlap": "Ange chunköverlappning", + "Enter Chunk Size": "Ange chunkstorlek", + "Enter Github Raw URL": "Ange Github Raw URL", + "Enter Google PSE API Key": "Ange Google PSE API-nyckel", + "Enter Google PSE Engine Id": "Ange Google PSE Engine Id", + "Enter Image Size (e.g. 512x512)": "Ange bildstorlek (t.ex. 512x512)", + "Enter language codes": "Skriv språkkoder", + "Enter model tag (e.g. {{modelTag}})": "Ange modelltagg (t.ex. {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Ange antal steg (t.ex. 50)", + "Enter Score": "Ange betyg", + "Enter Searxng Query URL": "Ange Searxng Query URL", + "Enter Serper API Key": "Ange Serper API-nyckel", + "Enter Serply API Key": "Ange Serply API-nyckel", + "Enter Serpstack API Key": "Ange Serpstack API-nyckel", + "Enter stop sequence": "Ange stoppsekvens", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "Ange Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Ange URL (t.ex. http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Ange URL (t.ex. http://localhost:11434)", + "Enter Your Email": "Ange din e-post", + "Enter Your Full Name": "Ange ditt fullständiga namn", + "Enter your message": "", + "Enter Your Password": "Ange ditt lösenord", + "Enter Your Role": "Ange din roll", + "Error": "Fel", + "Experimental": "Experimentell", + "Export": "Export", + "Export All Chats (All Users)": "Exportera alla chattar (alla användare)", + "Export chat (.json)": "Exportera chatt (.json)", + "Export Chats": "Exportera chattar", + "Export Documents Mapping": "Exportera dokumentmappning", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "Exportera modeller", + "Export Prompts": "Exportera instruktioner", + "Export Tools": "Exportera verktyg", + "External Models": "Externa modeller", + "Failed to create API Key.": "Misslyckades med att skapa API-nyckel.", + "Failed to read clipboard contents": "Misslyckades med att läsa urklippsinnehåll", + "Failed to update settings": "Misslyckades med att uppdatera inställningarna", + "February": "februari", + "Feel free to add specific details": "Tveka inte att lägga till specifika detaljer", + "File": "", + "File Mode": "Fil-läge", + "File not found.": "Fil hittades inte.", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Fingeravtrycksmanipulering upptäckt: Kan inte använda initialer som avatar. Återställning till standardprofilbild.", + "Fluidly stream large external response chunks": "Strömma flytande stora externa svarschunks", + "Focus chat input": "Fokusera på chattfältet", + "Followed instructions perfectly": "Följde instruktionerna perfekt", + "Form": "", + "Format your variables using square brackets like this:": "Formatera dina variabler med hakparenteser så här:", + "Frequency Penalty": "Straff för frekvens", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "Allmän", + "General Settings": "Allmänna inställningar", + "Generate Image": "Generera bild", + "Generating search query": "Genererar sökfråga", + "Generation Info": "Info om generation", + "Get up and running with": "", + "Global": "", + "Good Response": "Bra svar", + "Google PSE API Key": "Google PSE API-nyckel", + "Google PSE Engine Id": "Google PSE Engine Id", + "h:mm a": "h:mm a", + "has no conversations.": "har inga samtal.", + "Hello, {{name}}": "Hej, {{name}}", + "Help": "Hjälp", + "Hide": "Dölj", + "Hide Model": "", + "How can I help you today?": "Hur kan jag hjälpa dig idag?", + "Hybrid Search": "Hybrid sökning", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "Bildgenerering (experimentell)", + "Image Generation Engine": "Bildgenereringsmotor", + "Image Settings": "Bildinställningar", + "Images": "Bilder", + "Import Chats": "Importera chattar", + "Import Documents Mapping": "Importera dokumentmappning", + "Import Functions": "", + "Import Models": "Importera modeller", + "Import Prompts": "Importera instruktioner", + "Import Tools": "Importera verktyg", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Inkludera flaggan `--api` när du kör stable-diffusion-webui", + "Info": "Information", + "Input commands": "Indatakommandon", + "Install from Github URL": "Installera från Github-URL", + "Instant Auto-Send After Voice Transcription": "Skicka automatiskt efter rösttranskribering", + "Interface": "Gränssnitt", + "Invalid Tag": "Ogiltig tagg", + "January": "januari", + "join our Discord for help.": "gå med i vår Discord för hjälp.", + "JSON": "JSON", + "JSON Preview": "Förhandsversion av JSON", + "July": "juli", + "June": "juni", + "JWT Expiration": "JWT-utgångsdatum", + "JWT Token": "JWT-token", + "Keep Alive": "Keep Alive", + "Keyboard shortcuts": "Tangentbordsgenvägar", + "Knowledge": "Kunskap", + "Language": "Språk", + "large language models, locally.": "", + "Last Active": "Senast aktiv", + "Last Modified": "", + "Light": "Ljus", + "Listening...": "Lyssnar...", + "LLMs can make mistakes. Verify important information.": "LLM:er kan göra misstag. Granska viktig information.", + "Local Models": "Lokala modeller", + "LTR": "LTR", + "Made by OpenWebUI Community": "Skapad av OpenWebUI Community", + "Make sure to enclose them with": "Se till att bifoga dem med", + "Manage": "Hantera", + "Manage Models": "Hantera modeller", + "Manage Ollama Models": "Hantera Ollama-modeller", + "Manage Pipelines": "Hantera rörledningar", + "Manage Valves": "", + "March": "mars", + "Max Tokens (num_predict)": "Maximalt antal tokens (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Högst 3 modeller kan laddas ner samtidigt. Vänligen försök igen senare.", + "May": "maj", + "Memories accessible by LLMs will be shown here.": "Minnen som LLM:er kan komma åt visas här.", + "Memory": "Minnen", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Meddelanden du skickar efter att ha skapat din länk kommer inte att delas. Användare med URL:en kommer att kunna se delad chatt.", + "Minimum Score": "Tröskel", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Modellen '{{modelName}}' har laddats ner framgångsrikt.", + "Model '{{modelTag}}' is already in queue for downloading.": "Modellen '{{modelTag}}' är redan i kö för nedladdning.", + "Model {{modelId}} not found": "Modell {{modelId}} hittades inte", + "Model {{modelName}} is not vision capable": "Modellen {{modelName}} är inte synkapabel", + "Model {{name}} is now {{status}}": "Modellen {{name}} är nu {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Modellens filsystemväg upptäckt. Modellens kortnamn krävs för uppdatering, kan inte fortsätta.", + "Model ID": "Modell-ID", + "Model not selected": "Modell inte vald", + "Model Params": "Modell Params", + "Model updated successfully": "", + "Model Whitelisting": "Modellens vitlista", + "Model(s) Whitelisted": "Vitlistade modeller", + "Modelfile Content": "Modelfilens innehåll", + "Models": "Modeller", + "More": "Mer", + "Name": "Namn", + "Name Tag": "Namntag", + "Name your model": "Namnge din modell", + "New Chat": "Ny chatt", + "New Password": "Nytt lösenord", + "No content to speak": "", + "No documents found": "Inga dokument hittades", + "No file selected": "", + "No results found": "Inga resultat hittades", + "No search query generated": "Ingen sökfråga genererad", + "No source available": "Ingen tillgänglig källa", + "No valves to update": "", + "None": "Ingen", + "Not factually correct": "Inte faktiskt korrekt", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Obs: Om du anger en tröskel kommer sökningen endast att returnera dokument med ett betyg som är större än eller lika med tröskeln.", + "Notifications": "Notifikationer", + "November": "november", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "oktober", + "Off": "Av", + "Okay, Let's Go!": "Okej, nu kör vi!", + "OLED Dark": "Mörk (OLED)", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API inaktiverat", + "Ollama API is disabled": "Ollama API är inaktiverat", + "Ollama Version": "Ollama-version", + "On": "På", + "Only": "Endast", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Endast alfanumeriska tecken och bindestreck är tillåtna i kommandosträngen.", + "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.": "Hoppsan! Håll i dig! Dina filer är fortfarande i bearbetningsugnen. Vi lagar dem till perfektion. Var tålmodig så meddelar vi dig när de är redo.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Hoppsan! Det ser ut som om URL:en är ogiltig. Dubbelkolla gärna och försök igen.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Hoppsan! Du använder en ej stödd metod (endast frontend). Vänligen servera WebUI från backend.", + "Open AI (Dall-E)": "Öppna AI (Dall-E)", + "Open new chat": "Öppna ny chatt", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API-konfig", + "OpenAI API Key is required.": "OpenAI API-nyckel krävs.", + "OpenAI URL/Key required.": "OpenAI-URL/nyckel krävs.", + "or": "eller", + "Other": "Andra", + "Password": "Lösenord", + "PDF document (.pdf)": "PDF-dokument (.pdf)", + "PDF Extract Images (OCR)": "PDF Extrahera bilder (OCR)", + "pending": "väntande", + "Permission denied when accessing media devices": "Nekad behörighet vid åtkomst till mediaenheter", + "Permission denied when accessing microphone": "Nekad behörighet vid åtkomst till mikrofon", + "Permission denied when accessing microphone: {{error}}": "Tillstånd nekades vid åtkomst till mikrofon: {{error}}", + "Personalization": "Personalisering", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "Rörledningar", + "Pipelines Not Detected": "", + "Pipelines Valves": "Ventiler för rörledningar", + "Plain text (.txt)": "Text (.txt)", + "Playground": "Lekplats", + "Please carefully review the following warnings:": "", + "Positive attitude": "Positivt inställning", + "Previous 30 days": "Föregående 30 dagar", + "Previous 7 days": "Föregående 7 dagar", + "Profile Image": "Profilbild", + "Prompt": "Instruktion", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Instruktion (t.ex. Berätta en kuriosa om Romerska Imperiet)", + "Prompt Content": "Instruktionens innehåll", + "Prompt suggestions": "Instruktionsförslag", + "Prompts": "Instruktioner", + "Pull \"{{searchValue}}\" from Ollama.com": "Ladda ner \"{{searchValue}}\" från Ollama.com", + "Pull a model from Ollama.com": "Ladda ner en modell från Ollama.com", + "Query Params": "Inställningar för sökfråga", + "RAG Template": "RAG-mall", + "Read Aloud": "Läs igenom", + "Record voice": "Spela in röst", + "Redirecting you to OpenWebUI Community": "Omdirigerar dig till OpenWebUI Community", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Referera till dig själv som \"Användare\" (t.ex. \"Användaren lär sig spanska\")", + "Refused when it shouldn't have": "Avvisades när det inte borde ha gjort det", + "Regenerate": "Regenerera", + "Release Notes": "Versionsinformation", + "Remove": "Ta bort", + "Remove Model": "Ta bort modell", + "Rename": "Byt namn", + "Repeat Last N": "Upprepa senaste N", + "Request Mode": "Frågeläge", + "Reranking Model": "Reranking modell", + "Reranking model disabled": "Reranking modell inaktiverad", + "Reranking model set to \"{{reranking_model}}\"": "Reranking modell inställd på \"{{reranking_model}}\"", + "Reset": "", + "Reset Upload Directory": "Återställ uppladdningskatalog", + "Reset Vector Storage": "Återställ vektorlager", + "Response AutoCopy to Clipboard": "Svara AutoCopy till urklipp", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "Roll", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Kör", + "Save": "Spara", + "Save & Create": "Spara och skapa", + "Save & Update": "Spara och uppdatera", + "Save Tag": "", + "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": "Att spara chatloggar direkt till din webbläsares lagring stöds inte längre. Ta en stund och ladda ner och radera dina chattloggar genom att klicka på knappen nedan. Oroa dig inte, du kan enkelt importera dina chattloggar till backend genom", + "Scan": "Skanna", + "Scan complete!": "Skanning klar!", + "Scan for documents from {{path}}": "Skanna efter dokument från {{path}}", + "Search": "Sök", + "Search a model": "Sök efter en modell", + "Search Chats": "Sök i chattar", + "Search Documents": "Sök dokument", + "Search Functions": "", + "Search Models": "Sök modeller", + "Search Prompts": "Sök instruktioner", + "Search Query Generation Prompt": "Instruktion för generering av sökfrågor", + "Search Query Generation Prompt Length Threshold": "Tröskelvärde för generering av sökfrågor", + "Search Result Count": "Antal sökresultat", + "Search Tools": "Sökverktyg", + "Searched {{count}} sites_one": "Sökte på {{count}} sites_one", + "Searched {{count}} sites_other": "Sökte på {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "Söker \"{{searchQuery}}\"", + "Searxng Query URL": "Searxng Query URL", + "See readme.md for instructions": "Se readme.md för instruktioner", + "See what's new": "Se vad som är nytt", + "Seed": "Seed", + "Select a base model": "Välj en basmodell", + "Select a engine": "Välj en motor", + "Select a function": "", + "Select a mode": "Välj ett läge", + "Select a model": "Välj en modell", + "Select a pipeline": "Välj en rörledning", + "Select a pipeline url": "Välj en URL för rörledningen", + "Select a tool": "", + "Select an Ollama instance": "Välj en Ollama-instans", + "Select Documents": "Välj dokument", + "Select model": "Välj en modell", + "Select only one model to call": "Välj endast en modell att ringa", + "Selected model(s) do not support image inputs": "Valda modeller stöder inte bildinmatningar", + "Send": "Skicka", + "Send a Message": "Skicka ett meddelande", + "Send message": "Skicka meddelande", + "September": "september", + "Serper API Key": "Serper API-nyckel", + "Serply API Key": "Serply API-nyckel", + "Serpstack API Key": "Serpstack API-nyckel", + "Server connection verified": "Serveranslutning verifierad", + "Set as default": "Ange som standard", + "Set Default Model": "Ange standardmodell", + "Set embedding model (e.g. {{model}})": "Ställ in embedding modell (t.ex. {{model}})", + "Set Image Size": "Ange bildstorlek", + "Set reranking model (e.g. {{model}})": "Ställ in reranking modell (t.ex. {{model}})", + "Set Steps": "Ange steg", + "Set Task Model": "Ange uppgiftsmodell", + "Set Voice": "Ange röst", + "Settings": "Inställningar", + "Settings saved successfully!": "Inställningar sparades framgångsrikt!", + "Settings updated successfully": "Inställningar uppdaterades framgångsrikt", + "Share": "Dela", + "Share Chat": "Dela chatt", + "Share to OpenWebUI Community": "Dela till OpenWebUI Community", + "short-summary": "kort sammanfattning", + "Show": "Visa", + "Show Admin Details in Account Pending Overlay": "Visa administratörsinformation till väntande konton", + "Show Model": "", + "Show shortcuts": "Visa genvägar", + "Show your support!": "", + "Showcased creativity": "Visade kreativitet", + "Sign in": "Logga in", + "Sign Out": "Logga ut", + "Sign up": "Registrera dig", + "Signing in": "Loggar in", + "Source": "Källa", + "Speech recognition error: {{error}}": "Fel vid taligenkänning: {{error}}", + "Speech-to-Text Engine": "Tal-till-text-motor", + "Stop Sequence": "Stoppsekvens", + "STT Model": "Tal-till-text-modell", + "STT Settings": "Tal-till-text-inställningar", + "Submit": "Skicka in", + "Subtitle (e.g. about the Roman Empire)": "Undertext (t.ex. om Romerska Imperiet)", + "Success": "Framgång", + "Successfully updated.": "Uppdaterades framgångsrikt.", + "Suggested": "Föreslagen", + "Support": "", + "Support this plugin:": "", + "System": "System", + "System Prompt": "Systeminstruktion", + "Tags": "Taggar", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "Berätta mer:", + "Temperature": "Temperatur", + "Template": "Mall", + "Text Completion": "Textslutförande", + "Text-to-Speech Engine": "Text-till-tal-motor", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Tack för din feedback!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Betyget ska vara ett värde mellan 0.0 (0%) och 1.0 (100%).", + "Theme": "Tema", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Detta säkerställer att dina värdefulla samtal sparas säkert till din backend-databas. Tack!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Detta är en experimentell funktion som kanske inte fungerar som förväntat och som kan komma att ändras när som helst.", + "This setting does not sync across browsers or devices.": "Denna inställning synkroniseras inte mellan webbläsare eller enheter.", + "This will delete": "", + "Thorough explanation": "Djupare förklaring", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Tips: Uppdatera fler variabler genom att trycka på tabb-tangenten i chattinmatningen efter varje ersättning.", + "Title": "Titel", + "Title (e.g. Tell me a fun fact)": "Titel (t.ex. Berätta en kuriosa)", + "Title Auto-Generation": "Automatisk generering av titel", + "Title cannot be an empty string.": "Titeln får inte vara en tom sträng.", + "Title Generation Prompt": "Instruktion för titelgenerering", + "to": "till", + "To access the available model names for downloading,": "För att komma åt de tillgängliga modellnamnen för nedladdning,", + "To access the GGUF models available for downloading,": "För att komma åt de GGUF-modellerna som finns tillgängliga för nedladdning,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "För att få tillgång till WebUI, kontakta administratören. Administratörer kan hantera behörigheter från administrationspanelen.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Om du vill lägga till dokument här ska du först ladda upp dem till arbetsytan \"Dokument\".", + "to chat input.": "till chattinmatning.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Om du vill välja verktygslådor här måste du först lägga till dem i arbetsytan \"Verktyg\".", + "Today": "Idag", + "Toggle settings": "Växla inställningar", + "Toggle sidebar": "Växla sidofält", + "Tokens To Keep On Context Refresh (num_keep)": "Tokens att behålla vid kontextuppdatering (num_keep)", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Verktyg", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Topp K", + "Top P": "Topp P", + "Trouble accessing Ollama?": "Problem med att komma åt Ollama?", + "TTS Model": "Text-till-tal-modell", + "TTS Settings": "Text-till-tal-inställningar", + "TTS Voice": "Text-till-tal-röst", + "Type": "Typ", + "Type Hugging Face Resolve (Download) URL": "Skriv Hugging Face Resolve (nedladdning) URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "Oj då! Det uppstod ett problem med anslutningen till {{provider}}.", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "Uppdatera och kopiera länk", + "Update password": "Uppdatera lösenord", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "Ladda upp en GGUF-modell", + "Upload Files": "Ladda upp filer", + "Upload Pipeline": "Ladda upp rörledning", + "Upload Progress": "Uppladdningsframsteg", + "URL Mode": "URL-läge", + "Use '#' in the prompt input to load and select your documents.": "Använd '#' i instruktionsinmatningen för att ladda och välja dina dokument.", + "Use Gravatar": "Använd Gravatar", + "Use Initials": "Använd initialer", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "användare", + "User location successfully retrieved.": "", + "User Permissions": "Användarbehörigheter", + "Users": "Användare", + "Utilize": "Använd", + "Valid time units:": "Giltiga tidsenheter:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "variabel", + "variable to have them replaced with clipboard content.": "variabel för att få dem ersatta med urklippsinnehåll.", + "Version": "Version", + "Voice": "", + "Warning": "Varning", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Varning: Om du uppdaterar eller ändrar din embedding modell måste du importera alla dokument igen.", + "Web": "Webb", + "Web API": "Webb-API", + "Web Loader Settings": "Web Loader-inställningar", + "Web Params": "Web-parametrar", + "Web Search": "Webbsökning", + "Web Search Engine": "Webbsökmotor", + "Webhook URL": "Webhook-URL", + "WebUI Settings": "WebUI-inställningar", + "WebUI will make requests to": "WebUI kommer att skicka förfrågningar till", + "What’s New in": "Vad är nytt i", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "När historiken är avstängd visas inte nya chattar i denna webbläsare i din historik på någon av dina enheter.", + "Whisper (Local)": "Whisper (lokal)", + "Widescreen Mode": "Bredbildsläge", + "Workspace": "Arbetsyta", + "Write a prompt suggestion (e.g. Who are you?)": "Skriv ett instruktionsförslag (t.ex. Vem är du?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Skriv en sammanfattning på 50 ord som sammanfattar [ämne eller nyckelord].", + "Yesterday": "Igår", + "You": "Dig", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Du kan anpassa dina interaktioner med stora språkmodeller genom att lägga till minnen via knappen 'Hantera' nedan, så att de blir mer användbara och skräddarsydda för dig.", + "You cannot clone a base model": "Du kan inte klona en basmodell", + "You have no archived conversations.": "Du har inga arkiverade samtal.", + "You have shared this chat": "Du har delat denna chatt", + "You're a helpful assistant.": "Du är en hjälpsam assistent.", + "You're now logged in.": "Du är nu inloggad.", + "Your account status is currently pending activation.": "Ditt konto väntar på att bli aktiverat", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube Loader-inställningar" +} diff --git a/src/lib/i18n/locales/th-TH/translation.json b/src/lib/i18n/locales/th-TH/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..b566ac34c2c5a0986100bb8bdfcf3fb577e79ff9 --- /dev/null +++ b/src/lib/i18n/locales/th-TH/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' หรือ '-1' สำหรับไม่มีการหมดอายุ", + "(Beta)": "(เบต้า)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(เช่น `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(เช่น `sh webui.sh --api`)", + "(latest)": "(ล่าสุด)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: คุณไม่สามารถลบโมเดลพื้นฐานได้", + "{{modelName}} is thinking...": "{{modelName}} กำลังคิด...", + "{{user}}'s Chats": "การสนทนาของ {{user}}", + "{{webUIName}} Backend Required": "ต้องการ Backend ของ {{webUIName}}", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "ใช้โมเดลงานเมื่อทำงานเช่นการสร้างหัวข้อสำหรับการสนทนาและการค้นหาเว็บ", + "a user": "ผู้ใช้", + "About": "เกี่ยวกับ", + "Account": "บัญชี", + "Account Activation Pending": "การเปิดใช้งานบัญชีอยู่ระหว่างดำเนินการ", + "Accurate information": "ข้อมูลที่ถูกต้อง", + "Actions": "", + "Active Users": "ผู้ใช้ที่ใช้งานอยู่", + "Add": "เพิ่ม", + "Add a model id": "เพิ่มรหัสโมเดล", + "Add a short description about what this model does": "เพิ่มคำอธิบายสั้นๆ เกี่ยวกับสิ่งที่โมเดลนี้ทำ", + "Add a short title for this prompt": "เพิ่มหัวข้อสั้นๆ สำหรับพรอมต์นี้", + "Add a tag": "เพิ่มแท็ก", + "Add custom prompt": "เพิ่มพรอมต์ที่กำหนดเอง", + "Add Docs": "เพิ่มเอกสาร", + "Add Files": "เพิ่มไฟล์", + "Add Memory": "เพิ่มความจำ", + "Add message": "เพิ่มข้อความ", + "Add Model": "เพิ่มโมเดล", + "Add Tag": "", + "Add Tags": "เพิ่มแท็ก", + "Add User": "เพิ่มผู้ใช้", + "Adjusting these settings will apply changes universally to all users.": "การปรับการตั้งค่าเหล่านี้จะนำไปใช้กับผู้ใช้ทั้งหมด", + "admin": "ผู้ดูแลระบบ", + "Admin": "ผู้ดูแลระบบ", + "Admin Panel": "แผงผู้ดูแลระบบ", + "Admin Settings": "การตั้งค่าผู้ดูแลระบบ", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "ผู้ดูแลระบบสามารถเข้าถึงเครื่องมือทั้งหมดได้ตลอดเวลา; ผู้ใช้ต้องการเครื่องมือที่กำหนดต่อโมเดลในพื้นที่ทำงาน", + "Advanced Parameters": "พารามิเตอร์ขั้นสูง", + "Advanced Params": "พารามิเตอร์ขั้นสูง", + "all": "ทั้งหมด", + "All Documents": "เอกสารทั้งหมด", + "All Users": "ผู้ใช้ทั้งหมด", + "Allow": "อนุญาต", + "Allow Chat Deletion": "อนุญาตการลบการสนทนา", + "Allow non-local voices": "อนุญาตเสียงที่ไม่ใช่ท้องถิ่น", + "Allow User Location": "อนุญาตตำแหน่งผู้ใช้", + "Allow Voice Interruption in Call": "อนุญาตการแทรกเสียงในสาย", + "alphanumeric characters and hyphens": "อักขระตัวเลขและขีดกลาง", + "Already have an account?": "มีบัญชีอยู่แล้ว?", + "an assistant": "ผู้ช่วย", + "and": "และ", + "and create a new shared link.": "และสร้างลิงก์ที่แชร์ใหม่", + "API Base URL": "URL ฐานของ API", + "API Key": "คีย์ API", + "API Key created.": "สร้างคีย์ API แล้ว", + "API keys": "คีย์ API", + "April": "เมษายน", + "Archive": "เก็บถาวร", + "Archive All Chats": "เก็บถาวรการสนทนาทั้งหมด", + "Archived Chats": "การสนทนาที่เก็บถาวร", + "are allowed - Activate this command by typing": "ได้รับอนุญาต - เปิดใช้งานคำสั่งนี้โดยการพิมพ์", + "Are you sure?": "คุณแน่ใจหรือ?", + "Attach file": "แนบไฟล์", + "Attention to detail": "ใส่ใจในรายละเอียด", + "Audio": "เสียง", + "Audio settings updated successfully": "อัปเดตการตั้งค่าเสียงสำเร็จแล้ว", + "August": "สิงหาคม", + "Auto-playback response": "ตอบสนองการเล่นอัตโนมัติ", + "AUTOMATIC1111 Api Auth String": "สตริงการตรวจสอบ API ของ AUTOMATIC1111", + "AUTOMATIC1111 Base URL": "URL ฐานของ AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "ต้องการ URL ฐานของ AUTOMATIC1111", + "available!": "พร้อมใช้งาน!", + "Back": "กลับ", + "Bad Response": "การตอบสนองที่ไม่ดี", + "Banners": "แบนเนอร์", + "Base Model (From)": "โมเดลพื้นฐาน (จาก)", + "Batch Size (num_batch)": "ขนาดชุด (num_batch)", + "before": "ก่อน", + "Being lazy": "ขี้เกียจ", + "Brave Search API Key": "คีย์ API ของ Brave Search", + "Bypass SSL verification for Websites": "ข้ามการตรวจสอบ SSL สำหรับเว็บไซต์", + "Call": "โทร", + "Call feature is not supported when using Web STT engine": "ไม่รองรับฟีเจอร์การโทรเมื่อใช้เครื่องยนต์ Web STT", + "Camera": "กล้อง", + "Cancel": "ยกเลิก", + "Capabilities": "ความสามารถ", + "Change Password": "เปลี่ยนรหัสผ่าน", + "Chat": "แชท", + "Chat Background Image": "ภาพพื้นหลังแชท", + "Chat Bubble UI": "UI ฟองแชท", + "Chat Controls": "การควบคุมแชท", + "Chat direction": "ทิศทางการแชท", + "Chat History": "ประวัติการแชท", + "Chat History is off for this browser.": "ประวัติการแชทปิดอยู่สำหรับเบราว์เซอร์นี้", + "Chats": "แชท", + "Check Again": "ตรวจสอบอีกครั้ง", + "Check for updates": "ตรวจสอบการอัปเดต", + "Checking for updates...": "กำลังตรวจสอบการอัปเดต...", + "Choose a model before saving...": "เลือกโมเดลก่อนบันทึก...", + "Chunk Overlap": "ทับซ้อนส่วนข้อมูล", + "Chunk Params": "พารามิเตอร์ส่วนข้อมูล", + "Chunk Size": "ขนาดส่วนข้อมูล", + "Citation": "การอ้างอิง", + "Clear memory": "ล้างความจำ", + "Click here for help.": "คลิกที่นี่เพื่อขอความช่วยเหลือ", + "Click here to": "คลิกที่นี่เพื่อ", + "Click here to download user import template file.": "คลิกที่นี่เพื่อดาวน์โหลดไฟล์แม่แบบนำเข้าผู้ใช้", + "Click here to select": "คลิกที่นี่เพื่อเลือก", + "Click here to select a csv file.": "คลิกที่นี่เพื่อเลือกไฟล์ csv", + "Click here to select a py file.": "คลิกที่นี่เพื่อเลือกไฟล์ py", + "Click here to select documents.": "คลิกที่นี่เพื่อเลือกเอกสาร", + "click here.": "คลิกที่นี่", + "Click on the user role button to change a user's role.": "คลิกที่ปุ่มบทบาทผู้ใช้เพื่อเปลี่ยนบทบาทของผู้ใช้", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "การอนุญาตเขียนคลิปบอร์ดถูกปฏิเสธ โปรดตรวจสอบการตั้งค่าเบราว์เซอร์ของคุณเพื่อให้สิทธิ์ที่จำเป็น", + "Clone": "โคลน", + "Close": "ปิด", + "Code formatted successfully": "จัดรูปแบบโค้ดสำเร็จแล้ว", + "Collection": "คอลเลคชัน", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL ฐานของ ComfyUI", + "ComfyUI Base URL is required.": "ต้องการ URL ฐานของ ComfyUI", + "Command": "คำสั่ง", + "Concurrent Requests": "คำขอพร้อมกัน", + "Confirm": "ยืนยัน", + "Confirm Password": "ยืนยันรหัสผ่าน", + "Confirm your action": "ยืนยันการดำเนินการของคุณ", + "Connections": "การเชื่อมต่อ", + "Contact Admin for WebUI Access": "ติดต่อผู้ดูแลระบบสำหรับการเข้าถึง WebUI", + "Content": "เนื้อหา", + "Content Extraction": "การสกัดเนื้อหา", + "Context Length": "ความยาวของบริบท", + "Continue Response": "ตอบสนองต่อไป", + "Continue with {{provider}}": "ดำเนินการต่อด้วย {{provider}}", + "Controls": "การควบคุม", + "Copied shared chat URL to clipboard!": "คัดลอก URL แชทที่แชร์ไปยังคลิปบอร์ดแล้ว!", + "Copy": "คัดลอก", + "Copy last code block": "คัดลอกบล็อกโค้ดสุดท้าย", + "Copy last response": "คัดลอกการตอบสนองล่าสุด", + "Copy Link": "คัดลอกลิงก์", + "Copying to clipboard was successful!": "คัดลอกไปยังคลิปบอร์ดสำเร็จแล้ว!", + "Create a model": "สร้างโมเดล", + "Create Account": "สร้างบัญชี", + "Create new key": "สร้างคีย์ใหม่", + "Create new secret key": "สร้างคีย์ลับใหม่", + "Created at": "สร้างเมื่อ", + "Created At": "สร้างเมื่อ", + "Created by": "สร้างโดย", + "CSV Import": "นำเข้า CSV", + "Current Model": "โมเดลปัจจุบัน", + "Current Password": "รหัสผ่านปัจจุบัน", + "Custom": "กำหนดเอง", + "Customize models for a specific purpose": "ปรับแต่งโมเดลสำหรับวัตถุประสงค์เฉพาะ", + "Dark": "มืด", + "Dashboard": "แดชบอร์ด", + "Database": "ฐานข้อมูล", + "December": "ธันวาคม", + "Default": "ค่าเริ่มต้น", + "Default (Automatic1111)": "ค่าเริ่มต้น (Automatic1111)", + "Default (SentenceTransformers)": "ค่าเริ่มต้น (SentenceTransformers)", + "Default Model": "โมเดลค่าเริ่มต้น", + "Default model updated": "อัปเดตโมเดลค่าเริ่มต้นแล้ว", + "Default Prompt Suggestions": "คำแนะนำพรอมต์ค่าเริ่มต้น", + "Default User Role": "บทบาทผู้ใช้ค่าเริ่มต้น", + "delete": "ลบ", + "Delete": "ลบ", + "Delete a model": "ลบโมเดล", + "Delete All Chats": "ลบการสนทนาทั้งหมด", + "Delete chat": "ลบแชท", + "Delete Chat": "ลบแชท", + "Delete chat?": "ลบแชท?", + "Delete Doc": "", + "Delete function?": "ลบฟังก์ชัน?", + "Delete prompt?": "ลบพรอมต์?", + "delete this link": "ลบลิงก์นี้", + "Delete tool?": "ลบเครื่องมือ?", + "Delete User": "ลบผู้ใช้", + "Deleted {{deleteModelTag}}": "ลบ {{deleteModelTag}}", + "Deleted {{name}}": "ลบ {{name}}", + "Description": "คำอธิบาย", + "Didn't fully follow instructions": "ไม่ได้ปฏิบัติตามคำแนะนำทั้งหมด", + "Disabled": "ปิดใช้งาน", + "Discover a function": "ค้นหาฟังก์ชัน", + "Discover a model": "ค้นหาโมเดล", + "Discover a prompt": "ค้นหาพรอมต์", + "Discover a tool": "ค้นหาเครื่องมือ", + "Discover, download, and explore custom functions": "ค้นหา ดาวน์โหลด และสำรวจฟังก์ชันที่กำหนดเอง", + "Discover, download, and explore custom prompts": "ค้นหา ดาวน์โหลด และสำรวจพรอมต์ที่กำหนดเอง", + "Discover, download, and explore custom tools": "ค้นหา ดาวน์โหลด และสำรวจเครื่องมือที่กำหนดเอง", + "Discover, download, and explore model presets": "ค้นหา ดาวน์โหลด และสำรวจพรีเซ็ตโมเดล", + "Dismissible": "ยกเลิกได้", + "Display Emoji in Call": "แสดงอิโมจิในการโทร", + "Display the username instead of You in the Chat": "แสดงชื่อผู้ใช้แทนคุณในการแชท", + "Do not install functions from sources you do not fully trust.": "อย่าติดตั้งฟังก์ชันจากแหล่งที่คุณไม่ไว้วางใจอย่างเต็มที่", + "Do not install tools from sources you do not fully trust.": "อย่าติดตั้งเครื่องมือจากแหล่งที่คุณไม่ไว้วางใจอย่างเต็มที่", + "Document": "เอกสาร", + "Document Settings": "การตั้งค่าเอกสาร", + "Documentation": "เอกสารประกอบ", + "Documents": "เอกสาร", + "does not make any external connections, and your data stays securely on your locally hosted server.": "ไม่เชื่อมต่อภายนอกใดๆ และข้อมูลของคุณจะอยู่บนเซิร์ฟเวอร์ที่โฮสต์ในท้องถิ่นของคุณอย่างปลอดภัย", + "Don't Allow": "ไม่อนุญาต", + "Don't have an account?": "ยังไม่มีบัญชี?", + "don't install random functions from sources you don't trust.": "อย่าติดตั้งฟังก์ชันแบบสุ่มจากแหล่งที่คุณไม่ไว้วางใจ", + "don't install random tools from sources you don't trust.": "อย่าติดตั้งเครื่องมือแบบสุ่มจากแหล่งที่คุณไม่ไว้วางใจ", + "Don't like the style": "ไม่ชอบสไตล์นี้", + "Done": "เสร็จสิ้น", + "Download": "ดาวน์โหลด", + "Download canceled": "ยกเลิกการดาวน์โหลด", + "Download Database": "ดาวน์โหลดฐานข้อมูล", + "Drop any files here to add to the conversation": "วางไฟล์ใดๆ ที่นี่เพื่อเพิ่มในการสนทนา", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "เช่น '30s', '10m' หน่วยเวลาที่ถูกต้องคือ 's', 'm', 'h'", + "Edit": "แก้ไข", + "Edit Doc": "แก้ไขเอกสาร", + "Edit Memory": "แก้ไขความจำ", + "Edit User": "แก้ไขผู้ใช้", + "ElevenLabs": "", + "Email": "อีเมล", + "Embedding Batch Size": "ขนาดชุดการฝัง", + "Embedding Model": "โมเดลการฝัง", + "Embedding Model Engine": "เครื่องยนต์โมเดลการฝัง", + "Embedding model set to \"{{embedding_model}}\"": "ตั้งค่าโมเดลการฝังเป็น \"{{embedding_model}}\"", + "Enable Chat History": "เปิดใช้งานประวัติการแชท", + "Enable Community Sharing": "เปิดใช้งานการแชร์ในชุมชน", + "Enable New Sign Ups": "เปิดใช้งานการสมัครใหม่", + "Enable Web Search": "เปิดใช้งานการค้นหาเว็บ", + "Enabled": "เปิดใช้งาน", + "Engine": "เครื่องยนต์", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "ตรวจสอบว่าไฟล์ CSV ของคุณมี 4 คอลัมน์ในลำดับนี้: ชื่อ, อีเมล, รหัสผ่าน, บทบาท", + "Enter {{role}} message here": "ใส่ข้อความ {{role}} ที่นี่", + "Enter a detail about yourself for your LLMs to recall": "ใส่รายละเอียดเกี่ยวกับตัวคุณสำหรับ LLMs ของคุณให้จดจำ", + "Enter api auth string (e.g. username:password)": "ใส่สตริงการตรวจสอบ API (เช่น username:password)", + "Enter Brave Search API Key": "ใส่คีย์ API ของ Brave Search", + "Enter Chunk Overlap": "ใส่การทับซ้อนส่วนข้อมูล", + "Enter Chunk Size": "ใส่ขนาดส่วนข้อมูล", + "Enter Github Raw URL": "ใส่ URL ดิบของ Github", + "Enter Google PSE API Key": "ใส่คีย์ API ของ Google PSE", + "Enter Google PSE Engine Id": "ใส่รหัสเครื่องยนต์ของ Google PSE", + "Enter Image Size (e.g. 512x512)": "ใส่ขนาดภาพ (เช่น 512x512)", + "Enter language codes": "ใส่รหัสภาษา", + "Enter model tag (e.g. {{modelTag}})": "ใส่แท็กโมเดล (เช่น {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "ใส่จำนวนขั้นตอน (เช่น 50)", + "Enter Score": "ใส่คะแนน", + "Enter Searxng Query URL": "ใส URL การค้นหาของ Searxng", + "Enter Serper API Key": "ใส่คีย์ API ของ Serper", + "Enter Serply API Key": "ใส่คีย์ API ของ Serply", + "Enter Serpstack API Key": "ใส่คีย์ API ของ Serpstack", + "Enter stop sequence": "ใส่ลำดับหยุด", + "Enter system prompt": "ใส่พรอมต์ระบบ", + "Enter Tavily API Key": "ใส่คีย์ API ของ Tavily", + "Enter Tika Server URL": "ใส่ URL เซิร์ฟเวอร์ของ Tika", + "Enter Top K": "ใส่ Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "ใส่ URL (เช่น http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "ใส่ URL (เช่น http://localhost:11434)", + "Enter Your Email": "ใส่อีเมลของคุณ", + "Enter Your Full Name": "ใส่ชื่อเต็มของคุณ", + "Enter your message": "ใส่ข้อความของคุณ", + "Enter Your Password": "ใส่รหัสผ่านของคุณ", + "Enter Your Role": "ใส่บทบาทของคุณ", + "Error": "ข้อผิดพลาด", + "Experimental": "การทดลอง", + "Export": "ส่งออก", + "Export All Chats (All Users)": "ส่งออกการสนทนาทั้งหมด (ผู้ใช้ทั้งหมด)", + "Export chat (.json)": "ส่งออกการสนทนา (.json)", + "Export Chats": "ส่งออกการสนทนา", + "Export Documents Mapping": "ส่งออกการแมปเอกสาร", + "Export Functions": "ส่งออกฟังก์ชัน", + "Export LiteLLM config.yaml": "ส่งออกการตั้งค่า LiteLLM config.yaml", + "Export Models": "ส่งออกโมเดล", + "Export Prompts": "ส่งออกพรอมต์", + "Export Tools": "ส่งออกเครื่องมือ", + "External Models": "โมเดลภายนอก", + "Failed to create API Key.": "สร้างคีย์ API ล้มเหลว", + "Failed to read clipboard contents": "อ่านเนื้อหาคลิปบอร์ดล้มเหลว", + "Failed to update settings": "อัปเดตการตั้งค่าล้มเหลว", + "February": "กุมภาพันธ์", + "Feel free to add specific details": "สามารถเพิ่มรายละเอียดเฉพาะได้", + "File": "ไฟล์", + "File Mode": "โหมดไฟล์", + "File not found.": "ไม่พบไฟล์", + "Files": "ไฟล์", + "Filter is now globally disabled": "การกรองถูกปิดใช้งานทั่วโลกแล้ว", + "Filter is now globally enabled": "การกรองถูกเปิดใช้งานทั่วโลกแล้ว", + "Filters": "ตัวกรอง", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "ตรวจพบการปลอมแปลงลายนิ้วมือ: ไม่สามารถใช้ชื่อย่อเป็นอวตารได้ ใช้รูปโปรไฟล์เริ่มต้น", + "Fluidly stream large external response chunks": "สตรีมชิ้นส่วนการตอบสนองขนาดใหญ่จากภายนอกอย่างลื่นไหล", + "Focus chat input": "โฟกัสการป้อนแชท", + "Followed instructions perfectly": "ปฏิบัติตามคำแนะนำอย่างสมบูรณ์แบบ", + "Form": "ฟอร์ม", + "Format your variables using square brackets like this:": "จัดรูปแบบตัวแปรของคุณโดยใช้วงเล็บสี่เหลี่ยมเช่นนี้:", + "Frequency Penalty": "การลงโทษความถี่", + "Function created successfully": "สร้างฟังก์ชันสำเร็จ", + "Function deleted successfully": "ลบฟังก์ชันสำเร็จ", + "Function Description (e.g. A filter to remove profanity from text)": "คำอธิบายฟังก์ชัน (เช่น ตัวกรองเพื่อเอาคำหยาบออกจากข้อความ)", + "Function ID (e.g. my_filter)": "รหัสฟังก์ชัน (เช่น my_filter)", + "Function is now globally disabled": "ฟังก์ชันถูกปิดใช้งานทั่วโลกแล้ว", + "Function is now globally enabled": "ฟังก์ชันถูกเปิดใช้งานทั่วโลกแล้ว", + "Function Name (e.g. My Filter)": "ชื่อฟังก์ชัน (เช่น My Filter)", + "Function updated successfully": "อัปเดตฟังก์ชันสำเร็จ", + "Functions": "ฟังก์ชัน", + "Functions allow arbitrary code execution": "ฟังก์ชันอนุญาตการเรียกใช้โค้ดโดยพลการ", + "Functions allow arbitrary code execution.": "ฟังก์ชันอนุญาตการเรียกใช้โค้ดโดยพลการ", + "Functions imported successfully": "นำเข้าฟังก์ชันสำเร็จ", + "General": "ทั่วไป", + "General Settings": "การตั้งค่าทั่วไป", + "Generate Image": "สร้างภาพ", + "Generating search query": "สร้างคำค้นหา", + "Generation Info": "ข้อมูลการสร้าง", + "Get up and running with": "เริ่มต้นใช้งานด้วย", + "Global": "ทั่วโลก", + "Good Response": "การตอบสนองที่ดี", + "Google PSE API Key": "คีย์ API ของ Google PSE", + "Google PSE Engine Id": "รหัสเครื่องยนต์ของ Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "ไม่มีการสนทนา", + "Hello, {{name}}": "สวัสดี, {{name}}", + "Help": "ช่วยเหลือ", + "Hide": "ซ่อน", + "Hide Model": "ซ่อนโมเดล", + "How can I help you today?": "วันนี้ฉันจะช่วยอะไรคุณได้บ้าง?", + "Hybrid Search": "การค้นหาแบบไฮบริด", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "ฉันรับทราบว่าฉันได้อ่านและเข้าใจผลกระทบของการกระทำของฉัน ฉันทราบถึงความเสี่ยงที่เกี่ยวข้องกับการเรียกใช้โค้ดโดยพลการและฉันได้ตรวจสอบความน่าเชื่อถือของแหล่งที่มาแล้ว", + "Image Generation (Experimental)": "การสร้างภาพ (การทดลอง)", + "Image Generation Engine": "เครื่องยนต์การสร้างภาพ", + "Image Settings": "การตั้งค่าภาพ", + "Images": "ภาพ", + "Import Chats": "นำเข้าการสนทนา", + "Import Documents Mapping": "นำเข้าการแมปเอกสาร", + "Import Functions": "นำเข้าฟังก์ชัน", + "Import Models": "นำเข้าโมเดล", + "Import Prompts": "นำเข้าพรอมต์", + "Import Tools": "นำเข้าเครื่องมือ", + "Include `--api-auth` flag when running stable-diffusion-webui": "รวมแฟลก `--api-auth` เมื่อเรียกใช้ stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "รวมแฟลก `--api` เมื่อเรียกใช้ stable-diffusion-webui", + "Info": "ข้อมูล", + "Input commands": "คำสั่งป้อนข้อมูล", + "Install from Github URL": "ติดตั้งจาก URL ของ Github", + "Instant Auto-Send After Voice Transcription": "ส่งอัตโนมัติทันทีหลังจากการถอดเสียง", + "Interface": "อินเทอร์เฟซ", + "Invalid Tag": "แท็กไม่ถูกต้อง", + "January": "มกราคม", + "join our Discord for help.": "เข้าร่วม Discord ของเราเพื่อขอความช่วยเหลือ", + "JSON": "JSON", + "JSON Preview": "ดูตัวอย่าง JSON", + "July": "กรกฎาคม", + "June": "มิถุนายน", + "JWT Expiration": "การหมดอายุของ JWT", + "JWT Token": "โทเค็น JWT", + "Keep Alive": "คงอยู่", + "Keyboard shortcuts": "ทางลัดแป้นพิมพ์", + "Knowledge": "ความรู้", + "Language": "ภาษา", + "large language models, locally.": "โมเดลภาษาขนาดใหญ่ในเครื่อง", + "Last Active": "ใช้งานล่าสุด", + "Last Modified": "แก้ไขล่าสุด", + "Light": "แสง", + "Listening...": "กำลังฟัง...", + "LLMs can make mistakes. Verify important information.": "LLMs สามารถทำผิดพลาดได้ ตรวจสอบข้อมูลสำคัญ", + "Local Models": "โมเดลท้องถิ่น", + "LTR": "LTR", + "Made by OpenWebUI Community": "สร้างโดยชุมชน OpenWebUI", + "Make sure to enclose them with": "", + "Manage": "จัดการ", + "Manage Models": "จัดการโมเดล", + "Manage Ollama Models": "จัดการโมเดล Ollama", + "Manage Pipelines": "จัดการไปป์ไลน์", + "Manage Valves": "จัดการวาล์ว", + "March": "มีนาคม", + "Max Tokens (num_predict)": "โทเค็นสูงสุด (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "สามารถดาวน์โหลดโมเดลได้สูงสุด 3 โมเดลในเวลาเดียวกัน โปรดลองอีกครั้งในภายหลัง", + "May": "พฤษภาคม", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "ความจำ", + "Memory added successfully": "เพิ่มโมเดลสำเร็จ", + "Memory cleared successfully": "ล้าง", + "Memory deleted successfully": "ลบโมเดลสำเร็จ", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "ข้อความที่คุณส่งหลังจากสร้างลิงก์ของคุณแล้วจะไม่ถูกแชร์ ผู้ใช้ที่มี URL จะสามารถดูแชทที่แชร์ได้", + "Minimum Score": "คะแนนขั้นต่ำ", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "โมเดล '{{modelName}}' ถูกดาวน์โหลดเรียบร้อยแล้ว", + "Model '{{modelTag}}' is already in queue for downloading.": "โมเดล '{{modelTag}}' กำลังอยู่ในคิวสำหรับการดาวน์โหลด", + "Model {{modelId}} not found": "ไม่พบโมเดล {{modelId}}", + "Model {{modelName}} is not vision capable": "โมเดล {{modelName}} ไม่มีคุณสมบัติวิสชั่น", + "Model {{name}} is now {{status}}": "โมเดล {{name}} ขณะนี้ {{status}}", + "Model created successfully!": "สร้างโมเดลสำเร็จ!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "ตรวจพบเส้นทางระบบไฟล์ของโมเดล ต้องการชื่อย่อของโมเดลสำหรับการอัปเดต ไม่สามารถดำเนินการต่อได้", + "Model ID": "รหัสโมเดล", + "Model not selected": "ยังไม่ได้เลือกโมเดล", + "Model Params": "พารามิเตอร์ของโมเดล", + "Model updated successfully": "อัปเดตโมเดลเรียบร้อยแล้ว", + "Model Whitelisting": "การอนุญาตโมเดล", + "Model(s) Whitelisted": "โมเดลที่ได้รับอนุญาต", + "Modelfile Content": "เนื้อหาของไฟล์โมเดล", + "Models": "โมเดล", + "More": "เพิ่มเติม", + "Name": "ชื่อ", + "Name Tag": "ป้ายชื่อ", + "Name your model": "ตั้งชื่อโมเดลของคุณ", + "New Chat": "แชทใหม่", + "New Password": "รหัสผ่านใหม่", + "No content to speak": "ไม่มีเนื้อหาที่จะพูด", + "No documents found": "ไม่พบเอกสาร", + "No file selected": "ไม่ได้เลือกไฟล์", + "No results found": "ไม่มีผลลัพธ์", + "No search query generated": "ไม่มีการสร้างคำค้นหา", + "No source available": "ไม่มีแหล่งข้อมูล", + "No valves to update": "ไม่มีวาล์วที่จะอัปเดต", + "None": "ไม่มี", + "Not factually correct": "ไม่ถูกต้องตามข้อเท็จจริง", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "หมายเหตุ: หากคุณตั้งค่าคะแนนขั้นต่ำ การค้นหาจะคืนเอกสารที่มีคะแนนมากกว่าหรือเท่ากับคะแนนขั้นต่ำเท่านั้น", + "Notifications": "การแจ้งเตือน", + "November": "พฤศจิกายน", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth ID", + "October": "ตุลาคม", + "Off": "ปิด", + "Okay, Let's Go!": "ตกลง ไปกัน!", + "OLED Dark": "OLED โหมดมื", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "ปิด Ollama API", + "Ollama API is disabled": "Ollama API ถูกปิดใช้งาน", + "Ollama Version": "เวอร์ชั่น Ollama", + "On": "เปิด", + "Only": "เท่านั้น", + "Only alphanumeric characters and hyphens are allowed in the command string.": "อนุญาตให้ใช้เฉพาะอักขระตัวอักษรและตัวเลข รวมถึงเครื่องหมายขีดกลางในสตริงคำสั่งเท่านั้น", + "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.": "อุ๊บส์! ช้าก่อน! ไฟล์ของคุณยังอยู่ในขั้นตอนการประมวลผล เรากำลังจัดการให้สมบูรณ์แบบ โปรดอดทนสักครู่ แล้วเราจะแจ้งให้คุณทราบเมื่อไฟล์พร้อมแล้ว", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "อุ๊บส์! ดูเหมือนว่า URL ไม่ถูกต้อง กรุณาตรวจสอบและลองใหม่อีกครั้ง", + "Oops! There was an error in the previous response. Please try again or contact admin.": "อุ๊บส์! มีข้อผิดพลาดในคำตอบก่อนหน้า กรุณาลองใหม่อีกครั้งหรือติดต่อผู้ดูแลระบบ", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "อุ๊บส์! คุณกำลังใช้วิธีที่ไม่รองรับ (เฉพาะเว็บส่วนหน้า) กรุณาให้บริการ WebUI จากเว็บส่วนแบ็กเอนด์", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "เปิดแชทใหม่", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "เวอร์ชั่น Open WebUI (v{{OPEN_WEBUI_VERSION}}) ต่ำกว่าเวอร์ชั่นที่ต้องการ (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "การตั้งค่า OpenAI API", + "OpenAI API Key is required.": "จำเป็นต้องใช้คีย์ OpenAI API", + "OpenAI URL/Key required.": "จำเป็นต้องใช้ URL/คีย์ OpenAI", + "or": "หรือ", + "Other": "อื่น ๆ", + "Password": "รหัสผ่าน", + "PDF document (.pdf)": "เอกสาร PDF (.pdf)", + "PDF Extract Images (OCR)": "การแยกรูปภาพจาก PDF (OCR)", + "pending": "รอดำเนินการ", + "Permission denied when accessing media devices": "ถูกปฏิเสธเมื่อเข้าถึงอุปกรณ์", + "Permission denied when accessing microphone": "ถูกปฏิเสธเมื่อเข้าถึงไมโครโฟน", + "Permission denied when accessing microphone: {{error}}": "การอนุญาตถูกปฏิเสธเมื่อเข้าถึงไมโครโฟน: {{error}}", + "Personalization": "การปรับแต่ง", + "Pin": "ปักหมุด", + "Pinned": "ปักหมุดแล้ว", + "Pipeline deleted successfully": "ลบไปป์ไลน์เรียบร้อยแล้ว", + "Pipeline downloaded successfully": "ดาวน์โหลดไปป์ไลน์เรียบร้อยแล้ว", + "Pipelines": "ไปป์ไลน์", + "Pipelines Not Detected": "ไม่พบไปป์ไลน์", + "Pipelines Valves": "วาล์วของไปป์ไลน์", + "Plain text (.txt)": "ไฟล์ข้อความ (.txt)", + "Playground": "สนามทดสอบ", + "Please carefully review the following warnings:": "โปรดตรวจสอบคำเตือนต่อไปนี้อย่างละเอียด:", + "Positive attitude": "ทัศนคติด้านบวก", + "Previous 30 days": "30 วันที่ผ่านมา", + "Previous 7 days": "7 วันที่ผ่านมา", + "Profile Image": "รูปโปรไฟล์", + "Prompt": "พรอมต์", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "พรอมต์ (เช่น บอกข้อเท็จจริงที่น่าสนุกเกี่ยวกับจักรวรรดิโรมัน)", + "Prompt Content": "เนื้อหาพรอมต์", + "Prompt suggestions": "", + "Prompts": "พรอมต์", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "", + "Query Params": "พารามิเตอร์การค้นหา", + "RAG Template": "แม่แบบ RAG", + "Read Aloud": "อ่านออกเสียง", + "Record voice": "บันทึกเสียง", + "Redirecting you to OpenWebUI Community": "กำลังเปลี่ยนเส้นทางคุณไปยังชุมชน OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "เรียกตัวเองว่า \"ผู้ใช้\" (เช่น \"ผู้ใช้กำลังเรียนภาษาสเปน\")", + "Refused when it shouldn't have": "ปฏิเสธเมื่อไม่ควรทำ", + "Regenerate": "สร้างใหม่", + "Release Notes": "บันทึกรุ่น", + "Remove": "ลบ", + "Remove Model": "ลบโมเดล", + "Rename": "เปลี่ยนชื่อ", + "Repeat Last N": "ทำซ้ำครั้งล่าสุด N", + "Request Mode": "โหมดคำขอ", + "Reranking Model": "จัดอันดับใหม่โมเดล", + "Reranking model disabled": "ปิดการใช้งานโมเดลการจัดอันดับใหม่", + "Reranking model set to \"{{reranking_model}}\"": "ตั้งค่าโมเดลการจัดอันดับใหม่เป็น \"{{reranking_model}}\"", + "Reset": "รีเซ็ต", + "Reset Upload Directory": "รีเซ็ตไดเร็กทอรีการอัปโหลด", + "Reset Vector Storage": "รีเซ็ตการจัดเก็บเวกเตอร์", + "Response AutoCopy to Clipboard": "ตอบสนองการคัดลอกอัตโนมัติไปยังคลิปบอร์ด", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "ไม่สามารถเปิดการแจ้งเตือนการตอบสนองได้เนื่องจากเว็บไซต์ปฏิเสธ กรุณาเข้าการตั้งค่าเบราว์เซอร์ของคุณเพื่อให้สิทธิ์การเข้าถึงที่จำเป็น", + "Role": "บทบาท", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "รัน Llama 2, Code Llama และโมเดลอื่นๆ ปรับแต่งและสร้างของคุณเอง", + "Running": "กำลังทำงาน", + "Save": "บันทึก", + "Save & Create": "บันทึกและสร้าง", + "Save & Update": "บันทึกและอัปเดต", + "Save Tag": "", + "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": "การบันทึกบันทึกการสนทนาโดยตรงไปยังที่จัดเก็บในเบราว์เซอร์ของคุณไม่ได้รับการสนับสนุนอีกต่อไป โปรดสละเวลาสักครู่เพื่อดาวน์โหลดและลบบันทึกการสนทนาของคุณโดยคลิกที่ปุ่มด้านล่าง ไม่ต้องกังวล คุณสามารถนำเข้าบันทึกการสนทนาของคุณกลับไปยังส่วนแบ็กเอนด์ได้อย่างง่ายดายผ่าน", + "Scan": "สแกน", + "Scan complete!": "การสแกนเสร็จสมบูรณ์!", + "Scan for documents from {{path}}": "สแกนหาเอกสารจาก {{path}}", + "Search": "ค้นหา", + "Search a model": "ค้นหาโมเดล", + "Search Chats": "ค้นหาแชท", + "Search Documents": "ค้นหาเอกสาร", + "Search Functions": "ค้นหาฟังก์ชัน", + "Search Models": "ค้นหาโมเดล", + "Search Prompts": "ค้นหาพรอมต์", + "Search Query Generation Prompt": "พรอมต์การสร้างคำค้นหา", + "Search Query Generation Prompt Length Threshold": "เกณฑ์ความยาวของพรอมต์การสร้างคำค้นหา", + "Search Result Count": "จำนวนผลลัพธ์การค้นหา", + "Search Tools": "เครื่องมือค้นหา", + "Searched {{count}} sites_one": "ค้นหา {{count}} เว็บไซต์", + "Searched {{count}} sites_other": "ค้นหา {{count}} เว็บไซต์", + "Searching \"{{searchQuery}}\"": "กำลังค้นหา \"{{searchQuery}}\"", + "Searxng Query URL": "URL คำค้นหา", + "See readme.md for instructions": "ดู readme.md สำหรับคำแนะนำ", + "See what's new": "ดูสิ่งที่ใหม่", + "Seed": "Seed", + "Select a base model": "เลือกโมเดลฐาน", + "Select a engine": "เลือกเอนจิน", + "Select a function": "เลือกฟังก์ชัน", + "Select a mode": "เลือกโหมด", + "Select a model": "เลือกโมเดล", + "Select a pipeline": "เลือกไปป์ไลน์", + "Select a pipeline url": "เลือก URL ไปป์ไลน์", + "Select a tool": "เลือกเครื่องมือ", + "Select an Ollama instance": "เลือกอินสแตนซ์ Ollama", + "Select Documents": "เลือกเอกสาร", + "Select model": "เลือกโมเดล", + "Select only one model to call": "เลือกเพียงโมเดลเดียวที่จะใช้", + "Selected model(s) do not support image inputs": "โมเดลที่เลือกไม่รองรับภาพ", + "Send": "ส่ง", + "Send a Message": "ส่งข้อความ", + "Send message": "ส่งข้อความ", + "September": "กันยายน", + "Serper API Key": "คีย์ API ของ Serper", + "Serply API Key": "คีย์ API ของ Serply", + "Serpstack API Key": "คีย์ API ของ Serpstack", + "Server connection verified": "ยืนยันการเชื่อมต่อเซิร์ฟเวอร์แล้ว", + "Set as default": "ตั้งเป็นค่าเริ่มต้น", + "Set Default Model": "ตั้งโมเดลเริ่มต้น", + "Set embedding model (e.g. {{model}})": "ตั้งค่าโมเดลการฝัง (เช่น {{model}})", + "Set Image Size": "ตั้งค่าขนาดภาพ", + "Set reranking model (e.g. {{model}})": "ตั้งค่าโมเดลการจัดอันดับใหม่ (เช่น {{model}})", + "Set Steps": "ตั้งค่าขั้นตอน", + "Set Task Model": "ตั้งค่าโมเดลงาน", + "Set Voice": "ตั้งค่าเสียง", + "Settings": "การตั้งค่า", + "Settings saved successfully!": "บันทึกการตั้งค่าเรียบร้อยแล้ว!", + "Settings updated successfully": "อัปเดตการตั้งค่าเรียบร้อยแล้ว", + "Share": "แชร์", + "Share Chat": "แชร์แชท", + "Share to OpenWebUI Community": "แชร์ไปยังชุมชน OpenWebUI", + "short-summary": "สรุปสั้นๆ", + "Show": "แสดง", + "Show Admin Details in Account Pending Overlay": "แสดงรายละเอียดผู้ดูแลระบบในหน้าจอรอการอนุมัติบัญชี", + "Show Model": "แสดงโมเดล", + "Show shortcuts": "แสดงทางลัด", + "Show your support!": "แสดงการสนับสนุนของคุณ!", + "Showcased creativity": "แสดงความคิดสร้างสรรค์", + "Sign in": "ลงชื่อเข้าใช้", + "Sign Out": "ลงชื่อออก", + "Sign up": "สมัครสมาชิก", + "Signing in": "กำลังลงชื่อเข้าใช้", + "Source": "แหล่งที่มา", + "Speech recognition error: {{error}}": "ข้อผิดพลาดในการรู้จำเสียง: {{error}}", + "Speech-to-Text Engine": "เครื่องมือแปลงเสียงเป็นข้อความ", + "Stop Sequence": "หยุดลำดับ", + "STT Model": "โมเดลแปลงเสียงเป็นข้อความ", + "STT Settings": "การตั้งค่าแปลงเสียงเป็นข้อความ", + "Submit": "ส่ง", + "Subtitle (e.g. about the Roman Empire)": "คำบรรยาย (เช่น เกี่ยวกับจักรวรรดิโรมัน)", + "Success": "สำเร็จ", + "Successfully updated.": "อัปเดตเรียบร้อยแล้ว", + "Suggested": "แนะนำ", + "Support": "สนับสนุน", + "Support this plugin:": "สนับสนุนปลั๊กอินนี้:", + "System": "ระบบ", + "System Prompt": "ระบบพรอมต์", + "Tags": "ป้ายชื่อ", + "Tap to interrupt": "แตะเพื่อขัดจังหวะ", + "Tavily API Key": "คีย์ API ของ Tavily", + "Tell us more:": "บอกเรามากขึ้น:", + "Temperature": "อุณหภูมิ", + "Template": "แม่แบบ", + "Text Completion": "การเติมข้อความ", + "Text-to-Speech Engine": "เครื่องมือแปลงข้อความเป็นเสียง", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "ขอบคุณสำหรับความคิดเห็นของคุณ!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "นักพัฒนาที่อยู่เบื้องหลังปลั๊กอินนี้เป็นอาสาสมัครที่มีชื่นชอบการแบ่งบัน หากคุณพบว่าปลั๊กอินนี้มีประโยชน์ โปรดพิจารณาสนับสนุนการพัฒนาของเขา", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "คะแนนควรอยู่ระหว่าง 0.0 (0%) ถึง 1.0 (100%)", + "Theme": "ธีม", + "Thinking...": "กำลังคิด...", + "This action cannot be undone. Do you wish to continue?": "การกระทำนี้ไม่สามารถย้อนกลับได้ คุณต้องการดำเนินการต่อหรือไม่?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "สิ่งนี้ทำให้มั่นใจได้ว่าการสนทนาที่มีค่าของคุณจะถูกบันทึกอย่างปลอดภัยในฐานข้อมูลแบ็กเอนด์ของคุณ ขอบคุณ!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "นี่เป็นฟีเจอร์ทดลอง อาจไม่ทำงานตามที่คาดไว้และอาจมีการเปลี่ยนแปลงได้ตลอดเวลา", + "This setting does not sync across browsers or devices.": "การตั้งค่านี้ไม่ซิงค์ข้ามเบราว์เซอร์หรืออุปกรณ์", + "This will delete": "สิ่งนี้จะลบ", + "Thorough explanation": "คำอธิบายอย่างละเอียด", + "Tika": "Tika", + "Tika Server URL required.": "จำเป็นต้องมี URL ของเซิร์ฟเวอร์ Tika", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "เคล็ดลับ: อัปเดตช่องตัวแปรหลายช่องติดต่อกันโดยการกดปุ่มแท็บในช่องใส่ข้อความแชทหลังจากแต่ละการแทนที่", + "Title": "ชื่อเรื่อง", + "Title (e.g. Tell me a fun fact)": "ชื่อเรื่อง (เช่น บอกข้อเท็จจริงที่น่าสนุก)", + "Title Auto-Generation": "การสร้างชื่ออัตโนมัติ", + "Title cannot be an empty string.": "ชื่อเรื่องไม่สามารถเป็นสตริงว่างได้", + "Title Generation Prompt": "พรอมต์การสร้างชื่อเรื่อง", + "to": " ", + "To access the available model names for downloading,": "ในการเข้าถึงชื่อโมเดลที่มีให้ดาวน์โหลด", + "To access the GGUF models available for downloading,": "ในการเข้าถึงโมเดล GGUF ที่มีให้ดาวน์โหลด", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "ในการเข้าถึง WebUI โปรดติดต่อผู้ดูแลระบบ ผู้ดูแลระบบสามารถจัดการสถานะผู้ใช้จากแผงควบคุมผู้ดูแลระบบ", + "To add documents here, upload them to the \"Documents\" workspace first.": "ในการเพิ่มเอกสารที่นี่ อัปโหลดไปยังพื้นที่ทำงาน \"เอกสาร\" ก่อน", + "to chat input.": "ไปยังช่องใส่ข้อความแชท", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "ในการเลือกฟิลเตอร์ที่นี่ ให้เพิ่มไปยังพื้นที่ทำงาน \"ฟังก์ชัน\" ก่อน", + "To select toolkits here, add them to the \"Tools\" workspace first.": "ในการเลือกชุดเครื่องมือที่นี่ ให้เพิ่มไปยังพื้นที่ทำงาน \"เครื่องมือ\" ก่อน", + "Today": "วันนี้", + "Toggle settings": "สลับการตั้งค่า", + "Toggle sidebar": "สลับแถบด้านข้าง", + "Tokens To Keep On Context Refresh (num_keep)": "โทเค็นที่เก็บไว้เมื่อรีเฟรชบริบท (num_keep)", + "Tool created successfully": "สร้างเครื่องมือเรียบร้อยแล้ว", + "Tool deleted successfully": "ลบเครื่องมือเรียบร้อยแล้ว", + "Tool imported successfully": "นำเข้าเครื่องมือเรียบร้อยแล้ว", + "Tool updated successfully": "อัปเดตเครื่องมือเรียบร้อยแล้ว", + "Toolkit Description (e.g. A toolkit for performing various operations)": "คำอธิบายชุดเครื่องมือ (เช่น ชุดเครื่องมือสำหรับการดำเนินการต่างๆ)", + "Toolkit ID (e.g. my_toolkit)": "ID ชุดเครื่องมือ (เช่น my_toolkit)", + "Toolkit Name (e.g. My ToolKit)": "ชื่อชุดเครื่องมือ (เช่น My ToolKit)", + "Tools": "เครื่องมือ", + "Tools are a function calling system with arbitrary code execution": "เครื่องมือคือระบบการเรียกใช้ฟังก์ชันที่สามารถดำเนินการโค้ดใดๆ ได้", + "Tools have a function calling system that allows arbitrary code execution": "เครื่องมือมีระบบการเรียกใช้ฟังก์ชันที่สามารถดำเนินการโค้ดใดๆ ได้", + "Tools have a function calling system that allows arbitrary code execution.": "เครื่องมือมีระบบการเรียกใช้ฟังก์ชันที่สามารถดำเนินการโค้ดใดๆ ได้", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "มีปัญหาในการเข้าถึง Ollama?", + "TTS Model": "โมเดลแปลงข้อความเป็นเสียง", + "TTS Settings": "การตั้งค่าแปลงข้อความเป็นเสียง", + "TTS Voice": "เสียงแปลงข้อความเป็นเสียง", + "Type": "ประเภท", + "Type Hugging Face Resolve (Download) URL": "พิมพ์ URL ของ Hugging Face Resolve (Download)", + "Uh-oh! There was an issue connecting to {{provider}}.": "อุ๊ย! มีปัญหาในการเชื่อมต่อกับ {{provider}}", + "UI": "ส่วนติดต่อผู้ใช้", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "ไม่ทราบประเภทไฟล์ '{{file_type}}' ดำเนินการอัปโหลดไฟล์ต่อไป", + "Unpin": "ยกเลิกการปักหมุด", + "Update": "อัปเดต", + "Update and Copy Link": "อัปเดตและคัดลอกลิงก์", + "Update password": "อัปเดตรหัสผ่าน", + "Updated at": "อัปเดตเมื่อ", + "Upload": "อัปโหลด", + "Upload a GGUF model": "อัปโหลดโมเดล GGUF", + "Upload Files": "อัปโหลดไฟล์", + "Upload Pipeline": "อัปโหลดพายป์ไลน์", + "Upload Progress": "ความคืบหน้าการอัปโหลด", + "URL Mode": "โหมด URL", + "Use '#' in the prompt input to load and select your documents.": "ใช้ '#' ในการใส่พรอมต์เพื่อโหลดและเลือกเอกสารของคุณ", + "Use Gravatar": "ใช้ Gravatar", + "Use Initials": "ใช้ตัวย่อ", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "ผู้ใช้", + "User location successfully retrieved.": "ดึงตำแหน่งที่ตั้งของผู้ใช้เรียบร้อยแล้ว", + "User Permissions": "สิทธิ์ของผู้ใช้", + "Users": "ผู้ใช้", + "Utilize": "ใช้", + "Valid time units:": "หน่วยเวลาใช้ได้:", + "Valves": "วาล์ว", + "Valves updated": "วาล์วที่อัปเดตแล้ว", + "Valves updated successfully": "อัปเดตวาล์วเรียบร้อยแล้ว", + "variable": "ตัวแปร", + "variable to have them replaced with clipboard content.": "ตัวแปรเพื่อให้แทนที่ด้วยเนื้อหาคลิปบอร์ด", + "Version": "เวอร์ชัน", + "Voice": "เสียง", + "Warning": "คำเตือน", + "Warning:": "คำเตือน:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "คำเตือน: หากคุณอัปเดตหรือเปลี่ยนโมเดลการฝัง คุณจะต้องนำเข้าเอกสารทั้งหมดอีกครั้ง", + "Web": "เว็บ", + "Web API": "เว็บ API", + "Web Loader Settings": "การตั้งค่าเว็บโหลดเดอร์", + "Web Params": "พารามิเตอร์เว็บ", + "Web Search": "การค้นหาเว็บ", + "Web Search Engine": "เครื่องมือค้นหาเว็บ", + "Webhook URL": "URL ของ Webhook", + "WebUI Settings": "การตั้งค่า WebUI", + "WebUI will make requests to": "WebUI จะทำการร้องขอไปที่", + "What’s New in": "มีอะไรใหม่ใน", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "เมื่อปิดประวัติการใช้งาน แชทใหม่ในเบราว์เซอร์นี้จะไม่ปรากฏในประวัติของคุณในอุปกรณ์ใด ๆ", + "Whisper (Local)": "Whisper (โลคอล)", + "Widescreen Mode": "โหมดหน้าจอกว้าง", + "Workspace": "พื้นที่ทำงาน", + "Write a prompt suggestion (e.g. Who are you?)": "เขียนคำแนะนำพรอมต์ (เช่น คุณคือใคร?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "เขียนสรุปใน 50 คำที่สรุป [หัวข้อหรือคำสำคัญ]", + "Yesterday": "เมื่อวาน", + "You": "คุณ", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "คุณสามารถปรับแต่งการโต้ตอบของคุณกับ LLMs โดยเพิ่มความทรงจำผ่านปุ่ม 'จัดการ' ด้านล่าง ทำให้มันมีประโยชน์และเหมาะกับคุณมากขึ้น", + "You cannot clone a base model": "คุณไม่สามารถโคลนโมเดลฐานได้", + "You have no archived conversations.": "คุณไม่มีการสนทนาที่เก็บถาวร", + "You have shared this chat": "คุณได้แชร์แชทนี้แล้ว", + "You're a helpful assistant.": "คุณคือผู้ช่วยที่มีประโยชน์", + "You're now logged in.": "คุณเข้าสู่ระบบแล้ว", + "Your account status is currently pending activation.": "สถานะบัญชีของคุณกำลังรอการเปิดใช้งาน", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "การสนับสนุนทั้งหมดของคุณจะไปยังนักพัฒนาปลั๊กอินโดยตรง; Open WebUI ไม่รับส่วนแบ่งใด ๆ อย่างไรก็ตาม แพลตฟอร์มการระดมทุนที่เลือกอาจมีค่าธรรมเนียมของตัวเอง", + "Youtube": "Youtube", + "Youtube Loader Settings": "การตั้งค่าโหลดเดอร์ Youtube" +} diff --git a/src/lib/i18n/locales/tk-TM/transaltion.json b/src/lib/i18n/locales/tk-TM/transaltion.json new file mode 100644 index 0000000000000000000000000000000000000000..ae2fd7ba2fc78d42e223dc16558966e55e2a7639 --- /dev/null +++ b/src/lib/i18n/locales/tk-TM/transaltion.json @@ -0,0 +1,707 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' ýa-da '-1' möhlet ýok.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api`)": "(meselem, `sh webui.sh --api`)", + "(latest)": "(iň soňky)", + "{{ models }}": "{{ modeller }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Esasy modeli öçürip bilmersiňiz", + "{{modelName}} is thinking...": "{{modelName}} pikirlenýär...", + "{{user}}'s Chats": "{{user}}'iň Çatlary", + "{{webUIName}} Backend Required": "{{webUIName}} Backend Zerur", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Çatlar we web gözleg soraglary üçin başlyk döretmek ýaly wezipeleri ýerine ýetirýän wagty ulanylýar", + "a user": "ulanyjy", + "About": "Barada", + "Account": "Hasap", + "Accurate information": "Takyk maglumat", + "Add": "Goş", + "Add a model id": "Model ID goş", + "Add a short description about what this model does": "Bu modeliň näme edýändigi barada gysgaça düşündiriş goşuň", + "Add a short title for this prompt": "Bu düşündiriş üçin gysga başlyk goşuň", + "Add a tag": "Bir tag goşuň", + "Add custom prompt": "Özboluşly düşündiriş goşuň", + "Add Docs": "Resminamalar goş", + "Add Files": "Faýllar goş", + "Add Memory": "Ýat goş", + "Add message": "Habar goş", + "Add Model": "Model goş", + "Add Tags": "Taglar goş", + "Add User": "Ulanyjy goş", + "Adjusting these settings will apply changes universally to all users.": "Bu sazlamalary düzetmek ähli ulanyjylara birmeňzeş üýtgeşmeler girizer.", + "admin": "admin", + "Admin Panel": "Admin Paneli", + "Admin Settings": "Admin Sazlamalary", + "Advanced Parameters": "Ösen Parametrler", + "Advanced Params": "Ösen Parametrler", + "all": "ähli", + "All Documents": "Ähli Resminamalar", + "All Users": "Ähli Ulanyjylar", + "Allow": "Rugsat ber", + "Allow Chat Deletion": "Çaty öçürmäge rugsat ber", + "alphanumeric characters and hyphens": "harply-sanjy belgiler we defisler", + "Already have an account?": "Hasabyňyz barmy?", + "an assistant": "kömekçi", + "and": "we", + "and create a new shared link.": "we täze paýlaşylan baglanyşyk dörediň.", + "API Base URL": "API Esasy URL", + "API Key": "API Açar", + "API Key created.": "API Açar döredildi.", + "API keys": "API açarlary", + "April": "Aprel", + "Archive": "Arhiw", + "Archive All Chats": "Ähli Çatlary Arhiwle", + "Archived Chats": "Arhiwlenen Çatlar", + "are allowed - Activate this command by typing": "rugsat berilýär - bu buýrugy ýazyň", + "Are you sure?": "Kepillikmi?", + "Attach file": "Faýl goş", + "Attention to detail": "Detala üns", + "Audio": "Audio", + "August": "Awgust", + "Auto-playback response": "Awto-gaýtadan jogap", + "Auto-send input after 3 sec.": "3 sekuntdan soň awtomatiki ugrat", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Esasy URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Esasy URL zerur.", + "available!": "elýeterli!", + "Back": "Yzyna", + "Bad Response": "Erbet Jogap", + "Banners": "Bannerler", + "Base Model (From)": "Esasy Model (Kimden)", + "before": "öň", + "Being lazy": "Ýaltalyk", + "Brave Search API Key": "Brave Gözleg API Açar", + "Bypass SSL verification for Websites": "Web sahypalary üçin SSL barlagyny geçmek", + "Cancel": "Ýatyrmak", + "Capabilities": "Ukyplar", + "Change Password": "Paroly Üýtget", + "Chat": "Çat", + "Chat Bubble UI": "Çat Bubble UI", + "Chat direction": "Çat ugrukdyryş", + "Chat History": "Çat Taryhy", + "Chat History is off for this browser.": "Bu brauzer üçin Çat Taryhy öçürildi.", + "Chats": "Çatlar", + "Check Again": "Ýene Barla", + "Check for updates": "Täzelenmeleri barla", + "Checking for updates...": "Täzelenmeleri barlamak...", + "Choose a model before saving...": "Saklamazdan ozal model saýlaň...", + "Chunk Overlap": "Bölüm Aşyrmasy", + "Chunk Params": "Bölüm Parametrleri", + "Chunk Size": "Bölüm Ölçegi", + "Citation": "Sitata", + "Click here for help.": "Kömek üçin şu ýere basyň.", + "Click here to": "Şu ýere basyň", + "Click here to select": "Saýlamak üçin şu ýere basyň", + "Click here to select a csv file.": "CSV faýly saýlamak üçin şu ýere basyň.", + "Click here to select documents.": "Resminamalary saýlamak üçin şu ýere basyň.", + "click here.": "şu ýere basyň.", + "Click on the user role button to change a user's role.": "Ulanyjynyň roluny üýtgetmek üçin ulanyjy roly düwmesine basyň.", + "Clone": "Klon", + "Close": "Ýap", + "Collection": "Kolleksiýa", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Esasy URL", + "ComfyUI Base URL is required.": "ComfyUI Esasy URL zerur.", + "Command": "Buýruk", + "Concurrent Requests": "Meňzeş Haýyşlar", + "Confirm Password": "Paroly Tassyklap", + "Connections": "Baglanyşyklar", + "Content": "Mazmuny", + "Context Length": "Kontekst Uzynlygy", + "Continue Response": "Jogap Bermegi Dowam et", + "Conversation Mode": "Söhbet Reseimi", + "Copied shared chat URL to clipboard!": "Paýlaşylan çat URL buferine göçürildi!", + "Copy": "Göçür", + "Copy last code block": "Soňky kod blokyny göçür", + "Copy last response": "Soňky jogaby göçür", + "Copy Link": "Baglanyşygy Göçür", + "Copying to clipboard was successful!": "Buferine göçürmek üstünlikli boldy!", + "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şakdaky sorag üçin 3-5 sözden ybarat gysgaça söz düzümi dörediň, 3-5 söz çäklerine berk eýeriň we 'başlyk' sözüni ulanmaň:", + "Create a model": "Model döret", + "Create Account": "Hasap döret", + "Create new key": "Täze açar döret", + "Create new secret key": "Täze gizlin açar döret", + "Created at": "Döredilen wagty", + "Created At": "Döredilen wagty", + "Current Model": "Häzirki Model", + "Current Password": "Häzirki Parol", + "Custom": "Özboluşly", + "Customize models for a specific purpose": "Anyk maksat üçin modelleri düzmek", + "Dark": "Garaňky", + "Database": "Mazada", + "December": "Dekabr", + "Default": "Nokatlaýyn", + "Default (Automatic1111)": "Nokatlaýyn (Automatic1111)", + "Default (SentenceTransformers)": "Nokatlaýyn (SentenceTransformers)", + "Default (Web API)": "Nokatlaýyn (Web API)", + "Default Model": "Nokatlaýyn Model", + "Default model set successfully!": "Nokatlaýyn model üstünlikli gurnaldy!", + "Default Temperature": "Nokatlaýyn Temperaturasy", + "Define what this key is used for...": "Bu açaryň näme üçin ulanylýandygyny kesgitle...", + "Delete": "Öçür", + "Delete Account": "Hasaby Öçür", + "Delete Account Forever": "Hasaby Möhletsyz Öçür", + "Delete all": "Ählisini öçür", + "Delete All Chats": "Ähli Çatlary Öçür", + "Delete this Document": "Bu Resminamany Öçür", + "Deleted": "Öçürilen", + "Deleted Forever": "Möhletsyz Öçürilen", + "Description": "Düşündiriş", + "Developers": "Öndürijiler", + "Directory": "Katalog", + "Directory Type": "Katalog Typy", + "Disable": "Ýatyrmak", + "Disabled": "Ýatyrylan", + "Disconnected": "Baglanşyk kesildi", + "Display": "Görkeziş", + "Display Text": "Teksti Görkeziş", + "Document": "Resminama", + "Document File": "Resminama Faýly", + "Document Name": "Resminama Ady", + "Documents": "Resminamalar", + "Done": "Tamam", + "Downloading updates...": "Täzelenmeleri ýükläp...", + "Drag and drop files here": "Faýllary şu ýere süýräň we goýuň", + "Drop your .json, .txt, .csv or .md files here": "JSON, TXT, CSV ýa-da MD faýllaryňyzy şu ýere goýuň", + "Duplicate": "Göçür", + "Each request will contain a max of n documents": "Her haýyş n sany resminama bilen çäklenýär", + "Edit": "Redaktirle", + "Edit Labels": "Bellikleri Redaktirle", + "Edit message": "Habary Redaktirle", + "Edit Parameters": "Parametrleri Redaktirle", + "Email": "Email", + "Email Sent": "Email Iberildi", + "Enable": "Işjeňleşdir", + "Enable Character AI": "Häsiýetli AI Işjeňleşdir", + "Enable Web Search": "Web Gözlegini Işjeňleşdir", + "Enabled": "Işjeň", + "Encrypt messages": "Habarlar kodlansyn", + "Enter your email": "Email giriziň", + "Entries": "Girişler", + "Environment": "Daşky Gurşaw", + "Error": "Ýalňyşlyk", + "Error (400)": "Ýalňyşlyk (400)", + "Error (403)": "Ýalňyşlyk (403)", + "Error (404)": "Ýalňyşlyk (404)", + "Error (500)": "Ýalňyşlyk (500)", + "Error occurred": "Ýalňyşlyk ýüze çykdy", + "Example": "Mysal", + "Example(s)": "Mysal(lar)", + "Examples": "Mysallar", + "Explain a concept": "Bir konsepsiýany düşündiriň", + "Explore": "Barla", + "Export": "Eksport", + "Export All Data": "Ähli Maglumatlary Eksportla", + "Export data": "Maglumatlary Eksportla", + "External API": "Daşarky API", + "External API Endpoint": "Daşarky API Nokady", + "External ID": "Daşarky ID", + "Extra Models": "Goşmaça Modeller", + "Failed": "Netijesiz", + "Fallback": "Düşmek", + "False": "Ýalňyş", + "Family name": "Maşgala ady", + "FAQ": "Sorag-jogaplar", + "February": "Fewral", + "Field": "Meýdan", + "File": "Faýl", + "File Name": "Faýl Ady", + "File Type": "Faýl Typy", + "Files": "Faýllar", + "Fill out all required fields.": "Ähli zerur meýdanlary dolduryň.", + "Filter": "Süzgüç", + "Find your API Key here": "API Açaryňyzy şu ýerden tapyň", + "First name": "Ady", + "Following parameters are available in the command and are mandatory to specify:": "Buýruga degişli aşakdaky parametrler elýeterlidir we görkezmek hökmandyr:", + "For": "Üçin", + "Forever": "Möhletsyz", + "Forgot Password?": "Paroly unutdyňyzmy?", + "Forgot your password?": "Paroly unutdyňyzmy?", + "Free and Paid plans available": "Mugt we Tölegli planlar elýeterli", + "Friday": "Anna", + "From": "Kimden", + "Full Access": "Doly Elýeterlilik", + "Full Name": "Doly Ady", + "Full name": "Doly ady", + "Functions": "Funksiýalar", + "General": "Umumy", + "Generate": "Döret", + "Generate email templates": "Email şablonlaryny döret", + "Generate from a template": "Şablondan döret", + "Generate high-quality images": "Ýokary hilli suratlar döret", + "Generate professional profile descriptions": "Professional profil düşündirişleri döret", + "Generate prompts for language models": "Dil modelleri üçin düşündirişleri döret", + "Generate realistic photos": "Hakyky suratlary döret", + "Generate transcripts of audio and video": "Audio we wideo transkriptlerini döret", + "Generate translations": "Terjimeler döret", + "Generated from API keys": "API açarlaryndan döredildi", + "Generating...": "Döredilýär...", + "Generator": "Generator", + "Get Started": "Başlaň", + "Go": "Git", + "Go Back": "Yzyna Git", + "Go to": "Git", + "Go to link": "Baglanyşyga Git", + "Group": "Topar", + "Guidance": "Gollama", + "has been changed successfully": "üstünlikli üýtgedildi", + "have been changed successfully": "üstünlikli üýtgedildi", + "Hello": "Salam", + "Help": "Kömek", + "Hide": "Gizle", + "Hide all chats": "Ähli çatlary gizle", + "Hide Model": "Modeli Gizle", + "Hide Sidebar": "Gapdal Paneli Gizle", + "Hide Toolbar": "Gurallar Panelini Gizle", + "High Quality": "Ýokary Hilli", + "Home": "Baş Sahypa", + "Hours": "Sagady", + "Human-like responses": "Adam görnüşli jogaplar", + "Humorous": "Gülkünç", + "Identify objects in an image": "Suratda zatlary tanamak", + "If you need to quickly translate text from one language to another, use translation prompts.": "Bir dilden beýlekisine tekst terjime etmeli bolsaňyz, terjime düşündirişlerini ulanyň.", + "If you want to delete the account and all associated data, select": "Hasaby we degişli ähli maglumatlary öçürmek isleseňiz, saýlaň", + "If you're facing issues, try again later.": "Meseleler bar bolsa, soňra täzeden synanyşyň.", + "Image": "Surat", + "Image Generation": "Surat Döretme", + "Import": "Import", + "Import Data": "Maglumatlary Importla", + "In Progress": "Dowam edýär", + "Inactivity Timeout": "Işjeňsiz Töhmet", + "Incorrect email or password.": "Nädogry email ýa-da parol.", + "Info": "Maglumat", + "Information": "Maglumat", + "Information updated successfully!": "Maglumat üstünlikli täzelendi!", + "Input": "Girdi", + "Input Parameters": "Girdi Parametrleri", + "Installation Guide": "Gurnama Gollanma", + "Integrate custom models": "Özboluşly modelleri integrirle", + "Integrate with an external API": "Daşarky API bilen integrirle", + "Integration": "Integrasiýa", + "Internet Required": "Internet Zerur", + "Invalid API key.": "Nädogry API açar.", + "Invalid credentials, try again.": "Nädogry maglumatlar, täzeden synanyşyň.", + "Invalid or expired API key.": "Nädogry ýa-da möhleti geçen API açar.", + "It looks like we encountered an error. Please try again.": "Ýalňyşlyk ýüze çykdy. Täzeden synanyşyň.", + "January": "Ýanwar", + "Job title": "Iş Ady", + "Join": "Goşul", + "Join Date": "Goşulma Senesi", + "July": "Iýul", + "June": "Iýun", + "Just now": "Just now", + "Key": "Açar", + "Key (hidden)": "Açar (gizlin)", + "Key Details": "Açar Maglumatlar", + "Key Management": "Açar Dolandyryşy", + "Language": "Dil", + "Language Model": "Dil Modeli", + "Last access": "Soňky elýeterlilik", + "Last Access": "Soňky elýeterlilik", + "Last edited": "Soňky redaktirlenen", + "Last modified": "Soňky üýtgedilen", + "Last Modified": "Soňky üýtgedilen", + "Last name": "Familiýasy", + "Last update": "Soňky täzelenme", + "Last updated": "Soňky täzelenen", + "Later": "Soň", + "Launch": "Gur", + "Learn": "Öwren", + "Learn More": "Has köp öwreniň", + "License": "Rugsat", + "Light": "Açyk", + "Link": "Baglanyşyk", + "Link expired": "Baglanyşygyň möhleti geçdi", + "Load": "Ýükle", + "Loading": "Ýüklenýär", + "Local Models": "Ýerli Modeller", + "Log out": "Çyk", + "Logged out": "Çykdy", + "Logged out successfully": "Üstünlikli çykdy", + "Login": "Giriş", + "Login Required": "Giriş Zerur", + "Logs": "Loglar", + "Low": "Pes", + "Low Quality": "Pes Hilli", + "Maintain custom codebase": "Özboluşly kod bazasyny sakla", + "Management": "Dolandyryş", + "Manual Input": "El bilen Girdi", + "March": "Mart", + "Mark as Read": "Okalan hökmünde belläň", + "Match": "Gab", + "May": "Maý", + "Memory": "Ýat", + "Memory saved": "Ýat saklanyldy", + "Menu": "Menýu", + "Message": "Habar", + "Message limit reached for today. Please wait until tomorrow.": "Bu günki habar çägi geçdi. Ertir garaşyň.", + "Messages": "Habarlar", + "Method": "Usul", + "Microphone": "Mikrofon", + "Minute": "Minut", + "Minutes": "Minutlar", + "Model": "Model", + "Model Details": "Model Maglumatlary", + "Model History": "Model Taryhy", + "Model Management": "Model Dolandyryşy", + "Model name": "Model ady", + "Model URL": "Model URL", + "Mode": "Reseimi", + "Moderate Quality": "Orta Hilli", + "Modified": "Üýtgedilen", + "Modify User": "Ulanyjyny Üýtget", + "Monday": "Duşenbe", + "Monetization": "Pul gazanmak", + "Month": "Aý", + "More": "Has köp", + "More Info": "Has köp Maglumat", + "More options": "Has köp opsiýalar", + "Most Recent": "Iň Täze", + "Multiple file import is limited to": "Köp faýl importy çäkli", + "Name": "Ady", + "Name (hidden)": "Ady (gizlin)", + "Name is required": "Ady zerur", + "Navigate": "Gez", + "Need help?": "Kömek gerekmi?", + "New": "Täze", + "New Key": "Täze Açar", + "New Label": "Täze Bellik", + "New Password": "Täze Parol", + "New Secret Key": "Täze Gizlin Açar", + "New User": "Täze Ulanyjy", + "Next": "Indiki", + "No": "Ýok", + "No access": "Elýeterlilik ýok", + "No access.": "Elýeterlilik ýok.", + "No admins": "Adminler ýok", + "No archived chats": "Arhiwlenen çatlar ýok", + "No data found": "Maglumat tapylmady", + "No models available": "Modeller elýeterli däl", + "No permission to add a model.": "Model goşmak üçin rugsat ýok.", + "No permission to archive chat.": "Çaty arhiwlemek üçin rugsat ýok.", + "No permission to create chat.": "Çat döretmek üçin rugsat ýok.", + "No permission to delete chat.": "Çaty öçürmek üçin rugsat ýok.", + "No permission to edit chat.": "Çaty redaktirlemek üçin rugsat ýok.", + "No permission to view chat.": "Çaty görmek üçin rugsat ýok.", + "No shared chats": "Paýlaşylan çatlar ýok", + "No usage": "Ulanyş ýok", + "Non-admin users can only view chat details": "Admin däl ulanyjylar diňe çat maglumatlaryny görüp bilerler", + "None": "Hiç", + "Not Found": "Tapylmady", + "Not started": "Başlanmady", + "November": "Noýabr", + "October": "Oktýabr", + "Okay": "Bolýar", + "On": "Işjeň", + "Once you have added and configured your model, it will appear in the model dropdown list on the chat screen.": "Model goşup we konfigurirleýänden soň, çat ekranynda model aşak düşýän sanawda peýda bolar.", + "Only": "Diňe", + "Open": "Aç", + "OpenAI Base URL": "OpenAI Esasy URL", + "OpenAI Key": "OpenAI Açar", + "OpenAI Model": "OpenAI Model", + "OpenAI Token": "OpenAI Token", + "OpenAI URL": "OpenAI URL", + "Options": "Opsiýalar", + "Other": "Başga", + "Other Parameters": "Başga Parametrler", + "Owner": "Eýesi", + "Owner ID": "Eýesi ID", + "Page": "Sahypa", + "Parameter": "Parametr", + "Parameters": "Parametrler", + "Password": "Parol", + "Password must be at least 8 characters long": "Parol iň azyndan 8 harp bolmaly", + "Password must include at least one number and one letter": "Parol iň azyndan bir san we bir harp bolmaly", + "Paste copied text here...": "Göçürilen tekst şu ýere goýuň...", + "Paste text here...": "Tekst şu ýere goýuň...", + "PDF": "PDF", + "PDF Generation": "PDF Döretme", + "Pending": "Garaşylýar", + "Permission": "Rugsat", + "Personal Information": "Şahsy Maglumat", + "Photo": "Surat", + "Photos": "Suratlar", + "Please add a model.": "Model goşuň.", + "Please add more content": "Köp mazmun goşuň", + "Please enter your email to reset your password": "Parolyňyzy täzeden goýmak üçin email giriziň", + "Please try again later": "Soňra täzeden synanyşyň", + "Plugin": "Plagin", + "Plugin Settings": "Plagin Sazlamalary", + "Position": "Ýerleşýän ýeri", + "Post": "Post", + "Potential Risks": "Mümkin Töwekgelçilikler", + "Preparing your data...": "Maglumatlaryňyzy taýýarlaýar...", + "Preprocessing...": "Deslapky işlem...", + "Preview": "Öň-üşürgi", + "Previous": "Öňki", + "Print": "Çap et", + "Privacy Policy": "Gizlinlik Syýasaty", + "Processing": "Işlenýär", + "Profile": "Profil", + "Prompt": "Düşündiriş", + "Prompts": "Düşündirişler", + "Public": "Jemgyýetçilik", + "Quality": "Hil", + "Quantity": "Mukdar", + "Quick Start": "Çalt Başla", + "Read More": "Has köp oka", + "Realistic": "Hakyky", + "Recent": "Täze", + "Recent Access": "Täze Elýeterlilik", + "Recent Chats": "Täze Çatlar", + "Recent Documents": "Täze Resminamalar", + "Recent Files": "Täze Faýllar", + "Recipient": "Alyjy", + "Recognize speech and convert it to text": "Gürleýişi tanap tekste öwrüň", + "Records": "Ýazgylar", + "Reference": "Salgy", + "Refresh": "Täzeläň", + "Registration Date": "Hasaba alynma Senesi", + "Remove": "Aýyr", + "Remove Model": "Modeli Aýyr", + "Remove user": "Ulanyjyny aýyr", + "Rename": "Adyny Üýtget", + "Reorder": "Gaýtadan Sargyt Et", + "Request": "Haýyş", + "Required": "Zerur", + "Reset": "Täzeden Guruň", + "Reset Password": "Paroly Täzeden Guruň", + "Resources": "Resurslar", + "Response": "Jogap", + "Restored": "Dikeldilen", + "Results": "Netijeler", + "Review": "Syn", + "Review Prompt": "Düşündirişi Synla", + "Reviews": "Synlar", + "Role": "Roli", + "Save": "Sakla", + "Save Changes": "Üýtgeşmeleri Sakla", + "Saved": "Saklanan", + "Saturday": "Şenbe", + "Scale": "Şkalasy", + "Scan the code": "Kody Skanirle", + "Search": "Gözleg", + "Search All Chats": "Ähli Çatlary Gözle", + "Search for documents": "Resminamalary Gözle", + "Search for models": "Modelleri Gözle", + "Search for users": "Ulanyjylary Gözle", + "Search in chat": "Çatda gözle", + "Search query": "Gözleg soragy", + "Search...": "Gözleg...", + "Secret Key": "Gizlin Açar", + "See more": "Has köp gör", + "Select": "Saýla", + "Select a model": "Bir model saýla", + "Select a role": "Roli saýla", + "Select Chat": "Çat saýla", + "Select File": "Faýl saýla", + "Select Label": "Belligi saýla", + "Send": "Iber", + "Send and receive messages": "Habar iber we kabul et", + "Send Message": "Habar Iber", + "Sent": "Iberilen", + "Separate each entry with a new line": "Her girişi täze setir bilen aýraň", + "Separate multiple entries with a comma or new line": "Birnäçe girişi üzgüç ýa-da täze setir bilen aýraň", + "September": "Sentýabr", + "Server error": "Serwer ýalňyşlygy", + "Service Unavailable": "Hyzmat Elýeterli Däl", + "Session": "Sessia", + "Settings": "Sazlamalar", + "Setup": "Gurnama", + "Share": "Paýlaş", + "Share Chat": "Çaty Paýlaş", + "Share this chat with others": "Bu çaty beýlekiler bilen paýlaş", + "Shared": "Paýlaşylan", + "Shared Chats": "Paýlaşylan Çatlar", + "Show": "Görkez", + "Show all": "Ählisini görkez", + "Show all labels": "Ähli belligi görkez", + "Show All Prompts": "Ähli Düşündirişleri Görkez", + "Show API Keys": "API Açarlaryny Görkez", + "Show Model": "Modeli Görkez", + "Show Sidebar": "Gapdal Paneli Görkez", + "Show toolbar": "Gurallar Panelini Görkez", + "Sign In": "Giriş", + "Sign Out": "Çyk", + "Sign Up": "Hasaba al", + "Sign up": "Hasaba al", + "Sign up to get started": "Başlamak üçin hasaba alyň", + "Simple and advanced options available": "Ýönekeý we kämilleşdirilen opsiýalar elýeterli", + "Simply follow the guide to get started.": "Başlamak üçin görkezmä eýeriň.", + "Skip": "Geç", + "Smart Completion": "Akyldar Tamamlama", + "Software": "Programma üpjünçiligi", + "Sorry, an error occurred while processing your request.": "Bagyşlaň, haýyşyňyzy işlemekde ýalňyşlyk ýüze çykdy.", + "Sorry, the page you are looking for does not exist.": "Bagyşlaň, gözleýän sahypaňyz ýok.", + "Sorry, this link has expired.": "Bagyşlaň, bu baglanyşygyň möhleti geçdi.", + "Source": "Çeşme", + "Source Language": "Çeşme Dili", + "Space": "Ýer", + "Special Characters": "Aýratyn Harplar", + "Specify the model type": "Model typyny kesgitle", + "Standard": "Standart", + "Start": "Başla", + "Start a New Chat": "Täze Çat Başla", + "Start Chat": "Çat Başla", + "Start Date": "Başlangyç Sene", + "Start Time": "Başlanýan wagt", + "Started": "Başlandy", + "Status": "Ýagdaýy", + "Stop": "Bes et", + "Store your data securely": "Maglumatlaryňyzy howpsuz saklaň", + "Subject": "Tema", + "Submit": "Tabşyr", + "Success": "Üstünlik", + "Summary": "Jemleýji", + "Sunday": "Ýekşenbe", + "Support": "Goldaw", + "Switch to another model": "Başga modele geçiň", + "Switch to another model type": "Başga model typyna geçiň", + "System": "Sistema", + "System Requirements": "Sistema Talaplary", + "Table": "Jadwal", + "Tag": "Bellik", + "Tag List": "Bellik Sanawy", + "Take a tour": "Syýahat et", + "Talk to your data": "Maglumatlaryňyz bilen gürleşiň", + "Target Language": "Maksat Dili", + "Team": "Topar", + "Template": "Şablon", + "Templates": "Şablonlar", + "Temporary": "Wagtylaýyn", + "Test": "Synag", + "Text": "Tekst", + "Text Generation": "Tekst Döretme", + "Text to Image": "Tekstden Surat", + "Text to Speech": "Tekstden Söz", + "The data you need to integrate is currently unavailable.": "Integrirlemeli maglumatlaryňyz häzirki wagtda elýeterli däl.", + "The model has been added successfully!": "Model üstünlikli goşuldy!", + "The model type you are trying to add already exists.": "Goşmak isleýän model typyňyz eýýäm bar.", + "The page you requested could not be found.": "Soranyňyz sahypa tapylmady.", + "There are no prompts available at the moment.": "Häzirlikçe düşündirişler elýeterli däl.", + "There was an error adding the model.": "Model goşulmakda ýalňyşlyk ýüze çykdy.", + "There was an error deleting the model.": "Modeli öçürmekde ýalňyşlyk ýüze çykdy.", + "There was an error loading the models.": "Modelleri ýüklemekde ýalňyşlyk ýüze çykdy.", + "There was an error updating the model.": "Modeli täzeläp bolmady.", + "There was an error while archiving the chat.": "Çaty arhiwlemekde ýalňyşlyk ýüze çykdy.", + "There was an error while creating the chat.": "Çat döretmekde ýalňyşlyk ýüze çykdy.", + "There was an error while deleting the chat.": "Çaty öçürmekde ýalňyşlyk ýüze çykdy.", + "There was an error while editing the chat.": "Çaty redaktirlemekde ýalňyşlyk ýüze çykdy.", + "There was an error while fetching the chat.": "Çaty getirmekde ýalňyşlyk ýüze çykdy.", + "There was an error while saving the data.": "Maglumatlary saklamakda ýalňyşlyk ýüze çykdy.", + "There was an error while sending the message.": "Habary ibermekde ýalňyşlyk ýüze çykdy.", + "There was an error while updating the user.": "Ulanyjyny täzeläp bolmady.", + "These settings are global and will apply to all users and models.": "Bu sazlamalar umumy we ähli ulanyjylara we modellere degişlidir.", + "This action cannot be undone.": "Bu hereket yzyna dolanylyp bilinmez.", + "This email is already in use.": "Bu email eýýäm ulanylýar.", + "This email is not registered.": "Bu email hasaba alynmady.", + "This is the end of the chat": "Bu çatyň soňy", + "This link is expired or invalid.": "Bu baglanyşygyň möhleti geçdi ýa-da nädogry.", + "This model is already integrated.": "Bu model eýýäm integrirlenen.", + "This page does not exist.": "Bu sahypa ýok.", + "This will remove the user from the system.": "Bu ulanyjyny sistemadan aýyrar.", + "Thursday": "Penşenbe", + "Time": "Wagt", + "Time Limit Exceeded": "Wagt Limiti Geçdi", + "Timezone": "Wagt zolak", + "Title": "Ady", + "Today": "Şu gün", + "Token": "Token", + "Token limit exceeded.": "Token çägi geçdi.", + "Token or URL is incorrect.": "Token ýa-da URL nädogry.", + "Too many requests. Please try again later.": "Örän köp haýyşlar. Soňra täzeden synanyşyň.", + "Total Chats": "Jemi Çatlar", + "Total Memory": "Jemi Ýat", + "Total Storage": "Jemi Sakla", + "Total Users": "Jemi Ulanyjylar", + "Training": "Okuw", + "Tuesday": "Sişenbe", + "Type": "Typ", + "Unable to archive the chat.": "Çaty arhiwläp bolmady.", + "Unable to change the model.": "Modeli üýtgedip bolmady.", + "Unable to complete the request.": "Haýyşy tamamlap bolmady.", + "Unable to create chat.": "Çat döretmek mümkin däl.", + "Unable to delete the model.": "Modeli öçürmek mümkin däl.", + "Unable to delete the user.": "Ulanyjyny öçürmek mümkin däl.", + "Unable to find the requested resource.": "Soralan resurs tapylmady.", + "Unable to import data.": "Maglumatlary import edip bolmady.", + "Unable to load the chat.": "Çaty ýüklemek mümkin däl.", + "Unable to load the settings.": "Sazlamalary ýüklemek mümkin däl.", + "Unable to login.": "Giriş mümkin däl.", + "Unable to process the request.": "Haýyşy işläp bolmady.", + "Unable to reset password.": "Paroly täzeden gurmak mümkin däl.", + "Unable to retrieve data.": "Maglumatlary almak mümkin däl.", + "Unable to save": "Saklap bolmady", + "Unable to save the data.": "Maglumatlary saklap bolmady.", + "Unable to update": "Täzeläp bolmady", + "Unable to update the model.": "Modeli täzeläp bolmady.", + "Unable to update the user.": "Ulanyjyny täzeläp bolmady.", + "Unauthorized": "Rugsatsyz", + "Undo": "Yza al", + "Unlink": "Baglanyşygy aýyr", + "Unread Messages": "Okalmadyk Habarlar", + "Unshare": "Paýlaşma", + "Unverified": "Tassyklanmadyk", + "Update": "Täzeläň", + "Update successful": "Üstünlikli täzelenme", + "Updated": "Täzelenen", + "Updated at": "Täzelendi", + "Upload": "Ýükle", + "Upload Data": "Maglumat Ýükle", + "Upload file": "Faýl ýükle", + "Uploading": "Ýüklenýär", + "Usage": "Ulanyş", + "User": "Ulanyjy", + "User added": "Ulanyjy goşuldy", + "User deleted": "Ulanyjy öçürildi", + "User does not exist": "Ulanyjy ýok", + "User Guide": "Ulanyjy Gollanmasy", + "User ID": "Ulanyjy ID", + "User Management": "Ulanyjy Dolandyryşy", + "User Role": "Ulanyjy Roli", + "Username": "Ulanyjy Ady", + "Username is required": "Ulanyjy ady zerur", + "Users": "Ulanyjylar", + "Value": "Gymmaty", + "Verify": "Tassykla", + "Version": "Wersiýasy", + "View": "Gör", + "View All": "Ählisini gör", + "View archived": "Arhiwlenenleri gör", + "View Details": "Maglumatlary Gör", + "Voice Input": "Ses Girdi", + "Voice Recording": "Ses Ýazgysy", + "Volume": "Göwrümi", + "Warning": "Duýduryş", + "Wednesday": "Çarşenbe", + "Welcome": "Hoş geldiňiz", + "Welcome to ChatGPT! How can I help you today?": "ChatGPT-e hoş geldiňiz! Size nähili kömek edip bilerin?", + "Welcome to our service!": "Hyzmatymyza hoş geldiňiz!", + "Welcome!": "Hoş geldiňiz!", + "Width": "Ini", + "Work": "Iş", + "Write": "Ýaz", + "Write a review": "Syn ýaz", + "Write code": "Kod ýazyň", + "Write content": "Mazmun ýazyň", + "Year": "Ýyl", + "Yes": "Hawa", + "Yesterday": "Düýn", + "You are not authorized to view this content.": "Bu mazmuny görmek üçin rugsadyňyz ýok.", + "You can only add a maximum of": "Diňe iň köpüniň", + "You can request access from your administrator": "Administratoryňyzdan elýeterlilik haýyş edip bilersiňiz", + "You have reached your usage limit for today.": "Bu günki ulanyş çägiňize ýetdiňiz.", + "You have to choose a model first": "Ilki bilen model saýlamaly", + "You need a valid email": "Dogrudan email gerek", + "You will need to log in again to view the updated content": "Täzelenen mazmuny görmek üçin täzeden girmeli bolarsyňyz", + "Your data is safe with us": "Maglumatlaryňyz bizde howpsuz", + "Your email": "Emailiňiz", + "Your email address": "Email adresiňiz", + "Your message": "Habaryňyz", + "Your name": "Adyňyz", + "Your new password": "Täze parolyňyz", + "Your password": "Parolyňyz", + "Your payment is successful.": "Tölegiňiz üstünlikli boldy.", + "Your session has expired. Please log in again.": "Sessiaňyz tamamlandy. Täzeden giriň.", + "Your username": "Ulanyjy adyňyz", + "You're offline.": "Offline.", + "You've reached your token limit for the day.": "Günüňize token çägiňize ýetdiňiz.", + "ZIP Code": "Poçta Kody" +} diff --git a/src/lib/i18n/locales/tk-TW/translation.json b/src/lib/i18n/locales/tk-TW/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..b8c97a3a885d677287ec2d38c6906e59ff7f1f4b --- /dev/null +++ b/src/lib/i18n/locales/tk-TW/translation.json @@ -0,0 +1,714 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "", + "(Beta)": "", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "", + "(latest)": "", + "{{ models }}": "", + "{{ owner }}: You cannot delete a base model": "", + "{{modelName}} is thinking...": "", + "{{user}}'s Chats": "", + "{{webUIName}} Backend Required": "", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "", + "a user": "", + "About": "", + "Account": "", + "Account Activation Pending": "", + "Accurate information": "", + "Actions": "", + "Active Users": "", + "Add": "", + "Add a model id": "", + "Add a short description about what this model does": "", + "Add a short title for this prompt": "", + "Add a tag": "", + "Add custom prompt": "", + "Add Docs": "", + "Add Files": "", + "Add Memory": "", + "Add message": "", + "Add Model": "", + "Add Tag": "", + "Add Tags": "", + "Add User": "", + "Adjusting these settings will apply changes universally to all users.": "", + "admin": "", + "Admin": "", + "Admin Panel": "", + "Admin Settings": "", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "", + "Advanced Parameters": "", + "Advanced Params": "", + "all": "", + "All Documents": "", + "All Users": "", + "Allow": "", + "Allow Chat Deletion": "", + "Allow non-local voices": "", + "Allow User Location": "", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "", + "Already have an account?": "", + "an assistant": "", + "and": "", + "and create a new shared link.": "", + "API Base URL": "", + "API Key": "", + "API Key created.": "", + "API keys": "", + "April": "", + "Archive": "", + "Archive All Chats": "", + "Archived Chats": "", + "are allowed - Activate this command by typing": "", + "Are you sure?": "", + "Attach file": "", + "Attention to detail": "", + "Audio": "", + "Audio settings updated successfully": "", + "August": "", + "Auto-playback response": "", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "", + "AUTOMATIC1111 Base URL is required.": "", + "available!": "", + "Back": "", + "Bad Response": "", + "Banners": "", + "Base Model (From)": "", + "Batch Size (num_batch)": "", + "before": "", + "Being lazy": "", + "Brave Search API Key": "", + "Bypass SSL verification for Websites": "", + "Call": "", + "Call feature is not supported when using Web STT engine": "", + "Camera": "", + "Cancel": "", + "Capabilities": "", + "Change Password": "", + "Chat": "", + "Chat Background Image": "", + "Chat Bubble UI": "", + "Chat Controls": "", + "Chat direction": "", + "Chat History": "", + "Chat History is off for this browser.": "", + "Chats": "", + "Check Again": "", + "Check for updates": "", + "Checking for updates...": "", + "Choose a model before saving...": "", + "Chunk Overlap": "", + "Chunk Params": "", + "Chunk Size": "", + "Citation": "", + "Clear memory": "", + "Click here for help.": "", + "Click here to": "", + "Click here to download user import template file.": "", + "Click here to select": "", + "Click here to select a csv file.": "", + "Click here to select a py file.": "", + "Click here to select documents.": "", + "click here.": "", + "Click on the user role button to change a user's role.": "", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "", + "Close": "", + "Code formatted successfully": "", + "Collection": "", + "ComfyUI": "", + "ComfyUI Base URL": "", + "ComfyUI Base URL is required.": "", + "Command": "", + "Concurrent Requests": "", + "Confirm": "", + "Confirm Password": "", + "Confirm your action": "", + "Connections": "", + "Contact Admin for WebUI Access": "", + "Content": "", + "Content Extraction": "", + "Context Length": "", + "Continue Response": "", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "", + "Copy": "", + "Copy last code block": "", + "Copy last response": "", + "Copy Link": "", + "Copying to clipboard was successful!": "", + "Create a model": "", + "Create Account": "", + "Create new key": "", + "Create new secret key": "", + "Created at": "", + "Created At": "", + "Created by": "", + "CSV Import": "", + "Current Model": "", + "Current Password": "", + "Custom": "", + "Customize models for a specific purpose": "", + "Dark": "", + "Dashboard": "", + "Database": "", + "December": "", + "Default": "", + "Default (Automatic1111)": "", + "Default (SentenceTransformers)": "", + "Default Model": "", + "Default model updated": "", + "Default Prompt Suggestions": "", + "Default User Role": "", + "delete": "", + "Delete": "", + "Delete a model": "", + "Delete All Chats": "", + "Delete chat": "", + "Delete Chat": "", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "", + "Delete tool?": "", + "Delete User": "", + "Deleted {{deleteModelTag}}": "", + "Deleted {{name}}": "", + "Description": "", + "Didn't fully follow instructions": "", + "Disabled": "", + "Discover a function": "", + "Discover a model": "", + "Discover a prompt": "", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "", + "Dismissible": "", + "Display Emoji in Call": "", + "Display the username instead of You in the Chat": "", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "", + "Document Settings": "", + "Documentation": "", + "Documents": "", + "does not make any external connections, and your data stays securely on your locally hosted server.": "", + "Don't Allow": "", + "Don't have an account?": "", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "", + "Done": "", + "Download": "", + "Download canceled": "", + "Download Database": "", + "Drop any files here to add to the conversation": "", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "", + "Edit": "", + "Edit Doc": "", + "Edit Memory": "", + "Edit User": "", + "ElevenLabs": "", + "Email": "", + "Embedding Batch Size": "", + "Embedding Model": "", + "Embedding Model Engine": "", + "Embedding model set to \"{{embedding_model}}\"": "", + "Enable Chat History": "", + "Enable Community Sharing": "", + "Enable New Sign Ups": "", + "Enable Web Search": "", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "", + "Enter {{role}} message here": "", + "Enter a detail about yourself for your LLMs to recall": "", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "", + "Enter Chunk Overlap": "", + "Enter Chunk Size": "", + "Enter Github Raw URL": "", + "Enter Google PSE API Key": "", + "Enter Google PSE Engine Id": "", + "Enter Image Size (e.g. 512x512)": "", + "Enter language codes": "", + "Enter model tag (e.g. {{modelTag}})": "", + "Enter Number of Steps (e.g. 50)": "", + "Enter Score": "", + "Enter Searxng Query URL": "", + "Enter Serper API Key": "", + "Enter Serply API Key": "", + "Enter Serpstack API Key": "", + "Enter stop sequence": "", + "Enter system prompt": "", + "Enter Tavily API Key": "", + "Enter Tika Server URL": "", + "Enter Top K": "", + "Enter URL (e.g. http://127.0.0.1:7860/)": "", + "Enter URL (e.g. http://localhost:11434)": "", + "Enter Your Email": "", + "Enter Your Full Name": "", + "Enter your message": "", + "Enter Your Password": "", + "Enter Your Role": "", + "Error": "", + "Experimental": "", + "Export": "", + "Export All Chats (All Users)": "", + "Export chat (.json)": "", + "Export Chats": "", + "Export Documents Mapping": "", + "Export Functions": "", + "Export LiteLLM config.yaml": "", + "Export Models": "", + "Export Prompts": "", + "Export Tools": "", + "External Models": "", + "Failed to create API Key.": "", + "Failed to read clipboard contents": "", + "Failed to update settings": "", + "February": "", + "Feel free to add specific details": "", + "File": "", + "File Mode": "", + "File not found.": "", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "", + "Fluidly stream large external response chunks": "", + "Focus chat input": "", + "Followed instructions perfectly": "", + "Form": "", + "Format your variables using square brackets like this:": "", + "Frequency Penalty": "", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "", + "General Settings": "", + "Generate Image": "", + "Generating search query": "", + "Generation Info": "", + "Get up and running with": "", + "Global": "", + "Good Response": "", + "Google PSE API Key": "", + "Google PSE Engine Id": "", + "h:mm a": "", + "has no conversations.": "", + "Hello, {{name}}": "", + "Help": "", + "Hide": "", + "Hide Model": "", + "How can I help you today?": "", + "Hybrid Search": "", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "", + "Image Generation Engine": "", + "Image Settings": "", + "Images": "", + "Import Chats": "", + "Import Documents Mapping": "", + "Import Functions": "", + "Import Models": "", + "Import Prompts": "", + "Import Tools": "", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "", + "Info": "", + "Input commands": "", + "Install from Github URL": "", + "Instant Auto-Send After Voice Transcription": "", + "Interface": "", + "Invalid Tag": "", + "January": "", + "join our Discord for help.": "", + "JSON": "", + "JSON Preview": "", + "July": "", + "June": "", + "JWT Expiration": "", + "JWT Token": "", + "Keep Alive": "", + "Keyboard shortcuts": "", + "Knowledge": "", + "Language": "", + "large language models, locally.": "", + "Last Active": "", + "Last Modified": "", + "Light": "", + "Listening...": "", + "LLMs can make mistakes. Verify important information.": "", + "Local Models": "", + "LTR": "", + "Made by OpenWebUI Community": "", + "Make sure to enclose them with": "", + "Manage": "", + "Manage Models": "", + "Manage Ollama Models": "", + "Manage Pipelines": "", + "Manage Valves": "", + "March": "", + "Max Tokens (num_predict)": "", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "", + "May": "", + "Memories accessible by LLMs will be shown here.": "", + "Memory": "", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "", + "Minimum Score": "", + "Mirostat": "", + "Mirostat Eta": "", + "Mirostat Tau": "", + "MMMM DD, YYYY": "", + "MMMM DD, YYYY HH:mm": "", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "", + "Model '{{modelTag}}' is already in queue for downloading.": "", + "Model {{modelId}} not found": "", + "Model {{modelName}} is not vision capable": "", + "Model {{name}} is now {{status}}": "", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "", + "Model ID": "", + "Model not selected": "", + "Model Params": "", + "Model updated successfully": "", + "Model Whitelisting": "", + "Model(s) Whitelisted": "", + "Modelfile Content": "", + "Models": "", + "More": "", + "Name": "", + "Name Tag": "", + "Name your model": "", + "New Chat": "", + "New Password": "", + "No content to speak": "", + "No documents found": "", + "No file selected": "", + "No results found": "", + "No search query generated": "", + "No source available": "", + "No valves to update": "", + "None": "", + "Not factually correct": "", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "", + "Notifications": "", + "November": "", + "num_thread (Ollama)": "", + "OAuth ID": "", + "October": "", + "Off": "", + "Okay, Let's Go!": "", + "OLED Dark": "", + "Ollama": "", + "Ollama API": "", + "Ollama API disabled": "", + "Ollama API is disabled": "", + "Ollama Version": "", + "On": "", + "Only": "", + "Only alphanumeric characters and hyphens are allowed in the command string.": "", + "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.": "", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "", + "Oops! There was an error in the previous response. Please try again or contact admin.": "", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "", + "Open AI (Dall-E)": "", + "Open new chat": "", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "", + "OpenAI API": "", + "OpenAI API Config": "", + "OpenAI API Key is required.": "", + "OpenAI URL/Key required.": "", + "or": "", + "Other": "", + "Password": "", + "PDF document (.pdf)": "", + "PDF Extract Images (OCR)": "", + "pending": "", + "Permission denied when accessing media devices": "", + "Permission denied when accessing microphone": "", + "Permission denied when accessing microphone: {{error}}": "", + "Personalization": "", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "", + "Pipelines Not Detected": "", + "Pipelines Valves": "", + "Plain text (.txt)": "", + "Playground": "", + "Please carefully review the following warnings:": "", + "Positive attitude": "", + "Previous 30 days": "", + "Previous 7 days": "", + "Profile Image": "", + "Prompt": "", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "", + "Prompt Content": "", + "Prompt suggestions": "", + "Prompts": "", + "Pull \"{{searchValue}}\" from Ollama.com": "", + "Pull a model from Ollama.com": "", + "Query Params": "", + "RAG Template": "", + "Read Aloud": "", + "Record voice": "", + "Redirecting you to OpenWebUI Community": "", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "", + "Refused when it shouldn't have": "", + "Regenerate": "", + "Release Notes": "", + "Remove": "", + "Remove Model": "", + "Rename": "", + "Repeat Last N": "", + "Request Mode": "", + "Reranking Model": "", + "Reranking model disabled": "", + "Reranking model set to \"{{reranking_model}}\"": "", + "Reset": "", + "Reset Upload Directory": "", + "Reset Vector Storage": "", + "Response AutoCopy to Clipboard": "", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "", + "Rosé Pine": "", + "Rosé Pine Dawn": "", + "RTL": "", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "", + "Save": "", + "Save & Create": "", + "Save & Update": "", + "Save Tag": "", + "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": "", + "Scan": "", + "Scan complete!": "", + "Scan for documents from {{path}}": "", + "Search": "", + "Search a model": "", + "Search Chats": "", + "Search Documents": "", + "Search Functions": "", + "Search Models": "", + "Search Prompts": "", + "Search Query Generation Prompt": "", + "Search Query Generation Prompt Length Threshold": "", + "Search Result Count": "", + "Search Tools": "", + "Searched {{count}} sites_one": "", + "Searched {{count}} sites_other": "", + "Searching \"{{searchQuery}}\"": "", + "Searxng Query URL": "", + "See readme.md for instructions": "", + "See what's new": "", + "Seed": "", + "Select a base model": "", + "Select a engine": "", + "Select a function": "", + "Select a mode": "", + "Select a model": "", + "Select a pipeline": "", + "Select a pipeline url": "", + "Select a tool": "", + "Select an Ollama instance": "", + "Select Documents": "", + "Select model": "", + "Select only one model to call": "", + "Selected model(s) do not support image inputs": "", + "Send": "", + "Send a Message": "", + "Send message": "", + "September": "", + "Serper API Key": "", + "Serply API Key": "", + "Serpstack API Key": "", + "Server connection verified": "", + "Set as default": "", + "Set Default Model": "", + "Set embedding model (e.g. {{model}})": "", + "Set Image Size": "", + "Set reranking model (e.g. {{model}})": "", + "Set Steps": "", + "Set Task Model": "", + "Set Voice": "", + "Settings": "", + "Settings saved successfully!": "", + "Settings updated successfully": "", + "Share": "", + "Share Chat": "", + "Share to OpenWebUI Community": "", + "short-summary": "", + "Show": "", + "Show Admin Details in Account Pending Overlay": "", + "Show Model": "", + "Show shortcuts": "", + "Show your support!": "", + "Showcased creativity": "", + "Sign in": "", + "Sign Out": "", + "Sign up": "", + "Signing in": "", + "Source": "", + "Speech recognition error: {{error}}": "", + "Speech-to-Text Engine": "", + "Stop Sequence": "", + "STT Model": "", + "STT Settings": "", + "Submit": "", + "Subtitle (e.g. about the Roman Empire)": "", + "Success": "", + "Successfully updated.": "", + "Suggested": "", + "Support": "", + "Support this plugin:": "", + "System": "", + "System Prompt": "", + "Tags": "", + "Tap to interrupt": "", + "Tavily API Key": "", + "Tell us more:": "", + "Temperature": "", + "Template": "", + "Text Completion": "", + "Text-to-Speech Engine": "", + "Tfs Z": "", + "Thanks for your feedback!": "", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "", + "Theme": "", + "Thinking...": "", + "This action cannot be undone. Do you wish to continue?": "", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "", + "This setting does not sync across browsers or devices.": "", + "This will delete": "", + "Thorough explanation": "", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "", + "Title": "", + "Title (e.g. Tell me a fun fact)": "", + "Title Auto-Generation": "", + "Title cannot be an empty string.": "", + "Title Generation Prompt": "", + "to": "", + "To access the available model names for downloading,": "", + "To access the GGUF models available for downloading,": "", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "", + "To add documents here, upload them to the \"Documents\" workspace first.": "", + "to chat input.": "", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "", + "To select toolkits here, add them to the \"Tools\" workspace first.": "", + "Today": "", + "Toggle settings": "", + "Toggle sidebar": "", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "", + "Top P": "", + "Trouble accessing Ollama?": "", + "TTS Model": "", + "TTS Settings": "", + "TTS Voice": "", + "Type": "", + "Type Hugging Face Resolve (Download) URL": "", + "Uh-oh! There was an issue connecting to {{provider}}.": "", + "UI": "", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "", + "Unpin": "", + "Update": "", + "Update and Copy Link": "", + "Update password": "", + "Updated at": "", + "Upload": "", + "Upload a GGUF model": "", + "Upload Files": "", + "Upload Pipeline": "", + "Upload Progress": "", + "URL Mode": "", + "Use '#' in the prompt input to load and select your documents.": "", + "Use Gravatar": "", + "Use Initials": "", + "use_mlock (Ollama)": "", + "use_mmap (Ollama)": "", + "user": "", + "User location successfully retrieved.": "", + "User Permissions": "", + "Users": "", + "Utilize": "", + "Valid time units:": "", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "", + "variable to have them replaced with clipboard content.": "", + "Version": "", + "Voice": "", + "Warning": "", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "", + "Web": "", + "Web API": "", + "Web Loader Settings": "", + "Web Params": "", + "Web Search": "", + "Web Search Engine": "", + "Webhook URL": "", + "WebUI Settings": "", + "WebUI will make requests to": "", + "What’s New in": "", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "", + "Whisper (Local)": "", + "Widescreen Mode": "", + "Workspace": "", + "Write a prompt suggestion (e.g. Who are you?)": "", + "Write a summary in 50 words that summarizes [topic or keyword].": "", + "Yesterday": "", + "You": "", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "", + "You cannot clone a base model": "", + "You have no archived conversations.": "", + "You have shared this chat": "", + "You're a helpful assistant.": "", + "You're now logged in.": "", + "Your account status is currently pending activation.": "", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "", + "Youtube Loader Settings": "" +} diff --git a/src/lib/i18n/locales/tr-TR/translation.json b/src/lib/i18n/locales/tr-TR/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..d6af382a973d54fc772757648a4ba6afbea7b6b3 --- /dev/null +++ b/src/lib/i18n/locales/tr-TR/translation.json @@ -0,0 +1,714 @@ +{ + "'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 --api-auth username_password`)": "(e.g. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(örn. `sh webui.sh --api`)", + "(latest)": "(en son)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Temel modeli silemezsiniz", + "{{modelName}} is thinking...": "{{modelName}} düşünüyor...", + "{{user}}'s Chats": "{{user}} Sohbetleri", + "{{webUIName}} Backend Required": "{{webUIName}} Arkayüz Gerekli", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Bir görev modeli, sohbetler ve web arama sorguları için başlık oluşturma gibi görevleri yerine getirirken kullanılır", + "a user": "bir kullanıcı", + "About": "Hakkında", + "Account": "Hesap", + "Account Activation Pending": "Hesap Aktivasyonu Bekleniyor", + "Accurate information": "Doğru bilgi", + "Actions": "", + "Active Users": "Aktif Kullanıcılar", + "Add": "Ekle", + "Add a model id": "Model id ekle", + "Add a short description about what this model does": "Bu modelin ne yaptığı hakkında kısa bir açıklama ekle", + "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 custom prompt": "Özel prompt ekle", + "Add Docs": "Dökümanlar Ekle", + "Add Files": "Dosyalar Ekle", + "Add Memory": "Bellek Ekle", + "Add message": "Mesaj ekle", + "Add Model": "Model Ekle", + "Add Tag": "", + "Add Tags": "Etiketler ekle", + "Add User": "Kullanıcı 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": "Yönetici", + "Admin Panel": "Yönetici Paneli", + "Admin Settings": "Yönetici Ayarları", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Yöneticiler her zaman tüm araçlara erişebilir; kullanıcıların çalışma alanındaki model başına atanmış araçlara ihtiyacı vardır.", + "Advanced Parameters": "Gelişmiş Parametreler", + "Advanced Params": "Gelişmiş Parametreler", + "all": "tümü", + "All Documents": "Tüm Belgeler", + "All Users": "Tüm Kullanıcılar", + "Allow": "İzin ver", + "Allow Chat Deletion": "Sohbet Silmeye İzin Ver", + "Allow non-local voices": "Yerel olmayan seslere izin verin", + "Allow User Location": "Kullanıcı Konumuna İzin Ver", + "Allow Voice Interruption in Call": "Aramada Ses Kesintisine İ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", + "and create a new shared link.": "ve yeni bir paylaşılan bağlantı oluşturun.", + "API Base URL": "API Temel URL", + "API Key": "API Anahtarı", + "API Key created.": "API Anahtarı oluşturuldu.", + "API keys": "API anahtarları", + "April": "Nisan", + "Archive": "Arşiv", + "Archive All Chats": "Tüm Sohbetleri Arşivle", + "Archived Chats": "Arşivlenmiş Sohbetler", + "are allowed - Activate this command by typing": "izin verilir - Bu komutu yazarak etkinleştirin", + "Are you sure?": "Emin misiniz?", + "Attach file": "Dosya ekle", + "Attention to detail": "Ayrıntılara dikkat", + "Audio": "Ses", + "Audio settings updated successfully": "Ses ayarları başarıyla güncellendi", + "August": "Ağustos", + "Auto-playback response": "Yanıtı otomatik oynatma", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 API Kimlik Doğrulama Dizesi", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 Temel URL", + "AUTOMATIC1111 Base URL is required.": "AUTOMATIC1111 Temel URL gereklidir.", + "available!": "mevcut!", + "Back": "Geri", + "Bad Response": "Kötü Yanıt", + "Banners": "Afişler", + "Base Model (From)": "Temel Model ('den)", + "Batch Size (num_batch)": "Yığın Boyutu (num_batch)", + "before": "önce", + "Being lazy": "Tembelleşiyor", + "Brave Search API Key": "Brave Search API Anahtarı", + "Bypass SSL verification for Websites": "Web Siteleri için SSL doğrulamasını atlayın", + "Call": "Arama", + "Call feature is not supported when using Web STT engine": "Web STT motoru kullanılırken arama özelliği desteklenmiyor", + "Camera": "Kamera", + "Cancel": "İptal", + "Capabilities": "Yetenekler", + "Change Password": "Parola Değiştir", + "Chat": "Sohbet", + "Chat Background Image": "Sohbet Arka Plan Resmi", + "Chat Bubble UI": "Sohbet Balonu UI", + "Chat Controls": "", + "Chat direction": "Sohbet Yönü", + "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", + "Citation": "Alıntı", + "Clear memory": "Belleği temizle", + "Click here for help.": "Yardım için buraya tıklayın.", + "Click here to": "Şunu yapmak için buraya tıklayın:", + "Click here to download user import template file.": "Kullanıcı içe aktarma şablon dosyasını indirmek için buraya tıklayın.", + "Click here to select": "Seçmek için buraya tıklayın", + "Click here to select a csv file.": "Bir CSV dosyası seçmek için buraya tıklayın.", + "Click here to select a py file.": "Bir py dosyası 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.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Panoya yazma izni reddedildi. Tarayıcı ayarlarını kontrol ederek gerekli izinleri sağlayabilirsiniz.", + "Clone": "Klon", + "Close": "Kapat", + "Code formatted successfully": "Kod başarıyla biçimlendirildi", + "Collection": "Koleksiyon", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Temel URL", + "ComfyUI Base URL is required.": "ComfyUI Temel URL gerekli.", + "Command": "Komut", + "Concurrent Requests": "Eşzamanlı İstekler", + "Confirm": "Onayla", + "Confirm Password": "Parolayı Onayla", + "Confirm your action": "İşleminizi onaylayın", + "Connections": "Bağlantılar", + "Contact Admin for WebUI Access": "WebUI Erişimi için Yöneticiyle İletişime Geçin", + "Content": "İçerik", + "Content Extraction": "", + "Context Length": "Bağlam Uzunluğu", + "Continue Response": "Yanıta Devam Et", + "Continue with {{provider}}": "{{provider}} ile devam et", + "Controls": "", + "Copied shared chat URL to clipboard!": "Paylaşılan sohbet URL'si panoya kopyalandı!", + "Copy": "Kopyala", + "Copy last code block": "Son kod bloğunu kopyala", + "Copy last response": "Son yanıtı kopyala", + "Copy Link": "Bağlantıyı Kopyala", + "Copying to clipboard was successful!": "Panoya kopyalama başarılı!", + "Create a model": "Bir model oluştur", + "Create Account": "Hesap Oluştur", + "Create new key": "Yeni anahtar oluştur", + "Create new secret key": "Yeni gizli anahtar oluştur", + "Created at": "Oluşturulma tarihi", + "Created At": "Şu Tarihte Oluşturuldu:", + "Created by": "Şunun tarafından oluşturuldu:", + "CSV Import": "CSV İçe Aktarma", + "Current Model": "Mevcut Model", + "Current Password": "Mevcut Parola", + "Custom": "Özel", + "Customize models for a specific purpose": "Modelleri belirli amaçlar için özelleştir", + "Dark": "Koyu", + "Dashboard": "Kontrol Paneli", + "Database": "Veritabanı", + "December": "Aralık", + "Default": "Varsayılan", + "Default (Automatic1111)": "Varsayılan (Automatic1111)", + "Default (SentenceTransformers)": "Varsayılan (SentenceTransformers)", + "Default Model": "Varsayılan Model", + "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": "Sil", + "Delete a model": "Bir modeli sil", + "Delete All Chats": "Tüm Sohbetleri Sil", + "Delete chat": "Sohbeti sil", + "Delete Chat": "Sohbeti Sil", + "Delete chat?": "Sohbeti sil?", + "Delete Doc": "", + "Delete function?": "Fonksiyonu sil?", + "Delete prompt?": "Promptu sil?", + "delete this link": "bu bağlantıyı sil", + "Delete tool?": "Aracı sil?", + "Delete User": "Kullanıcıyı Sil", + "Deleted {{deleteModelTag}}": "{{deleteModelTag}} silindi", + "Deleted {{name}}": "{{name}} silindi", + "Description": "Açıklama", + "Didn't fully follow instructions": "Talimatları tam olarak takip etmedi", + "Disabled": "", + "Discover a function": "Bir fonksiyon keşfedin", + "Discover a model": "Bir model keşfedin", + "Discover a prompt": "Bir prompt keşfedin", + "Discover a tool": "Bir araç keşfedin", + "Discover, download, and explore custom functions": "Özel fonksiyonları keşfedin, indirin ve inceleyin", + "Discover, download, and explore custom prompts": "Özel promptları keşfedin, indirin ve inceleyin", + "Discover, download, and explore custom tools": "Özel araçları keşfedin, indirin ve inceleyin", + "Discover, download, and explore model presets": "Model ön ayarlarını keşfedin, indirin ve inceleyin", + "Dismissible": "Reddedilebilir", + "Display Emoji in Call": "Aramada Emoji Göster", + "Display the username instead of You in the Chat": "Sohbet'te Siz yerine kullanıcı adını göster", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "Belge", + "Document Settings": "Belge Ayarları", + "Documentation": "Dökümantasyon", + "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?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "Tarzını beğenmedim", + "Done": "Tamamlandı", + "Download": "İndir", + "Download canceled": "İndirme iptal edildi", + "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": "Düzenle", + "Edit Doc": "Belgeyi Düzenle", + "Edit Memory": "Belleği Düzenle", + "Edit User": "Kullanıcıyı Düzenle", + "ElevenLabs": "", + "Email": "E-posta", + "Embedding Batch Size": "Gömme Yığın Boyutu", + "Embedding Model": "Gömme Modeli", + "Embedding Model Engine": "Gömme Modeli Motoru", + "Embedding model set to \"{{embedding_model}}\"": "Gömme modeli \"{{embedding_model}}\" olarak ayarlandı", + "Enable Chat History": "Sohbet Geçmişini Etkinleştir", + "Enable Community Sharing": "Topluluk Paylaşımını Etkinleştir", + "Enable New Sign Ups": "Yeni Kayıtları Etkinleştir", + "Enable Web Search": "Web Aramasını Etkinleştir", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "CSV dosyanızın şu sırayla 4 sütun içerdiğinden emin olun: İsim, E-posta, Şifre, Rol.", + "Enter {{role}} message here": "Buraya {{role}} mesajını girin", + "Enter a detail about yourself for your LLMs to recall": "LLM'lerinizin hatırlaması için kendiniz hakkında bir bilgi girin", + "Enter api auth string (e.g. username:password)": "Api auth dizesini girin (örn. kullanıcı adı:parola)", + "Enter Brave Search API Key": "Brave Search API Anahtarını Girin", + "Enter Chunk Overlap": "Chunk Örtüşmesini Girin", + "Enter Chunk Size": "Chunk Boyutunu Girin", + "Enter Github Raw URL": "Github Raw URL'sini girin", + "Enter Google PSE API Key": "Google PSE API Anahtarını Girin", + "Enter Google PSE Engine Id": "Google PSE Engine Id'sini Girin", + "Enter Image Size (e.g. 512x512)": "Görüntü Boyutunu Girin (örn. 512x512)", + "Enter language codes": "Dil kodlarını girin", + "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 Score": "Skoru Girin", + "Enter Searxng Query URL": "Searxng Sorgu URL'sini girin", + "Enter Serper API Key": "Serper API Anahtarını Girin", + "Enter Serply API Key": "Serply API Anahtarını Girin", + "Enter Serpstack API Key": "Serpstack API Anahtarını Girin", + "Enter stop sequence": "Durdurma dizisini girin", + "Enter system prompt": "", + "Enter Tavily API Key": "Tavily API Anahtarını Girin", + "Enter Tika Server URL": "", + "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 URL (e.g. http://localhost:11434)": "URL'yi Girin (e.g. http://localhost:11434)", + "Enter Your Email": "E-postanızı Girin", + "Enter Your Full Name": "Tam Adınızı Girin", + "Enter your message": "", + "Enter Your Password": "Parolanızı Girin", + "Enter Your Role": "Rolünüzü Girin", + "Error": "Hata", + "Experimental": "Deneysel", + "Export": "Dışa Aktar", + "Export All Chats (All Users)": "Tüm Sohbetleri Dışa Aktar (Tüm Kullanıcılar)", + "Export chat (.json)": "Sohbeti dışa aktar (.json)", + "Export Chats": "Sohbetleri Dışa Aktar", + "Export Documents Mapping": "Belge Eşlemesini Dışa Aktar", + "Export Functions": "Fonksiyonları Dışa Aktar", + "Export LiteLLM config.yaml": "LiteLLM config.yaml'ı Dışa Aktar", + "Export Models": "Modelleri Dışa Aktar", + "Export Prompts": "Promptları Dışa Aktar", + "Export Tools": "Araçları Dışa Aktar", + "External Models": "Modelleri Dışa Aktar", + "Failed to create API Key.": "API Anahtarı oluşturulamadı.", + "Failed to read clipboard contents": "Pano içeriği okunamadı", + "Failed to update settings": "Ayarlar güncellenemedi", + "February": "Şubat", + "Feel free to add specific details": "Spesifik ayrıntılar eklemekten çekinmeyin", + "File": "Dosya", + "File Mode": "Dosya Modu", + "File not found.": "Dosya bulunamadı.", + "Files": "", + "Filter is now globally disabled": "Filtre artık global olarak devre dışı", + "Filter is now globally enabled": "Filtre artık global olarak devrede", + "Filters": "Filtreler", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Parmak izi sahteciliği tespit edildi: Avatar olarak baş harfler kullanılamıyor. Varsayılan profil resmine dönülüyor.", + "Fluidly stream large external response chunks": "Büyük harici yanıt chunklarını akıcı bir şekilde yayınlayın", + "Focus chat input": "Sohbet girişine odaklan", + "Followed instructions perfectly": "Talimatları mükemmel şekilde takip etti", + "Form": "Form", + "Format your variables using square brackets like this:": "Değişkenlerinizi şu şekilde kare parantezlerle biçimlendirin:", + "Frequency Penalty": "Frekans Cezası", + "Function created successfully": "Fonksiyon başarıyla oluşturuldu", + "Function deleted successfully": "Fonksiyon başarıyla silindi", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "Fonksiyon başarıyla güncellendi", + "Functions": "Fonksiyonlar", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "Fonksiyonlar başarıyla içe aktarıldı", + "General": "Genel", + "General Settings": "Genel Ayarlar", + "Generate Image": "Görsel Üret", + "Generating search query": "Arama sorgusu oluşturma", + "Generation Info": "Üretim Bilgisi", + "Get up and running with": "", + "Global": "Global", + "Good Response": "İyi Yanıt", + "Google PSE API Key": "Google PSE API Anahtarı", + "Google PSE Engine Id": "Google PSE Engine Id", + "h:mm a": "h:mm a", + "has no conversations.": "hiç konuşması yok.", + "Hello, {{name}}": "Merhaba, {{name}}", + "Help": "Yardım", + "Hide": "Gizle", + "Hide Model": "Modeli Gizle", + "How can I help you today?": "Bugün size nasıl yardımcı olabilirim?", + "Hybrid Search": "Karma Arama", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "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 Functions": "Fonksiyonları İçe Aktar", + "Import Models": "Modelleri İçe Aktar", + "Import Prompts": "Promptları İçe Aktar", + "Import Tools": "Araçları İçe Aktar", + "Include `--api-auth` flag when running stable-diffusion-webui": "stable-diffusion-webui çalıştırılırken `--api-auth` bayrağını dahil edin", + "Include `--api` flag when running stable-diffusion-webui": "stable-diffusion-webui çalıştırılırken `--api` bayrağını dahil edin", + "Info": "Bilgi", + "Input commands": "Giriş komutları", + "Install from Github URL": "Github URL'sinden yükleyin", + "Instant Auto-Send After Voice Transcription": "Ses Transkripsiyonundan Sonra Anında Otomatik Gönder", + "Interface": "Arayüz", + "Invalid Tag": "Geçersiz etiket", + "January": "Ocak", + "join our Discord for help.": "yardım için Discord'umuza katılın.", + "JSON": "JSON", + "JSON Preview": "JSON Önizlemesi", + "July": "Temmuz", + "June": "Haziran", + "JWT Expiration": "JWT Bitişi", + "JWT Token": "JWT Token", + "Keep Alive": "Canlı Tut", + "Keyboard shortcuts": "Klavye kısayolları", + "Knowledge": "Bilgi", + "Language": "Dil", + "large language models, locally.": "", + "Last Active": "Son Aktivite", + "Last Modified": "Son Düzenleme", + "Light": "Açık", + "Listening...": "Dinleniyor...", + "LLMs can make mistakes. Verify important information.": "LLM'ler hata yapabilir. Önemli bilgileri doğrulayın.", + "Local Models": "Yerel Modeller", + "LTR": "LTR", + "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": "Yönet", + "Manage Models": "Modelleri Yönet", + "Manage Ollama Models": "Ollama Modellerini Yönet", + "Manage Pipelines": "Pipelineları Yönet", + "Manage Valves": "Valvleri Yönet", + "March": "Mart", + "Max Tokens (num_predict)": "Maksimum Token (num_predict)", + "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.", + "May": "Mayıs", + "Memories accessible by LLMs will be shown here.": "LLM'ler tarafından erişilebilen bellekler burada gösterilecektir.", + "Memory": "Bellek", + "Memory added successfully": "Bellek başarıyla eklendi", + "Memory cleared successfully": "Bellek başarıyle temizlendi", + "Memory deleted successfully": "Bellek başarıyla silindi", + "Memory updated successfully": "Bellek başarıyla güncellendi", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Bağlantınızı oluşturduktan sonra gönderdiğiniz mesajlar paylaşılmayacaktır. URL'ye sahip kullanıcılar paylaşılan sohbeti görüntüleyebilecektir.", + "Minimum Score": "Minimum Skor", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "DD MMMM YYYY", + "MMMM DD, YYYY HH:mm": "DD MMMM YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "DD MMMM YYYY hh:mm:ss A", + "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}} is not vision capable": "Model {{modelName}} görüntü yeteneğine sahip değil", + "Model {{name}} is now {{status}}": "{{name}} modeli artık {{status}}", + "Model created successfully!": "Model başarıyla oluşturuldu!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Model dosya sistemi yolu algılandı. Güncelleme için model kısa adı gerekli, devam edilemiyor.", + "Model ID": "Model ID", + "Model not selected": "Model seçilmedi", + "Model Params": "Model Parametreleri", + "Model updated successfully": "Model başarıyla güncellendi", + "Model Whitelisting": "Model Beyaz Listeye Alma", + "Model(s) Whitelisted": "Model(ler) Beyaz Listeye Alındı", + "Modelfile Content": "Model Dosyası İçeriği", + "Models": "Modeller", + "More": "Daha Fazla", + "Name": "Ad", + "Name Tag": "Ad Etiketi", + "Name your model": "Modelinizi Adlandırın", + "New Chat": "Yeni Sohbet", + "New Password": "Yeni Parola", + "No content to speak": "Konuşacak içerik yok", + "No documents found": "Hiçbir belge bulunamadı", + "No file selected": "Hiçbir dosya seçilmedi", + "No results found": "Sonuç bulunamadı", + "No search query generated": "Hiç arama sorgusu oluşturulmadı", + "No source available": "Kaynak mevcut değil", + "No valves to update": "Güncellenecek valvler yok", + "None": "Yok", + "Not factually correct": "Gerçeklere göre doğru değil", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Not: Minimum bir skor belirlerseniz, arama yalnızca minimum skora eşit veya daha yüksek bir skora sahip belgeleri getirecektir.", + "Notifications": "Bildirimler", + "November": "Kasım", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth ID", + "October": "Ekim", + "Off": "Kapalı", + "Okay, Let's Go!": "Tamam, Hadi Başlayalım!", + "OLED Dark": "OLED Koyu", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API'si devre dışı", + "Ollama API is disabled": "Ollama API'si devre dışı", + "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! There was an error in the previous response. Please try again or contact admin.": "Hop! Önceki yanıtta bir hata oluştu. Lütfen tekrar deneyin veya yönetici ile iletişime geçin.", + "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 AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Yeni sohbet aç", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API Konfigürasyonu", + "OpenAI API Key is required.": "OpenAI API Anahtarı gereklidir.", + "OpenAI URL/Key required.": "OpenAI URL/Anahtar gereklidir.", + "or": "veya", + "Other": "Diğer", + "Password": "Parola", + "PDF document (.pdf)": "PDF belgesi (.pdf)", + "PDF Extract Images (OCR)": "PDF Görüntülerini Çıkart (OCR)", + "pending": "beklemede", + "Permission denied when accessing media devices": "Medya cihazlarına erişim izni reddedildi", + "Permission denied when accessing microphone": "Mikrofona erişim izni reddedildi", + "Permission denied when accessing microphone: {{error}}": "Mikrofona erişim izni reddedildi: {{error}}", + "Personalization": "Kişiselleştirme", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "Pipeline başarıyla silindi", + "Pipeline downloaded successfully": "Pipeline başarıyla güncellendi", + "Pipelines": "Pipelinelar", + "Pipelines Not Detected": "Pipeline Tespit Edilmedi", + "Pipelines Valves": "Pipeline Valvleri", + "Plain text (.txt)": "Düz metin (.txt)", + "Playground": "Oyun Alanı", + "Please carefully review the following warnings:": "", + "Positive attitude": "Olumlu yaklaşım", + "Previous 30 days": "Önceki 30 gün", + "Previous 7 days": "Önceki 7 gün", + "Profile Image": "Profil Fotoğrafı", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (örn. Roma İmparatorluğu hakkında ilginç bir bilgi verin)", + "Prompt Content": "Prompt İçeriği", + "Prompt suggestions": "Prompt önerileri", + "Prompts": "Promptlar", + "Pull \"{{searchValue}}\" from Ollama.com": "Ollama.com'dan \"{{searchValue}}\" çekin", + "Pull a model from Ollama.com": "Ollama.com'dan bir model çekin", + "Query Params": "Sorgu Parametreleri", + "RAG Template": "RAG Şablonu", + "Read Aloud": "Sesli Oku", + "Record voice": "Ses kaydı yap", + "Redirecting you to OpenWebUI Community": "OpenWebUI Topluluğuna yönlendiriliyorsunuz", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Kendinizden \"User\" olarak bahsedin (örneğin, \"User İspanyolca öğreniyor\")", + "Refused when it shouldn't have": "Reddedilmemesi gerekirken reddedildi", + "Regenerate": "Tekrar Oluştur", + "Release Notes": "Sürüm Notları", + "Remove": "Kaldır", + "Remove Model": "Modeli Kaldır", + "Rename": "Yeniden Adlandır", + "Repeat Last N": "Son N'yi Tekrar Et", + "Request Mode": "İstek Modu", + "Reranking Model": "Yeniden Sıralama Modeli", + "Reranking model disabled": "Yeniden sıralama modeli devre dışı bırakıldı", + "Reranking model set to \"{{reranking_model}}\"": "Yeniden sıralama modeli \"{{reranking_model}}\" olarak ayarlandı", + "Reset": "Sıfırla", + "Reset Upload Directory": "Yükleme Dizinini Sıfırla", + "Reset Vector Storage": "Vektör Depolamayı Sıfırla", + "Response AutoCopy to Clipboard": "Yanıtı Panoya Otomatik Kopyala", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Web sitesi izinleri reddedildiğinden yanıt bildirimleri etkinleştirilemiyor. Gerekli erişimi sağlamak için lütfen tarayıcı ayarlarınızı ziyaret edin.", + "Role": "Rol", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "Çalışıyor", + "Save": "Kaydet", + "Save & Create": "Kaydet ve Oluştur", + "Save & Update": "Kaydet ve Güncelle", + "Save Tag": "", + "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 a model": "Bir model ara", + "Search Chats": "Sohbetleri Ara", + "Search Documents": "Belgeleri Ara", + "Search Functions": "Fonksiyonları Ara", + "Search Models": "Modelleri Ara", + "Search Prompts": "Prompt Ara", + "Search Query Generation Prompt": "Arama Sorgusu Üretme Promptu", + "Search Query Generation Prompt Length Threshold": "Arama Sorgusu Üretme Promptu Uzunluk Sınırı", + "Search Result Count": "Arama Sonucu Sayısı", + "Search Tools": "Arama Araçları", + "Searched {{count}} sites_one": "Arandı {{count}} sites_one", + "Searched {{count}} sites_other": "Arandı {{count}} sites_other", + "Searching \"{{searchQuery}}\"": "\"{{searchQuery}}\" aranıyor", + "Searxng Query URL": "Searxng Sorgu URL'si", + "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 base model": "Bir temel model seç", + "Select a engine": "Bir motor seç", + "Select a function": "Bir fonksiyon seç", + "Select a mode": "Bir mod seç", + "Select a model": "Bir model seç", + "Select a pipeline": "Bir pipeline seç", + "Select a pipeline url": "Bir pipeline URL'si seç", + "Select a tool": "Bir araç seç", + "Select an Ollama instance": "Bir Ollama örneği seçin", + "Select Documents": "Bir Doküman Seç", + "Select model": "Model seç", + "Select only one model to call": "Arama için sadece bir model seç", + "Selected model(s) do not support image inputs": "Seçilen model(ler) görüntü girişlerini desteklemiyor", + "Send": "Gönder", + "Send a Message": "Bir Mesaj Gönder", + "Send message": "Mesaj gönder", + "September": "Eylül", + "Serper API Key": "Serper API Anahtarı", + "Serply API Key": "Serply API Anahtarı", + "Serpstack API Key": "Serpstack API Anahtarı", + "Server connection verified": "Sunucu bağlantısı doğrulandı", + "Set as default": "Varsayılan olarak ayarla", + "Set Default Model": "Varsayılan Modeli Ayarla", + "Set embedding model (e.g. {{model}})": "Gömme modelini ayarlayın (örn. {{model}})", + "Set Image Size": "Görüntü Boyutunu Ayarla", + "Set reranking model (e.g. {{model}})": "Yeniden sıralama modelini ayarlayın (örn. {{model}})", + "Set Steps": "Adımları Ayarla", + "Set Task Model": "Görev Modeli Ayarla", + "Set Voice": "Ses Ayarla", + "Settings": "Ayarlar", + "Settings saved successfully!": "Ayarlar başarıyla kaydedildi!", + "Settings updated successfully": "Ayarlar başarıyla güncellendi", + "Share": "Paylaş", + "Share Chat": "Sohbeti Paylaş", + "Share to OpenWebUI Community": "OpenWebUI Topluluğu ile Paylaş", + "short-summary": "kısa-özet", + "Show": "Göster", + "Show Admin Details in Account Pending Overlay": "Yönetici Ayrıntılarını Hesap Bekliyor Ekranında Göster", + "Show Model": "Modeli Göster", + "Show shortcuts": "Kısayolları göster", + "Show your support!": "Desteğinizi gösterin!", + "Showcased creativity": "Sergilenen yaratıcılık", + "Sign in": "Oturum aç", + "Sign Out": "Çıkış Yap", + "Sign up": "Kaydol", + "Signing in": "Oturum açma", + "Source": "Kaynak", + "Speech recognition error: {{error}}": "Konuşma tanıma hatası: {{error}}", + "Speech-to-Text Engine": "Konuşmadan Metne Motoru", + "Stop Sequence": "Diziyi Durdur", + "STT Model": "STT Modeli", + "STT Settings": "STT Ayarları", + "Submit": "Gönder", + "Subtitle (e.g. about the Roman Empire)": "Alt başlık (örn. Roma İmparatorluğu hakkında)", + "Success": "Başarılı", + "Successfully updated.": "Başarıyla güncellendi.", + "Suggested": "Önerilen", + "Support": "", + "Support this plugin:": "", + "System": "Sistem", + "System Prompt": "Sistem Promptu", + "Tags": "Etiketler", + "Tap to interrupt": "Durdurmak için dokunun", + "Tavily API Key": "Tavily API Anahtarı", + "Tell us more:": "Bize daha fazlasını anlat:", + "Temperature": "Temperature", + "Template": "Şablon", + "Text Completion": "Metin Tamamlama", + "Text-to-Speech Engine": "Metinden Sese Motoru", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Geri bildiriminiz için teşekkürler!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Puan 0.0 (%0) ile 1.0 (%100) arasında bir değer olmalıdır.", + "Theme": "Tema", + "Thinking...": "Düşünüyor...", + "This action cannot be undone. Do you wish to continue?": "Bu eylem geri alınamaz. Devam etmek istiyor musunuz?", + "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 is an experimental feature, it may not function as expected and is subject to change at any time.": "Bu deneysel bir özelliktir, beklendiği gibi çalışmayabilir ve her an değişiklik yapılabilir.", + "This setting does not sync across browsers or devices.": "Bu ayar tarayıcılar veya cihazlar arasında senkronize edilmez.", + "This will delete": "Bu silinecek", + "Thorough explanation": "Kapsamlı açıklama", + "Tika": "", + "Tika Server URL required.": "", + "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 (e.g. Tell me a fun fact)": "Başlık (e.g. Bana ilginç bir bilgi ver)", + "Title Auto-Generation": "Otomatik Başlık Oluşturma", + "Title cannot be an empty string.": "Başlık boş bir dize olamaz.", + "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 access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "WebUI'ye erişmek için lütfen yöneticiyle iletişime geçin. Yöneticiler kullanıcı durumlarını Yönetici Panelinden yönetebilir.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Buraya belge eklemek için öncelikle bunları \"Belgeler\" çalışma alanına yükleyin.", + "to chat input.": "sohbet girişine.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Filtreleri burada seçmek için öncelikle bunları \"İşlevler\" çalışma alanına ekleyin.", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Araçları burada seçmek için öncelikle bunları \"Araçlar\" çalışma alanına ekleyin.", + "Today": "Bugün", + "Toggle settings": "Ayarları Aç/Kapat", + "Toggle sidebar": "Kenar Çubuğunu Aç/Kapat", + "Tokens To Keep On Context Refresh (num_keep)": "Bağlam Yenilemesinde Korunacak Tokenler (num_keep)", + "Tool created successfully": "Araç başarıyla oluşturuldu", + "Tool deleted successfully": "Araç başarıyla silindi", + "Tool imported successfully": "Araç başarıyla içe aktarıldı", + "Tool updated successfully": "Araç başarıyla güncellendi", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "Araçlar", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Ollama'ya erişmede sorun mu yaşıyorsunuz?", + "TTS Model": "TTS Modeli", + "TTS Settings": "TTS Ayarları", + "TTS Voice": "TTS Sesi", + "Type": "Tür", + "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.", + "UI": "UI", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Bilinmeyen dosya türü '{{file_type}}'. Yine de dosya yükleme işlemine devam ediliyor.", + "Unpin": "", + "Update": "Güncelle", + "Update and Copy Link": "Güncelle ve Bağlantıyı Kopyala", + "Update password": "Parolayı Güncelle", + "Updated at": "Şu tarihte güncellendi:", + "Upload": "Yükle", + "Upload a GGUF model": "Bir GGUF modeli yükle", + "Upload Files": "Dosyaları Yükle", + "Upload Pipeline": "Pipeline 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", + "Use Initials": "Baş Harfleri Kullan", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "kullanıcı", + "User location successfully retrieved.": "Kullanıcı konumu başarıyla alındı.", + "User Permissions": "Kullanıcı İzinleri", + "Users": "Kullanıcılar", + "Utilize": "Kullan", + "Valid time units:": "Geçerli zaman birimleri:", + "Valves": "Valvler", + "Valves updated": "Valvler güncellendi", + "Valves updated successfully": "Valvler başarıyla güncellendi", + "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", + "Voice": "Ses", + "Warning": "Uyarı", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Uyarı: Gömme modelinizi günceller veya değiştirirseniz, tüm belgeleri yeniden içe aktarmanız gerekecektir.", + "Web": "Web", + "Web API": "Web API", + "Web Loader Settings": "Web Yükleyici Ayarları", + "Web Params": "Web Parametreleri", + "Web Search": "Web Araması", + "Web Search Engine": "Web Arama Motoru", + "Webhook URL": "Webhook URL", + "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)", + "Widescreen Mode": "Geniş Ekran Modu", + "Workspace": "Çalışma Alanı", + "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.", + "Yesterday": "Dün", + "You": "Sen", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Aşağıdaki 'Yönet' düğmesi aracılığıyla bellekler ekleyerek LLM'lerle etkileşimlerinizi kişiselleştirebilir, onları daha yararlı ve size özel hale getirebilirsiniz.", + "You cannot clone a base model": "Bir temel modeli klonlayamazsınız", + "You have no archived conversations.": "Arşivlenmiş sohbetleriniz yok.", + "You have shared this chat": "Bu sohbeti paylaştınız", + "You're a helpful assistant.": "Sen yardımsever bir asistansın.", + "You're now logged in.": "Şimdi giriş yaptınız.", + "Your account status is currently pending activation.": "Hesap durumunuz şu anda etkinleştirilmeyi bekliyor.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube Yükleyici Ayarları" +} diff --git a/src/lib/i18n/locales/uk-UA/translation.json b/src/lib/i18n/locales/uk-UA/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..9f726dc278d628fafec78545a6d27a526eab4584 --- /dev/null +++ b/src/lib/i18n/locales/uk-UA/translation.json @@ -0,0 +1,716 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' or '-1' для відсутності терміну дії.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(e.g. `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(e.g. `sh webui.sh --api`)", + "(latest)": "(остання)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Ви не можете видалити базову модель.", + "{{modelName}} is thinking...": "{{modelName}} думає...", + "{{user}}'s Chats": "Чати {{user}}а", + "{{webUIName}} Backend Required": "Необхідно підключення бекенду {{webUIName}}", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Модель задач використовується при виконанні таких завдань, як генерація заголовків для чатів та пошукових запитів в Інтернеті", + "a user": "користувача", + "About": "Про програму", + "Account": "Обліковий запис", + "Account Activation Pending": "Очікування активації облікового запису", + "Accurate information": "Точна інформація", + "Actions": "", + "Active Users": "Активні користувачі", + "Add": "Додати", + "Add a model id": "Додайте id моделі", + "Add a short description about what this model does": "Додайте короткий опис того, що робить ця модель", + "Add a short title for this prompt": "Додати коротку назву для цього промту", + "Add a tag": "Додайте тег", + "Add custom prompt": "Додати користувацьку підказку", + "Add Docs": "Додати документи", + "Add Files": "Додати файли", + "Add Memory": "Додати пам'ять", + "Add message": "Додати повідомлення", + "Add Model": "Додати модель", + "Add Tag": "", + "Add Tags": "додати теги", + "Add User": "Додати користувача", + "Adjusting these settings will apply changes universally to all users.": "Зміни в цих налаштуваннях будуть застосовані для всіх користувачів.", + "admin": "адмін", + "Admin": "Адмін", + "Admin Panel": "Адмін-панель", + "Admin Settings": "Адмін-панель", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Адміністратори мають доступ до всіх інструментів у будь-який час; користувачам потрібні інструменти, призначені для кожної моделі в робочій області.", + "Advanced Parameters": "Розширені параметри", + "Advanced Params": "Розширені параметри", + "all": "всі", + "All Documents": "Усі документи", + "All Users": "Всі користувачі", + "Allow": "Дозволити", + "Allow Chat Deletion": "Дозволити видалення чату", + "Allow non-local voices": "Дозволити не локальні голоси", + "Allow User Location": "Доступ до місцезнаходження", + "Allow Voice Interruption in Call": "Дозволити переривання голосу під час виклику", + "alphanumeric characters and hyphens": "алфавітно-цифрові символи та дефіси", + "Already have an account?": "Вже є обліковий запис?", + "an assistant": "асистента", + "and": "та", + "and create a new shared link.": "і створіть нове спільне посилання.", + "API Base URL": "URL-адреса API", + "API Key": "Ключ API", + "API Key created.": "Ключ API створено.", + "API keys": "Ключі API", + "April": "Квітень", + "Archive": "Архів", + "Archive All Chats": "Архівувати всі чати", + "Archived Chats": "Архівовані чати", + "are allowed - Activate this command by typing": "дозволено - активізуйте цю команду набором", + "Are you sure?": "Ви впевнені?", + "Attach file": "Прикріпити файл", + "Attention to detail": "Увага до деталей", + "Audio": "Аудіо", + "Audio settings updated successfully": "Налаштування звуку успішно оновлено", + "August": "Серпень", + "Auto-playback response": "Автоматичне відтворення відповіді", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Рядок авторизації API", + "AUTOMATIC1111 Base URL": "URL-адреса AUTOMATIC1111", + "AUTOMATIC1111 Base URL is required.": "Необхідна URL-адреса AUTOMATIC1111.", + "available!": "доступно!", + "Back": "Назад", + "Bad Response": "Неправильна відповідь", + "Banners": "Прапори", + "Base Model (From)": "Базова модель (від)", + "Batch Size (num_batch)": "Розмір партії (num_batch)", + "before": "до того, як", + "Being lazy": "Не поспішати", + "Brave Search API Key": "Ключ API пошуку Brave", + "Bypass SSL verification for Websites": "Обхід SSL-перевірки для веб-сайтів", + "Call": "Виклик", + "Call feature is not supported when using Web STT engine": "Функція виклику не підтримується при використанні Web STT (розпізнавання мовлення) рушія", + "Camera": "Камера", + "Cancel": "Скасувати", + "Capabilities": "Можливості", + "Change Password": "Змінити пароль", + "Chat": "Чат", + "Chat Background Image": "Фонове зображення чату", + "Chat Bubble UI": "Чат у вигляді бульбашок", + "Chat Controls": "Керування чатом", + "Chat direction": "Напрям чату", + "Chat History": "Історія чату", + "Chat History is off for this browser.": "Історія чату вимкнена для цього браузера.", + "Chats": "Чати", + "Check Again": "Перевірити ще раз", + "Check for updates": "Перевірити оновлення", + "Checking for updates...": "Перевірка оновлень...", + "Choose a model before saving...": "Оберіть модель перед збереженням...", + "Chunk Overlap": "Перекриття фрагментів", + "Chunk Params": "Параметри фрагментів", + "Chunk Size": "Розмір фрагменту", + "Citation": "Цитування", + "Clear memory": "Очистити пам'ять", + "Click here for help.": "Клацніть тут, щоб отримати допомогу.", + "Click here to": "Натисніть тут, щоб", + "Click here to download user import template file.": "Натисніть тут, щоб завантажити файл шаблону імпорту користувача.", + "Click here to select": "Натисніть тут, щоб обрати", + "Click here to select a csv file.": "Натисніть тут, щоб обрати csv-файл.", + "Click here to select a py file.": "Натисніть тут, щоб обрати py-файл.", + "Click here to select documents.": "Натисніть тут, щоб обрати документи.", + "click here.": "клацніть тут.", + "Click on the user role button to change a user's role.": "Натисніть кнопку ролі користувача, щоб змінити роль користувача.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Відмовлено в дозволі на запис до буфера обміну. Будь ласка, перевірте налаштування вашого браузера, щоб надати необхідний доступ.", + "Clone": "Клонувати", + "Close": "Закрити", + "Code formatted successfully": "Код успішно відформатовано", + "Collection": "Колекція", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "URL-адреса ComfyUI", + "ComfyUI Base URL is required.": "Необхідно вказати URL-адресу ComfyUI.", + "Command": "Команда", + "Concurrent Requests": "Одночасні запити", + "Confirm": "Підтвердити", + "Confirm Password": "Підтвердіть пароль", + "Confirm your action": "Підтвердіть свою дію", + "Connections": "З'єднання", + "Contact Admin for WebUI Access": "Зверніться до адміна для отримання доступу до WebUI", + "Content": "Зміст", + "Content Extraction": "Вилучення вмісту", + "Context Length": "Довжина контексту", + "Continue Response": "Продовжити відповідь", + "Continue with {{provider}}": "Продовжити з {{provider}}", + "Controls": "Керування", + "Copied shared chat URL to clipboard!": "Скопійовано URL-адресу спільного чату в буфер обміну!", + "Copy": "Копіювати", + "Copy last code block": "Копіювати останній блок коду", + "Copy last response": "Копіювати останню відповідь", + "Copy Link": "Копіювати посилання", + "Copying to clipboard was successful!": "Копіювання в буфер обміну виконано успішно!", + "Create a model": "Створити модель", + "Create Account": "Створити обліковий запис", + "Create new key": "Створити новий ключ", + "Create new secret key": "Створити новий секретний ключ", + "Created at": "Створено у", + "Created At": "Створено у", + "Created by": "Створено", + "CSV Import": "Імпорт CSV", + "Current Model": "Поточна модель", + "Current Password": "Поточний пароль", + "Custom": "Налаштувати", + "Customize models for a specific purpose": "Налаштуйте моделі для конкретних цілей", + "Dark": "Темна", + "Dashboard": "Панель керування", + "Database": "База даних", + "December": "Грудень", + "Default": "За замовчуванням", + "Default (Automatic1111)": "За замовчуванням (Automatic1111)", + "Default (SentenceTransformers)": "За замовчуванням (SentenceTransformers)", + "Default Model": "Модель за замовчуванням", + "Default model updated": "Модель за замовчуванням оновлено", + "Default Prompt Suggestions": "Пропозиції промтів замовчуванням", + "Default User Role": "Роль користувача за замовчуванням", + "delete": "видалити", + "Delete": "Видалити", + "Delete a model": "Видалити модель", + "Delete All Chats": "Видалити усі чати", + "Delete chat": "Видалити чат", + "Delete Chat": "Видалити чат", + "Delete chat?": "Видалити чат?", + "Delete Doc": "", + "Delete function?": "Видалити функцію?", + "Delete prompt?": "Видалити промт?", + "delete this link": "видалити це посилання", + "Delete tool?": "Видалити інструмент?", + "Delete User": "Видалити користувача", + "Deleted {{deleteModelTag}}": "Видалено {{deleteModelTag}}", + "Deleted {{name}}": "Видалено {{name}}", + "Description": "Опис", + "Didn't fully follow instructions": "Не повністю дотримувалися інструкцій", + "Disabled": "Вимкнено", + "Discover a function": "Знайдіть функцію", + "Discover a model": "Знайдіть модель", + "Discover a prompt": "Знайдіть промт", + "Discover a tool": "Знайдіть інструмент", + "Discover, download, and explore custom functions": "Знайдіть, завантажте та досліджуйте налаштовані функції", + "Discover, download, and explore custom prompts": "Знайдіть, завантажте та досліджуйте налаштовані промти", + "Discover, download, and explore custom tools": "Знайдіть, завантажте та досліджуйте налаштовані інструменти", + "Discover, download, and explore model presets": "Знайдіть, завантажте та досліджуйте налаштування моделей", + "Dismissible": "Неприйнятно", + "Display Emoji in Call": "Відображати емодзі у викликах", + "Display the username instead of You in the Chat": "Показувати ім'я користувача замість 'Ви' в чаті", + "Do not install functions from sources you do not fully trust.": "Не встановлюйте функції з джерел, яким ви не повністю довіряєте.", + "Do not install tools from sources you do not fully trust.": "Не встановлюйте інструменти з джерел, яким ви не повністю довіряєте.", + "Document": "Документ", + "Document Settings": "Налаштування документа", + "Documentation": "Документація", + "Documents": "Документи", + "does not make any external connections, and your data stays securely on your locally hosted server.": "не встановлює жодних зовнішніх з'єднань, і ваші дані залишаються в безпеці на вашому локальному сервері.", + "Don't Allow": "Не дозволяти", + "Don't have an account?": "Немає облікового запису?", + "don't install random functions from sources you don't trust.": "не встановлюйте випадкові функції з джерел, яким ви не довіряєте.", + "don't install random tools from sources you don't trust.": "не встановлюйте випадкові інструменти з джерел, яким ви не довіряєте.", + "Don't like the style": "Не подобається стиль", + "Done": "Готово", + "Download": "Завантажити", + "Download canceled": "Завантаження скасовано", + "Download Database": "Завантажити базу даних", + "Drop any files here to add to the conversation": "Перетягніть сюди файли, щоб додати до розмови", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "напр., '30s','10m'. Дійсні одиниці часу: 'с', 'хв', 'г'.", + "Edit": "Редагувати", + "Edit Doc": "Редагувати документ", + "Edit Memory": "Редагувати пам'ять", + "Edit User": "Редагувати користувача", + "ElevenLabs": "", + "Email": "Електронна пошта", + "Embedding Batch Size": "Розмір пакету під час вбудовування", + "Embedding Model": "Модель вбудовування", + "Embedding Model Engine": "Рушій моделі вбудовування ", + "Embedding model set to \"{{embedding_model}}\"": "Встановлена модель вбудовування \"{{embedding_model}}\"", + "Enable Chat History": "Увімкнути історію чату", + "Enable Community Sharing": "Увімкнути спільний доступ", + "Enable New Sign Ups": "Дозволити нові реєстрації", + "Enable Web Search": "Увімкнути веб-пошук", + "Enabled": "Увімкнено", + "Engine": "Рушій", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Переконайтеся, що ваш CSV-файл містить 4 колонки в такому порядку: Ім'я, Email, Пароль, Роль.", + "Enter {{role}} message here": "Введіть повідомлення {{role}} тут", + "Enter a detail about yourself for your LLMs to recall": "Введіть відомості про себе для запам'ятовування вашими LLM.", + "Enter api auth string (e.g. username:password)": "Введіть рядок авторизації api (напр, ім'я користувача:пароль)", + "Enter Brave Search API Key": "Введіть ключ API для пошуку Brave", + "Enter Chunk Overlap": "Введіть перекриття фрагменту", + "Enter Chunk Size": "Введіть розмір фрагменту", + "Enter Github Raw URL": "Введіть Raw URL-адресу Github", + "Enter Google PSE API Key": "Введіть ключ API Google PSE", + "Enter Google PSE Engine Id": "Введіть Google PSE Engine Id", + "Enter Image Size (e.g. 512x512)": "Введіть розмір зображення (напр., 512x512)", + "Enter language codes": "Введіть мовні коди", + "Enter model tag (e.g. {{modelTag}})": "Введіть тег моделі (напр., {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Введіть кількість кроків (напр., 50)", + "Enter Score": "Введіть бал", + "Enter Searxng Query URL": "Введіть URL-адресу запиту Searxng", + "Enter Serper API Key": "Введіть ключ API Serper", + "Enter Serply API Key": "Введіть ключ API Serply", + "Enter Serpstack API Key": "Введіть ключ API Serpstack", + "Enter stop sequence": "Введіть символ зупинки", + "Enter system prompt": "Введіть системний промт", + "Enter Tavily API Key": "Введіть ключ API Tavily", + "Enter Tika Server URL": "Введіть URL-адресу сервера Tika ", + "Enter Top K": "Введіть Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Введіть URL-адресу (напр., http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Введіть URL-адресу (напр., http://localhost:11434)", + "Enter Your Email": "Введіть вашу електронну пошту", + "Enter Your Full Name": "Введіть ваше ім'я", + "Enter your message": "Введіть повідомлення ", + "Enter Your Password": "Введіть ваш пароль", + "Enter Your Role": "Введіть вашу роль", + "Error": "Помилка", + "Experimental": "Експериментальне", + "Export": "Експорт", + "Export All Chats (All Users)": "Експортувати всі чати (всіх користувачів)", + "Export chat (.json)": "Експорт чату (.json)", + "Export Chats": "Експортувати чати", + "Export Documents Mapping": "Експортувати відображення документів", + "Export Functions": "Експорт функцій ", + "Export LiteLLM config.yaml": "Експорт LiteLLM config.yaml", + "Export Models": "Експорт моделей", + "Export Prompts": "Експортувати промти", + "Export Tools": "Експортувати інструменти", + "External Models": "Зовнішні моделі", + "Failed to create API Key.": "Не вдалося створити API ключ.", + "Failed to read clipboard contents": "Не вдалося прочитати вміст буфера обміну", + "Failed to update settings": "Не вдалося оновити налаштування", + "February": "Лютий", + "Feel free to add specific details": "Не соромтеся додавати конкретні деталі", + "File": "Файл", + "File Mode": "Файловий режим", + "File not found.": "Файл не знайдено.", + "Files": "", + "Filter is now globally disabled": "Фільтр глобально вимкнено", + "Filter is now globally enabled": "Фільтр увімкнено глобально", + "Filters": "Фільтри", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Виявлено підробку відбитків: Неможливо використовувати ініціали як аватар. Повернення до зображення профілю за замовчуванням.", + "Fluidly stream large external response chunks": "Плавно передавати великі фрагменти зовнішніх відповідей", + "Focus chat input": "Фокус вводу чату", + "Followed instructions perfectly": "Бездоганно дотримувався інструкцій", + "Form": "Форма", + "Format your variables using square brackets like this:": "Форматуйте свої змінні квадратними дужками так:", + "Frequency Penalty": "Штраф за частоту", + "Function created successfully": "Функцію успішно створено", + "Function deleted successfully": "Функцію успішно видалено", + "Function Description (e.g. A filter to remove profanity from text)": "Опис функції (напр., фільтр для видалення ненормативної лексики з тексту)", + "Function ID (e.g. my_filter)": "Ідентифікатор функції (напр., my_filter)", + "Function is now globally disabled": "Функція зараз глобально вимкнена", + "Function is now globally enabled": "Функція зараз глобально увімкнена ", + "Function Name (e.g. My Filter)": "Назва функції (напр., My Filter)", + "Function updated successfully": "Функцію успішно оновлено", + "Functions": "Функції", + "Functions allow arbitrary code execution": "Функції дозволяють виконання довільного коду", + "Functions allow arbitrary code execution.": "Функції дозволяють виконання довільного коду.", + "Functions imported successfully": "Функції успішно імпортовано", + "General": "Загальні", + "General Settings": "Загальні налаштування", + "Generate Image": "Створити зображення", + "Generating search query": "Сформувати пошуковий запит", + "Generation Info": "Інформація про генерацію", + "Get up and running with": "Почніть працювати з", + "Global": "Глоб.", + "Good Response": "Гарна відповідь", + "Google PSE API Key": "Ключ API Google PSE", + "Google PSE Engine Id": "Id рушія Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "не має розмов.", + "Hello, {{name}}": "Привіт, {{name}}", + "Help": "Допоможіть", + "Hide": "Приховати", + "Hide Model": "Приховати модель", + "How can I help you today?": "Чим я можу допомогти вам сьогодні?", + "Hybrid Search": "Гібридний пошук", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Я підтверджую, що прочитав і розумію наслідки своїх дій. Я усвідомлюю ризики, пов'язані з виконанням довільного коду, і перевірив надійність джерела.", + "Image Generation (Experimental)": "Генерування зображень (експериментально)", + "Image Generation Engine": "Механізм генерації зображень", + "Image Settings": "Налаштування зображення", + "Images": "Зображення", + "Import Chats": "Імпортувати чати", + "Import Documents Mapping": "Імпортувати відображення документів", + "Import Functions": "Імпорт функцій ", + "Import Models": "Імпорт моделей", + "Import Prompts": "Імпортувати промти", + "Import Tools": "Імпортувати інструменти", + "Include `--api-auth` flag when running stable-diffusion-webui": "Включіть прапорець `--api-auth` під час запуску stable-diffusion-webui", + "Include `--api` flag when running stable-diffusion-webui": "Включіть прапор `--api` при запуску stable-diffusion-webui", + "Info": "Інфо", + "Input commands": "Команди вводу", + "Install from Github URL": "Встановіть з URL-адреси Github", + "Instant Auto-Send After Voice Transcription": "Миттєва автоматична відправка після транскрипції голосу", + "Interface": "Інтерфейс", + "Invalid Tag": "Недійсний тег", + "January": "Січень", + "join our Discord for help.": "приєднуйтеся до нашого Discord для допомоги.", + "JSON": "JSON", + "JSON Preview": "Перегляд JSON", + "July": "Липень", + "June": "Червень", + "JWT Expiration": "Термін дії JWT", + "JWT Token": "Токен JWT", + "Keep Alive": "Зберегти активність", + "Keyboard shortcuts": "Клавіатурні скорочення", + "Knowledge": "Знання", + "Language": "Мова", + "large language models, locally.": "великими мовними моделями, локально.", + "Last Active": "Остання активність", + "Last Modified": "Востаннє змінено", + "Light": "Світла", + "Listening...": "Слухаю...", + "LLMs can make mistakes. Verify important information.": "LLMs можуть помилятися. Перевірте важливу інформацію.", + "Local Models": "Локальні моделі", + "LTR": "LTR", + "Made by OpenWebUI Community": "Зроблено спільнотою OpenWebUI", + "Make sure to enclose them with": "Переконайтеся, що вони закриті", + "Manage": "Керувати", + "Manage Models": "Керування моделями", + "Manage Ollama Models": "Керування моделями Ollama", + "Manage Pipelines": "Керування конвеєрами", + "Manage Valves": "Керування клапанами", + "March": "Березень", + "Max Tokens (num_predict)": "Макс токенів (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Максимум 3 моделі можна завантажити одночасно. Будь ласка, спробуйте пізніше.", + "May": "Травень", + "Memories accessible by LLMs will be shown here.": "Пам'ять, яка доступна LLM, буде показана тут.", + "Memory": "Пам'ять", + "Memory added successfully": "Пам'ять додано успішно", + "Memory cleared successfully": "Пам'ять успішно очищено", + "Memory deleted successfully": "Пам'ять успішно видалено", + "Memory updated successfully": "Пам'ять успішно оновлено", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Повідомлення, які ви надішлете після створення посилання, не будуть доступні для інших. Користувачі, які мають URL, зможуть переглядати спільний чат.", + "Minimum Score": "Мінімальний бал", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "Модель '{{modelName}}' успішно завантажено.", + "Model '{{modelTag}}' is already in queue for downloading.": "Модель '{{modelTag}}' вже знаходиться в черзі на завантаження.", + "Model {{modelId}} not found": "Модель {{modelId}} не знайдено", + "Model {{modelName}} is not vision capable": "Модель {{modelName}} не здатна бачити", + "Model {{name}} is now {{status}}": "Модель {{name}} тепер має {{status}}", + "Model created successfully!": "Модель створено успішно!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Виявлено шлях до файлової системи моделі. Для оновлення потрібно вказати коротке ім'я моделі, не вдасться продовжити.", + "Model ID": "ID моделі", + "Model not selected": "Модель не вибрана", + "Model Params": "Параметри моделі", + "Model updated successfully": "Модель успішно оновлено", + "Model Whitelisting": "Модель білого списку", + "Model(s) Whitelisted": "Модель(і) білого списку", + "Modelfile Content": "Вміст файлу моделі", + "Models": "Моделі", + "More": "Більше", + "Name": "Ім'я", + "Name Tag": "Назва тегу", + "Name your model": "Назвіть свою модель", + "New Chat": "Новий чат", + "New Password": "Новий пароль", + "No content to speak": "Нема чого говорити", + "No documents found": "Документів не знайдено", + "No file selected": "Файл не обрано", + "No results found": "Не знайдено жодного результату", + "No search query generated": "Пошуковий запит не сформовано", + "No source available": "Джерело не доступне", + "No valves to update": "Немає клапанів для оновлення", + "None": "Нема", + "Not factually correct": "Не відповідає дійсності", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Примітка: Якщо ви встановите мінімальну кількість балів, пошук поверне лише документи з кількістю балів, більшою або рівною мінімальній кількості балів.", + "Notifications": "Сповіщення", + "November": "Листопад", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "OAuth ID", + "October": "Жовтень", + "Off": "Вимк", + "Okay, Let's Go!": "Гаразд, давайте почнемо!", + "OLED Dark": "Темний OLED", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API вимкнено", + "Ollama API is disabled": "API Ollama вимкнено", + "Ollama Version": "Версія Ollama", + "On": "Увімк", + "Only": "Тільки", + "Only alphanumeric characters and hyphens are allowed in the command string.": "У рядку команди дозволено використовувати лише алфавітно-цифрові символи та дефіси.", + "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.": "Ой! Зачекайте, будь ласка! Ваші файли ще готуються. Ми робимо все, щоб вони були ідеальними. Будь ласка, будьте терплячі, ми повідомимо вам, коли вони будуть готові.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Упс! Схоже, що URL-адреса невірна. Будь ласка, перевірте ще раз та спробуйте ще раз.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Упс! У попередній відповіді сталася помилка. Будь ласка, спробуйте ще раз або зверніться до адміністратора.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Упс! Ви використовуєте непідтримуваний метод (тільки для фронтенду). Будь ласка, обслуговуйте WebUI з бекенду.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Відкрити новий чат", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Open WebUI версія (v{{OPEN_WEBUI_VERSION}}) нижча за необхідну версію (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "Конфігурація OpenAI API", + "OpenAI API Key is required.": "Потрібен ключ OpenAI API.", + "OpenAI URL/Key required.": "Потрібен OpenAI URL/ключ.", + "or": "або", + "Other": "Інше", + "Password": "Пароль", + "PDF document (.pdf)": "PDF документ (.pdf)", + "PDF Extract Images (OCR)": "Розпізнавання зображень з PDF (OCR)", + "pending": "на розгляді", + "Permission denied when accessing media devices": "Відмовлено в доступі до медіапристроїв", + "Permission denied when accessing microphone": "Відмовлено у доступі до мікрофона", + "Permission denied when accessing microphone: {{error}}": "Доступ до мікрофона заборонено: {{error}}", + "Personalization": "Персоналізація", + "Pin": "Зачепити", + "Pinned": "Зачеплено", + "Pipeline deleted successfully": "Конвеєр успішно видалено", + "Pipeline downloaded successfully": "Конвеєр успішно завантажено", + "Pipelines": "Конвеєри", + "Pipelines Not Detected": "Конвеєрів не знайдено", + "Pipelines Valves": "Клапани конвеєрів", + "Plain text (.txt)": "Простий текст (.txt)", + "Playground": "Майданчик", + "Please carefully review the following warnings:": "Будь ласка, уважно ознайомтеся з наступними попередженнями:", + "Positive attitude": "Позитивне ставлення", + "Previous 30 days": "Попередні 30 днів", + "Previous 7 days": "Попередні 7 днів", + "Profile Image": "Зображення профілю", + "Prompt": "Підказка", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Підказка (напр., розкажіть мені цікавий факт про Римську імперію)", + "Prompt Content": "Зміст промту", + "Prompt suggestions": "Швидкі промти", + "Prompts": "Промти", + "Pull \"{{searchValue}}\" from Ollama.com": "Завантажити \"{{searchValue}}\" з Ollama.com»", + "Pull a model from Ollama.com": "Завантажити модель з Ollama.com", + "Query Params": "Параметри запиту", + "RAG Template": "Шаблон RAG", + "Read Aloud": "Читати вголос", + "Record voice": "Записати голос", + "Redirecting you to OpenWebUI Community": "Перенаправляємо вас до спільноти OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Називайте себе \"Користувач\" (наприклад, \"Користувач вивчає іспанську мову\")", + "Refused when it shouldn't have": "Відмовив, коли не мав би", + "Regenerate": "Регенерувати", + "Release Notes": "Нотатки до випуску", + "Remove": "Видалити", + "Remove Model": "Видалити модель", + "Rename": "Перейменувати", + "Repeat Last N": "Повторити останні N", + "Request Mode": "Режим запиту", + "Reranking Model": "Модель переранжування", + "Reranking model disabled": "Модель переранжування вимкнена", + "Reranking model set to \"{{reranking_model}}\"": "Модель переранжування встановлено на \"{{reranking_model}}\"", + "Reset": "Скидання", + "Reset Upload Directory": "Скинути каталог завантажень", + "Reset Vector Storage": "Скинути векторне сховище", + "Response AutoCopy to Clipboard": "Автокопіювання відповіді в буфер обміну", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Сповіщення про відповіді не можуть бути активовані, оскільки вам було відмовлено в доступі до веб-сайту. Будь ласка, відвідайте налаштування вашого браузера, щоб надати необхідний доступ.", + "Role": "Роль", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "Запустіть Llama 2, Code Llama та інші моделі. Налаштуйте та створіть власну.", + "Running": "Виконується", + "Save": "Зберегти", + "Save & Create": "Зберегти та створити", + "Save & Update": "Зберегти та оновити", + "Save Tag": "", + "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": "Збереження журналів чату безпосередньо в сховище вашого браузера більше не підтримується. Будь ласка, завантажте та видаліть журнали чату, натиснувши кнопку нижче. Не хвилюйтеся, ви можете легко повторно імпортувати журнали чату до бекенду через", + "Scan": "Сканування", + "Scan complete!": "Сканування завершено!", + "Scan for documents from {{path}}": "Сканування документів з {{path}}", + "Search": "Пошук", + "Search a model": "Шукати модель", + "Search Chats": "Пошук в чатах", + "Search Documents": "Пошук документів", + "Search Functions": "Пошук функцій", + "Search Models": "Пошук моделей", + "Search Prompts": "Пошук промтів", + "Search Query Generation Prompt": "Підказка для формування пошукового промту", + "Search Query Generation Prompt Length Threshold": "Поріг довжини пошукового запиту для генерації підказки", + "Search Result Count": "Кількість результатів пошуку", + "Search Tools": "Пошуку інструментів ", + "Searched {{count}} sites_one": "Переглянуто {{count}} сайт", + "Searched {{count}} sites_few": "Переглянуто {{count}} сайти", + "Searched {{count}} sites_many": "Переглянуто {{count}} сайтів", + "Searched {{count}} sites_other": "Переглянуто {{count}} сайтів", + "Searching \"{{searchQuery}}\"": "Шукаю \"{{searchQuery}}\"", + "Searxng Query URL": "URL-адреса запиту Searxng", + "See readme.md for instructions": "Див. readme.md для інструкцій", + "See what's new": "Подивіться, що нового", + "Seed": "Сід", + "Select a base model": "Обрати базову модель", + "Select a engine": "Оберіть рушій", + "Select a function": "Оберіть функцію", + "Select a mode": "Оберіть режим", + "Select a model": "Оберіть модель", + "Select a pipeline": "Оберіть конвеєр", + "Select a pipeline url": "Оберіть адресу конвеєра", + "Select a tool": "Оберіть інструмент", + "Select an Ollama instance": "Оберіть екземпляр Ollama", + "Select Documents": "Оберіть документи", + "Select model": "Обрати модель", + "Select only one model to call": "Оберіть лише одну модель для виклику", + "Selected model(s) do not support image inputs": "Вибрані модель(і) не підтримують вхідні зображення", + "Send": "Надіслати", + "Send a Message": "Надіслати повідомлення", + "Send message": "Надіслати повідомлення", + "September": "Вересень", + "Serper API Key": "Ключ API Serper", + "Serply API Key": "Ключ API Serply", + "Serpstack API Key": "Ключ API Serpstack", + "Server connection verified": "З'єднання з сервером підтверджено", + "Set as default": "Встановити за замовчуванням", + "Set Default Model": "Встановити модель за замовчуванням", + "Set embedding model (e.g. {{model}})": "Встановити модель вбудовування (напр, {{model}})", + "Set Image Size": "Встановити розмір зображення", + "Set reranking model (e.g. {{model}})": "Встановити модель переранжування (напр., {{model}})", + "Set Steps": "Встановити кроки", + "Set Task Model": "Встановити модель задач", + "Set Voice": "Встановити голос", + "Settings": "Налаштування", + "Settings saved successfully!": "Налаштування успішно збережено!", + "Settings updated successfully": "Налаштування успішно оновлені", + "Share": "Поділитися", + "Share Chat": "Поділитися чатом", + "Share to OpenWebUI Community": "Поділитися зі спільнотою OpenWebUI", + "short-summary": "короткий зміст", + "Show": "Показати", + "Show Admin Details in Account Pending Overlay": "Відобразити дані адміна у вікні очікування облікового запису", + "Show Model": "Показати модель", + "Show shortcuts": "Показати клавіатурні скорочення", + "Show your support!": "Підтримайте нас!", + "Showcased creativity": "Продемонстрований креатив", + "Sign in": "Увійти", + "Sign Out": "Вийти", + "Sign up": "Зареєструватися", + "Signing in": "Увійдіть в систему", + "Source": "Джерело", + "Speech recognition error: {{error}}": "Помилка розпізнавання мови: {{error}}", + "Speech-to-Text Engine": "Система розпізнавання мови", + "Stop Sequence": "Символ зупинки", + "STT Model": "Модель STT ", + "STT Settings": "Налаштування STT", + "Submit": "Надіслати", + "Subtitle (e.g. about the Roman Empire)": "Підзаголовок (напр., про Римську імперію)", + "Success": "Успіх", + "Successfully updated.": "Успішно оновлено.", + "Suggested": "Запропоновано", + "Support": "Підтримати", + "Support this plugin:": "Підтримайте цей плагін:", + "System": "Система", + "System Prompt": "Системний промт", + "Tags": "Теги", + "Tap to interrupt": "Натисніть, щоб перервати", + "Tavily API Key": "Ключ API Tavily", + "Tell us more:": "Розкажи нам більше:", + "Temperature": "Температура", + "Template": "Шаблон", + "Text Completion": "Завершення тексту", + "Text-to-Speech Engine": "Система синтезу мови", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Дякуємо за ваш відгук!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Розробники цього плагіна - пристрасні волонтери зі спільноти. Якщо ви вважаєте цей плагін корисним, будь ласка, зробіть свій внесок у його розвиток.", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Оцінка повинна бути в діапазоні від 0.0 (0%) до 1.0 (100%).", + "Theme": "Тема", + "Thinking...": "Думаю...", + "This action cannot be undone. Do you wish to continue?": "Цю дію не можна скасувати. Ви бажаєте продовжити?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Це забезпечує збереження ваших цінних розмов у безпечному бекенд-сховищі. Дякуємо!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Це експериментальна функція, вона може працювати не так, як очікувалося, і може бути змінена в будь-який час.", + "This setting does not sync across browsers or devices.": "Це налаштування не синхронізується між браузерами або пристроями.", + "This will delete": "Це призведе до видалення", + "Thorough explanation": "Детальне пояснення", + "Tika": "Tika", + "Tika Server URL required.": "Потрібна URL-адреса сервера Tika.", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Порада: Оновіть кілька слотів змінних послідовно, натискаючи клавішу табуляції у вікні чату після кожної заміни.", + "Title": "Заголовок", + "Title (e.g. Tell me a fun fact)": "Заголовок (напр., Розкажіть мені цікавий факт)", + "Title Auto-Generation": "Автогенерація заголовків", + "Title cannot be an empty string.": "Заголовок не може бути порожнім рядком.", + "Title Generation Prompt": "Промт для генерування заголовків", + "to": "в", + "To access the available model names for downloading,": "Щоб отримати доступ до назв доступних для завантаження моделей,", + "To access the GGUF models available for downloading,": "Щоб отримати доступ до моделей GGUF, які можна завантажити,,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Щоб отримати доступ до веб-інтерфейсу, зверніться до адміністратора. Адміністратори можуть керувати статусами користувачів з Панелі адміністратора.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Щоб додати документи сюди, спочатку завантажте їх до робочої області \"Документи\".", + "to chat input.": "в чаті.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Щоб обрати фільтри тут, спочатку додайте їх до робочої області \"Функції\".", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Щоб обрати тут набори інструментів, спочатку додайте їх до робочої області \"Інструменти\".", + "Today": "Сьогодні", + "Toggle settings": "Переключити налаштування", + "Toggle sidebar": "Переключити бокову панель", + "Tokens To Keep On Context Refresh (num_keep)": "Токени для збереження при оновленні контексту (num_keep)", + "Tool created successfully": "Інструмент успішно створено", + "Tool deleted successfully": "Інструмент успішно видалено", + "Tool imported successfully": "Інструмент успішно імпортовано", + "Tool updated successfully": "Інструмент успішно оновлено", + "Toolkit Description (e.g. A toolkit for performing various operations)": "Опис інструментарію (напр., набір інструментів для виконання різних операцій)", + "Toolkit ID (e.g. my_toolkit)": "Ідентифікатор набору інструментів (напр., my_toolkit)", + "Toolkit Name (e.g. My ToolKit)": "Назва інструментарію (напр., My ToolKit)", + "Tools": "Інструменти", + "Tools are a function calling system with arbitrary code execution": "Інструменти - це система виклику функцій з довільним виконанням коду", + "Tools have a function calling system that allows arbitrary code execution": "Інструменти мають систему виклику функцій, яка дозволяє виконання довільного коду", + "Tools have a function calling system that allows arbitrary code execution.": "Інструменти мають систему виклику функцій, яка дозволяє виконання довільного коду.", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Проблеми з доступом до Ollama?", + "TTS Model": "Модель TTS", + "TTS Settings": "Налаштування TTS", + "TTS Voice": "Голос TTS", + "Type": "Тип", + "Type Hugging Face Resolve (Download) URL": "Введіть URL ресурсу Hugging Face Resolve (завантаження)", + "Uh-oh! There was an issue connecting to {{provider}}.": "Ой! Виникла проблема при підключенні до {{provider}}.", + "UI": "Користувацький інтерфейс", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Невідомий тип файлу '{{file_type}}'. Завантаження файлу все одно продовжується.", + "Unpin": "Відчепити", + "Update": "Оновлення", + "Update and Copy Link": "Оновлення та копіювання посилання", + "Update password": "Оновити пароль", + "Updated at": "Оновлено на", + "Upload": "Завантажити", + "Upload a GGUF model": "Завантажити GGUF модель", + "Upload Files": "Завантажити файли", + "Upload Pipeline": "Завантажити конвеєр", + "Upload Progress": "Прогрес завантаження", + "URL Mode": "Режим URL-адреси", + "Use '#' in the prompt input to load and select your documents.": "Для введення промтів до веб-сторінок (URL) або вибору документів, будь ласка, використовуйте символ '#'.", + "Use Gravatar": "Змінити аватар", + "Use Initials": "Використовувати ініціали", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "користувач", + "User location successfully retrieved.": "Місцезнаходження користувача успішно знайдено.", + "User Permissions": "Права користувача", + "Users": "Користувачі", + "Utilize": "Використовувати", + "Valid time units:": "Дійсні одиниці часу:", + "Valves": "Клапани", + "Valves updated": "Клапани оновлено", + "Valves updated successfully": "Клапани успішно оновлено", + "variable": "змінна", + "variable to have them replaced with clipboard content.": "змінна, щоб замінити їх вмістом буфера обміну.", + "Version": "Версія", + "Voice": "Голос", + "Warning": "Увага!", + "Warning:": "Увага:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Попередження: Якщо ви оновлюєте або змінюєте модель вбудовування, вам потрібно буде повторно імпортувати всі документи.", + "Web": "Веб", + "Web API": "Веб-API", + "Web Loader Settings": "Налаштування веб-завантажувача", + "Web Params": "Налаштування веб-завантажувача", + "Web Search": "Веб-пошук", + "Web Search Engine": "Веб-пошукова система", + "Webhook URL": "URL веб-запиту", + "WebUI Settings": "Налаштування WebUI", + "WebUI will make requests to": "WebUI буде робити запити до", + "What’s New in": "Що нового в", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Коли історія вимкнена, нові чати в цьому браузері не будуть відображатися в історії на жодному з ваших пристроїв.", + "Whisper (Local)": "Whisper (Локально)", + "Widescreen Mode": "Широкоекранний режим", + "Workspace": "Робочий простір", + "Write a prompt suggestion (e.g. Who are you?)": "Напишіть промт (напр., Хто ти?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Напишіть стислий зміст у 50 слів, який узагальнює [тема або ключове слово].", + "Yesterday": "Вчора", + "You": "Ви", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Ви можете налаштувати ваші взаємодії з мовними моделями, додавши спогади через кнопку 'Керувати' внизу, що зробить їх більш корисними та персоналізованими для вас.", + "You cannot clone a base model": "Базову модель не можна клонувати", + "You have no archived conversations.": "У вас немає архівованих розмов.", + "You have shared this chat": "Ви поділилися цим чатом", + "You're a helpful assistant.": "Ви корисний асистент.", + "You're now logged in.": "Ви увійшли в систему.", + "Your account status is currently pending activation.": "Статус вашого облікового запису наразі очікує на активацію.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Весь ваш внесок піде безпосередньо розробнику плагіна; Open WebUI не бере жодних відсотків. Однак, обрана платформа фінансування може мати свої власні збори.", + "Youtube": "Youtube", + "Youtube Loader Settings": "Налаштування завантажувача Youtube" +} diff --git a/src/lib/i18n/locales/vi-VN/translation.json b/src/lib/i18n/locales/vi-VN/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..7b0736f96b4c5e2d1ba7dc50e8491a24e84cd09d --- /dev/null +++ b/src/lib/i18n/locales/vi-VN/translation.json @@ -0,0 +1,713 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' hoặc '-1' không hết hạn.", + "(Beta)": "(Beta)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(vd: `sh webui.sh --api`)", + "(latest)": "(mới nhất)", + "{{ models }}": "{{ mô hình }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}: Bạn không thể xóa base model", + "{{modelName}} is thinking...": "{{modelName}} đang suy nghĩ...", + "{{user}}'s Chats": "{{user}}'s Chats", + "{{webUIName}} Backend Required": "{{webUIName}} Yêu cầu Backend", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "Mô hình tác vụ được sử dụng khi thực hiện các tác vụ như tạo tiêu đề cho cuộc trò chuyện và truy vấn tìm kiếm trên web", + "a user": "người sử dụng", + "About": "Giới thiệu", + "Account": "Tài khoản", + "Account Activation Pending": "Tài khoản đang chờ kích hoạt", + "Accurate information": "Thông tin chính xác", + "Actions": "", + "Active Users": "Người dùng đang hoạt động", + "Add": "Thêm", + "Add a model id": "Thêm model id", + "Add a short description about what this model does": "Thêm mô tả ngắn về những khả năng của model", + "Add a short title for this prompt": "Thêm tiêu đề ngắn cho prompt này", + "Add a tag": "Thêm thẻ (tag)", + "Add custom prompt": "Thêm prompt tùy chỉnh", + "Add Docs": "Thêm tài liệu", + "Add Files": "Thêm tệp", + "Add Memory": "Thêm bộ nhớ", + "Add message": "Thêm tin nhắn", + "Add Model": "Thêm model", + "Add Tag": "Thêm thẻ", + "Add Tags": "thêm thẻ", + "Add User": "Thêm người dùng", + "Adjusting these settings will apply changes universally to all users.": "Các thay đổi cài đặt này sẽ áp dụng cho tất cả người sử dụng.", + "admin": "quản trị viên", + "Admin": "Quản trị", + "Admin Panel": "Trang Quản trị", + "Admin Settings": "Cài đặt hệ thống", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "Quản trị viên luôn có quyền truy cập vào tất cả các tool; người dùng cần các tools được chỉ định cho mỗi mô hình trong workspace.", + "Advanced Parameters": "Các tham số Nâng cao", + "Advanced Params": "Các tham số Nâng cao", + "all": "tất cả", + "All Documents": "Tất cả tài liệu", + "All Users": "Danh sách người sử dụng", + "Allow": "Cho phép", + "Allow Chat Deletion": "Cho phép Xóa nội dung chat", + "Allow non-local voices": "Cho phép giọng nói không bản xứ", + "Allow User Location": "Cho phép sử dụng vị trí người dùng", + "Allow Voice Interruption in Call": "Cho phép gián đoạn giọng nói trong cuộc gọi", + "alphanumeric characters and hyphens": "ký tự số và gạch nối", + "Already have an account?": "Bạn đã có tài khoản?", + "an assistant": "trợ lý", + "and": "và", + "and create a new shared link.": "và tạo một link chia sẻ mới", + "API Base URL": "Đường dẫn tới API (API Base URL)", + "API Key": "API Key", + "API Key created.": "Khóa API đã tạo", + "API keys": "API Keys", + "April": "Tháng 4", + "Archive": "Lưu trữ", + "Archive All Chats": "Lưu tất cả các cuộc Chat", + "Archived Chats": "Lưu các cuộc Chat", + "are allowed - Activate this command by typing": "được phép - Kích hoạt lệnh này bằng cách gõ", + "Are you sure?": "Bạn có chắc chắn không?", + "Attach file": "Đính kèm file", + "Attention to detail": "Có sự chú ý đến chi tiết của vấn đề", + "Audio": "Âm thanh", + "Audio settings updated successfully": "Đã cập nhật cài đặt âm thanh thành công", + "August": "Tháng 8", + "Auto-playback response": "Tự động phát lại phản hồi (Auto-playback)", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "Đường dẫn kết nối tới AUTOMATIC1111 (Base URL)", + "AUTOMATIC1111 Base URL is required.": "Base URL của AUTOMATIC1111 là bắt buộc.", + "available!": "có sẵn!", + "Back": "Quay lại", + "Bad Response": "Trả lời KHÔNG tốt", + "Banners": "Biểu ngữ", + "Base Model (From)": "Mô hình cơ sở (từ)", + "Batch Size (num_batch)": "", + "before": "trước", + "Being lazy": "Lười biếng", + "Brave Search API Key": "Khóa API tìm kiếm dũng cảm", + "Bypass SSL verification for Websites": "Bỏ qua xác thực SSL cho các trang web", + "Call": "Gọi", + "Call feature is not supported when using Web STT engine": "Tính năng gọi điện không được hỗ trợ khi sử dụng công cụ Web STT", + "Camera": "", + "Cancel": "Hủy bỏ", + "Capabilities": "Năng lực", + "Change Password": "Đổi Mật khẩu", + "Chat": "Trò chuyện", + "Chat Background Image": "Hình nền trò chuyện", + "Chat Bubble UI": "Bảng chat", + "Chat Controls": "", + "Chat direction": "Hướng chat", + "Chat History": "Lịch sử chat", + "Chat History is off for this browser.": "Lịch sử chat đã tắt cho trình duyệt này.", + "Chats": "Chat", + "Check Again": "Kiểm tra Lại", + "Check for updates": "Kiểm tra cập nhật", + "Checking for updates...": "Đang kiểm tra cập nhật...", + "Choose a model before saving...": "Chọn mô hình trước khi lưu...", + "Chunk Overlap": "Chồng lấn (overlap)", + "Chunk Params": "Tham số khối (chunk)", + "Chunk Size": "Kích thước khối (size)", + "Citation": "Trích dẫn", + "Clear memory": "Xóa bộ nhớ", + "Click here for help.": "Bấm vào đây để được trợ giúp.", + "Click here to": "Nhấn vào đây để", + "Click here to download user import template file.": "Bấm vào đây để tải xuống tệp template của người dùng.", + "Click here to select": "Bấm vào đây để chọn", + "Click here to select a csv file.": "Nhấn vào đây để chọn tệp csv", + "Click here to select a py file.": "Nhấn vào đây để chọn tệp py", + "Click here to select documents.": "Bấm vào đây để chọn tài liệu.", + "click here.": "bấm vào đây.", + "Click on the user role button to change a user's role.": "Bấm vào nút trong cột VAI TRÒ để thay đổi quyền của người sử dụng.", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "Quyền ghi vào clipboard bị từ chối. Vui lòng kiểm tra cài đặt trên trình duyệt của bạn để được cấp quyền truy cập cần thiết.", + "Clone": "Nhân bản", + "Close": "Đóng", + "Code formatted successfully": "Mã được định dạng thành công", + "Collection": "Tổng hợp mọi tài liệu", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI Base URL", + "ComfyUI Base URL is required.": "Base URL của ComfyUI là bắt buộc.", + "Command": "Lệnh", + "Concurrent Requests": "Các truy vấn đồng thời", + "Confirm": "Xác nhận", + "Confirm Password": "Xác nhận Mật khẩu", + "Confirm your action": "Xác nhận hành động của bạn", + "Connections": "Kết nối", + "Contact Admin for WebUI Access": "Liên hệ với Quản trị viên để được cấp quyền truy cập", + "Content": "Nội dung", + "Content Extraction": "Trích xuất nội dung", + "Context Length": "Độ dài ngữ cảnh (Context Length)", + "Continue Response": "Tiếp tục trả lời", + "Continue with {{provider}}": "Tiếp tục với {{provider}}", + "Controls": "", + "Copied shared chat URL to clipboard!": "Đã sao chép link chia sẻ trò chuyện vào clipboard!", + "Copy": "Sao chép", + "Copy last code block": "Sao chép khối mã cuối cùng", + "Copy last response": "Sao chép phản hồi cuối cùng", + "Copy Link": "Sao chép link", + "Copying to clipboard was successful!": "Sao chép vào clipboard thành công!", + "Create a model": "Tạo model", + "Create Account": "Tạo Tài khoản", + "Create new key": "Tạo key mới", + "Create new secret key": "Tạo key bí mật mới", + "Created at": "Được tạo vào lúc", + "Created At": "Tạo lúc", + "Created by": "Tạo bởi", + "CSV Import": "Nạp CSV", + "Current Model": "Mô hình hiện tại", + "Current Password": "Mật khẩu hiện tại", + "Custom": "Tùy chỉnh", + "Customize models for a specific purpose": "Tùy chỉnh model cho những mục đích riêng", + "Dark": "Tối", + "Dashboard": "", + "Database": "Cơ sở dữ liệu", + "December": "Tháng 12", + "Default": "Mặc định", + "Default (Automatic1111)": "Mặc định (Automatic1111)", + "Default (SentenceTransformers)": "Mặc định (SentenceTransformers)", + "Default Model": "Model mặc định", + "Default model updated": "Mô hình mặc định đã được cập nhật", + "Default Prompt Suggestions": "Đề xuất prompt mặc định", + "Default User Role": "Vai trò mặc định", + "delete": "xóa", + "Delete": "Xóa", + "Delete a model": "Xóa mô hình", + "Delete All Chats": "Xóa mọi cuộc Chat", + "Delete chat": "Xóa nội dung chat", + "Delete Chat": "Xóa chat", + "Delete chat?": "Xóa chat?", + "Delete Doc": "Xóa tài liệu", + "Delete function?": "Xóa function?", + "Delete prompt?": "Xóa prompt?", + "delete this link": "Xóa link này", + "Delete tool?": "Xóa tool?", + "Delete User": "Xóa người dùng", + "Deleted {{deleteModelTag}}": "Đã xóa {{deleteModelTag}}", + "Deleted {{name}}": "Đã xóa {{name}}", + "Description": "Mô tả", + "Didn't fully follow instructions": "Không tuân theo chỉ dẫn một cách đầy đủ", + "Disabled": "Đã tắt", + "Discover a function": "Khám phá function", + "Discover a model": "Khám phá model", + "Discover a prompt": "Khám phá thêm prompt mới", + "Discover a tool": "Khám vá tool", + "Discover, download, and explore custom functions": "Tìm kiếm, tải về và khám phá thêm các function tùy chỉnh", + "Discover, download, and explore custom prompts": "Tìm kiếm, tải về và khám phá thêm các prompt tùy chỉnh", + "Discover, download, and explore custom tools": "Tìm kiếm, tải về và khám phá thêm các tool tùy chỉnh", + "Discover, download, and explore model presets": "Tìm kiếm, tải về và khám phá thêm các model presets", + "Dismissible": "Có thể loại bỏ", + "Display Emoji in Call": "Hiển thị Emoji trong cuộc gọi", + "Display the username instead of You in the Chat": "Hiển thị tên người sử dụng thay vì 'Bạn' trong nội dung chat", + "Do not install functions from sources you do not fully trust.": "Không cài đặt các functions từ các nguồn mà bạn không hoàn toàn tin tưởng.", + "Do not install tools from sources you do not fully trust.": "Không cài đặt các tools từ những nguồn mà bạn không hoàn toàn tin tưởng.", + "Document": "Tài liệu", + "Document Settings": "Cấu hình kho tài liệu", + "Documentation": "Tài liệu", + "Documents": "Tài liệu", + "does not make any external connections, and your data stays securely on your locally hosted server.": "không thực hiện bất kỳ kết nối ngoài nào, và dữ liệu của bạn vẫn được lưu trữ an toàn trên máy chủ lưu trữ cục bộ của bạn.", + "Don't Allow": "Không Cho phép", + "Don't have an account?": "Không có tài khoản?", + "don't install random functions from sources you don't trust.": "không cài đặt các function từ các nguồn mà bạn không tin tưởng.", + "don't install random tools from sources you don't trust.": "không cài đặt các tools từ các nguồn mà bạn không tin tưởng.", + "Don't like the style": "Không thích phong cách trả lời", + "Done": "Hoàn thành", + "Download": "Tải về", + "Download canceled": "Đã hủy download", + "Download Database": "Tải xuống Cơ sở dữ liệu", + "Drop any files here to add to the conversation": "Thả bất kỳ tệp nào ở đây để thêm vào nội dung chat", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "vd: '30s','10m'. Đơn vị thời gian hợp lệ là 's', 'm', 'h'.", + "Edit": "Chỉnh sửa", + "Edit Doc": "Thay đổi tài liệu", + "Edit Memory": "Sửa Memory", + "Edit User": "Thay đổi thông tin người sử dụng", + "ElevenLabs": "", + "Email": "Email", + "Embedding Batch Size": "", + "Embedding Model": "Mô hình embedding", + "Embedding Model Engine": "Trình xử lý embedding", + "Embedding model set to \"{{embedding_model}}\"": "Mô hình embedding đã được thiết lập thành \"{{embedding_model}}\"", + "Enable Chat History": "Bật Lịch sử chat", + "Enable Community Sharing": "Kích hoạt Chia sẻ Cộng đồng", + "Enable New Sign Ups": "Cho phép đăng ký mới", + "Enable Web Search": "Kích hoạt tìm kiếm Web", + "Enabled": "Đã bật", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "Đảm bảo tệp CSV của bạn bao gồm 4 cột theo thứ tự sau: Name, Email, Password, Role.", + "Enter {{role}} message here": "Nhập yêu cầu của {{role}} ở đây", + "Enter a detail about yourself for your LLMs to recall": "Nhập chi tiết về bản thân của bạn để LLMs có thể nhớ", + "Enter api auth string (e.g. username:password)": "Nhập chuỗi xác thực api (ví dụ: username: mật khẩu)", + "Enter Brave Search API Key": "Nhập API key cho Brave Search", + "Enter Chunk Overlap": "Nhập Chunk chồng lấn (overlap)", + "Enter Chunk Size": "Nhập Kích thước Chunk", + "Enter Github Raw URL": "Nhập URL cho Github Raw", + "Enter Google PSE API Key": "Nhập Google PSE API Key", + "Enter Google PSE Engine Id": "Nhập Google PSE Engine Id", + "Enter Image Size (e.g. 512x512)": "Nhập Kích thước ảnh (vd: 512x512)", + "Enter language codes": "Nhập mã ngôn ngữ", + "Enter model tag (e.g. {{modelTag}})": "Nhập thẻ mô hình (vd: {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "Nhập số Steps (vd: 50)", + "Enter Score": "Nhập Score", + "Enter Searxng Query URL": "Nhập Query URL cho Searxng", + "Enter Serper API Key": "Nhập Serper API Key", + "Enter Serply API Key": "Nhập Serply API Key", + "Enter Serpstack API Key": "Nhập Serpstack API Key", + "Enter stop sequence": "Nhập stop sequence", + "Enter system prompt": "Nhập system prompt", + "Enter Tavily API Key": "Nhập Tavily API Key", + "Enter Tika Server URL": "Nhập URL cho Tika Server", + "Enter Top K": "Nhập Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "Nhập URL (vd: http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "Nhập URL (vd: http://localhost:11434)", + "Enter Your Email": "Nhập Email của bạn", + "Enter Your Full Name": "Nhập Họ và Tên của bạn", + "Enter your message": "Nhập tin nhắn của bạn", + "Enter Your Password": "Nhập Mật khẩu của bạn", + "Enter Your Role": "Nhập vai trò của bạn", + "Error": "Lỗi", + "Experimental": "Thử nghiệm", + "Export": "Xuất khẩu", + "Export All Chats (All Users)": "Tải về tất cả nội dung chat (tất cả mọi người)", + "Export chat (.json)": "Tải chat (.json)", + "Export Chats": "Tải nội dung chat về máy", + "Export Documents Mapping": "Tải cấu trúc tài liệu về máy", + "Export Functions": "Tải Functions về máy", + "Export LiteLLM config.yaml": "", + "Export Models": "Tải Models về máy", + "Export Prompts": "Tải các prompt về máy", + "Export Tools": "Tải Tools về máy", + "External Models": "Các model ngoài", + "Failed to create API Key.": "Lỗi khởi tạo API Key", + "Failed to read clipboard contents": "Không thể đọc nội dung clipboard", + "Failed to update settings": "Lỗi khi cập nhật các cài đặt", + "February": "Tháng 2", + "Feel free to add specific details": "Mô tả chi tiết về chất lượng của câu hỏi và phương án trả lời", + "File": "Tệp", + "File Mode": "Chế độ Tệp văn bản", + "File not found.": "Không tìm thấy tệp.", + "Files": "Tệp", + "Filter is now globally disabled": "Bộ lọc hiện đã bị vô hiệu hóa trên toàn hệ thống", + "Filter is now globally enabled": "Bộ lọc hiện được kích hoạt trên toàn hệ thống", + "Filters": "Lọc", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Phát hiện giả mạo vân tay: Không thể sử dụng tên viết tắt làm hình đại diện. Mặc định là hình ảnh hồ sơ mặc định.", + "Fluidly stream large external response chunks": "Truyền tải các khối phản hồi bên ngoài lớn một cách trôi chảy", + "Focus chat input": "Tập trung vào nội dung chat", + "Followed instructions perfectly": "Tuân theo chỉ dẫn một cách hoàn hảo", + "Form": "", + "Format your variables using square brackets like this:": "Định dạng các biến của bạn bằng cách sử dụng dấu ngoặc vuông như thế này:", + "Frequency Penalty": "Hình phạt tần số", + "Function created successfully": "Function được tạo thành công", + "Function deleted successfully": "Function đã bị xóa", + "Function Description (e.g. A filter to remove profanity from text)": "Mô tả Function (ví dụ: Bộ lọc để loại bỏ các ngôn từ tục tĩu khỏi văn bản)", + "Function ID (e.g. my_filter)": "Mã Function (ví dụ: my_filter)", + "Function is now globally disabled": "Function hiện đã bị vô hiệu hóa trên toàn hệ thống", + "Function is now globally enabled": "Function đã được kích hoạt trên toàn hệ thống", + "Function Name (e.g. My Filter)": "Tên Function (ví dụ: My Filter)", + "Function updated successfully": "Function được cập nhật thành công", + "Functions": "", + "Functions allow arbitrary code execution": "Các Function cho phép thực thi mã tùy ý", + "Functions allow arbitrary code execution.": "Các Function cho phép thực thi mã tùy ý.", + "Functions imported successfully": "Các function đã được nạp thành công", + "General": "Cài đặt chung", + "General Settings": "Cấu hình chung", + "Generate Image": "Sinh ảnh", + "Generating search query": "Tạo truy vấn tìm kiếm", + "Generation Info": "Thông tin chung", + "Get up and running with": "Khởi động và chạy với", + "Global": "Toàn hệ thống", + "Good Response": "Trả lời tốt", + "Google PSE API Key": "Khóa API Google PSE", + "Google PSE Engine Id": "ID công cụ Google PSE", + "h:mm a": "h:mm a", + "has no conversations.": "không có hội thoại", + "Hello, {{name}}": "Xin chào {{name}}", + "Help": "Trợ giúp", + "Hide": "Ẩn", + "Hide Model": "Ẩn mô hình", + "How can I help you today?": "Tôi có thể giúp gì cho bạn hôm nay?", + "Hybrid Search": "Tìm kiếm Hybrid", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "Tôi thừa nhận rằng tôi đã đọc và tôi hiểu ý nghĩa của hành động của mình. Tôi nhận thức được những rủi ro liên quan đến việc thực thi mã tùy ý và tôi đã xác minh độ tin cậy của nguồn.", + "Image Generation (Experimental)": "Tạo ảnh (thử nghiệm)", + "Image Generation Engine": "Công cụ tạo ảnh", + "Image Settings": "Cài đặt ảnh", + "Images": "Hình ảnh", + "Import Chats": "Nạp lại nội dung chat", + "Import Documents Mapping": "Nạp cấu trúc tài liệu", + "Import Functions": "Nạp Functions", + "Import Models": "Nạp model", + "Import Prompts": "Nạp các prompt lên hệ thống", + "Import Tools": "Tạp Tools", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "Bao gồm flag `--api` khi chạy stable-diffusion-webui", + "Info": "Thông tin", + "Input commands": "Nhập các câu lệnh", + "Install from Github URL": "Cài đặt từ Github URL", + "Instant Auto-Send After Voice Transcription": "Tự động gửi ngay lập tức sau khi phiên dịch giọng nói", + "Interface": "Giao diện", + "Invalid Tag": "Tag không hợp lệ", + "January": "Tháng 1", + "join our Discord for help.": "tham gia Discord của chúng tôi để được trợ giúp.", + "JSON": "JSON", + "JSON Preview": "Xem trước JSON", + "July": "Tháng 7", + "June": "Tháng 6", + "JWT Expiration": "JWT Hết hạn", + "JWT Token": "Token JWT", + "Keep Alive": "Giữ kết nối", + "Keyboard shortcuts": "Phím tắt", + "Knowledge": "Kiến thức", + "Language": "Ngôn ngữ", + "large language models, locally.": "các mô hình ngôn ngữ lớn, mang tính địa phương", + "Last Active": "Truy cập gần nhất", + "Last Modified": "Lần sửa gần nhất", + "Light": "Sáng", + "Listening...": "Đang nghe...", + "LLMs can make mistakes. Verify important information.": "Hệ thống có thể tạo ra nội dung không chính xác hoặc sai. Hãy kiểm chứng kỹ lưỡng thông tin trước khi tiếp nhận và sử dụng.", + "Local Models": "", + "LTR": "LTR", + "Made by OpenWebUI Community": "Được tạo bởi Cộng đồng OpenWebUI", + "Make sure to enclose them with": "Hãy chắc chắn bao quanh chúng bằng", + "Manage": "Quản lý", + "Manage Models": "Quản lý mô hình", + "Manage Ollama Models": "Quản lý mô hình với Ollama", + "Manage Pipelines": "Quản lý Pipelines", + "Manage Valves": "Quản lý Valves", + "March": "Tháng 3", + "Max Tokens (num_predict)": "Tokens tối đa (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "Tối đa 3 mô hình có thể được tải xuống cùng lúc. Vui lòng thử lại sau.", + "May": "Tháng 5", + "Memories accessible by LLMs will be shown here.": "Memory có thể truy cập bởi LLMs sẽ hiển thị ở đây.", + "Memory": "Memory", + "Memory added successfully": "Memory đã được thêm thành công", + "Memory cleared successfully": "Memory đã bị xóa", + "Memory deleted successfully": "Memory đã bị loại bỏ", + "Memory updated successfully": "Memory đã cập nhật thành công", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "Tin nhắn bạn gửi sau khi tạo liên kết sẽ không được chia sẻ. Người dùng có URL sẽ có thể xem cuộc trò chuyện được chia sẻ.", + "Minimum Score": "Score tối thiểu", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "", + "Model '{{modelName}}' has been successfully downloaded.": "Mô hình '{{modelName}}' đã được tải xuống thành công.", + "Model '{{modelTag}}' is already in queue for downloading.": "Mô hình '{{modelTag}}' đã có trong hàng đợi để tải xuống.", + "Model {{modelId}} not found": "Không tìm thấy Mô hình {{modelId}}", + "Model {{modelName}} is not vision capable": "Model {{modelName}} không có khả năng nhìn", + "Model {{name}} is now {{status}}": "Model {{name}} bây giờ là {{status}}", + "Model created successfully!": "Model đã được tạo thành công", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "Đường dẫn hệ thống tệp mô hình được phát hiện. Tên viết tắt mô hình là bắt buộc để cập nhật, không thể tiếp tục.", + "Model ID": "ID mẫu", + "Model not selected": "Chưa chọn Mô hình", + "Model Params": "Mô hình Params", + "Model updated successfully": "Model đã được cập nhật thành công", + "Model Whitelisting": "Whitelist mô hình", + "Model(s) Whitelisted": "các mô hình được cho vào danh sách Whitelist", + "Modelfile Content": "Nội dung Tệp Mô hình", + "Models": "Mô hình", + "More": "Thêm", + "Name": "Tên", + "Name Tag": "Tên Thẻ", + "Name your model": "Tên model", + "New Chat": "Tạo chat mới", + "New Password": "Mật khẩu mới", + "No content to speak": "Không có nội dung để nói", + "No documents found": "Không tìm thấy tài liệu nào", + "No file selected": "Chưa có tệp nào được chọn", + "No results found": "Không tìm thấy kết quả", + "No search query generated": "Không có truy vấn tìm kiếm nào được tạo ra", + "No source available": "Không có nguồn", + "No valves to update": "Chưa có valves nào được cập nhật", + "None": "Không ai", + "Not factually correct": "Không chính xác so với thực tế", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "Lưu ý: Nếu bạn đặt điểm (Score) tối thiểu thì tìm kiếm sẽ chỉ trả về những tài liệu có điểm lớn hơn hoặc bằng điểm tối thiểu.", + "Notifications": "Thông báo trên máy tính (Notification)", + "November": "Tháng 11", + "num_thread (Ollama)": "num_thread (Ollama)", + "OAuth ID": "", + "October": "Tháng 10", + "Off": "Tắt", + "Okay, Let's Go!": "Được rồi, Bắt đầu thôi!", + "OLED Dark": "OLED Dark", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "API Ollama bị vô hiệu hóa", + "Ollama API is disabled": "Ollama API đang bị vô hiệu hóa", + "Ollama Version": "Phiên bản Ollama", + "On": "Bật", + "Only": "Only", + "Only alphanumeric characters and hyphens are allowed in the command string.": "Chỉ ký tự số và gạch nối được phép trong chuỗi lệnh.", + "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.": "Vui lòng kiên nhẫn chờ đợi! Các tệp của bạn vẫn đang trong được phân tích và xử lý. Chúng tôi đang cố gắng hoàn thành chúng. Vui lòng kiên nhẫn và chúng tôi sẽ cho bạn biết khi chúng sẵn sàng.", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "Rất tiếc! URL dường như không hợp lệ. Vui lòng kiểm tra lại và thử lại.", + "Oops! There was an error in the previous response. Please try again or contact admin.": "Rất tiếc! Đã xảy ra lỗi trong phản hồi trước đó. Vui lòng thử lại hoặc liên hệ với quản trị viên.", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "Rất tiếc! Bạn đang sử dụng một phương thức không được hỗ trợ (chỉ dành cho frontend). Vui lòng cung cấp phương thức cho WebUI từ phía backend.", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "Mở nội dung chat mới", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "Phiên bản Open WebUI (v{{OPEN_WEBUI_VERSION}}) hiện thấp hơn phiên bản bắt buộc (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "API OpenAI", + "OpenAI API Config": "Cấu hình API OpenAI", + "OpenAI API Key is required.": "Bắt buộc nhập API OpenAI Key.", + "OpenAI URL/Key required.": "Yêu cầu URL/Key API OpenAI.", + "or": "hoặc", + "Other": "Khác", + "Password": "Mật khẩu", + "PDF document (.pdf)": "Tập tin PDF (.pdf)", + "PDF Extract Images (OCR)": "Trích xuất ảnh từ PDF (OCR)", + "pending": "đang chờ phê duyệt", + "Permission denied when accessing media devices": "Quyền truy cập các thiết bị đa phương tiện bị từ chối", + "Permission denied when accessing microphone": "Quyền truy cập micrô bị từ chối", + "Permission denied when accessing microphone: {{error}}": "Quyền truy cập micrô bị từ chối: {{error}}", + "Personalization": "Cá nhân hóa", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "Pipeline đã bị xóa", + "Pipeline downloaded successfully": "Pipeline đã được tải về thành công", + "Pipelines": "", + "Pipelines Not Detected": "Chưa tìm thấy Pipelines", + "Pipelines Valves": "", + "Plain text (.txt)": "Văn bản thô (.txt)", + "Playground": "Thử nghiệm (Playground)", + "Please carefully review the following warnings:": "Vui lòng xem xét cẩn thận các cảnh báo sau:", + "Positive attitude": "Thái độ tích cực", + "Previous 30 days": "30 ngày trước", + "Previous 7 days": "7 ngày trước", + "Profile Image": "Ảnh đại diện", + "Prompt": "Prompt", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "Prompt (ví dụ: Hãy kể cho tôi một sự thật thú vị về Đế chế La Mã)", + "Prompt Content": "Nội dung prompt", + "Prompt suggestions": "Gợi ý prompt", + "Prompts": "Prompt", + "Pull \"{{searchValue}}\" from Ollama.com": "Tải \"{{searchValue}}\" từ Ollama.com", + "Pull a model from Ollama.com": "Tải mô hình từ Ollama.com", + "Query Params": "Tham số Truy vấn", + "RAG Template": "Mẫu prompt cho RAG", + "Read Aloud": "Đọc ra loa", + "Record voice": "Ghi âm", + "Redirecting you to OpenWebUI Community": "Đang chuyển hướng bạn đến Cộng đồng OpenWebUI", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "Hãy coi bản thân mình như \"Người dùng\" (ví dụ: \"Người dùng đang học Tiếng Tây Ban Nha\")", + "Refused when it shouldn't have": "Từ chối trả lời mà nhẽ không nên làm vậy", + "Regenerate": "Tạo sinh lại câu trả lời", + "Release Notes": "Mô tả những cập nhật mới", + "Remove": "Xóa", + "Remove Model": "Xóa model", + "Rename": "Đổi tên", + "Repeat Last N": "Repeat Last N", + "Request Mode": "Request Mode", + "Reranking Model": "Reranking Model", + "Reranking model disabled": "Reranking model disabled", + "Reranking model set to \"{{reranking_model}}\"": "Reranking model được đặt thành \"{{reranking_model}}\"", + "Reset": "Xóa toàn bộ", + "Reset Upload Directory": "Xóa toàn bộ thư mục Upload", + "Reset Vector Storage": "Cài đặt lại Vector Storage", + "Response AutoCopy to Clipboard": "Tự động Sao chép Phản hồi vào clipboard", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "Không thể kích hoạt thông báo vì trang web không cấp quyền. Vui lòng truy cập cài đặt trình duyệt của bạn để cấp quyền cần thiết.", + "Role": "Vai trò", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "Chạy Llama 2, Code Llama và các mô hình khác. Tùy chỉnh hoặc mô hình riêng của bạn.", + "Running": "Đang chạy", + "Save": "Lưu", + "Save & Create": "Lưu & Tạo", + "Save & Update": "Lưu & Cập nhật", + "Save Tag": "Lưu Thẻ", + "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": "Không còn hỗ trợ lưu trữ lịch sử chat trực tiếp vào bộ nhớ trình duyệt của bạn. Vui lòng dành thời gian để tải xuống và xóa lịch sử chat của bạn bằng cách nhấp vào nút bên dưới. Đừng lo lắng, bạn có thể dễ dàng nhập lại lịch sử chat của mình vào backend thông qua", + "Scan": "Quét tài liệu", + "Scan complete!": "Quét hoàn tất!", + "Scan for documents from {{path}}": "Quét tài liệu từ đường dẫn: {{path}}", + "Search": "Tìm kiếm", + "Search a model": "Tìm model", + "Search Chats": "Tìm kiếm các cuộc Chat", + "Search Documents": "Tìm tài liệu", + "Search Functions": "Tìm kiếm Functions", + "Search Models": "Tìm model", + "Search Prompts": "Tìm prompt", + "Search Query Generation Prompt": "Prompt tạo câu truy vấn, tìm kiếm", + "Search Query Generation Prompt Length Threshold": "Ngưỡng độ dài prompt tạo câu truy vấn, tìm kiếm", + "Search Result Count": "Số kết quả tìm kiếm", + "Search Tools": "Tìm kiếm Tools", + "Searched {{count}} sites_other": "Đã tìm thấy {{count}} trang web", + "Searching \"{{searchQuery}}\"": "Đang tìm \"{{searchQuery}}\"", + "Searxng Query URL": "URL truy vấn Searxng", + "See readme.md for instructions": "Xem readme.md để biết hướng dẫn", + "See what's new": "Xem những cập nhật mới", + "Seed": "Seed", + "Select a base model": "Chọn một base model", + "Select a engine": "Chọn dịch vụ", + "Select a function": "Chọn function", + "Select a mode": "Chọn một chế độ", + "Select a model": "Chọn mô hình", + "Select a pipeline": "Chọn một quy trình", + "Select a pipeline url": "Chọn url quy trình", + "Select a tool": "Chọn tool", + "Select an Ollama instance": "Chọn một thực thể Ollama", + "Select Documents": "Chọn tài liệu", + "Select model": "Chọn model", + "Select only one model to call": "Chọn model để gọi", + "Selected model(s) do not support image inputs": "Model được lựa chọn không hỗ trợ đầu vào là hình ảnh", + "Send": "Gửi", + "Send a Message": "Gửi yêu cầu", + "Send message": "Gửi yêu cầu", + "September": "Tháng 9", + "Serper API Key": "Khóa API Serper", + "Serply API Key": "", + "Serpstack API Key": "Khóa API Serpstack", + "Server connection verified": "Kết nối máy chủ đã được xác minh", + "Set as default": "Đặt làm mặc định", + "Set Default Model": "Đặt Mô hình Mặc định", + "Set embedding model (e.g. {{model}})": "Thiết lập mô hình embedding (ví dụ: {{model}})", + "Set Image Size": "Đặt Kích thước ảnh", + "Set reranking model (e.g. {{model}})": "Thiết lập mô hình reranking (ví dụ: {{model}})", + "Set Steps": "Đặt Số Bước", + "Set Task Model": "Đặt Mô hình Tác vụ", + "Set Voice": "Đặt Giọng nói", + "Settings": "Cài đặt", + "Settings saved successfully!": "Cài đặt đã được lưu thành công!", + "Settings updated successfully": "Các cài đặt đã được cập nhật thành công", + "Share": "Chia sẻ", + "Share Chat": "Chia sẻ Chat", + "Share to OpenWebUI Community": "Chia sẻ đến Cộng đồng OpenWebUI", + "short-summary": "tóm tắt ngắn", + "Show": "Hiển thị", + "Show Admin Details in Account Pending Overlay": "Hiển thị thông tin của Quản trị viên trên màn hình hiển thị Tài khoản đang chờ xử lý", + "Show Model": "Hiện mô hình", + "Show shortcuts": "Hiển thị phím tắt", + "Show your support!": "Thể hiện sự ủng hộ của bạn!", + "Showcased creativity": "Thể hiện sự sáng tạo", + "Sign in": "Đăng nhập", + "Sign Out": "Đăng xuất", + "Sign up": "Đăng ký", + "Signing in": "Đăng nhập", + "Source": "Nguồn", + "Speech recognition error: {{error}}": "Lỗi nhận dạng giọng nói: {{error}}", + "Speech-to-Text Engine": "Công cụ Nhận dạng Giọng nói", + "Stop Sequence": "Trình tự Dừng", + "STT Model": "", + "STT Settings": "Cài đặt Nhận dạng Giọng nói", + "Submit": "Gửi", + "Subtitle (e.g. about the Roman Empire)": "Phụ đề (ví dụ: về Đế chế La Mã)", + "Success": "Thành công", + "Successfully updated.": "Đã cập nhật thành công.", + "Suggested": "Gợi ý một số mẫu prompt", + "Support": "Hỗ trợ", + "Support this plugin:": "Hỗ trợ plugin này:", + "System": "Hệ thống", + "System Prompt": "Prompt Hệ thống (System Prompt)", + "Tags": "Thẻ", + "Tap to interrupt": "Chạm để ngừng", + "Tavily API Key": "", + "Tell us more:": "Hãy cho chúng tôi hiểu thêm về chất lượng của câu trả lời:", + "Temperature": "Mức độ sáng tạo", + "Template": "Mẫu", + "Text Completion": "Hoàn tất Văn bản", + "Text-to-Speech Engine": "Công cụ Chuyển Văn bản thành Giọng nói", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "Cám ơn bạn đã gửi phản hồi!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "Các nhà phát triển đằng sau plugin này là những tình nguyện viên nhiệt huyết của cộng đồng. Nếu bạn thấy plugin này hữu ích, vui lòng cân nhắc đóng góp cho sự phát triển của nó.", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "Điểm (score) phải có giá trị từ 0,0 (0%) đến 1,0 (100%).", + "Theme": "Chủ đề", + "Thinking...": "Đang suy luận...", + "This action cannot be undone. Do you wish to continue?": "Hành động này không thể được hoàn tác. Bạn có muốn tiếp tục không?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "Điều này đảm bảo rằng các nội dung chat có giá trị của bạn được lưu an toàn vào cơ sở dữ liệu backend của bạn. Cảm ơn bạn!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "Đây là tính năng thử nghiệm, có thể không hoạt động như mong đợi và có thể thay đổi bất kỳ lúc nào.", + "This setting does not sync across browsers or devices.": "Cài đặt này không đồng bộ hóa trên các trình duyệt hoặc thiết bị.", + "This will delete": "Chat này sẽ bị xóa", + "Thorough explanation": "Giải thích kỹ lưỡng", + "Tika": "", + "Tika Server URL required.": "Bắt buộc phải nhập URL cho Tika Server ", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "Mẹo: Cập nhật nhiều khe biến liên tiếp bằng cách nhấn phím tab trong đầu vào trò chuyện sau mỗi việc thay thế.", + "Title": "Tiêu đề", + "Title (e.g. Tell me a fun fact)": "Tiêu đề (ví dụ: Hãy kể cho tôi một sự thật thú vị về...)", + "Title Auto-Generation": "Tự động Tạo Tiêu đề", + "Title cannot be an empty string.": "Tiêu đề không được phép bỏ trống", + "Title Generation Prompt": "Prompt tạo tiêu đề", + "to": " - ", + "To access the available model names for downloading,": "Để truy cập các tên mô hình có sẵn để tải xuống,", + "To access the GGUF models available for downloading,": "Để truy cập các mô hình GGUF có sẵn để tải xuống,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "Để truy cập vui lòng liên hệ với quản trị viên.", + "To add documents here, upload them to the \"Documents\" workspace first.": "Để thêm tài liệu, trước tiên hãy upload chúng lên khu vực \"Tài liệu\".", + "to chat input.": "đến đầu vào trò chuyện.", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "Để chọn các filters, bạn phải thêm chúng vào workspace \"Functions\" trước.", + "To select toolkits here, add them to the \"Tools\" workspace first.": "Để chọn các tookits, bạn phải thêm chúng vào workspace \"Tools\" trước.", + "Today": "Hôm nay", + "Toggle settings": "Bật/tắt cài đặt", + "Toggle sidebar": "Bật/tắt thanh bên", + "Tokens To Keep On Context Refresh (num_keep)": "", + "Tool created successfully": "Tool đã được tạo thành công", + "Tool deleted successfully": "Tool đã bị xóa", + "Tool imported successfully": "Tool đã được nạp thành công", + "Tool updated successfully": "Tool đã được cập nhật thành công", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "", + "Tools are a function calling system with arbitrary code execution": "Tools là một hệ thống gọi function với việc thực thi mã tùy ý", + "Tools have a function calling system that allows arbitrary code execution": "Các Tools có hệ thống gọi function cho phép thực thi mã tùy ý", + "Tools have a function calling system that allows arbitrary code execution.": "Các Tools có hệ thống gọi function cho phép thực thi mã tùy ý.", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "Gặp vấn đề khi truy cập Ollama?", + "TTS Model": "", + "TTS Settings": "Cài đặt Chuyển văn bản thành Giọng nói", + "TTS Voice": "", + "Type": "Kiểu", + "Type Hugging Face Resolve (Download) URL": "Nhập URL Hugging Face Resolve (Tải xuống)", + "Uh-oh! There was an issue connecting to {{provider}}.": "Ồ! Đã xảy ra sự cố khi kết nối với {{provider}}.", + "UI": "Giao diện", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "Loại tệp không xác định '{{file_type}}'. Vẫn tiếp tục tải tập tin lên.", + "Unpin": "", + "Update": "Cập nhật", + "Update and Copy Link": "Cập nhật và sao chép link", + "Update password": "Cập nhật mật khẩu", + "Updated at": "Cập nhật lúc", + "Upload": "", + "Upload a GGUF model": "Tải lên mô hình GGUF", + "Upload Files": "Tải tệp lên máy chủ", + "Upload Pipeline": "", + "Upload Progress": "Tiến trình tải tệp lên hệ thống", + "URL Mode": "Chế độ URL", + "Use '#' in the prompt input to load and select your documents.": "Sử dụng '#' trong đầu vào của prompt để tải về và lựa chọn tài liệu của bạn cần truy vấn.", + "Use Gravatar": "Sử dụng Gravatar", + "Use Initials": "Sử dụng tên viết tắt", + "use_mlock (Ollama)": "use_mlock (Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "Người sử dụng", + "User location successfully retrieved.": "Đã truy xuất thành công vị trí của người dùng.", + "User Permissions": "Phân quyền sử dụng", + "Users": "Người sử dụng", + "Utilize": "Sử dụng", + "Valid time units:": "Đơn vị thời gian hợp lệ:", + "Valves": "", + "Valves updated": "Cập nhật Valves", + "Valves updated successfully": "Đã cập nhật Valves thành công", + "variable": "biến", + "variable to have them replaced with clipboard content.": "biến để có chúng được thay thế bằng nội dung clipboard.", + "Version": "Version", + "Voice": "Giọng nói", + "Warning": "Cảnh báo", + "Warning:": "Cảnh báo:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "Cảnh báo: Nếu cập nhật hoặc thay đổi embedding model, bạn sẽ cần cập nhật lại tất cả tài liệu.", + "Web": "Web", + "Web API": "", + "Web Loader Settings": "Cài đặt Web Loader", + "Web Params": "Web Params", + "Web Search": "Tìm kiếm Web", + "Web Search Engine": "Chức năng Tìm kiếm Web", + "Webhook URL": "Webhook URL", + "WebUI Settings": "Cài đặt WebUI", + "WebUI will make requests to": "WebUI sẽ thực hiện các yêu cầu đến", + "What’s New in": "Thông tin mới về", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "Khi chế độ lịch sử chat đã tắt, các nội dung chat mới trên trình duyệt này sẽ không xuất hiện trên bất kỳ thiết bị nào của bạn.", + "Whisper (Local)": "", + "Widescreen Mode": "Chế độ màn hình rộng", + "Workspace": "Workspace", + "Write a prompt suggestion (e.g. Who are you?)": "Hãy viết một prompt (vd: Bạn là ai?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "Viết một tóm tắt trong vòng 50 từ cho [chủ đề hoặc từ khóa].", + "Yesterday": "Hôm qua", + "You": "Bạn", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "Bạn có thể cá nhân hóa các tương tác của mình với LLM bằng cách thêm bộ nhớ thông qua nút 'Quản lý' bên dưới, làm cho chúng hữu ích hơn và phù hợp với bạn hơn.", + "You cannot clone a base model": "Bạn không thể nhân bản base model", + "You have no archived conversations.": "Bạn chưa lưu trữ một nội dung chat nào", + "You have shared this chat": "Bạn vừa chia sẻ chat này", + "You're a helpful assistant.": "Bạn là một trợ lý hữu ích.", + "You're now logged in.": "Bạn đã đăng nhập.", + "Your account status is currently pending activation.": "Tài khoản của bạn hiện đang ở trạng thái chờ kích hoạt.", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "Toàn bộ đóng góp của bạn sẽ được chuyển trực tiếp đến nhà phát triển plugin; Open WebUI không lấy bất kỳ tỷ lệ phần trăm nào. Tuy nhiên, nền tảng được chọn tài trợ có thể có phí riêng.", + "Youtube": "Youtube", + "Youtube Loader Settings": "Cài đặt Youtube Loader" +} diff --git a/src/lib/i18n/locales/zh-CN/translation.json b/src/lib/i18n/locales/zh-CN/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..268bb1ac3999b9db12ecb5fe41eb5fccf8ad7e48 --- /dev/null +++ b/src/lib/i18n/locales/zh-CN/translation.json @@ -0,0 +1,713 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 或 '-1' 表示无过期时间。", + "(Beta)": "(测试版)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "(例如 `sh webui.sh --api --api-auth username_password`)", + "(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`)", + "(latest)": "(最新版)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}:您不能删除基础模型", + "{{modelName}} is thinking...": "{{modelName}} 正在思考...", + "{{user}}'s Chats": "{{user}} 的对话记录", + "{{webUIName}} Backend Required": "需要 {{webUIName}} 后端", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "任务模型用于执行生成对话标题和网络搜索查询等任务", + "a user": "用户", + "About": "关于", + "Account": "账号", + "Account Activation Pending": "账号待激活", + "Accurate information": "提供的信息很准确", + "Actions": "", + "Active Users": "当前在线用户", + "Add": "添加", + "Add a model id": "添加一个模型 ID", + "Add a short description about what this model does": "添加有关该模型能力的简短描述", + "Add a short title for this prompt": "为此提示词添加一个简短的标题", + "Add a tag": "添加标签", + "Add custom prompt": "添加自定义提示词", + "Add Docs": "添加文档", + "Add Files": "添加文件", + "Add Memory": "添加记忆", + "Add message": "添加消息", + "Add Model": "添加模型", + "Add Tag": "", + "Add Tags": "添加标签", + "Add User": "添加用户", + "Adjusting these settings will apply changes universally to all users.": "调整这些设置将会对所有用户应用更改。", + "admin": "管理员", + "Admin": "管理员联系方式", + "Admin Panel": "管理员面板", + "Admin Settings": "管理员设置", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "管理员拥有所有工具的访问权限;用户则需在工作空间中为每个模型单独分配工具。", + "Advanced Parameters": "高级参数", + "Advanced Params": "高级参数", + "all": "所有", + "All Documents": "所有文档", + "All Users": "所有用户", + "Allow": "允许", + "Allow Chat Deletion": "允许删除聊天记录", + "Allow non-local voices": "允许调用非本地音色", + "Allow User Location": "允许获取您的位置", + "Allow Voice Interruption in Call": "允许通话中的打断语音", + "alphanumeric characters and hyphens": "字母数字字符和连字符", + "Already have an account?": "已经拥有账号了?", + "an assistant": "助手", + "and": "和", + "and create a new shared link.": "并创建一个新的分享链接。", + "API Base URL": "API 基础地址", + "API Key": "API 密钥", + "API Key created.": "API 密钥已创建。", + "API keys": "API 密钥", + "April": "四月", + "Archive": "归档", + "Archive All Chats": "归档所有对话记录", + "Archived Chats": "已归档对话", + "are allowed - Activate this command by typing": "允许 - 通过输入来激活这个命令", + "Are you sure?": "是否确定?", + "Attach file": "添加文件", + "Attention to detail": "注重细节", + "Audio": "语音", + "Audio settings updated successfully": "语音设置更新成功", + "August": "八月", + "Auto-playback response": "自动念出回复内容", + "AUTOMATIC1111 Api Auth String": "AUTOMATIC1111 Api鉴权字符串", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 基础地址", + "AUTOMATIC1111 Base URL is required.": "需要 AUTOMATIC1111 基础地址。", + "available!": "版本可用!", + "Back": "返回", + "Bad Response": "点踩此回答", + "Banners": "公告横幅", + "Base Model (From)": "基础模型 (来自)", + "Batch Size (num_batch)": "批大小 (num_batch)", + "before": "对话", + "Being lazy": "懒惰", + "Brave Search API Key": "Brave Search API 密钥", + "Bypass SSL verification for Websites": "绕过网站的 SSL 验证", + "Call": "呼叫", + "Call feature is not supported when using Web STT engine": "使用 Web 语音转文字引擎时不支持呼叫功能。", + "Camera": "摄像头", + "Cancel": "取消", + "Capabilities": "能力", + "Change Password": "更改密码", + "Chat": "对话", + "Chat Background Image": "对话背景图片", + "Chat Bubble UI": "气泡样式对话", + "Chat Controls": "对话高级设置", + "Chat direction": "对话样式方向", + "Chat History": "对话历史记录", + "Chat History is off for this browser.": "此浏览器已关闭对话历史记录功能。", + "Chats": "对话", + "Check Again": "刷新重试", + "Check for updates": "检查更新", + "Checking for updates...": "正在检查更新...", + "Choose a model before saving...": "保存前选择一个模型...", + "Chunk Overlap": "块重叠 (Chunk Overlap)", + "Chunk Params": "块参数 (Chunk Params)", + "Chunk Size": "块大小 (Chunk Size)", + "Citation": "引文", + "Clear memory": "清除记忆", + "Click here for help.": "点击这里获取帮助。", + "Click here to": "单击", + "Click here to download user import template file.": "单击此处下载用户导入所需的模板文件。", + "Click here to select": "点击这里选择", + "Click here to select a csv file.": "单击此处选择 csv 文件。", + "Click here to select a py file.": "单击此处选择 py 文件。", + "Click here to select documents.": "单击选择文档", + "click here.": "点击这里。", + "Click on the user role button to change a user's role.": "点击角色前方的组别按钮以更改用户所属权限组。", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "写入剪贴板时被拒绝。请检查浏览器设置,授予必要权限。", + "Clone": "复制", + "Close": "关闭", + "Code formatted successfully": "代码格式化成功", + "Collection": "集合", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI 基础地址", + "ComfyUI Base URL is required.": "ComfyUI 基础地址为必需填写。", + "Command": "命令", + "Concurrent Requests": "并发请求", + "Confirm": "确认", + "Confirm Password": "确认密码", + "Confirm your action": "确认您的操作", + "Connections": "外部连接", + "Contact Admin for WebUI Access": "请联系管理员以获取访问权限", + "Content": "内容", + "Content Extraction": "内容提取", + "Context Length": "上下文长度", + "Continue Response": "继续生成", + "Continue with {{provider}}": "使用 {{provider}} 继续", + "Controls": "对话高级设置", + "Copied shared chat URL to clipboard!": "已复制此对话分享链接至剪贴板!", + "Copy": "复制", + "Copy last code block": "复制最后一个代码块中的代码", + "Copy last response": "复制最后一次回复内容", + "Copy Link": "复制链接", + "Copying to clipboard was successful!": "成功复制到剪贴板!", + "Create a model": "创建一个模型", + "Create Account": "创建账号", + "Create new key": "创建新密钥", + "Create new secret key": "创建新安全密钥", + "Created at": "创建于", + "Created At": "创建于", + "Created by": "作者", + "CSV Import": "通过 CSV 文件导入", + "Current Model": "当前模型", + "Current Password": "当前密码", + "Custom": "自定义", + "Customize models for a specific purpose": "定制专用目的模型", + "Dark": "暗色", + "Dashboard": "仪表板", + "Database": "数据库", + "December": "十二月", + "Default": "默认", + "Default (Automatic1111)": "默认(Automatic1111)", + "Default (SentenceTransformers)": "默认(SentenceTransformers)", + "Default Model": "默认模型", + "Default model updated": "默认模型已更新", + "Default Prompt Suggestions": "默认提示词建议", + "Default User Role": "默认用户角色", + "delete": "删除", + "Delete": "删除", + "Delete a model": "删除一个模型", + "Delete All Chats": "删除所有对话记录", + "Delete chat": "删除对话记录", + "Delete Chat": "删除对话记录", + "Delete chat?": "删除对话记录?", + "Delete Doc": "删除文档", + "Delete function?": "删除函数?", + "Delete prompt?": "删除提示词?", + "delete this link": "此处删除这个链接", + "Delete tool?": "删除工具?", + "Delete User": "删除用户", + "Deleted {{deleteModelTag}}": "已删除 {{deleteModelTag}}", + "Deleted {{name}}": "已删除 {{name}}", + "Description": "描述", + "Didn't fully follow instructions": "没有完全遵照指示", + "Disabled": "禁用", + "Discover a function": "发现更多函数", + "Discover a model": "发现更多模型", + "Discover a prompt": "发现更多提示词", + "Discover a tool": "发现更多工具", + "Discover, download, and explore custom functions": "发现、下载并探索更多函数", + "Discover, download, and explore custom prompts": "发现、下载并探索更多自定义提示词", + "Discover, download, and explore custom tools": "发现、下载并探索更多工具", + "Discover, download, and explore model presets": "发现、下载并探索更多模型预设", + "Dismissible": "是否可关闭", + "Display Emoji in Call": "在通话中显示 Emoji 表情符号", + "Display the username instead of You in the Chat": "在对话中显示用户名而不是“你”", + "Do not install functions from sources you do not fully trust.": "切勿安装来源不完全可信的函数。", + "Do not install tools from sources you do not fully trust.": "切勿安装来源不完全可信的工具。", + "Document": "文档", + "Document Settings": "文档设置", + "Documentation": "帮助文档", + "Documents": "文档", + "does not make any external connections, and your data stays securely on your locally hosted server.": "不会与外部建立任何连接,您的数据会安全地存储在本地托管的服务器上。", + "Don't Allow": "不允许", + "Don't have an account?": "没有账号?", + "don't install random functions from sources you don't trust.": "切勿随意从不完全可信的来源安装函数。", + "don't install random tools from sources you don't trust.": "切勿随意从不完全可信的来源安装工具。", + "Don't like the style": "不喜欢这个文风", + "Done": "完成", + "Download": "下载", + "Download canceled": "下载已取消", + "Download Database": "下载数据库", + "Drop any files here to add to the conversation": "拖动文件到此处以添加到对话中", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如 '30s','10m'。有效的时间单位是秒:'s',分:'m',时:'h'。", + "Edit": "编辑", + "Edit Doc": "编辑文档", + "Edit Memory": "编辑记忆", + "Edit User": "编辑用户", + "ElevenLabs": "ElevenLabs", + "Email": "电子邮箱", + "Embedding Batch Size": "嵌入层批处理大小 (Embedding Batch Size)", + "Embedding Model": "语义向量模型", + "Embedding Model Engine": "语义向量模型引擎", + "Embedding model set to \"{{embedding_model}}\"": "语义向量模型设置为 \"{{embedding_model}}\"", + "Enable Chat History": "启用对话历史记录", + "Enable Community Sharing": "启用分享至社区", + "Enable New Sign Ups": "允许新用户注册", + "Enable Web Search": "启用网络搜索", + "Enabled": "启用", + "Engine": "引擎", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "确保您的 CSV 文件按以下顺序包含 4 列: 姓名、电子邮箱、密码、角色。", + "Enter {{role}} message here": "在此处输入 {{role}} 信息", + "Enter a detail about yourself for your LLMs to recall": "输入一个关于你自己的详细信息,方便你的大语言模型记住这些内容", + "Enter api auth string (e.g. username:password)": "输入api鉴权路径 (例如:username:password)", + "Enter Brave Search API Key": "输入 Brave Search API 密钥", + "Enter Chunk Overlap": "输入块重叠 (Chunk Overlap)", + "Enter Chunk Size": "输入块大小 (Chunk Size)", + "Enter Github Raw URL": "输入 Github Raw 地址", + "Enter Google PSE API Key": "输入 Google PSE API 密钥", + "Enter Google PSE Engine Id": "输入 Google PSE 引擎 ID", + "Enter Image Size (e.g. 512x512)": "输入图像分辨率 (例如:512x512)", + "Enter language codes": "输入语言代码", + "Enter model tag (e.g. {{modelTag}})": "输入模型标签 (例如:{{modelTag}})", + "Enter Number of Steps (e.g. 50)": "输入步骤数 (Steps) (例如:50)", + "Enter Score": "输入评分", + "Enter Searxng Query URL": "输入 Searxng 查询地址", + "Enter Serper API Key": "输入 Serper API 密钥", + "Enter Serply API Key": "输入 Serply API 密钥", + "Enter Serpstack API Key": "输入 Serpstack API 密钥", + "Enter stop sequence": "输入停止序列 (Stop Sequence)", + "Enter system prompt": "输入系统提示词 (Prompt)", + "Enter Tavily API Key": "输入 Tavily API 密钥", + "Enter Tika Server URL": "输入 Tika 服务器地址", + "Enter Top K": "输入 Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "输入地址 (例如:http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "输入地址 (例如:http://localhost:11434)", + "Enter Your Email": "输入您的电子邮箱", + "Enter Your Full Name": "输入您的名称", + "Enter your message": "输入您的消息", + "Enter Your Password": "输入您的密码", + "Enter Your Role": "输入您的权限组", + "Error": "错误", + "Experimental": "实验性", + "Export": "导出", + "Export All Chats (All Users)": "导出所有用户对话", + "Export chat (.json)": "JSON 文件 (.json)", + "Export Chats": "导出对话", + "Export Documents Mapping": "导出文档映射", + "Export Functions": "导出函数", + "Export LiteLLM config.yaml": "导出 LteLLM config.yaml 文件", + "Export Models": "导出模型", + "Export Prompts": "导出提示词", + "Export Tools": "导出工具", + "External Models": "外部模型", + "Failed to create API Key.": "无法创建 API 密钥。", + "Failed to read clipboard contents": "无法读取剪贴板内容", + "Failed to update settings": "无法更新设置", + "February": "二月", + "Feel free to add specific details": "欢迎补充具体细节", + "File": "文件", + "File Mode": "文件模式", + "File not found.": "文件未找到。", + "Files": "文件", + "Filter is now globally disabled": "过滤器已全局禁用", + "Filter is now globally enabled": "过滤器已全局启用", + "Filters": "过滤器", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "检测到指纹伪造:无法使用姓名缩写作为头像。默认使用默认个人形象。", + "Fluidly stream large external response chunks": "流畅地传输外部大型响应块数据", + "Focus chat input": "聚焦对话输入", + "Followed instructions perfectly": "完全按照指示执行", + "Form": "手动创建", + "Format your variables using square brackets like this:": "使用这样的方括号格式化你的变量:", + "Frequency Penalty": "频率惩罚", + "Function created successfully": "函数创建成功", + "Function deleted successfully": "函数删除成功", + "Function Description (e.g. A filter to remove profanity from text)": "函数描述(例如:一个用于从文本中过滤脏话的过滤器)", + "Function ID (e.g. my_filter)": "函数 ID (例如:my_filter)", + "Function is now globally disabled": "函数全局已禁用", + "Function is now globally enabled": "函数全局已启用", + "Function Name (e.g. My Filter)": "函数名称(例如:我的过滤器)", + "Function updated successfully": "函数更新成功", + "Functions": "函数", + "Functions allow arbitrary code execution": "注意:函数有权执行任意代码", + "Functions allow arbitrary code execution.": "注意:函数有权执行任意代码。", + "Functions imported successfully": "函数导入成功", + "General": "通用", + "General Settings": "通用设置", + "Generate Image": "生成图像", + "Generating search query": "生成搜索查询", + "Generation Info": "生成信息", + "Get up and running with": "启动并运行", + "Global": "全局", + "Good Response": "点赞此回答", + "Google PSE API Key": "Google PSE API 密钥", + "Google PSE Engine Id": "Google PSE 引擎 ID", + "h:mm a": "HH:mm", + "has no conversations.": "没有对话。", + "Hello, {{name}}": "您好,{{name}}", + "Help": "帮助", + "Hide": "隐藏", + "Hide Model": "隐藏", + "How can I help you today?": "有什么我能帮您的吗?", + "Hybrid Search": "混合搜索", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "我已阅读并理解我的行为所带来的影响,明白执行任意代码所涉及的风险。且我已验证代码来源可信度。", + "Image Generation (Experimental)": "图像生成(实验性)", + "Image Generation Engine": "图像生成引擎", + "Image Settings": "图像设置", + "Images": "图像", + "Import Chats": "导入对话记录", + "Import Documents Mapping": "导入文档映射", + "Import Functions": "导入函数", + "Import Models": "导入模型", + "Import Prompts": "导入提示词", + "Import Tools": "导入工具", + "Include `--api-auth` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api-auth` 标志", + "Include `--api` flag when running stable-diffusion-webui": "运行 stable-diffusion-webui 时包含 `--api` 标志", + "Info": "信息", + "Input commands": "输入命令", + "Install from Github URL": "从 Github URL 安装", + "Instant Auto-Send After Voice Transcription": "语音转录文字后即时自动发送", + "Interface": "界面", + "Invalid Tag": "无效标签", + "January": "一月", + "join our Discord for help.": "加入我们的 Discord 寻求帮助。", + "JSON": "JSON", + "JSON Preview": "JSON 预览", + "July": "七月", + "June": "六月", + "JWT Expiration": "JWT 过期", + "JWT Token": "JWT 令牌", + "Keep Alive": "保持活动", + "Keyboard shortcuts": "键盘快捷键", + "Knowledge": "知识库", + "Language": "语言", + "large language models, locally.": "本地大语言模型", + "Last Active": "最后在线时间", + "Last Modified": "最后修改时间", + "Light": "浅色", + "Listening...": "正在倾听...", + "LLMs can make mistakes. Verify important information.": "大语言模型可能会生成误导性错误信息,请对关键信息加以验证。", + "Local Models": "本地模型", + "LTR": "从左至右", + "Made by OpenWebUI Community": "由 OpenWebUI 社区制作", + "Make sure to enclose them with": "确保将它们包含在内", + "Manage": "管理", + "Manage Models": "管理模型", + "Manage Ollama Models": "管理 Ollama 模型", + "Manage Pipelines": "管理 Pipeline", + "Manage Valves": "管理值", + "March": "三月", + "Max Tokens (num_predict)": "最多 Token (num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "最多可以同时下载 3 个模型,请稍后重试。", + "May": "五月", + "Memories accessible by LLMs will be shown here.": "大语言模型可访问的记忆将在此显示。", + "Memory": "记忆", + "Memory added successfully": "记忆添加成功", + "Memory cleared successfully": "记忆清除成功", + "Memory deleted successfully": "记忆删除成功", + "Memory updated successfully": "记忆更新成功", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "创建链接后发送的消息不会被共享。具有 URL 的用户将能够查看共享对话。", + "Minimum Score": "最低分", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "YYYY年 MM月 DD日", + "MMMM DD, YYYY HH:mm": "YYYY年 MM月 DD日 HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "YYYY年 MM月 DD日 hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "模型'{{modelName}}'已成功下载。", + "Model '{{modelTag}}' is already in queue for downloading.": "模型'{{modelTag}}'已在下载队列中。", + "Model {{modelId}} not found": "未找到模型 {{modelId}}", + "Model {{modelName}} is not vision capable": "模型 {{modelName}} 不支持视觉能力", + "Model {{name}} is now {{status}}": "模型 {{name}} 现在是 {{status}}", + "Model created successfully!": "模型创建成功!", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "检测到模型文件系统路径,无法继续进行。更新操作需要提供模型简称。", + "Model ID": "模型 ID", + "Model not selected": "未选择模型", + "Model Params": "模型参数", + "Model updated successfully": "模型更新成功", + "Model Whitelisting": "白名单模型", + "Model(s) Whitelisted": "模型已加入白名单", + "Modelfile Content": "模型文件内容", + "Models": "模型", + "More": "更多", + "Name": "名称", + "Name Tag": "标签", + "Name your model": "为您的模型命名", + "New Chat": "新对话", + "New Password": "新密码", + "No content to speak": "没有内容可朗读", + "No documents found": "未找到文档", + "No file selected": "未选中文件", + "No results found": "未找到结果", + "No search query generated": "未生成搜索查询", + "No source available": "没有可用来源", + "No valves to update": "没有需要更新的值", + "None": "无", + "Not factually correct": "事实并非如此", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "注意:如果设置了最低分数,搜索只会返回分数大于或等于最低分数的文档。", + "Notifications": "桌面通知", + "November": "十一月", + "num_thread (Ollama)": "num_thread(Ollama)", + "OAuth ID": "OAuth ID", + "October": "十月", + "Off": "关闭", + "Okay, Let's Go!": "确认,开始使用!", + "OLED Dark": "黑色", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API 已禁用", + "Ollama API is disabled": "Ollama API 已禁用", + "Ollama Version": "Ollama 版本", + "On": "开启", + "Only": "仅", + "Only alphanumeric characters and hyphens are allowed in the command string.": "命令字符串中只允许使用英文字母,数字 (0-9) 以及连字符 (-)。", + "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.": "糟糕!请稍等!您的文件还在处理中。我们正在努力让它们达到最佳效果。请耐心等待,准备好后我们会通知您。", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "糟糕!此链接似乎为无效链接。请检查后重试。", + "Oops! There was an error in the previous response. Please try again or contact admin.": "糟糕!之前的回复出现了错误。请重试或联系管理员。", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "糟糕!你正在使用不被支持的方法(仅前端)。请从后端提供 WebUI 服务。", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "打开新对话", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "当前 Open WebUI 版本 (v{{OPEN_WEBUI_VERSION}}) 低于所需的版本 (v{{REQUIRED_VERSION}})", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API 配置", + "OpenAI API Key is required.": "需要 OpenAI API 密钥。", + "OpenAI URL/Key required.": "需要 OpenAI URL/Key", + "or": "或", + "Other": "其他", + "Password": "密码", + "PDF document (.pdf)": "PDF 文档 (.pdf)", + "PDF Extract Images (OCR)": "PDF 图像处理 (使用 OCR)", + "pending": "待激活", + "Permission denied when accessing media devices": "申请媒体设备权限被拒绝", + "Permission denied when accessing microphone": "申请麦克风权限被拒绝", + "Permission denied when accessing microphone: {{error}}": "申请麦克风权限被拒绝:{{error}}", + "Personalization": "个性化", + "Pin": "置顶", + "Pinned": "已置顶", + "Pipeline deleted successfully": "Pipeline 删除成功", + "Pipeline downloaded successfully": "Pipeline 下载成功", + "Pipelines": "Pipeline", + "Pipelines Not Detected": "未检测到 Pipeline", + "Pipelines Valves": "Pipeline 值", + "Plain text (.txt)": "TXT 文档 (.txt)", + "Playground": "AI 对话游乐场", + "Please carefully review the following warnings:": "请仔细阅读以下警告信息:", + "Positive attitude": "积极的态度", + "Previous 30 days": "过去 30 天", + "Previous 7 days": "过去 7 天", + "Profile Image": "用户头像", + "Prompt": "提示词", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示(例如:给我讲一个关于罗马帝国的趣事。)", + "Prompt Content": "提示词内容", + "Prompt suggestions": "提示词建议", + "Prompts": "提示词", + "Pull \"{{searchValue}}\" from Ollama.com": "从 Ollama.com 拉取 \"{{searchValue}}\"", + "Pull a model from Ollama.com": "从 Ollama.com 拉取一个模型", + "Query Params": "查询参数", + "RAG Template": "RAG 提示词模板", + "Read Aloud": "朗读", + "Record voice": "录音", + "Redirecting you to OpenWebUI Community": "正在将您重定向到 OpenWebUI 社区", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "使用\"User\" (用户) 来指代自己(例如:“User 正在学习西班牙语”)", + "Refused when it shouldn't have": "无理拒绝", + "Regenerate": "重新生成", + "Release Notes": "更新日志", + "Remove": "移除", + "Remove Model": "移除模型", + "Rename": "重命名", + "Repeat Last N": "重复最后 N 次", + "Request Mode": "请求模式", + "Reranking Model": "重排模型", + "Reranking model disabled": "重排模型已禁用", + "Reranking model set to \"{{reranking_model}}\"": "重排模型设置为 \"{{reranking_model}}\"", + "Reset": "重置", + "Reset Upload Directory": "重置上传目录", + "Reset Vector Storage": "重置向量存储", + "Response AutoCopy to Clipboard": "自动复制回复到剪贴板", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "无法激活回复时发送通知。请检查浏览器设置,并授予必要的访问权限。", + "Role": "权限组", + "Rosé Pine": "Rosé Pine", + "Rosé Pine Dawn": "Rosé Pine Dawn", + "RTL": "从右至左", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "运行 Llama 2、Code Llama 和其他模型。自定义和创建您自己的模型。", + "Running": "运行中", + "Save": "保存", + "Save & Create": "保存并创建", + "Save & Update": "保存并更新", + "Save Tag": "保存标签", + "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": "我们不再支持将聊天记录直接保存到浏览器的存储空间。请点击下面的按钮下载并删除您的聊天记录。别担心,您可以轻松地将聊天记录重新导入到后台。", + "Scan": "立即扫描", + "Scan complete!": "扫描完成!", + "Scan for documents from {{path}}": "从 {{path}} 扫描文档", + "Search": "搜索", + "Search a model": "搜索模型", + "Search Chats": "搜索对话", + "Search Documents": "搜索文档", + "Search Functions": "搜索函数", + "Search Models": "搜索模型", + "Search Prompts": "搜索提示词", + "Search Query Generation Prompt": "搜索查询生成提示", + "Search Query Generation Prompt Length Threshold": "搜索查询生成提示长度阈值", + "Search Result Count": "搜索结果数量", + "Search Tools": "搜索工具", + "Searched {{count}} sites_other": "搜索到 {{count}} 个结果", + "Searching \"{{searchQuery}}\"": "搜索 \"{{searchQuery}}\" 中", + "Searxng Query URL": "Searxng 查询 URL", + "See readme.md for instructions": "查看 readme.md 以获取说明", + "See what's new": "查阅最新更新内容", + "Seed": "种子 (Seed)", + "Select a base model": "选择一个基础模型", + "Select a engine": "选择一个搜索引擎", + "Select a function": "选择一个函数", + "Select a mode": "选择一个模式", + "Select a model": "选择一个模型", + "Select a pipeline": "选择一个管道", + "Select a pipeline url": "选择一个管道 URL", + "Select a tool": "选择一个工具", + "Select an Ollama instance": "选择一个 Ollama 实例", + "Select Documents": "选择文档", + "Select model": "选择模型", + "Select only one model to call": "请仅选择一个模型来呼叫", + "Selected model(s) do not support image inputs": "已选择的模型不支持发送图像", + "Send": "发送", + "Send a Message": "输入消息", + "Send message": "发送消息", + "September": "九月", + "Serper API Key": "Serper API 密钥", + "Serply API Key": "Serply API 密钥", + "Serpstack API Key": "Serpstack API 密钥", + "Server connection verified": "已验证服务器连接", + "Set as default": "设为默认", + "Set Default Model": "设置默认模型", + "Set embedding model (e.g. {{model}})": "设置语义向量模型 (例如:{{model}})", + "Set Image Size": "设置图片分辨率", + "Set reranking model (e.g. {{model}})": "设置重排模型 (例如:{{model}})", + "Set Steps": "设置步骤", + "Set Task Model": "设置任务模型", + "Set Voice": "设置音色", + "Settings": "设置", + "Settings saved successfully!": "设置已保存", + "Settings updated successfully": "设置成功更新", + "Share": "分享", + "Share Chat": "分享对话", + "Share to OpenWebUI Community": "分享到 OpenWebUI 社区", + "short-summary": "简短总结", + "Show": "显示", + "Show Admin Details in Account Pending Overlay": "在用户待激活界面中显示管理员邮箱等详细信息", + "Show Model": "显示", + "Show shortcuts": "显示快捷方式", + "Show your support!": "表达你的支持!", + "Showcased creativity": "很有创意", + "Sign in": "登录", + "Sign Out": "登出", + "Sign up": "注册", + "Signing in": "正在登录", + "Source": "来源", + "Speech recognition error: {{error}}": "语音识别错误:{{error}}", + "Speech-to-Text Engine": "语音转文本引擎", + "Stop Sequence": "停止序列 (Stop Sequence)", + "STT Model": "语音转文本模型", + "STT Settings": "语音转文本设置", + "Submit": "提交", + "Subtitle (e.g. about the Roman Empire)": "副标题(例如:关于罗马帝国的副标题)", + "Success": "成功", + "Successfully updated.": "成功更新。", + "Suggested": "建议", + "Support": "支持", + "Support this plugin:": "支持此插件", + "System": "系统", + "System Prompt": "系统提示词 (System Prompt)", + "Tags": "标签", + "Tap to interrupt": "点击以中断", + "Tavily API Key": "Tavily API 密钥", + "Tell us more:": "请告诉我们更多细节", + "Temperature": "温度 (Temperature)", + "Template": "模板", + "Text Completion": "文本完成", + "Text-to-Speech Engine": "文本转语音引擎", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "感谢您的反馈!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "本插件的背后开发者是社区中热情的志愿者。如果此插件有帮助到您,烦请考虑一下为它的开发做出贡献。", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "分值应介于 0.0(0%)和 1.0(100%)之间。", + "Theme": "主题", + "Thinking...": "正在思考...", + "This action cannot be undone. Do you wish to continue?": "此操作无法撤销。是否确认继续?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "这将确保您的宝贵对话被安全地保存到后台数据库中。感谢!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "这是一个实验功能,可能不会如预期那样工作,而且可能随时发生变化。", + "This setting does not sync across browsers or devices.": "此设置不会在浏览器或设备之间同步。", + "This will delete": "这将删除", + "Thorough explanation": "解释较为详细", + "Tika": "Tika", + "Tika Server URL required.": "请输入 Tika 服务器地址。", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "提示:在每次替换后,在对话输入中按 Tab 键可以连续更新多个变量。", + "Title": "标题", + "Title (e.g. Tell me a fun fact)": "标题(例如 给我讲一个有趣的事实)", + "Title Auto-Generation": "自动生成标题", + "Title cannot be an empty string.": "标题不能为空。", + "Title Generation Prompt": "用于自动生成标题的提示词", + "to": "到", + "To access the available model names for downloading,": "要访问可下载的模型名称,", + "To access the GGUF models available for downloading,": "要访问可下载的 GGUF 模型,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "请联系管理员以访问。管理员可以在后台管理面板中管理用户状态。", + "To add documents here, upload them to the \"Documents\" workspace first.": "要在此处添加文档,请先将它们上传到工作空间中的“文档”内。", + "to chat input.": "到对话输入。", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "要在这里选择过滤器,请先将它们添加到工作空间中的“函数”。", + "To select toolkits here, add them to the \"Tools\" workspace first.": "要在这里选择工具包,请先将它们添加到工作空间中的“工具”。", + "Today": "今天", + "Toggle settings": "切换设置", + "Toggle sidebar": "切换侧边栏", + "Tokens To Keep On Context Refresh (num_keep)": "在语境刷新时需保留的 Tokens", + "Tool created successfully": "工具创建成功", + "Tool deleted successfully": "工具删除成功", + "Tool imported successfully": "工具导入成功", + "Tool updated successfully": "工具更新成功", + "Toolkit Description (e.g. A toolkit for performing various operations)": "工具包描述(例如:用于执行各种操作的工具包)", + "Toolkit ID (e.g. my_toolkit)": "工具包 ID(例如:my_toolkit)", + "Toolkit Name (e.g. My ToolKit)": "工具包名(例如:我的工具包)", + "Tools": "工具", + "Tools are a function calling system with arbitrary code execution": "工具是一个具有任意代码执行能力的函数调用系统", + "Tools have a function calling system that allows arbitrary code execution": "注意:工具有权执行任意代码", + "Tools have a function calling system that allows arbitrary code execution.": "注意:工具有权执行任意代码。", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "访问 Ollama 时遇到问题?", + "TTS Model": "文本转语音模型", + "TTS Settings": "文本转语音设置", + "TTS Voice": "文本转语音音色", + "Type": "类型", + "Type Hugging Face Resolve (Download) URL": "输入 Hugging Face 解析(下载)URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "糟糕!连接到 {{provider}} 时出现问题。", + "UI": "界面", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "未知文件类型“{{file_type}}”,将无视继续上传文件。", + "Unpin": "取消置顶", + "Update": "更新", + "Update and Copy Link": "更新和复制链接", + "Update password": "更新密码", + "Updated at": "更新于", + "Upload": "上传", + "Upload a GGUF model": "上传一个 GGUF 模型", + "Upload Files": "上传文件", + "Upload Pipeline": "上传 Pipeline", + "Upload Progress": "上传进度", + "URL Mode": "URL 模式", + "Use '#' in the prompt input to load and select your documents.": "在输入框中输入'#'号来选择你需要发送的文档。", + "Use Gravatar": "使用来自 Gravatar 的头像", + "Use Initials": "使用首个字符作为头像", + "use_mlock (Ollama)": "use_mlock(Ollama)", + "use_mmap (Ollama)": "use_mmap (Ollama)", + "user": "用户", + "User location successfully retrieved.": "成功检索到用户位置。", + "User Permissions": "用户权限", + "Users": "用户", + "Utilize": "利用", + "Valid time units:": "有效时间单位:", + "Valves": "值", + "Valves updated": "已更新值", + "Valves updated successfully": "值更新成功", + "variable": "变量", + "variable to have them replaced with clipboard content.": "变量将被剪贴板内容替换。", + "Version": "版本", + "Voice": "语音", + "Warning": "警告", + "Warning:": "警告:", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告:如果您修改了语义向量模型,则需要重新导入所有文档。", + "Web": "网页", + "Web API": "网页 API", + "Web Loader Settings": "网页爬取设置", + "Web Params": "网络爬取设置", + "Web Search": "网络搜索", + "Web Search Engine": "网络搜索引擎", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI 设置", + "WebUI will make requests to": "WebUI 将请求", + "What’s New in": "最近更新内容于", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "当关闭历史记录功能时,在此浏览器上新的对话记录将不会同步到您其他设备的历史记录中。", + "Whisper (Local)": "Whisper (本地)", + "Widescreen Mode": "宽屏模式", + "Workspace": "工作空间", + "Write a prompt suggestion (e.g. Who are you?)": "写一个提示词建议(例如:你是谁?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "用 50 个字写一个总结 [主题或关键词]。", + "Yesterday": "昨天", + "You": "你", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "通过点击下方的“管理”按钮,你可以添加记忆,以个性化大语言模型的互动,使其更有用,更符合你的需求。", + "You cannot clone a base model": "你不能复制基础模型", + "You have no archived conversations.": "你没有已归档的对话。", + "You have shared this chat": "你之前已经分享过此", + "You're a helpful assistant.": "你是一个有帮助的助手。", + "You're now logged in.": "已登录。", + "Your account status is currently pending activation.": "您的账号当前状态为待激活。", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "您的全部捐款将直接给到插件开发者,Open WebUI 不会收取任何比例。但众筹平台可能会有服务费、抽成。", + "Youtube": "YouTube", + "Youtube Loader Settings": "YouTube 爬取设置" +} diff --git a/src/lib/i18n/locales/zh-TW/translation.json b/src/lib/i18n/locales/zh-TW/translation.json new file mode 100644 index 0000000000000000000000000000000000000000..27f7653685f8593044f86751033bfb746f5d0add --- /dev/null +++ b/src/lib/i18n/locales/zh-TW/translation.json @@ -0,0 +1,713 @@ +{ + "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 或 '-1' 表示無期限。", + "(Beta)": "(測試版)", + "(e.g. `sh webui.sh --api --api-auth username_password`)": "", + "(e.g. `sh webui.sh --api`)": "(例如 `sh webui.sh --api`)", + "(latest)": "(最新版)", + "{{ models }}": "{{ models }}", + "{{ owner }}: You cannot delete a base model": "{{ owner }}:您無法刪除基礎模型", + "{{modelName}} is thinking...": "{{modelName}} 正在思考...", + "{{user}}'s Chats": "{{user}} 的聊天", + "{{webUIName}} Backend Required": "需要 {{webUIName}} 後端", + "A task model is used when performing tasks such as generating titles for chats and web search queries": "在執行任務時使用任務模型,例如為聊天和網頁搜尋查詢生成標題", + "a user": "使用者", + "About": "關於", + "Account": "帳號", + "Account Activation Pending": "帳號啟用中", + "Accurate information": "準確資訊", + "Actions": "", + "Active Users": "活躍使用者", + "Add": "新增", + "Add a model id": "新增模型 ID", + "Add a short description about what this model does": "為這個模型新增一個簡短描述", + "Add a short title for this prompt": "為這個提示詞新增一個簡短的標題", + "Add a tag": "新增標籤", + "Add custom prompt": "新增自訂提示詞", + "Add Docs": "新增文件", + "Add Files": "新增檔案", + "Add Memory": "新增記憶", + "Add message": "新增訊息", + "Add Model": "新增模型", + "Add Tag": "", + "Add Tags": "新增標籤", + "Add User": "新增使用者", + "Adjusting these settings will apply changes universally to all users.": "調整這些設定將對所有使用者進行更改。", + "admin": "管理員", + "Admin": "管理員", + "Admin Panel": "管理員控制台", + "Admin Settings": "管理設定", + "Admins have access to all tools at all times; users need tools assigned per model in the workspace.": "管理員隨時可以使用所有工具;使用者需要在工作區中為每個模型分配工具。", + "Advanced Parameters": "進階參數", + "Advanced Params": "進階參數", + "all": "所有", + "All Documents": "所有文件", + "All Users": "所有使用者", + "Allow": "允許", + "Allow Chat Deletion": "允許刪除聊天紀錄", + "Allow non-local voices": "允許非本機語音", + "Allow User Location": "允許使用者位置", + "Allow Voice Interruption in Call": "", + "alphanumeric characters and hyphens": "英文字母、數字(0~9)和連字元(-)", + "Already have an account?": "已經有帳號了嗎?", + "an assistant": "助手", + "and": "和", + "and create a new shared link.": "並建立一個新的共享連結。", + "API Base URL": "API 基本 URL", + "API Key": "API 金鑰", + "API Key created.": "API 金鑰已建立。", + "API keys": "API 金鑰", + "April": "4 月", + "Archive": "封存", + "Archive All Chats": "封存所有聊天紀錄", + "Archived Chats": "已封存的聊天紀錄", + "are allowed - Activate this command by typing": "是允許的 - 透過輸入", + "Are you sure?": "您確定嗎?", + "Attach file": "附加檔案", + "Attention to detail": "細節精確", + "Audio": "音訊", + "Audio settings updated successfully": "", + "August": "8 月", + "Auto-playback response": "自動播放回答", + "AUTOMATIC1111 Api Auth String": "", + "AUTOMATIC1111 Base URL": "AUTOMATIC1111 基本 URL", + "AUTOMATIC1111 Base URL is required.": "需要 AUTOMATIC1111 基本 URL", + "available!": "可用!", + "Back": "返回", + "Bad Response": "錯誤回應", + "Banners": "橫幅", + "Base Model (From)": "基礎模型(來自)", + "Batch Size (num_batch)": "批次大小(num_batch)", + "before": "前", + "Being lazy": "懶人模式", + "Brave Search API Key": "Brave 搜尋 API 金鑰", + "Bypass SSL verification for Websites": "跳過網站的 SSL 驗證", + "Call": "呼叫", + "Call feature is not supported when using Web STT engine": "使用 Web STT 引擎時不支援呼叫功能", + "Camera": "相機", + "Cancel": "取消", + "Capabilities": "功能", + "Change Password": "修改密碼", + "Chat": "聊天", + "Chat Background Image": "", + "Chat Bubble UI": "聊天氣泡介面", + "Chat Controls": "", + "Chat direction": "聊天方向", + "Chat History": "聊天紀錄", + "Chat History is off for this browser.": "此瀏覽器已關閉聊天紀錄。", + "Chats": "聊天", + "Check Again": "重新檢查", + "Check for updates": "檢查更新", + "Checking for updates...": "正在檢查更新...", + "Choose a model before saving...": "儲存前選擇一個模型...", + "Chunk Overlap": "區塊重疊", + "Chunk Params": "區塊參數", + "Chunk Size": "區塊大小", + "Citation": "引文", + "Clear memory": "清除記憶", + "Click here for help.": "點選這裡尋求幫助。", + "Click here to": "點選這裡", + "Click here to download user import template file.": "點選這裡下載使用者匯入的範本", + "Click here to select": "點選這裡選擇", + "Click here to select a csv file.": "點選這裡選擇 csv 檔案。", + "Click here to select a py file.": "點選這裡選擇 py 檔案。", + "Click here to select documents.": "點選這裡選擇文件。", + "click here.": "點選這裡。", + "Click on the user role button to change a user's role.": "點選使用者角色按鈕以更改使用者的角色。", + "Clipboard write permission denied. Please check your browser settings to grant the necessary access.": "", + "Clone": "複製", + "Close": "關閉", + "Code formatted successfully": "", + "Collection": "收藏", + "ComfyUI": "ComfyUI", + "ComfyUI Base URL": "ComfyUI 基本 URL", + "ComfyUI Base URL is required.": "需要 ComfyUI 基本 URL", + "Command": "命令", + "Concurrent Requests": "同時請求", + "Confirm": "", + "Confirm Password": "確認密碼", + "Confirm your action": "", + "Connections": "連線", + "Contact Admin for WebUI Access": "聯絡管理員以取得 WebUI 存取權", + "Content": "內容", + "Content Extraction": "", + "Context Length": "上下文長度", + "Continue Response": "繼續回答", + "Continue with {{provider}}": "", + "Controls": "", + "Copied shared chat URL to clipboard!": "已複製共享聊天連結到剪貼簿!", + "Copy": "複製", + "Copy last code block": "複製最後一個程式碼區塊", + "Copy last response": "複製最後一個回答", + "Copy Link": "複製連結", + "Copying to clipboard was successful!": "成功複製到剪貼簿!", + "Create a model": "建立模型", + "Create Account": "建立帳號", + "Create new key": "建立新金鑰", + "Create new secret key": "建立新金鑰", + "Created at": "建立於", + "Created At": "建立於", + "Created by": "", + "CSV Import": "", + "Current Model": "目前模型", + "Current Password": "目前密碼", + "Custom": "自訂", + "Customize models for a specific purpose": "為特定目的自訂模型", + "Dark": "暗色", + "Dashboard": "儀表板", + "Database": "資料庫", + "December": "12 月", + "Default": "預設", + "Default (Automatic1111)": "預設(Automatic1111)", + "Default (SentenceTransformers)": "預設(SentenceTransformers)", + "Default Model": "預設模型", + "Default model updated": "預設模型已更新", + "Default Prompt Suggestions": "預設提示詞建議", + "Default User Role": "預設使用者角色", + "delete": "刪除", + "Delete": "刪除", + "Delete a model": "刪除一個模型", + "Delete All Chats": "刪除所有聊天紀錄", + "Delete chat": "刪除聊天紀錄", + "Delete Chat": "刪除聊天紀錄", + "Delete chat?": "", + "Delete Doc": "", + "Delete function?": "", + "Delete prompt?": "", + "delete this link": "刪除此連結", + "Delete tool?": "", + "Delete User": "刪除使用者", + "Deleted {{deleteModelTag}}": "已刪除 {{deleteModelTag}}", + "Deleted {{name}}": "已刪除 {{name}}", + "Description": "描述", + "Didn't fully follow instructions": "未完全遵循指示", + "Disabled": "", + "Discover a function": "", + "Discover a model": "發現新模型", + "Discover a prompt": "發現新提示詞", + "Discover a tool": "", + "Discover, download, and explore custom functions": "", + "Discover, download, and explore custom prompts": "發現、下載並探索自訂提示詞", + "Discover, download, and explore custom tools": "", + "Discover, download, and explore model presets": "發現、下載並探索模型預設值", + "Dismissible": "可忽略", + "Display Emoji in Call": "在呼叫中顯示表情符號", + "Display the username instead of You in the Chat": "在聊天中顯示使用者名稱而不是「您」", + "Do not install functions from sources you do not fully trust.": "", + "Do not install tools from sources you do not fully trust.": "", + "Document": "文件", + "Document Settings": "文件設定", + "Documentation": "文件", + "Documents": "文件", + "does not make any external connections, and your data stays securely on your locally hosted server.": "不會與外部連線,您的資料會安全地留在您的本機伺服器上。", + "Don't Allow": "不允許", + "Don't have an account?": "還沒有註冊帳號?", + "don't install random functions from sources you don't trust.": "", + "don't install random tools from sources you don't trust.": "", + "Don't like the style": "不喜歡這個樣式?", + "Done": "", + "Download": "下載", + "Download canceled": "下載已取消", + "Download Database": "下載資料庫", + "Drop any files here to add to the conversation": "拖拽任意檔案到此處以新增至對話", + "e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.": "例如 '30s', '10m'。有效的時間單位為 's', 'm', 'h'。", + "Edit": "編輯", + "Edit Doc": "編輯文件", + "Edit Memory": "編輯記憶", + "Edit User": "編輯使用者", + "ElevenLabs": "", + "Email": "電子郵件", + "Embedding Batch Size": "嵌入批次大小", + "Embedding Model": "嵌入模型", + "Embedding Model Engine": "嵌入模型引擎", + "Embedding model set to \"{{embedding_model}}\"": "嵌入模型已設定為 \"{{embedding_model}}\"", + "Enable Chat History": "啟用聊天紀錄", + "Enable Community Sharing": "啟用社群分享", + "Enable New Sign Ups": "允許註冊新帳號", + "Enable Web Search": "啟用網頁搜尋", + "Enabled": "", + "Engine": "", + "Ensure your CSV file includes 4 columns in this order: Name, Email, Password, Role.": "請確保您的 CSV 檔案包含這四個欄位,並按照此順序:名稱、電子郵件、密碼、角色。", + "Enter {{role}} message here": "在這裡輸入 {{role}} 訊息", + "Enter a detail about yourself for your LLMs to recall": "輸入 LLM 記憶的詳細內容", + "Enter api auth string (e.g. username:password)": "", + "Enter Brave Search API Key": "輸入 Brave 搜尋 API 金鑰", + "Enter Chunk Overlap": "輸入區塊重疊", + "Enter Chunk Size": "輸入區塊大小", + "Enter Github Raw URL": "輸入 Github Raw URL", + "Enter Google PSE API Key": "輸入 Google PSE API 金鑰", + "Enter Google PSE Engine Id": "輸入 Google PSE 引擎 ID", + "Enter Image Size (e.g. 512x512)": "輸入圖片大小(例如 512x512)", + "Enter language codes": "輸入語言代碼", + "Enter model tag (e.g. {{modelTag}})": "輸入模型標籤(例如 {{modelTag}})", + "Enter Number of Steps (e.g. 50)": "輸入步數(例如 50)", + "Enter Score": "輸入分數", + "Enter Searxng Query URL": "輸入 Searxng 查詢 URL", + "Enter Serper API Key": "輸入 Serper API 金鑰", + "Enter Serply API Key": "輸入 Serply API 金鑰", + "Enter Serpstack API Key": "輸入 Serpstack API 金鑰", + "Enter stop sequence": "輸入停止序列", + "Enter system prompt": "", + "Enter Tavily API Key": "輸入 Tavily API 金鑰", + "Enter Tika Server URL": "", + "Enter Top K": "輸入 Top K", + "Enter URL (e.g. http://127.0.0.1:7860/)": "輸入 URL(例如 http://127.0.0.1:7860/)", + "Enter URL (e.g. http://localhost:11434)": "輸入 URL(例如 http://localhost:11434)", + "Enter Your Email": "輸入您的電子郵件", + "Enter Your Full Name": "輸入您的全名", + "Enter your message": "", + "Enter Your Password": "輸入您的密碼", + "Enter Your Role": "輸入您的角色", + "Error": "錯誤", + "Experimental": "實驗性功能", + "Export": "匯出", + "Export All Chats (All Users)": "匯出所有聊天紀錄(所有使用者)", + "Export chat (.json)": "匯出聊天紀錄(.json)", + "Export Chats": "匯出聊天紀錄", + "Export Documents Mapping": "匯出文件對映", + "Export Functions": "匯出功能", + "Export LiteLLM config.yaml": "", + "Export Models": "匯出模型", + "Export Prompts": "匯出提示詞", + "Export Tools": "匯出工具", + "External Models": "外部模型", + "Failed to create API Key.": "無法建立 API 金鑰。", + "Failed to read clipboard contents": "無法讀取剪貼簿內容", + "Failed to update settings": "無法更新設定", + "February": "2 月", + "Feel free to add specific details": "請隨意新增詳細內容。", + "File": "檔案", + "File Mode": "檔案模式", + "File not found.": "找不到檔案。", + "Files": "", + "Filter is now globally disabled": "", + "Filter is now globally enabled": "", + "Filters": "篩選器", + "Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "偽造偵測:無法使用初始頭像。預設為預設個人影象。", + "Fluidly stream large external response chunks": "流暢地傳輸大型外部回應區塊", + "Focus chat input": "聚焦聊天輸入框", + "Followed instructions perfectly": "完全遵循指示", + "Form": "表單", + "Format your variables using square brackets like this:": "像這樣使用方括號來格式化您的變數:", + "Frequency Penalty": "頻率懲罰", + "Function created successfully": "", + "Function deleted successfully": "", + "Function Description (e.g. A filter to remove profanity from text)": "", + "Function ID (e.g. my_filter)": "", + "Function is now globally disabled": "", + "Function is now globally enabled": "", + "Function Name (e.g. My Filter)": "", + "Function updated successfully": "", + "Functions": "功能", + "Functions allow arbitrary code execution": "", + "Functions allow arbitrary code execution.": "", + "Functions imported successfully": "", + "General": "常用", + "General Settings": "常用設定", + "Generate Image": "生成圖片", + "Generating search query": "生成搜尋查詢", + "Generation Info": "生成資訊", + "Get up and running with": "", + "Global": "", + "Good Response": "優秀的回應", + "Google PSE API Key": "Google PSE API 金鑰", + "Google PSE Engine Id": "Google PSE 引擎 ID", + "h:mm a": "h:mm a", + "has no conversations.": "沒有對話", + "Hello, {{name}}": "您好,{{name}}", + "Help": "幫助", + "Hide": "隱藏", + "Hide Model": "隱藏模型", + "How can I help you today?": "今天能為您做些什麼?", + "Hybrid Search": "混合搜尋", + "I acknowledge that I have read and I understand the implications of my action. I am aware of the risks associated with executing arbitrary code and I have verified the trustworthiness of the source.": "", + "Image Generation (Experimental)": "影像生成(實驗性功能)", + "Image Generation Engine": "影像生成引擎", + "Image Settings": "圖片設定", + "Images": "圖片", + "Import Chats": "匯入聊天紀錄", + "Import Documents Mapping": "匯入文件對映", + "Import Functions": "匯入功能", + "Import Models": "匯入模型", + "Import Prompts": "匯入提示詞", + "Import Tools": "匯入工具", + "Include `--api-auth` flag when running stable-diffusion-webui": "", + "Include `--api` flag when running stable-diffusion-webui": "在執行 stable-diffusion-webui 時加上 `--api` 標誌", + "Info": "資訊", + "Input commands": "輸入命令", + "Install from Github URL": "從 Github URL 安裝", + "Instant Auto-Send After Voice Transcription": "語音轉錄後立即自動傳送", + "Interface": "介面", + "Invalid Tag": "無效標籤", + "January": "1 月", + "join our Discord for help.": "加入我們的 Discord 尋求幫助。", + "JSON": "JSON", + "JSON Preview": "JSON 預覽", + "July": "7 月", + "June": "6 月", + "JWT Expiration": "JWT 過期時間", + "JWT Token": "JWT Token", + "Keep Alive": "保持活躍", + "Keyboard shortcuts": "鍵盤快速鍵", + "Knowledge": "知識", + "Language": "語言", + "large language models, locally.": "", + "Last Active": "最後活動", + "Last Modified": "最後修改", + "Light": "亮色", + "Listening...": "正在聆聽...", + "LLMs can make mistakes. Verify important information.": "LLM 可能會產生錯誤。請驗證重要資訊。", + "Local Models": "本機模型", + "LTR": "LTR", + "Made by OpenWebUI Community": "由 OpenWebUI 社群製作", + "Make sure to enclose them with": "請確保變數有被以下符號框住:", + "Manage": "管理", + "Manage Models": "管理模型", + "Manage Ollama Models": "管理 Ollama 模型", + "Manage Pipelines": "管理管線", + "Manage Valves": "", + "March": "3 月", + "Max Tokens (num_predict)": "最大 Token(num_predict)", + "Maximum of 3 models can be downloaded simultaneously. Please try again later.": "最多可以同時下載 3 個模型。請稍後再試。", + "May": "5 月", + "Memories accessible by LLMs will be shown here.": "LLM 記憶將會顯示在此處。", + "Memory": "記憶", + "Memory added successfully": "", + "Memory cleared successfully": "", + "Memory deleted successfully": "", + "Memory updated successfully": "", + "Messages you send after creating your link won't be shared. Users with the URL will be able to view the shared chat.": "建立連結後傳送的訊息將不會被共享。具有 URL 的使用者將會能夠檢視共享的聊天。", + "Minimum Score": "最低分數", + "Mirostat": "Mirostat", + "Mirostat Eta": "Mirostat Eta", + "Mirostat Tau": "Mirostat Tau", + "MMMM DD, YYYY": "MMMM DD, YYYY", + "MMMM DD, YYYY HH:mm": "MMMM DD, YYYY HH:mm", + "MMMM DD, YYYY hh:mm:ss A": "MMMM DD, YYYY hh:mm:ss A", + "Model '{{modelName}}' has been successfully downloaded.": "'{{modelName}}' 模型已成功下載。", + "Model '{{modelTag}}' is already in queue for downloading.": "'{{modelTag}}' 模型已經在下載佇列中。", + "Model {{modelId}} not found": "找不到 {{modelId}} 模型", + "Model {{modelName}} is not vision capable": "{{modelName}} 模型不適用於視覺", + "Model {{name}} is now {{status}}": "{{name}} 模型現在是 {{status}}", + "Model created successfully!": "", + "Model filesystem path detected. Model shortname is required for update, cannot continue.": "已偵測到模型檔案系統路徑。需要更新模型簡稱,無法繼續。", + "Model ID": "模型 ID", + "Model not selected": "未選擇模型", + "Model Params": "模型參數", + "Model updated successfully": "", + "Model Whitelisting": "白名單模型", + "Model(s) Whitelisted": "模型已加入白名單", + "Modelfile Content": "Modelfile 內容", + "Models": "模型", + "More": "更多", + "Name": "名稱", + "Name Tag": "名稱標籤", + "Name your model": "請輸入模型名稱", + "New Chat": "新增聊天", + "New Password": "新密碼", + "No content to speak": "", + "No documents found": "找不到文件", + "No file selected": "", + "No results found": "沒有找到結果", + "No search query generated": "沒有生成搜尋查詢", + "No source available": "沒有可用的來源", + "No valves to update": "", + "None": "無", + "Not factually correct": "與真實資訊不符", + "Note: If you set a minimum score, the search will only return documents with a score greater than or equal to the minimum score.": "註:如果設定最低分數,則搜尋將只返回分數大於或等於最低分數的文件。", + "Notifications": "通知", + "November": "11 月", + "num_thread (Ollama)": "num_thread(Ollama)", + "OAuth ID": "", + "October": "10 月", + "Off": "關閉", + "Okay, Let's Go!": "好的,啟動吧!", + "OLED Dark": "OLED 深色", + "Ollama": "Ollama", + "Ollama API": "Ollama API", + "Ollama API disabled": "Ollama API 已停用", + "Ollama API is disabled": "Ollama API 已停用", + "Ollama Version": "Ollama 版本", + "On": "開啟", + "Only": "僅有", + "Only alphanumeric characters and hyphens are allowed in the command string.": "命令字串中只能包含英文字母、數字(0~9)和連字元(-)。", + "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.": "哎呀!請稍等!您的文件還在處理中。我們正最佳化文件,請耐心等待,一旦準備好,我們會通知您。", + "Oops! Looks like the URL is invalid. Please double-check and try again.": "哎呀!看起來 URL 無效。請仔細檢查後再試一次。", + "Oops! There was an error in the previous response. Please try again or contact admin.": "哎呀!先前的回應發生錯誤。請重試或聯絡管理員", + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "哎呀!您正在使用不支援的方法(僅有前端)。請從後端提供 WebUI。", + "Open AI (Dall-E)": "Open AI (Dall-E)", + "Open new chat": "開啟新聊天", + "Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})": "", + "OpenAI": "OpenAI", + "OpenAI API": "OpenAI API", + "OpenAI API Config": "OpenAI API 設定", + "OpenAI API Key is required.": "需要 OpenAI API 金鑰。", + "OpenAI URL/Key required.": "需要 OpenAI URL/金鑰。", + "or": "或", + "Other": "其他", + "Password": "密碼", + "PDF document (.pdf)": "PDF 文件 (.pdf)", + "PDF Extract Images (OCR)": "PDF 影像擷取(OCR 光學文字辨識)", + "pending": "待審查", + "Permission denied when accessing media devices": "存取媒體裝置時被拒絕權限", + "Permission denied when accessing microphone": "存取麥克風時被拒絕權限", + "Permission denied when accessing microphone: {{error}}": "存取麥克風時被拒絕權限:{{error}}", + "Personalization": "個人化", + "Pin": "", + "Pinned": "", + "Pipeline deleted successfully": "", + "Pipeline downloaded successfully": "", + "Pipelines": "管線", + "Pipelines Not Detected": "", + "Pipelines Valves": "管線閥門", + "Plain text (.txt)": "純文字 (.txt)", + "Playground": "AI 對話遊樂場", + "Please carefully review the following warnings:": "", + "Positive attitude": "積極態度", + "Previous 30 days": "前 30 天", + "Previous 7 days": "前 7 天", + "Profile Image": "個人影像", + "Prompt": "提示詞", + "Prompt (e.g. Tell me a fun fact about the Roman Empire)": "提示詞(例如:告訴我關於羅馬帝國的一些趣事)", + "Prompt Content": "提示詞內容", + "Prompt suggestions": "提示詞建議", + "Prompts": "提示詞", + "Pull \"{{searchValue}}\" from Ollama.com": "從 Ollama.com 下載 \"{{searchValue}}\"", + "Pull a model from Ollama.com": "從 Ollama.com 下載模型", + "Query Params": "查詢參數", + "RAG Template": "RAG 範例", + "Read Aloud": "讀出", + "Record voice": "錄音", + "Redirecting you to OpenWebUI Community": "將您重新導向到 OpenWebUI 社群", + "Refer to yourself as \"User\" (e.g., \"User is learning Spanish\")": "將自己稱為「使用者」(例如,「使用者正在學習西班牙語」)", + "Refused when it shouldn't have": "不該拒絕時拒絕了", + "Regenerate": "重新生成", + "Release Notes": "發布說明", + "Remove": "移除", + "Remove Model": "移除模型", + "Rename": "重新命名", + "Repeat Last N": "重複最後 N 次", + "Request Mode": "請求模式", + "Reranking Model": "重新排序模型", + "Reranking model disabled": "重新排序模型已停用", + "Reranking model set to \"{{reranking_model}}\"": "重新排序模型設定為 \"{{reranking_model}}\"", + "Reset": "重設", + "Reset Upload Directory": "重設上傳目錄", + "Reset Vector Storage": "重設向量儲存空間", + "Response AutoCopy to Clipboard": "自動複製回答到剪貼簿", + "Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.": "", + "Role": "角色", + "Rosé Pine": "玫瑰松", + "Rosé Pine Dawn": "黎明玫瑰松", + "RTL": "RTL", + "Run Llama 2, Code Llama, and other models. Customize and create your own.": "", + "Running": "運作中", + "Save": "儲存", + "Save & Create": "儲存並建立", + "Save & Update": "儲存並更新", + "Save Tag": "", + "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": "現已不支援將聊天紀錄儲存到瀏覽器儲存空間中。請點選下面的按鈕下載並刪除您的聊天記錄。別擔心,您可以透過以下方式輕鬆地重新匯入您的聊天記錄到後端", + "Scan": "掃描", + "Scan complete!": "掃描完成!", + "Scan for documents from {{path}}": "從 {{path}} 掃描文件", + "Search": "搜尋", + "Search a model": "搜尋模型", + "Search Chats": "搜尋聊天", + "Search Documents": "搜尋文件", + "Search Functions": "搜尋功能", + "Search Models": "搜尋模型", + "Search Prompts": "搜尋提示詞", + "Search Query Generation Prompt": "搜尋查詢生成提示詞", + "Search Query Generation Prompt Length Threshold": "搜尋查詢生成提示詞長度閾值", + "Search Result Count": "搜尋結果數量", + "Search Tools": "搜尋工具", + "Searched {{count}} sites_other": "搜尋了 {{count}} 個網站", + "Searching \"{{searchQuery}}\"": "正在搜尋 \"{{searchQuery}}\"", + "Searxng Query URL": "Searxng 查詢 URL", + "See readme.md for instructions": "檢視 readme.md 取得指南", + "See what's new": "檢視最新內容", + "Seed": "種子", + "Select a base model": "選擇基礎模型", + "Select a engine": "選擇引擎", + "Select a function": "", + "Select a mode": "選擇模式", + "Select a model": "選擇一個模型", + "Select a pipeline": "選擇管線", + "Select a pipeline url": "選擇管線 URL", + "Select a tool": "", + "Select an Ollama instance": "選擇 Ollama 執行個體", + "Select Documents": "選擇文件", + "Select model": "選擇模型", + "Select only one model to call": "僅選擇一個模型來呼叫", + "Selected model(s) do not support image inputs": "已選擇模型不支援影像輸入", + "Send": "傳送", + "Send a Message": "傳送訊息", + "Send message": "傳送訊息", + "September": "9 月", + "Serper API Key": "Serper API 金鑰", + "Serply API Key": "Serply API 金鑰", + "Serpstack API Key": "Serpstack API 金鑰", + "Server connection verified": "已驗證伺服器連線", + "Set as default": "設為預設", + "Set Default Model": "設定預設模型", + "Set embedding model (e.g. {{model}})": "設定嵌入模型(例如:{{model}})", + "Set Image Size": "設定圖片大小", + "Set reranking model (e.g. {{model}})": "設定重新排序模型(例如:{{model}})", + "Set Steps": "設定步數", + "Set Task Model": "設定任務模型", + "Set Voice": "設定語音", + "Settings": "設定", + "Settings saved successfully!": "成功儲存設定", + "Settings updated successfully": "設定更新成功", + "Share": "分享", + "Share Chat": "分享聊天", + "Share to OpenWebUI Community": "分享到 OpenWebUI 社群", + "short-summary": "簡短摘要", + "Show": "顯示", + "Show Admin Details in Account Pending Overlay": "在帳號待審覆蓋層中顯示管理員詳細資訊", + "Show Model": "顯示模型", + "Show shortcuts": "顯示快速鍵", + "Show your support!": "", + "Showcased creativity": "展示創造性", + "Sign in": "登入", + "Sign Out": "登出", + "Sign up": "註冊", + "Signing in": "正在登入", + "Source": "來源", + "Speech recognition error: {{error}}": "語音識別錯誤:{{error}}", + "Speech-to-Text Engine": "語音轉文字引擎", + "Stop Sequence": "停止序列", + "STT Model": "STT 模型", + "STT Settings": "語音轉文字設定", + "Submit": "提交", + "Subtitle (e.g. about the Roman Empire)": "副標題(例如:關於羅馬帝國)", + "Success": "成功", + "Successfully updated.": "更新成功。", + "Suggested": "建議", + "Support": "", + "Support this plugin:": "", + "System": "系統", + "System Prompt": "系統提示詞", + "Tags": "標籤", + "Tap to interrupt": "點選以中斷", + "Tavily API Key": "Tavily API 金鑰", + "Tell us more:": "告訴我們更多:", + "Temperature": "溫度", + "Template": "範本", + "Text Completion": "文字補全", + "Text-to-Speech Engine": "文字轉語音引擎", + "Tfs Z": "Tfs Z", + "Thanks for your feedback!": "感謝您的回饋!", + "The developers behind this plugin are passionate volunteers from the community. If you find this plugin helpful, please consider contributing to its development.": "", + "The score should be a value between 0.0 (0%) and 1.0 (100%).": "分數應該介於 0.0(0%)和 1.0(100%)之間。", + "Theme": "主題", + "Thinking...": "正在思考...", + "This action cannot be undone. Do you wish to continue?": "此動作無法被復原。您想要繼續進行嗎?", + "This ensures that your valuable conversations are securely saved to your backend database. Thank you!": "這確保您寶貴的對話安全地儲存到您的後端資料庫。謝謝!", + "This is an experimental feature, it may not function as expected and is subject to change at any time.": "這是一個實驗性功能,可能無法如預期運作,並且隨時可能更改。", + "This setting does not sync across browsers or devices.": "此設定不會在瀏覽器或裝置間同步。", + "This will delete": "", + "Thorough explanation": "詳細說明", + "Tika": "", + "Tika Server URL required.": "", + "Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.": "提示:透過在每次替換後在聊天輸入框中按 Tab 鍵連續更新多個變數。", + "Title": "標題", + "Title (e.g. Tell me a fun fact)": "標題(例如:告訴我一個有趣的事)", + "Title Auto-Generation": "自動產生標題", + "Title cannot be an empty string.": "標題不能為空字串", + "Title Generation Prompt": "自動產生標題的提示詞", + "to": "到", + "To access the available model names for downloading,": "若想檢視可供下載的模型名稱,", + "To access the GGUF models available for downloading,": "若想檢視可供下載的 GGUF 模型名稱,", + "To access the WebUI, please reach out to the administrator. Admins can manage user statuses from the Admin Panel.": "若要存取 WebUI,請聯絡管理員。管理員可以從管理面板管理使用者狀態。", + "To add documents here, upload them to the \"Documents\" workspace first.": "若要在此新增文件,請先將它們上傳到「文件」工作區。", + "to chat input.": "到聊天輸入框來啟動此命令。", + "To select actions here, add them to the \"Functions\" workspace first.": "", + "To select filters here, add them to the \"Functions\" workspace first.": "若要在此選擇篩選器,請先將它們新增到「功能」工作區。", + "To select toolkits here, add them to the \"Tools\" workspace first.": "若要在此選擇工具包,請先將它們新增到「工具」工作區。", + "Today": "今天", + "Toggle settings": "切換設定", + "Toggle sidebar": "切換側邊欄", + "Tokens To Keep On Context Refresh (num_keep)": "上下文重新整理時保留的 Token 數量(num_keep)", + "Tool created successfully": "", + "Tool deleted successfully": "", + "Tool imported successfully": "", + "Tool updated successfully": "", + "Toolkit Description (e.g. A toolkit for performing various operations)": "", + "Toolkit ID (e.g. my_toolkit)": "", + "Toolkit Name (e.g. My ToolKit)": "", + "Tools": "工具", + "Tools are a function calling system with arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution": "", + "Tools have a function calling system that allows arbitrary code execution.": "", + "Top K": "Top K", + "Top P": "Top P", + "Trouble accessing Ollama?": "存取 Ollama 時遇到問題?", + "TTS Model": "文字轉語音(TTS)模型", + "TTS Settings": "文字轉語音(TTS)設定", + "TTS Voice": "文字轉語音(TTS)聲調", + "Type": "類型", + "Type Hugging Face Resolve (Download) URL": "輸入 Hugging Face 解析後的(下載)URL", + "Uh-oh! There was an issue connecting to {{provider}}.": "哎呀!連線到 {{provider}} 時出現問題。", + "UI": "使用者界面", + "Unknown file type '{{file_type}}'. Proceeding with the file upload anyway.": "未知的檔案類型 '{{file_type}}'。但仍會繼續上傳。", + "Unpin": "", + "Update": "更新", + "Update and Copy Link": "更新並複製連結", + "Update password": "更新密碼", + "Updated at": "更新於", + "Upload": "上傳", + "Upload a GGUF model": "上傳一個 GGUF 模型", + "Upload Files": "上傳檔案", + "Upload Pipeline": "上傳管線", + "Upload Progress": "上傳進度", + "URL Mode": "URL 模式", + "Use '#' in the prompt input to load and select your documents.": "在輸入框中輸入 '#' 以載入並選擇您的文件。", + "Use Gravatar": "使用 Gravatar", + "Use Initials": "使用初始頭像", + "use_mlock (Ollama)": "use_mlock(Ollama)", + "use_mmap (Ollama)": "use_mmap(Ollama)", + "user": "使用者", + "User location successfully retrieved.": "", + "User Permissions": "使用者權限", + "Users": "使用者", + "Utilize": "使用", + "Valid time units:": "有效時間單位:", + "Valves": "", + "Valves updated": "", + "Valves updated successfully": "", + "variable": "變數", + "variable to have them replaced with clipboard content.": "變數將替換為剪貼簿內容", + "Version": "版本", + "Voice": "", + "Warning": "警告", + "Warning:": "", + "Warning: If you update or change your embedding model, you will need to re-import all documents.": "警告:如果更新或更改您的嵌入模型,則需要重新匯入所有文件", + "Web": "網頁", + "Web API": "網頁 API", + "Web Loader Settings": "網頁載入器設定", + "Web Params": "網頁參數", + "Web Search": "網頁搜尋", + "Web Search Engine": "網頁搜尋引擎", + "Webhook URL": "Webhook URL", + "WebUI Settings": "WebUI 設定", + "WebUI will make requests to": "WebUI 將會存取", + "What’s New in": "全新內容", + "When history is turned off, new chats on this browser won't appear in your history on any of your devices.": "當歷史被關閉時,這個瀏覽器上的新聊天將不會出現在任何裝置的歷史記錄中", + "Whisper (Local)": "Whisper(本地)", + "Widescreen Mode": "寬螢幕模式", + "Workspace": "工作區", + "Write a prompt suggestion (e.g. Who are you?)": "寫一個提示詞建議(例如:您是誰?)", + "Write a summary in 50 words that summarizes [topic or keyword].": "寫一個 50 字的摘要來概括 [主題或關鍵詞]。", + "Yesterday": "昨天", + "You": "您", + "You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you.": "您可以透過下方的「管理」按鈕新增記憶,個人化您的 LLM 互動,使其更有幫助並更符合您的需求。", + "You cannot clone a base model": "您不能複製基礎模型", + "You have no archived conversations.": "您沒有任何已封存的對話", + "You have shared this chat": "您已分享此聊天", + "You're a helpful assistant.": "您是一位善於協助他人的助手。", + "You're now logged in.": "已登入。", + "Your account status is currently pending activation.": "您的帳號狀態目前待啟用。", + "Your entire contribution will go directly to the plugin developer; Open WebUI does not take any percentage. However, the chosen funding platform might have its own fees.": "", + "Youtube": "Youtube", + "Youtube Loader Settings": "Youtube 載入器設定" +} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..856f2b6c38aec1085db88189bcf492dbb49a1c45 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b0257c4b94ccef7db9fef52504b78b9672780b3 --- /dev/null +++ b/src/lib/stores/index.ts @@ -0,0 +1,172 @@ +import { APP_NAME } from '$lib/constants'; +import { type Writable, writable } from 'svelte/store'; +import type { GlobalModelConfig, ModelConfig } from '$lib/apis'; +import type { Banner } from '$lib/types'; +import type { Socket } from 'socket.io-client'; + +// Backend +export const WEBUI_NAME = writable(APP_NAME); +export const config: Writable<Config | undefined> = writable(undefined); +export const user: Writable<SessionUser | undefined> = writable(undefined); + +// Frontend +export const MODEL_DOWNLOAD_POOL = writable({}); + +export const mobile = writable(false); + +export const socket: Writable<null | Socket> = writable(null); +export const activeUserCount: Writable<null | number> = writable(null); +export const USAGE_POOL: Writable<null | string[]> = writable(null); + +export const theme = writable('system'); +export const chatId = writable(''); + +export const chats = writable([]); +export const pinnedChats = writable([]); +export const tags = writable([]); + +export const models: Writable<Model[]> = writable([]); +export const prompts: Writable<Prompt[]> = writable([]); +export const documents: Writable<Document[]> = writable([]); + +export const tools = writable([]); +export const functions = writable([]); + +export const banners: Writable<Banner[]> = writable([]); + +export const settings: Writable<Settings> = writable({}); + +export const showSidebar = writable(false); +export const showSettings = writable(false); +export const showArchivedChats = writable(false); +export const showChangelog = writable(false); +export const showCallOverlay = writable(false); + +export type Model = OpenAIModel | OllamaModel; + +type BaseModel = { + id: string; + name: string; + info?: ModelConfig; +}; + +export interface OpenAIModel extends BaseModel { + external: boolean; + source?: string; +} + +export interface OllamaModel extends BaseModel { + details: OllamaModelDetails; + size: number; + description: string; + model: string; + modified_at: string; + digest: string; +} + +type OllamaModelDetails = { + parent_model: string; + format: string; + family: string; + families: string[] | null; + parameter_size: string; + quantization_level: string; +}; + +type Settings = { + models?: string[]; + conversationMode?: boolean; + speechAutoSend?: boolean; + responseAutoPlayback?: boolean; + audio?: AudioSettings; + showUsername?: boolean; + saveChatHistory?: boolean; + notificationEnabled?: boolean; + title?: TitleSettings; + splitLargeDeltas?: boolean; + chatDirection: 'LTR' | 'RTL'; + + system?: string; + requestFormat?: string; + keepAlive?: string; + seed?: number; + temperature?: string; + repeat_penalty?: string; + top_k?: string; + top_p?: string; + num_ctx?: string; + num_batch?: string; + num_keep?: string; + options?: ModelOptions; +}; + +type ModelOptions = { + stop?: boolean; +}; + +type AudioSettings = { + STTEngine?: string; + TTSEngine?: string; + speaker?: string; + model?: string; + nonLocalVoices?: boolean; +}; + +type TitleSettings = { + auto?: boolean; + model?: string; + modelExternal?: string; + prompt?: string; +}; + +type Prompt = { + command: string; + user_id: string; + title: string; + content: string; + timestamp: number; +}; + +type Document = { + collection_name: string; + filename: string; + name: string; + title: string; +}; + +type Config = { + status: boolean; + name: string; + version: string; + default_locale: string; + default_models: string; + default_prompt_suggestions: PromptSuggestion[]; + features: { + auth: boolean; + auth_trusted_header: boolean; + enable_signup: boolean; + enable_login_form: boolean; + enable_web_search?: boolean; + enable_image_generation: boolean; + enable_admin_export: boolean; + enable_community_sharing: boolean; + }; + oauth: { + providers: { + [key: string]: string; + }; + }; +}; + +type PromptSuggestion = { + content: string; + title: [string, string]; +}; + +type SessionUser = { + id: string; + email: string; + name: string; + role: string; + profile_image_url: string; +}; diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2d9156c8daf85d451f58b6062ef5d8c8ba1b09a2 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,9 @@ +export type Banner = { + id: string; + type: string; + title?: string; + content: string; + url?: string; + dismissible?: boolean; + timestamp: number; +}; diff --git a/src/lib/utils/_template_old.ts b/src/lib/utils/_template_old.ts new file mode 100644 index 0000000000000000000000000000000000000000..f69ef226011117f94de622616dcf85596731dc44 --- /dev/null +++ b/src/lib/utils/_template_old.ts @@ -0,0 +1,66 @@ +import { titleGenerationTemplate } from '$lib/utils/index'; +import { expect, test } from 'vitest'; + +test('titleGenerationTemplate correctly replaces {{prompt}} placeholder', () => { + const template = 'Hello {{prompt}}!'; + const prompt = 'world'; + const expected = 'Hello world!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate correctly replaces {{prompt:start:<length>}} placeholder', () => { + const template = 'Hello {{prompt:start:3}}!'; + const prompt = 'world'; + const expected = 'Hello wor!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate correctly replaces {{prompt:end:<length>}} placeholder', () => { + const template = 'Hello {{prompt:end:3}}!'; + const prompt = 'world'; + const expected = 'Hello rld!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate correctly replaces {{prompt:middletruncate:<length>}} placeholder when prompt length is greater than length', () => { + const template = 'Hello {{prompt:middletruncate:4}}!'; + const prompt = 'world'; + const expected = 'Hello wo...ld!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate correctly replaces {{prompt:middletruncate:<length>}} placeholder when prompt length is less than or equal to length', () => { + const template = 'Hello {{prompt:middletruncate:5}}!'; + const prompt = 'world'; + const expected = 'Hello world!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate returns original template when no placeholders are present', () => { + const template = 'Hello world!'; + const prompt = 'world'; + const expected = 'Hello world!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate does not replace placeholders inside of replaced placeholders', () => { + const template = 'Hello {{prompt}}!'; + const prompt = 'World, {{prompt}} injection'; + const expected = 'Hello World, {{prompt}} injection!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); + +test('titleGenerationTemplate correctly replaces multiple placeholders', () => { + const template = 'Hello {{prompt}}! This is {{prompt:start:3}}!'; + const prompt = 'world'; + const expected = 'Hello world! This is wor!'; + const actual = titleGenerationTemplate(template, prompt); + expect(actual).toBe(expected); +}); diff --git a/src/lib/utils/characters/index.ts b/src/lib/utils/characters/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..af3436693a69aa978c4127262894fc5059aeeebd --- /dev/null +++ b/src/lib/utils/characters/index.ts @@ -0,0 +1,197 @@ +import CRC32 from 'crc-32'; + +export const parseFile = async (file) => { + if (file.type === 'application/json') { + return await parseJsonFile(file); + } else if (file.type === 'image/png') { + return await parsePngFile(file); + } else { + throw new Error('Unsupported file type'); + } +}; + +const parseJsonFile = async (file) => { + const text = await file.text(); + const json = JSON.parse(text); + + const character = extractCharacter(json); + + return { + file, + json, + formats: detectFormats(json), + character + }; +}; + +const parsePngFile = async (file) => { + const arrayBuffer = await file.arrayBuffer(); + const text = parsePngText(arrayBuffer); + const json = JSON.parse(text); + + const image = URL.createObjectURL(file); + const character = extractCharacter(json); + + return { + file, + json, + image, + formats: detectFormats(json), + character + }; +}; + +const parsePngText = (arrayBuffer) => { + const textChunkKeyword = 'chara'; + const chunks = readPngChunks(new Uint8Array(arrayBuffer)); + + const textChunk = chunks + .filter((chunk) => chunk.type === 'tEXt') + .map((chunk) => decodeTextChunk(chunk.data)) + .find((entry) => entry.keyword === textChunkKeyword); + + if (!textChunk) { + throw new Error(`No PNG text chunk named "${textChunkKeyword}" found`); + } + + try { + return new TextDecoder().decode(Uint8Array.from(atob(textChunk.text), (c) => c.charCodeAt(0))); + } catch (e) { + throw new Error('Unable to parse "chara" field as base64', e); + } +}; + +const readPngChunks = (data) => { + const isValidPng = + data[0] === 0x89 && + data[1] === 0x50 && + data[2] === 0x4e && + data[3] === 0x47 && + data[4] === 0x0d && + data[5] === 0x0a && + data[6] === 0x1a && + data[7] === 0x0a; + + if (!isValidPng) throw new Error('Invalid PNG file'); + + let chunks = []; + let offset = 8; // Skip PNG signature + + while (offset < data.length) { + let length = + (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; + let type = String.fromCharCode.apply(null, data.slice(offset + 4, offset + 8)); + let chunkData = data.slice(offset + 8, offset + 8 + length); + let crc = + (data[offset + 8 + length] << 24) | + (data[offset + 8 + length + 1] << 16) | + (data[offset + 8 + length + 2] << 8) | + data[offset + 8 + length + 3]; + + if (CRC32.buf(chunkData, CRC32.str(type)) !== crc) { + throw new Error(`Invalid CRC for chunk type "${type}"`); + } + + chunks.push({ type, data: chunkData, crc }); + offset += 12 + length; + } + + return chunks; +}; + +const decodeTextChunk = (data) => { + let i = 0; + const keyword = []; + const text = []; + + for (; i < data.length && data[i] !== 0; i++) { + keyword.push(String.fromCharCode(data[i])); + } + + for (i++; i < data.length; i++) { + text.push(String.fromCharCode(data[i])); + } + + return { keyword: keyword.join(''), text: text.join('') }; +}; + +const extractCharacter = (json) => { + function getTrimmedValue(json, keys) { + return keys + .map((key) => { + const keyParts = key.split('.'); + let value = json; + for (const part of keyParts) { + if (value && value[part] != null) { + value = value[part]; + } else { + value = null; + break; + } + } + return value && value.trim(); + }) + .find((value) => value); + } + + const name = getTrimmedValue(json, ['char_name', 'name', 'data.name']); + const summary = getTrimmedValue(json, ['personality', 'title', 'data.description']); + const personality = getTrimmedValue(json, ['char_persona', 'description', 'data.personality']); + const scenario = getTrimmedValue(json, ['world_scenario', 'scenario', 'data.scenario']); + const greeting = getTrimmedValue(json, [ + 'char_greeting', + 'greeting', + 'first_mes', + 'data.first_mes' + ]); + const examples = getTrimmedValue(json, [ + 'example_dialogue', + 'mes_example', + 'definition', + 'data.mes_example' + ]); + + return { name, summary, personality, scenario, greeting, examples }; +}; + +const detectFormats = (json) => { + const formats = []; + + if ( + json.char_name && + json.char_persona && + json.world_scenario && + json.char_greeting && + json.example_dialogue + ) + formats.push('Text Generation Character'); + if ( + json.name && + json.personality && + json.description && + json.scenario && + json.first_mes && + json.mes_example + ) + formats.push('TavernAI Character'); + if ( + json.character && + json.character.name && + json.character.title && + json.character.description && + json.character.greeting && + json.character.definition + ) + formats.push('CharacterAI Character'); + if ( + json.info && + json.info.character && + json.info.character.name && + json.info.character.title && + json.info.character.description && + json.info.character.greeting + ) + formats.push('CharacterAI History'); + + return formats; +}; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9200de968b4314626af5654d4eba61fee14d825a --- /dev/null +++ b/src/lib/utils/index.ts @@ -0,0 +1,781 @@ +import { v4 as uuidv4 } from 'uuid'; +import sha256 from 'js-sha256'; +import { WEBUI_BASE_URL } from '$lib/constants'; + +////////////////////////// +// Helper functions +////////////////////////// + +const convertLatexToSingleLine = (content) => { + // Patterns to match multiline LaTeX blocks + const patterns = [ + /(\$\$[\s\S]*?\$\$)/g, // Match $$ ... $$ + /(\\\[[\s\S]*?\\\])/g, // Match \[ ... \] + /(\\begin\{[a-z]+\}[\s\S]*?\\end\{[a-z]+\})/g // Match \begin{...} ... \end{...} + ]; + + patterns.forEach((pattern) => { + content = content.replace(pattern, (match) => { + return match.replace(/\s*\n\s*/g, ' ').trim(); + }); + }); + + return content; +}; + +export const sanitizeResponseContent = (content: string) => { + // replace single backslash with double backslash + content = content.replace(/\\/g, '\\\\'); + content = convertLatexToSingleLine(content); + + // First, temporarily replace valid <video> tags with a placeholder + const videoTagRegex = /<video\s+src="([^"]+)"\s+controls><\/video>/gi; + const placeholders: string[] = []; + content = content.replace(videoTagRegex, (_, src) => { + const placeholder = `{{VIDEO_${placeholders.length}}}`; + placeholders.push(`<video src="${src}" controls></video>`); + return placeholder; + }); + + // Now apply the sanitization to the rest of the content + content = content + .replace(/<\|[a-z]*$/, '') + .replace(/<\|[a-z]+\|$/, '') + .replace(/<$/, '') + .replaceAll(/<\|[a-z]+\|>/g, ' ') + .replaceAll('<', '<') + .replaceAll('>', '>') + .trim(); + + // Replace placeholders with original <video> tags + placeholders.forEach((placeholder, index) => { + content = content.replace(`{{VIDEO_${index}}}`, placeholder); + }); + + return content.trim(); +}; + +export const replaceTokens = (content, char, user) => { + const charToken = /{{char}}/gi; + const userToken = /{{user}}/gi; + const videoIdToken = /{{VIDEO_FILE_ID_([a-f0-9-]+)}}/gi; // Regex to capture the video ID + const htmlIdToken = /{{HTML_FILE_ID_([a-f0-9-]+)}}/gi; // Regex to capture the HTML ID + + // Replace {{char}} if char is provided + if (char !== undefined && char !== null) { + content = content.replace(charToken, char); + } + + // Replace {{user}} if user is provided + if (user !== undefined && user !== null) { + content = content.replace(userToken, user); + } + + // Replace video ID tags with corresponding <video> elements + content = content.replace(videoIdToken, (match, fileId) => { + const videoUrl = `${WEBUI_BASE_URL}/api/v1/files/${fileId}/content`; + return `<video src="${videoUrl}" controls></video>`; + }); + + // Replace HTML ID tags with corresponding HTML content + content = content.replace(htmlIdToken, (match, fileId) => { + const htmlUrl = `${WEBUI_BASE_URL}/api/v1/files/${fileId}/content`; + return `<iframe src="${htmlUrl}" width="100%" frameborder="0" onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"></iframe>`; + }); + + return content; +}; + +export const revertSanitizedResponseContent = (content: string) => { + return content.replaceAll('<', '<').replaceAll('>', '>'); +}; + +export const capitalizeFirstLetter = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); +}; + +export const splitStream = (splitOn) => { + let buffer = ''; + return new TransformStream({ + transform(chunk, controller) { + buffer += chunk; + const parts = buffer.split(splitOn); + parts.slice(0, -1).forEach((part) => controller.enqueue(part)); + buffer = parts[parts.length - 1]; + }, + flush(controller) { + if (buffer) controller.enqueue(buffer); + } + }); +}; + +export const convertMessagesToHistory = (messages) => { + const history = { + messages: {}, + currentId: null + }; + + let parentMessageId = null; + let messageId = null; + + for (const message of messages) { + messageId = uuidv4(); + + if (parentMessageId !== null) { + history.messages[parentMessageId].childrenIds = [ + ...history.messages[parentMessageId].childrenIds, + messageId + ]; + } + + history.messages[messageId] = { + ...message, + id: messageId, + parentId: parentMessageId, + childrenIds: [] + }; + + parentMessageId = messageId; + } + + history.currentId = messageId; + return history; +}; + +export const getGravatarURL = (email) => { + // Trim leading and trailing whitespace from + // an email address and force all characters + // to lower case + const address = String(email).trim().toLowerCase(); + + // Create a SHA256 hash of the final string + const hash = sha256(address); + + // Grab the actual image URL + 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 = async (text) => { + let result = false; + if (!navigator.clipboard) { + const textArea = document.createElement('textarea'); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.position = 'fixed'; + + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + try { + const successful = document.execCommand('copy'); + const msg = successful ? 'successful' : 'unsuccessful'; + console.log('Fallback: Copying text command was ' + msg); + result = true; + } catch (err) { + console.error('Fallback: Oops, unable to copy', err); + } + + document.body.removeChild(textArea); + return result; + } + + result = await navigator.clipboard + .writeText(text) + .then(() => { + console.log('Async: Copying to clipboard was successful!'); + return true; + }) + .catch((error) => { + console.error('Async: Could not copy text: ', error); + return false; + }); + + return result; +}; + +export const compareVersion = (latest, current) => { + return current === '0.0.0' + ? false + : current.localeCompare(latest, undefined, { + numeric: true, + sensitivity: 'case', + caseFirst: 'upper' + }) < 0; +}; + +export const findWordIndices = (text) => { + const regex = /\[([^\]]+)\]/g; + const matches = []; + let match; + + while ((match = regex.exec(text)) !== null) { + matches.push({ + word: match[1], + startIndex: match.index, + endIndex: regex.lastIndex - 1 + }); + } + + return matches; +}; + +export const removeFirstHashWord = (inputString) => { + // Split the string into an array of words + const words = inputString.split(' '); + + // Find the index of the first word that starts with # + const index = words.findIndex((word) => word.startsWith('#')); + + // Remove the first word with # + if (index !== -1) { + words.splice(index, 1); + } + + // Join the remaining words back into a string + const resultString = words.join(' '); + + return resultString; +}; + +export const transformFileName = (fileName) => { + // Convert to lowercase + const lowerCaseFileName = fileName.toLowerCase(); + + // Remove special characters using regular expression + const sanitizedFileName = lowerCaseFileName.replace(/[^\w\s]/g, ''); + + // Replace spaces with dashes + const finalFileName = sanitizedFileName.replace(/\s+/g, '-'); + + return finalFileName; +}; + +export const calculateSHA256 = async (file) => { + // Create a FileReader to read the file asynchronously + const reader = new FileReader(); + + // Define a promise to handle the file reading + const readFile = new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + }); + + // Read the file as an ArrayBuffer + reader.readAsArrayBuffer(file); + + try { + // Wait for the FileReader to finish reading the file + const buffer = await readFile; + + // Convert the ArrayBuffer to a Uint8Array + const uint8Array = new Uint8Array(buffer); + + // Calculate the SHA-256 hash using Web Crypto API + const hashBuffer = await crypto.subtle.digest('SHA-256', uint8Array); + + // Convert the hash to a hexadecimal string + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, '0')).join(''); + + return `${hashHex}`; + } catch (error) { + console.error('Error calculating SHA-256 hash:', error); + throw error; + } +}; + +export const getImportOrigin = (_chats) => { + // Check what external service chat imports are from + if ('mapping' in _chats[0]) { + return 'openai'; + } + return 'webui'; +}; + +export const getUserPosition = async (raw = false) => { + // Get the user's location using the Geolocation API + const position = await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition(resolve, reject); + }).catch((error) => { + console.error('Error getting user location:', error); + throw error; + }); + + if (!position) { + return 'Location not available'; + } + + // Extract the latitude and longitude from the position + const { latitude, longitude } = position.coords; + + if (raw) { + return { latitude, longitude }; + } else { + return `${latitude.toFixed(3)}, ${longitude.toFixed(3)} (lat, long)`; + } +}; + +const convertOpenAIMessages = (convo) => { + // Parse OpenAI chat messages and create chat dictionary for creating new chats + const mapping = convo['mapping']; + const messages = []; + let currentId = ''; + let lastId = null; + + for (let message_id in mapping) { + const message = mapping[message_id]; + currentId = message_id; + try { + if ( + messages.length == 0 && + (message['message'] == null || + (message['message']['content']['parts']?.[0] == '' && + message['message']['content']['text'] == null)) + ) { + // Skip chat messages with no content + continue; + } else { + const new_chat = { + id: message_id, + parentId: lastId, + childrenIds: message['children'] || [], + role: message['message']?.['author']?.['role'] !== 'user' ? 'assistant' : 'user', + content: + message['message']?.['content']?.['parts']?.[0] || + message['message']?.['content']?.['text'] || + '', + model: 'gpt-3.5-turbo', + done: true, + context: null + }; + messages.push(new_chat); + lastId = currentId; + } + } catch (error) { + console.log('Error with', message, '\nError:', error); + } + } + + let history = {}; + messages.forEach((obj) => (history[obj.id] = obj)); + + const chat = { + history: { + currentId: currentId, + messages: history // Need to convert this to not a list and instead a json object + }, + models: ['gpt-3.5-turbo'], + messages: messages, + options: {}, + timestamp: convo['create_time'], + title: convo['title'] ?? 'New Chat' + }; + return chat; +}; + +const validateChat = (chat) => { + // Because ChatGPT sometimes has features we can't use like DALL-E or migh have corrupted messages, need to validate + const messages = chat.messages; + + // Check if messages array is empty + if (messages.length === 0) { + return false; + } + + // Last message's children should be an empty array + const lastMessage = messages[messages.length - 1]; + if (lastMessage.childrenIds.length !== 0) { + return false; + } + + // First message's parent should be null + const firstMessage = messages[0]; + if (firstMessage.parentId !== null) { + return false; + } + + // Every message's content should be a string + for (let message of messages) { + if (typeof message.content !== 'string') { + return false; + } + } + + return true; +}; + +export const convertOpenAIChats = (_chats) => { + // Create a list of dictionaries with each conversation from import + const chats = []; + let failed = 0; + for (let convo of _chats) { + const chat = convertOpenAIMessages(convo); + + if (validateChat(chat)) { + chats.push({ + id: convo['id'], + user_id: '', + title: convo['title'], + chat: chat, + timestamp: convo['timestamp'] + }); + } else { + failed++; + } + } + console.log(failed, 'Conversations could not be imported'); + return chats; +}; + +export const isValidHttpUrl = (string) => { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === 'http:' || url.protocol === 'https:'; +}; + +export const removeEmojis = (str) => { + // Regular expression to match emojis + const emojiRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/g; + + // Replace emojis with an empty string + return str.replace(emojiRegex, ''); +}; + +export const removeFormattings = (str) => { + return str.replace(/(\*)(.*?)\1/g, '').replace(/(```)(.*?)\1/gs, ''); +}; + +export const extractSentences = (text) => { + // This regular expression matches code blocks marked by triple backticks + const codeBlockRegex = /```[\s\S]*?```/g; + + let codeBlocks = []; + let index = 0; + + // Temporarily replace code blocks with placeholders and store the blocks separately + text = text.replace(codeBlockRegex, (match) => { + let placeholder = `\u0000${index}\u0000`; // Use a unique placeholder + codeBlocks[index++] = match; + return placeholder; + }); + + // Split the modified text into sentences based on common punctuation marks, avoiding these blocks + let sentences = text.split(/(?<=[.!?])\s+/); + + // Restore code blocks and process sentences + sentences = sentences.map((sentence) => { + // Check if the sentence includes a placeholder for a code block + return sentence.replace(/\u0000(\d+)\u0000/g, (_, idx) => codeBlocks[idx]); + }); + + return sentences + .map((sentence) => removeFormattings(removeEmojis(sentence.trim()))) + .filter((sentence) => sentence); +}; + +export const extractSentencesForAudio = (text) => { + return extractSentences(text).reduce((mergedTexts, currentText) => { + const lastIndex = mergedTexts.length - 1; + if (lastIndex >= 0) { + const previousText = mergedTexts[lastIndex]; + const wordCount = previousText.split(/\s+/).length; + if (wordCount < 2) { + mergedTexts[lastIndex] = previousText + ' ' + currentText; + } else { + mergedTexts.push(currentText); + } + } else { + mergedTexts.push(currentText); + } + return mergedTexts; + }, []); +}; + +export const blobToFile = (blob, fileName) => { + // Create a new File object from the Blob + const file = new File([blob], fileName, { type: blob.type }); + return file; +}; + +/** + * @param {string} template - The template string containing placeholders. + * @returns {string} The template string with the placeholders replaced by the prompt. + */ +export const promptTemplate = ( + template: string, + user_name?: string, + user_location?: string +): string => { + // Get the current date + const currentDate = new Date(); + + // Format the date to YYYY-MM-DD + const formattedDate = + currentDate.getFullYear() + + '-' + + String(currentDate.getMonth() + 1).padStart(2, '0') + + '-' + + String(currentDate.getDate()).padStart(2, '0'); + + // Format the time to HH:MM:SS AM/PM + const currentTime = currentDate.toLocaleTimeString('en-US', { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: true + }); + + // Replace {{CURRENT_DATETIME}} in the template with the formatted datetime + template = template.replace('{{CURRENT_DATETIME}}', `${formattedDate} ${currentTime}`); + + // Replace {{CURRENT_DATE}} in the template with the formatted date + template = template.replace('{{CURRENT_DATE}}', formattedDate); + + // Replace {{CURRENT_TIME}} in the template with the formatted time + template = template.replace('{{CURRENT_TIME}}', currentTime); + + if (user_name) { + // Replace {{USER_NAME}} in the template with the user's name + template = template.replace('{{USER_NAME}}', user_name); + } + + if (user_location) { + // Replace {{USER_LOCATION}} in the template with the current location + template = template.replace('{{USER_LOCATION}}', user_location); + } + + return template; +}; + +/** + * This function is used to replace placeholders in a template string with the provided prompt. + * The placeholders can be in the following formats: + * - `{{prompt}}`: This will be replaced with the entire prompt. + * - `{{prompt:start:<length>}}`: This will be replaced with the first <length> characters of the prompt. + * - `{{prompt:end:<length>}}`: This will be replaced with the last <length> characters of the prompt. + * - `{{prompt:middletruncate:<length>}}`: This will be replaced with the prompt truncated to <length> characters, with '...' in the middle. + * + * @param {string} template - The template string containing placeholders. + * @param {string} prompt - The string to replace the placeholders with. + * @returns {string} The template string with the placeholders replaced by the prompt. + */ +export const titleGenerationTemplate = (template: string, prompt: string): string => { + template = template.replace( + /{{prompt}}|{{prompt:start:(\d+)}}|{{prompt:end:(\d+)}}|{{prompt:middletruncate:(\d+)}}/g, + (match, startLength, endLength, middleLength) => { + if (match === '{{prompt}}') { + return prompt; + } else if (match.startsWith('{{prompt:start:')) { + return prompt.substring(0, startLength); + } else if (match.startsWith('{{prompt:end:')) { + return prompt.slice(-endLength); + } else if (match.startsWith('{{prompt:middletruncate:')) { + if (prompt.length <= middleLength) { + return prompt; + } + const start = prompt.slice(0, Math.ceil(middleLength / 2)); + const end = prompt.slice(-Math.floor(middleLength / 2)); + return `${start}...${end}`; + } + return ''; + } + ); + + template = promptTemplate(template); + + return template; +}; + +export const approximateToHumanReadable = (nanoseconds: number) => { + const seconds = Math.floor((nanoseconds / 1e9) % 60); + const minutes = Math.floor((nanoseconds / 6e10) % 60); + const hours = Math.floor((nanoseconds / 3.6e12) % 24); + + const results: string[] = []; + + if (seconds >= 0) { + results.push(`${seconds}s`); + } + + if (minutes > 0) { + results.push(`${minutes}m`); + } + + if (hours > 0) { + results.push(`${hours}h`); + } + + return results.reverse().join(' '); +}; + +export const getTimeRange = (timestamp) => { + const now = new Date(); + const date = new Date(timestamp * 1000); // Convert Unix timestamp to milliseconds + + // Calculate the difference in milliseconds + const diffTime = now.getTime() - date.getTime(); + const diffDays = diffTime / (1000 * 3600 * 24); + + const nowDate = now.getDate(); + const nowMonth = now.getMonth(); + const nowYear = now.getFullYear(); + + const dateDate = date.getDate(); + const dateMonth = date.getMonth(); + const dateYear = date.getFullYear(); + + if (nowYear === dateYear && nowMonth === dateMonth && nowDate === dateDate) { + return 'Today'; + } else if (nowYear === dateYear && nowMonth === dateMonth && nowDate - dateDate === 1) { + return 'Yesterday'; + } else if (diffDays <= 7) { + return 'Previous 7 days'; + } else if (diffDays <= 30) { + return 'Previous 30 days'; + } else if (nowYear === dateYear) { + return date.toLocaleString('default', { month: 'long' }); + } else { + return date.getFullYear().toString(); + } +}; + +/** + * Extract frontmatter as a dictionary from the specified content string. + * @param content {string} - The content string with potential frontmatter. + * @returns {Object} - The extracted frontmatter as a dictionary. + */ +export const extractFrontmatter = (content) => { + const frontmatter = {}; + let frontmatterStarted = false; + let frontmatterEnded = false; + const frontmatterPattern = /^\s*([a-z_]+):\s*(.*)\s*$/i; + + // Split content into lines + const lines = content.split('\n'); + + // Check if the content starts with triple quotes + if (lines[0].trim() !== '"""') { + return {}; + } + + frontmatterStarted = true; + + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + + if (line.includes('"""')) { + if (frontmatterStarted) { + frontmatterEnded = true; + break; + } + } + + if (frontmatterStarted && !frontmatterEnded) { + const match = frontmatterPattern.exec(line); + if (match) { + const [, key, value] = match; + frontmatter[key.trim()] = value.trim(); + } + } + } + + return frontmatter; +}; + +// Function to determine the best matching language +export const bestMatchingLanguage = (supportedLanguages, preferredLanguages, defaultLocale) => { + const languages = supportedLanguages.map((lang) => lang.code); + + const match = preferredLanguages + .map((prefLang) => languages.find((lang) => lang.startsWith(prefLang))) + .find(Boolean); + + console.log(languages, preferredLanguages, match, defaultLocale); + return match || defaultLocale; +}; diff --git a/src/lib/utils/rag/index.ts b/src/lib/utils/rag/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ba1f29f888f0e2aae6294179d25ea30b47fcde9e --- /dev/null +++ b/src/lib/utils/rag/index.ts @@ -0,0 +1,24 @@ +import { getRAGTemplate } from '$lib/apis/rag'; + +export const RAGTemplate = async (token: string, context: string, query: string) => { + let template = await getRAGTemplate(token).catch(() => { + return `Use the following context as your learned knowledge, inside <context></context> XML tags. + <context> + [context] + </context> + + When answer to user: + - If you don't know, just say that you don't know. + - If you don't know when you are not sure, ask for clarification. + Avoid mentioning that you obtained the information from the context. + And answer according to the language of the user's question. + + Given the context information, answer the query. + Query: [query]`; + }); + + template = template.replace(/\[context\]/g, context); + template = template.replace(/\[query\]/g, query); + + return template; +}; diff --git a/src/lib/utils/transitions/index.ts b/src/lib/utils/transitions/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d919f561e6483ba6b20f2af29c243e904fdee57 --- /dev/null +++ b/src/lib/utils/transitions/index.ts @@ -0,0 +1,48 @@ +import { cubicOut } from 'svelte/easing'; +import type { TransitionConfig } from 'svelte/transition'; + +type FlyAndScaleParams = { + y?: number; + start?: number; + duration?: number; +}; + +const defaultFlyAndScaleParams = { y: -8, start: 0.95, duration: 200 }; + +export const flyAndScale = (node: Element, params?: FlyAndScaleParams): TransitionConfig => { + const style = getComputedStyle(node); + const transform = style.transform === 'none' ? '' : style.transform; + const withDefaults = { ...defaultFlyAndScaleParams, ...params }; + + const scaleConversion = (valueA: number, scaleA: [number, number], scaleB: [number, number]) => { + const [minA, maxA] = scaleA; + const [minB, maxB] = scaleB; + + const percentage = (valueA - minA) / (maxA - minA); + const valueB = percentage * (maxB - minB) + minB; + + return valueB; + }; + + const styleToString = (style: Record<string, number | string | undefined>): string => { + return Object.keys(style).reduce((str, key) => { + if (style[key] === undefined) return str; + return str + `${key}:${style[key]};`; + }, ''); + }; + + return { + duration: withDefaults.duration ?? 200, + delay: 0, + css: (t) => { + const y = scaleConversion(t, [0, 1], [withDefaults.y, 0]); + const scale = scaleConversion(t, [0, 1], [withDefaults.start, 1]); + + return styleToString({ + transform: `${transform} translate3d(0, ${y}px, 0) scale(${scale})`, + opacity: t + }); + }, + easing: cubicOut + }; +}; diff --git a/src/lib/workers/pyodide.worker.ts b/src/lib/workers/pyodide.worker.ts new file mode 100644 index 0000000000000000000000000000000000000000..b27c00629524df88b80e467d940975a50e14e4d4 --- /dev/null +++ b/src/lib/workers/pyodide.worker.ts @@ -0,0 +1,70 @@ +import { loadPyodide, type PyodideInterface } from 'pyodide'; + +declare global { + interface Window { + stdout: string | null; + stderr: string | null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: any; + pyodide: PyodideInterface; + packages: string[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; + } +} + +async function loadPyodideAndPackages(packages: string[] = []) { + self.stdout = null; + self.stderr = null; + self.result = null; + + self.pyodide = await loadPyodide({ + indexURL: '/pyodide/', + stdout: (text) => { + console.log('Python output:', text); + + if (self.stdout) { + self.stdout += `${text}\n`; + } else { + self.stdout = `${text}\n`; + } + }, + stderr: (text) => { + console.log('An error occurred:', text); + if (self.stderr) { + self.stderr += `${text}\n`; + } else { + self.stderr = `${text}\n`; + } + }, + packages: ['micropip'] + }); + + const micropip = self.pyodide.pyimport('micropip'); + + // await micropip.set_index_urls('https://pypi.org/pypi/{package_name}/json'); + await micropip.install(packages); +} + +self.onmessage = async (event) => { + const { id, code, ...context } = event.data; + + console.log(event.data); + + // The worker copies the context in its own "memory" (an object mapping name to values) + for (const key of Object.keys(context)) { + self[key] = context[key]; + } + + // make sure loading is done + await loadPyodideAndPackages(self.packages); + + try { + self.result = await self.pyodide.runPythonAsync(code); + } catch (error) { + self.stderr = error.toString(); + } + self.postMessage({ id, result: self.result, stdout: self.stdout, stderr: self.stderr }); +}; + +export default {}; diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..875ebf4eb9b14313953f673dd980adea1f7fa26c --- /dev/null +++ b/src/routes/(app)/+layout.svelte @@ -0,0 +1,297 @@ +<script lang="ts"> + import { toast } from 'svelte-sonner'; + import { onMount, tick, getContext } from 'svelte'; + import { openDB, deleteDB } from 'idb'; + import fileSaver from 'file-saver'; + const { saveAs } = fileSaver; + + import { goto } from '$app/navigation'; + + import { getModels as _getModels } from '$lib/apis'; + import { getAllChatTags } from '$lib/apis/chats'; + + import { getPrompts } from '$lib/apis/prompts'; + import { getDocs } from '$lib/apis/documents'; + import { getTools } from '$lib/apis/tools'; + + import { getBanners } from '$lib/apis/configs'; + import { getUserSettings } from '$lib/apis/users'; + + import { + user, + showSettings, + settings, + models, + prompts, + documents, + tags, + banners, + showChangelog, + config, + showCallOverlay, + tools, + functions + } from '$lib/stores'; + + import SettingsModal from '$lib/components/chat/SettingsModal.svelte'; + import Sidebar from '$lib/components/layout/Sidebar.svelte'; + import ChangelogModal from '$lib/components/ChangelogModal.svelte'; + import AccountPending from '$lib/components/layout/Overlay/AccountPending.svelte'; + import { getFunctions } from '$lib/apis/functions'; + + const i18n = getContext('i18n'); + + let loaded = false; + let DB = null; + let localDBChats = []; + + const getModels = async () => { + return _getModels(localStorage.token); + }; + + onMount(async () => { + if ($user === undefined) { + await goto('/auth'); + } else if (['user', 'admin'].includes($user.role)) { + try { + // Check if IndexedDB exists + DB = await openDB('Chats', 1); + + if (DB) { + const chats = await DB.getAllFromIndex('chats', 'timestamp'); + localDBChats = chats.map((item, idx) => chats[chats.length - 1 - idx]); + + if (localDBChats.length === 0) { + await deleteDB('Chats'); + } + } + + console.log(DB); + } catch (error) { + // IndexedDB Not Found + } + + const userSettings = await getUserSettings(localStorage.token).catch((error) => { + console.error(error); + return null; + }); + + if (userSettings) { + await settings.set(userSettings.ui); + } else { + await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); + } + + await Promise.all([ + (async () => { + models.set(await getModels()); + })(), + (async () => { + prompts.set(await getPrompts(localStorage.token)); + })(), + (async () => { + documents.set(await getDocs(localStorage.token)); + })(), + (async () => { + tools.set(await getTools(localStorage.token)); + })(), + (async () => { + functions.set(await getFunctions(localStorage.token)); + })(), + (async () => { + banners.set(await getBanners(localStorage.token)); + })(), + (async () => { + tags.set(await getAllChatTags(localStorage.token)); + })() + ]); + + document.addEventListener('keydown', function (event) { + const isCtrlPressed = event.ctrlKey || event.metaKey; // metaKey is for Cmd key on Mac + // Check if the Shift key is pressed + const isShiftPressed = event.shiftKey; + + // Check if Ctrl + Shift + O is pressed + if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'o') { + event.preventDefault(); + console.log('newChat'); + document.getElementById('sidebar-new-chat-button')?.click(); + } + + // Check if Shift + Esc is pressed + if (isShiftPressed && event.key === 'Escape') { + event.preventDefault(); + console.log('focusInput'); + document.getElementById('chat-textarea')?.focus(); + } + + // Check if Ctrl + Shift + ; is pressed + if (isCtrlPressed && isShiftPressed && event.key === ';') { + event.preventDefault(); + console.log('copyLastCodeBlock'); + const button = [...document.getElementsByClassName('copy-code-button')]?.at(-1); + button?.click(); + } + + // Check if Ctrl + Shift + C is pressed + if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 'c') { + event.preventDefault(); + console.log('copyLastResponse'); + const button = [...document.getElementsByClassName('copy-response-button')]?.at(-1); + console.log(button); + button?.click(); + } + + // Check if Ctrl + Shift + S is pressed + if (isCtrlPressed && isShiftPressed && event.key.toLowerCase() === 's') { + event.preventDefault(); + console.log('toggleSidebar'); + document.getElementById('sidebar-toggle-button')?.click(); + } + + // Check if Ctrl + Shift + Backspace is pressed + if (isCtrlPressed && isShiftPressed && event.key === 'Backspace') { + event.preventDefault(); + console.log('deleteChat'); + document.getElementById('delete-chat-button')?.click(); + } + + // Check if Ctrl + . is pressed + if (isCtrlPressed && event.key === '.') { + event.preventDefault(); + console.log('openSettings'); + showSettings.set(!$showSettings); + } + + // Check if Ctrl + / is pressed + if (isCtrlPressed && event.key === '/') { + event.preventDefault(); + console.log('showShortcuts'); + document.getElementById('show-shortcuts-button')?.click(); + } + }); + + if ($user.role === 'admin') { + showChangelog.set(localStorage.version !== $config.version); + } + + await tick(); + } + + loaded = true; + }); +</script> + +<SettingsModal bind:show={$showSettings} /> +<ChangelogModal bind:show={$showChangelog} /> + +<div class="app relative"> + <div + class=" text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900 h-screen max-h-[100dvh] overflow-auto flex flex-row" + > + {#if loaded} + {#if !['user', 'admin'].includes($user.role)} + <AccountPending /> + {:else if localDBChats.length > 0} + <div class="fixed w-full h-full flex z-50"> + <div + class="absolute w-full h-full backdrop-blur-md bg-white/20 dark:bg-gray-900/50 flex justify-center" + > + <div class="m-auto pb-44 flex flex-col justify-center"> + <div class="max-w-md"> + <div class="text-center dark:text-white text-2xl font-medium z-50"> + Important Update<br /> Action Required for Chat Log Storage + </div> + + <div class=" mt-4 text-center text-sm dark:text-gray-200 w-full"> + {$i18n.t( + "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" + )} + <span class="font-semibold dark:text-white" + >{$i18n.t('Settings')} > {$i18n.t('Chats')} > {$i18n.t('Import Chats')}</span + >. {$i18n.t( + 'This ensures that your valuable conversations are securely saved to your backend database. Thank you!' + )} + </div> + + <div class=" mt-6 mx-auto relative group w-fit"> + <button + class="relative z-20 flex px-5 py-2 rounded-full bg-white border border-gray-100 dark:border-none hover:bg-gray-100 transition font-medium text-sm" + on:click={async () => { + let blob = new Blob([JSON.stringify(localDBChats)], { + type: 'application/json' + }); + saveAs(blob, `chat-export-${Date.now()}.json`); + + const tx = DB.transaction('chats', 'readwrite'); + await Promise.all([tx.store.clear(), tx.done]); + await deleteDB('Chats'); + + localDBChats = []; + }} + > + Download & Delete + </button> + + <button + class="text-xs text-center w-full mt-2 text-gray-400 underline" + on:click={async () => { + localDBChats = []; + }}>{$i18n.t('Close')}</button + > + </div> + </div> + </div> + </div> + </div> + {/if} + + <Sidebar /> + <slot /> + {/if} + </div> +</div> + +<style> + .loading { + display: inline-block; + clip-path: inset(0 1ch 0 0); + animation: l 1s steps(3) infinite; + letter-spacing: -0.5px; + } + + @keyframes l { + to { + clip-path: inset(0 -1ch 0 0); + } + } + + pre[class*='language-'] { + position: relative; + overflow: auto; + + /* make space */ + margin: 5px 0; + padding: 1.75rem 0 1.75rem 1rem; + border-radius: 10px; + } + + pre[class*='language-'] button { + position: absolute; + top: 5px; + right: 5px; + + font-size: 0.9rem; + padding: 0.15rem; + background-color: #828282; + + border: ridge 1px #7b7b7c; + border-radius: 5px; + text-shadow: #c4c4c4 0 0 2px; + } + + pre[class*='language-'] button:hover { + cursor: pointer; + background-color: #bcbabb; + } +</style> diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..08026e7aa09650afbe47a8c0d0efc8cbfb6a56d0 --- /dev/null +++ b/src/routes/(app)/+page.svelte @@ -0,0 +1,7 @@ +<script lang="ts"> + import Chat from '$lib/components/chat/Chat.svelte'; + import Help from '$lib/components/layout/Help.svelte'; +</script> + +<Help /> +<Chat /> diff --git a/src/routes/(app)/admin/+layout.svelte b/src/routes/(app)/admin/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e77c24177d982c2753e0a74717a762f82efac129 --- /dev/null +++ b/src/routes/(app)/admin/+layout.svelte @@ -0,0 +1,82 @@ +<script lang="ts"> + import { onMount, getContext } from 'svelte'; + + import { WEBUI_NAME, showSidebar } from '$lib/stores'; + import MenuLines from '$lib/components/icons/MenuLines.svelte'; + import { page } from '$app/stores'; + + const i18n = getContext('i18n'); +</script> + +<svelte:head> + <title> + {$i18n.t('Admin Panel')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div + class=" flex flex-col w-full min-h-screen max-h-screen {$showSidebar + ? 'md:max-w-[calc(100%-260px)]' + : ''}" +> + <div class=" px-4 pt-3 mt-0.5 mb-1"> + <div class=" flex items-center gap-1"> + <div class="{$showSidebar ? 'md:hidden' : ''} mr-1 self-start flex flex-none items-center"> + <button + id="sidebar-toggle-button" + class="cursor-pointer p-1 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" + on:click={() => { + showSidebar.set(!$showSidebar); + }} + > + <div class=" m-auto self-center"> + <MenuLines /> + </div> + </button> + </div> + <div class="flex items-center text-xl font-semibold">{$i18n.t('Admin Panel')}</div> + </div> + </div> + + <div class="px-4 my-1"> + <div + class="flex scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-xl bg-transparent/10 p-1" + > + <a + class="min-w-fit rounded-lg p-1.5 px-3 {['/admin', '/admin/'].includes($page.url.pathname) + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/admin">{$i18n.t('Dashboard')}</a + > + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/admin/settings') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/admin/settings">{$i18n.t('Settings')}</a + > + + <!-- <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/documents') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/documents" + > + {$i18n.t('Documents')} + </a> + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/playground') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/playground">{$i18n.t('Playground')}</a + > --> + </div> + </div> + + <hr class=" my-2 dark:border-gray-850" /> + + <div class=" py-1 px-5 flex-1 max-h-full overflow-y-auto"> + <slot /> + </div> +</div> diff --git a/src/routes/(app)/admin/+page.svelte b/src/routes/(app)/admin/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..26375c9c3a470a5cc0b245e74780137f45d6583d --- /dev/null +++ b/src/routes/(app)/admin/+page.svelte @@ -0,0 +1,426 @@ +<script> + import { WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores'; + import { goto } from '$app/navigation'; + import { onMount, getContext } from 'svelte'; + + import dayjs from 'dayjs'; + import relativeTime from 'dayjs/plugin/relativeTime'; + dayjs.extend(relativeTime); + + import { toast } from 'svelte-sonner'; + + import { updateUserRole, getUsers, deleteUserById } from '$lib/apis/users'; + + import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; + import Pagination from '$lib/components/common/Pagination.svelte'; + import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte'; + import Tooltip from '$lib/components/common/Tooltip.svelte'; + import UserChatsModal from '$lib/components/admin/UserChatsModal.svelte'; + import AddUserModal from '$lib/components/admin/AddUserModal.svelte'; + import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte'; + + const i18n = getContext('i18n'); + + let loaded = false; + let tab = ''; + let users = []; + + let search = ''; + let selectedUser = null; + + let page = 1; + + let showDeleteConfirmDialog = false; + let showAddUserModal = false; + + let showUserChatsModal = false; + let showEditUserModal = false; + + const updateRoleHandler = async (id, role) => { + const res = await updateUserRole(localStorage.token, id, role).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + users = await getUsers(localStorage.token); + } + }; + + const editUserPasswordHandler = async (id, password) => { + const res = await deleteUserById(localStorage.token, id).catch((error) => { + toast.error(error); + return null; + }); + if (res) { + users = await getUsers(localStorage.token); + toast.success($i18n.t('Successfully updated.')); + } + }; + + const deleteUserHandler = async (id) => { + const res = await deleteUserById(localStorage.token, id).catch((error) => { + toast.error(error); + return null; + }); + if (res) { + users = await getUsers(localStorage.token); + } + }; + + onMount(async () => { + if ($user?.role !== 'admin') { + await goto('/'); + } else { + users = await getUsers(localStorage.token); + } + loaded = true; + }); + let sortKey = 'created_at'; // default sort key + let sortOrder = 'asc'; // default sort order + + function setSortKey(key) { + if (sortKey === key) { + sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'; + } else { + sortKey = key; + sortOrder = 'asc'; + } + } +</script> + +<ConfirmDialog + bind:show={showDeleteConfirmDialog} + on:confirm={() => { + deleteUserHandler(selectedUser.id); + }} +/> + +{#key selectedUser} + <EditUserModal + bind:show={showEditUserModal} + {selectedUser} + sessionUser={$user} + on:save={async () => { + users = await getUsers(localStorage.token); + }} + /> +{/key} + +<AddUserModal + bind:show={showAddUserModal} + on:save={async () => { + users = await getUsers(localStorage.token); + }} +/> +<UserChatsModal bind:show={showUserChatsModal} user={selectedUser} /> + +{#if loaded} + <div class="mt-0.5 mb-3 gap-1 flex flex-col md:flex-row justify-between"> + <div class="flex md:self-center 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="flex gap-1"> + <input + class="w-full md:w-60 rounded-xl 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 class="flex gap-0.5"> + <Tooltip content={$i18n.t('Add User')}> + <button + class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition font-medium text-sm flex items-center space-x-1" + on:click={() => { + showAddUserModal = !showAddUserModal; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z" + /> + </svg> + </button> + </Tooltip> + </div> + </div> + </div> + + <div class="scrollbar-hidden relative whitespace-nowrap overflow-x-auto max-w-full"> + <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto max-w-full"> + <thead 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 cursor-pointer select-none" + on:click={() => setSortKey('role')} + > + {$i18n.t('Role')} + {#if sortKey === 'role'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('name')} + > + {$i18n.t('Name')} + {#if sortKey === 'name'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('email')} + > + {$i18n.t('Email')} + {#if sortKey === 'email'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('oauth_sub')} + > + {$i18n.t('OAuth ID')} + {#if sortKey === 'oauth_sub'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('last_active_at')} + > + {$i18n.t('Last Active')} + {#if sortKey === 'last_active_at'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + <th + scope="col" + class="px-3 py-2 cursor-pointer select-none" + on:click={() => setSortKey('created_at')} + > + {$i18n.t('Created at')} + {#if sortKey === 'created_at'} + {sortOrder === 'asc' ? '▲' : '▼'} + {:else} + <span class="invisible">▲</span> + {/if} + </th> + + <th scope="col" class="px-3 py-2 text-right" /> + </tr> + </thead> + <tbody> + {#each users + .filter((user) => { + if (search === '') { + return true; + } else { + let name = user.name.toLowerCase(); + const query = search.toLowerCase(); + return name.includes(query); + } + }) + .sort((a, b) => { + if (a[sortKey] < b[sortKey]) return sortOrder === 'asc' ? -1 : 1; + if (a[sortKey] > b[sortKey]) return sortOrder === 'asc' ? 1 : -1; + return 0; + }) + .slice((page - 1) * 20, page * 20) as user} + <tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-850 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-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={() => { + if (user.role === 'user') { + updateRoleHandler(user.id, 'admin'); + } else if (user.role === 'pending') { + updateRoleHandler(user.id, 'user'); + } else { + updateRoleHandler(user.id, 'pending'); + } + }} + > + <div + class="w-1 h-1 rounded-full {user.role === 'admin' && + 'bg-sky-600 dark:bg-sky-300'} {user.role === 'user' && + 'bg-green-600 dark:bg-green-300'} {user.role === 'pending' && + 'bg-gray-600 dark:bg-gray-300'}" + /> + {$i18n.t(user.role)}</button + > + </td> + <td class="px-3 py-2 font-medium text-gray-900 dark:text-white w-max"> + <div class="flex flex-row w-max"> + <img + class=" rounded-full w-6 h-6 object-cover mr-2.5" + src={user.profile_image_url.startsWith(WEBUI_BASE_URL) || + user.profile_image_url.startsWith('https://www.gravatar.com/avatar/') || + user.profile_image_url.startsWith('data:') + ? user.profile_image_url + : `/user.png`} + alt="user" + /> + + <div class=" font-medium self-center">{user.name}</div> + </div> + </td> + <td class=" px-3 py-2"> {user.email} </td> + + <td class=" px-3 py-2"> {user.oauth_sub ?? ''} </td> + + <td class=" px-3 py-2"> + {dayjs(user.last_active_at * 1000).fromNow()} + </td> + + <td class=" px-3 py-2"> + {dayjs(user.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))} + </td> + + <td class="px-3 py-2 text-right"> + <div class="flex justify-end w-full"> + {#if user.role !== 'admin'} + <Tooltip content={$i18n.t('Chats')}> + <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 () => { + showUserChatsModal = !showUserChatsModal; + selectedUser = user; + }} + > + <ChatBubbles /> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Edit User')}> + <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 () => { + showEditUserModal = !showEditUserModal; + selectedUser = user; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + /> + </svg> + </button> + </Tooltip> + + <Tooltip content={$i18n.t('Delete User')}> + <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 () => { + showDeleteConfirmDialog = true; + selectedUser = user; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" + /> + </svg> + </button> + </Tooltip> + {:else} + <Tooltip content={$i18n.t('Edit User')}> + <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 () => { + showEditUserModal = !showEditUserModal; + selectedUser = user; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="w-4 h-4" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" + /> + </svg> + </button> + </Tooltip> + {/if} + </div> + </td> + </tr> + {/each} + </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> + + <Pagination bind:page count={users.length} /> +{/if} + +<style> + .font-mona { + font-family: 'Mona Sans'; + } + + .scrollbar-hidden::-webkit-scrollbar { + display: none; /* for Chrome, Safari and Opera */ + } + + .scrollbar-hidden { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ + } +</style> diff --git a/src/routes/(app)/admin/settings/+page.svelte b/src/routes/(app)/admin/settings/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a0a86f43569c15d30cfd6e56582a897d363fc7ae --- /dev/null +++ b/src/routes/(app)/admin/settings/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Settings from '$lib/components/admin/Settings.svelte'; +</script> + +<Settings /> diff --git a/src/routes/(app)/c/[id]/+page.svelte b/src/routes/(app)/c/[id]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..2cc68782eb61c109f9a42843aee429693583ae4c --- /dev/null +++ b/src/routes/(app)/c/[id]/+page.svelte @@ -0,0 +1,9 @@ +<script lang="ts"> + import { page } from '$app/stores'; + + import Chat from '$lib/components/chat/Chat.svelte'; + import Help from '$lib/components/layout/Help.svelte'; +</script> + +<Help /> +<Chat chatIdProp={$page.params.id} /> diff --git a/src/routes/(app)/playground/+layout.svelte b/src/routes/(app)/playground/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..74579665c9aa8e3b659cd6a8eb3a991b34dfe25b --- /dev/null +++ b/src/routes/(app)/playground/+layout.svelte @@ -0,0 +1,47 @@ +<script lang="ts"> + import { onMount, getContext } from 'svelte'; + import { WEBUI_NAME, showSidebar, functions } from '$lib/stores'; + import MenuLines from '$lib/components/icons/MenuLines.svelte'; + import { page } from '$app/stores'; + + const i18n = getContext('i18n'); + + onMount(async () => {}); +</script> + +<svelte:head> + <title> + {$i18n.t('Playground')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div + class=" flex flex-col w-full min-h-screen max-h-screen {$showSidebar + ? 'md:max-w-[calc(100%-260px)]' + : ''}" +> + <div class=" px-4 pt-3 mt-0.5 mb-1"> + <div class=" flex items-center gap-1"> + <div class="{$showSidebar ? 'md:hidden' : ''} mr-1 self-start flex flex-none items-center"> + <button + id="sidebar-toggle-button" + class="cursor-pointer p-1 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" + on:click={() => { + showSidebar.set(!$showSidebar); + }} + > + <div class=" m-auto self-center"> + <MenuLines /> + </div> + </button> + </div> + <div class="flex items-center text-xl font-semibold">{$i18n.t('Playground')}</div> + </div> + </div> + + <hr class=" my-2 dark:border-gray-850" /> + + <div class=" py-1 px-5 flex-1 max-h-full overflow-y-auto"> + <slot /> + </div> +</div> diff --git a/src/routes/(app)/playground/+page.svelte b/src/routes/(app)/playground/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..3147c8d0eba6bf544da8a5aba99a283bb978022a --- /dev/null +++ b/src/routes/(app)/playground/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Playground from '$lib/components/playground/Playground.svelte'; +</script> + +<Playground /> diff --git a/src/routes/(app)/workspace/+layout.svelte b/src/routes/(app)/workspace/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..794bd7ed53efca2c2d5ef42264638c3fad053fa9 --- /dev/null +++ b/src/routes/(app)/workspace/+layout.svelte @@ -0,0 +1,98 @@ +<script lang="ts"> + import { onMount, getContext } from 'svelte'; + + import { WEBUI_NAME, showSidebar, functions } from '$lib/stores'; + import MenuLines from '$lib/components/icons/MenuLines.svelte'; + import { page } from '$app/stores'; + import { getFunctions } from '$lib/apis/functions'; + + const i18n = getContext('i18n'); + + onMount(async () => { + // functions.set(await getFunctions(localStorage.token)); + }); +</script> + +<svelte:head> + <title> + {$i18n.t('Workspace')} | {$WEBUI_NAME} + </title> +</svelte:head> + +<div + class=" flex flex-col w-full min-h-screen max-h-screen {$showSidebar + ? 'md:max-w-[calc(100%-260px)]' + : ''}" +> + <div class=" px-4 pt-3 mt-0.5 mb-1"> + <div class=" flex items-center gap-1"> + <div class="{$showSidebar ? 'md:hidden' : ''} mr-1 self-start flex flex-none items-center"> + <button + id="sidebar-toggle-button" + class="cursor-pointer p-1 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition" + on:click={() => { + showSidebar.set(!$showSidebar); + }} + > + <div class=" m-auto self-center"> + <MenuLines /> + </div> + </button> + </div> + <div class="flex items-center text-xl font-semibold">{$i18n.t('Workspace')}</div> + </div> + </div> + + <div class="px-4 my-1"> + <div + class="flex scrollbar-none overflow-x-auto w-fit text-center text-sm font-medium rounded-xl bg-transparent/10 p-1" + > + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/models') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/models">{$i18n.t('Models')}</a + > + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/prompts') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/prompts">{$i18n.t('Prompts')}</a + > + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/documents') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/documents" + > + {$i18n.t('Documents')} + </a> + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/tools') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/tools" + > + {$i18n.t('Tools')} + </a> + + <a + class="min-w-fit rounded-lg p-1.5 px-3 {$page.url.pathname.includes('/workspace/functions') + ? 'bg-gray-50 dark:bg-gray-850' + : ''} transition" + href="/workspace/functions" + > + {$i18n.t('Functions')} + </a> + </div> + </div> + + <hr class=" my-2 dark:border-gray-850" /> + + <div class=" py-1 px-5 flex-1 max-h-full overflow-y-auto"> + <slot /> + </div> +</div> diff --git a/src/routes/(app)/workspace/+page.svelte b/src/routes/(app)/workspace/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a33774cfab81003a3427b64f336dfe508747480d --- /dev/null +++ b/src/routes/(app)/workspace/+page.svelte @@ -0,0 +1,8 @@ +<script lang="ts"> + import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + + onMount(() => { + goto('/workspace/models'); + }); +</script> diff --git a/src/routes/(app)/workspace/documents/+page.svelte b/src/routes/(app)/workspace/documents/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..4bba5511592b6e0f7233b8a2daaf0871aae56916 --- /dev/null +++ b/src/routes/(app)/workspace/documents/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Documents from '$lib/components/workspace/Documents.svelte'; +</script> + +<Documents /> diff --git a/src/routes/(app)/workspace/functions/+page.svelte b/src/routes/(app)/workspace/functions/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..8c6c212fab3f704f3953896b6b48f399bb723ad2 --- /dev/null +++ b/src/routes/(app)/workspace/functions/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Functions from '$lib/components/workspace/Functions.svelte'; +</script> + +<Functions /> diff --git a/src/routes/(app)/workspace/functions/create/+page.svelte b/src/routes/(app)/workspace/functions/create/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..deee266e5120a8aabb527f096be2f15686335579 --- /dev/null +++ b/src/routes/(app)/workspace/functions/create/+page.svelte @@ -0,0 +1,98 @@ +<script> + import { toast } from 'svelte-sonner'; + import { onMount, getContext } from 'svelte'; + import { goto } from '$app/navigation'; + + import { functions, models } from '$lib/stores'; + import { createNewFunction, getFunctions } from '$lib/apis/functions'; + import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte'; + import { getModels } from '$lib/apis'; + import { compareVersion, extractFrontmatter } from '$lib/utils'; + import { WEBUI_VERSION } from '$lib/constants'; + + const i18n = getContext('i18n'); + + let mounted = false; + let clone = false; + let func = null; + + const saveHandler = async (data) => { + console.log(data); + + const manifest = extractFrontmatter(data.content); + if (compareVersion(manifest?.required_open_webui_version ?? '0.0.0', WEBUI_VERSION)) { + console.log('Version is lower than required'); + toast.error( + $i18n.t( + 'Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})', + { + OPEN_WEBUI_VERSION: WEBUI_VERSION, + REQUIRED_VERSION: manifest?.required_open_webui_version ?? '0.0.0' + } + ) + ); + return; + } + + const res = await createNewFunction(localStorage.token, { + id: data.id, + name: data.name, + meta: data.meta, + content: data.content + }).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Function created successfully')); + functions.set(await getFunctions(localStorage.token)); + models.set(await getModels(localStorage.token)); + + await goto('/workspace/functions'); + } + }; + + onMount(() => { + window.addEventListener('message', async (event) => { + if ( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( + event.origin + ) + ) + return; + + func = JSON.parse(event.data); + console.log(func); + }); + + if (window.opener ?? false) { + window.opener.postMessage('loaded', '*'); + } + + if (sessionStorage.function) { + func = JSON.parse(sessionStorage.function); + sessionStorage.removeItem('function'); + + console.log(func); + clone = true; + } + + mounted = true; + }); +</script> + +{#if mounted} + {#key func?.content} + <FunctionEditor + id={func?.id ?? ''} + name={func?.name ?? ''} + meta={func?.meta ?? { description: '' }} + content={func?.content ?? ''} + {clone} + on:save={(e) => { + saveHandler(e.detail); + }} + /> + {/key} +{/if} diff --git a/src/routes/(app)/workspace/functions/edit/+page.svelte b/src/routes/(app)/workspace/functions/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..366890c04bd55878e283d83ff95bcfc6195d7762 --- /dev/null +++ b/src/routes/(app)/workspace/functions/edit/+page.svelte @@ -0,0 +1,88 @@ +<script> + import { toast } from 'svelte-sonner'; + import { onMount, getContext } from 'svelte'; + + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { functions, models } from '$lib/stores'; + import { updateFunctionById, getFunctions, getFunctionById } from '$lib/apis/functions'; + + import FunctionEditor from '$lib/components/workspace/Functions/FunctionEditor.svelte'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import { getModels } from '$lib/apis'; + import { compareVersion, extractFrontmatter } from '$lib/utils'; + import { WEBUI_VERSION } from '$lib/constants'; + + const i18n = getContext('i18n'); + + let func = null; + + const saveHandler = async (data) => { + console.log(data); + + const manifest = extractFrontmatter(data.content); + if (compareVersion(manifest?.required_open_webui_version ?? '0.0.0', WEBUI_VERSION)) { + console.log('Version is lower than required'); + toast.error( + $i18n.t( + 'Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})', + { + OPEN_WEBUI_VERSION: WEBUI_VERSION, + REQUIRED_VERSION: manifest?.required_open_webui_version ?? '0.0.0' + } + ) + ); + return; + } + + const res = await updateFunctionById(localStorage.token, func.id, { + id: data.id, + name: data.name, + meta: data.meta, + content: data.content + }).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Function updated successfully')); + functions.set(await getFunctions(localStorage.token)); + models.set(await getModels(localStorage.token)); + } + }; + + onMount(async () => { + console.log('mounted'); + const id = $page.url.searchParams.get('id'); + + if (id) { + func = await getFunctionById(localStorage.token, id).catch((error) => { + toast.error(error); + goto('/workspace/functions'); + return null; + }); + + console.log(func); + } + }); +</script> + +{#if func} + <FunctionEditor + edit={true} + id={func.id} + name={func.name} + meta={func.meta} + content={func.content} + on:save={(e) => { + saveHandler(e.detail); + }} + /> +{:else} + <div class="flex items-center justify-center h-full"> + <div class=" pb-16"> + <Spinner /> + </div> + </div> +{/if} diff --git a/src/routes/(app)/workspace/models/+page.svelte b/src/routes/(app)/workspace/models/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..f656ad781d36d421abeb0000f1d7020d43efb2e2 --- /dev/null +++ b/src/routes/(app)/workspace/models/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Models from '$lib/components/workspace/Models.svelte'; +</script> + +<Models /> diff --git a/src/routes/(app)/workspace/models/create/+page.svelte b/src/routes/(app)/workspace/models/create/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b47d37bf3867964857997375e22226bc9a19799e --- /dev/null +++ b/src/routes/(app)/workspace/models/create/+page.svelte @@ -0,0 +1,766 @@ +<script> + import { v4 as uuidv4 } from 'uuid'; + import { toast } from 'svelte-sonner'; + import { goto } from '$app/navigation'; + import { settings, user, config, models, tools, functions } from '$lib/stores'; + + import TurndownService from 'turndown'; + + import { onMount, tick, getContext } from 'svelte'; + import { addNewModel, getModelById, getModelInfos } from '$lib/apis/models'; + import { getModels } from '$lib/apis'; + + import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; + import Checkbox from '$lib/components/common/Checkbox.svelte'; + import Tags from '$lib/components/common/Tags.svelte'; + import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte'; + import ToolsSelector from '$lib/components/workspace/Models/ToolsSelector.svelte'; + import { stringify } from 'postcss'; + import { parseFile } from '$lib/utils/characters'; + import FiltersSelector from '$lib/components/workspace/Models/FiltersSelector.svelte'; + import ActionsSelector from '$lib/components/workspace/Models/ActionsSelector.svelte'; + + const i18n = getContext('i18n'); + + let filesInputElement; + let inputFiles; + + let showAdvanced = false; + let showPreview = false; + + let loading = false; + let success = false; + + // /////////// + // Model + // /////////// + + let id = ''; + let name = ''; + + let info = { + id: '', + base_model_id: null, + name: '', + meta: { + profile_image_url: null, + description: '', + suggestion_prompts: [ + { + content: '' + } + ] + }, + params: { + system: '' + } + }; + + let params = {}; + let capabilities = { + vision: true + }; + + let toolIds = []; + let knowledge = []; + let filterIds = []; + let actionIds = []; + + $: if (name) { + id = name + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9-]/g, '') + .toLowerCase(); + } + + const addUsage = (base_model_id) => { + const baseModel = $models.find((m) => m.id === base_model_id); + + if (baseModel) { + if (baseModel.owned_by === 'openai') { + capabilities.usage = baseModel.info?.meta?.capabilities?.usage ?? false; + } else { + delete capabilities.usage; + } + capabilities = capabilities; + } + }; + + const submitHandler = async () => { + loading = true; + + info.id = id; + info.name = name; + info.meta.capabilities = capabilities; + + if (knowledge.length > 0) { + info.meta.knowledge = knowledge; + } else { + if (info.meta.knowledge) { + delete info.meta.knowledge; + } + } + + if (toolIds.length > 0) { + info.meta.toolIds = toolIds; + } else { + if (info.meta.toolIds) { + delete info.meta.toolIds; + } + } + + if (filterIds.length > 0) { + info.meta.filterIds = filterIds; + } else { + if (info.meta.filterIds) { + delete info.meta.filterIds; + } + } + + if (actionIds.length > 0) { + info.meta.actionIds = actionIds; + } else { + if (info.meta.actionIds) { + delete info.meta.actionIds; + } + } + + info.params.stop = params.stop ? params.stop.split(',').filter((s) => s.trim()) : null; + Object.keys(info.params).forEach((key) => { + if (info.params[key] === '' || info.params[key] === null) { + delete info.params[key]; + } + }); + + if ($models.find((m) => m.id === info.id)) { + toast.error( + `Error: A model with the ID '${info.id}' already exists. Please select a different ID to proceed.` + ); + loading = false; + success = false; + return success; + } + + if (info) { + const res = await addNewModel(localStorage.token, { + ...info, + meta: { + ...info.meta, + profile_image_url: info.meta.profile_image_url ?? '/static/favicon.png', + suggestion_prompts: info.meta.suggestion_prompts + ? info.meta.suggestion_prompts.filter((prompt) => prompt.content !== '') + : null + }, + params: { ...info.params, ...params } + }); + + if (res) { + await models.set(await getModels(localStorage.token)); + toast.success($i18n.t('Model created successfully!')); + await goto('/workspace/models'); + } + } + + loading = false; + success = false; + }; + + const initModel = async (model) => { + name = model.name; + await tick(); + + id = model.id; + + if (model.info.base_model_id) { + const base_model = $models + .filter((m) => !m?.preset) + .find((m) => + [model.info.base_model_id, `${model.info.base_model_id}:latest`].includes(m.id) + ); + + console.log('base_model', base_model); + + if (!base_model) { + model.info.base_model_id = null; + } else if ($models.find((m) => m.id === `${model.info.base_model_id}:latest`)) { + model.info.base_model_id = `${model.info.base_model_id}:latest`; + } + } + + params = { ...params, ...model?.info?.params }; + params.stop = params?.stop ? (params?.stop ?? []).join(',') : null; + + capabilities = { ...capabilities, ...(model?.info?.meta?.capabilities ?? {}) }; + toolIds = model?.info?.meta?.toolIds ?? []; + + if (model?.info?.meta?.filterIds) { + filterIds = [...model?.info?.meta?.filterIds]; + } + + if (model?.info?.meta?.actionIds) { + actionIds = [...model?.info?.meta?.actionIds]; + } + + info = { + ...info, + ...model.info + }; + + console.log(info); + }; + + onMount(async () => { + window.addEventListener('message', async (event) => { + if ( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:5173'].includes( + event.origin + ) + ) + return; + + const model = JSON.parse(event.data); + console.log(model); + + initModel(model); + }); + + if (window.opener ?? false) { + window.opener.postMessage('loaded', '*'); + } + + if (sessionStorage.model) { + const model = JSON.parse(sessionStorage.model); + sessionStorage.removeItem('model'); + + console.log(model); + initModel(model); + } + }); +</script> + +<div class="w-full max-h-full"> + <input + bind:this={filesInputElement} + bind:files={inputFiles} + type="file" + hidden + accept="image/*" + on:change={() => { + let reader = new FileReader(); + reader.onload = async (event) => { + let originalImageUrl = `${event.target.result}`; + + let character = await parseFile(inputFiles[0]).catch((error) => { + return null; + }); + + console.log(character); + + if (character && character.character) { + character = character.character; + console.log(character); + + name = character.name; + + const pattern = /<\/?[a-z][\s\S]*>/i; + if (character.summary.match(pattern)) { + const turndownService = new TurndownService(); + info.meta.description = turndownService.turndown(character.summary); + } else { + info.meta.description = character.summary; + } + + info.params.system = `Personality: ${character.personality}${ + character?.scenario ? `\nScenario: ${character.scenario}` : '' + }${character?.greeting ? `\First Message: ${character.greeting}` : ''}${ + character?.examples ? `\nExamples: ${character.examples}` : '' + }`; + } + + const img = new Image(); + img.src = originalImageUrl; + + img.onload = function () { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate the aspect ratio of the image + const aspectRatio = img.width / img.height; + + // Calculate the new width and height to fit within 100x100 + let newWidth, newHeight; + if (aspectRatio > 1) { + newWidth = 250 * aspectRatio; + newHeight = 250; + } else { + newWidth = 250; + newHeight = 250 / aspectRatio; + } + + // Set the canvas size + canvas.width = 250; + canvas.height = 250; + + // Calculate the position to center the image + const offsetX = (250 - newWidth) / 2; + const offsetY = (250 - newHeight) / 2; + + // Draw the image on the canvas + ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); + + // Get the base64 representation of the compressed image + const compressedSrc = canvas.toDataURL('image/jpeg'); + + // Display the compressed image + info.meta.profile_image_url = compressedSrc; + + inputFiles = null; + }; + }; + + if ( + inputFiles && + inputFiles.length > 0 && + ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) + ) { + reader.readAsDataURL(inputFiles[0]); + } else { + console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); + inputFiles = null; + } + }} + /> + + <button + class="flex space-x-1" + on:click={() => { + goto('/workspace/models'); + }} + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + <!-- <hr class="my-3 dark:border-gray-850" /> --> + + <form + class="flex flex-col max-w-2xl mx-auto mt-4 mb-10" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="flex justify-center my-4"> + <div class="self-center"> + <button + class=" {info.meta.profile_image_url + ? '' + : 'p-4'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200 flex items-center" + type="button" + on:click={() => { + filesInputElement.click(); + }} + > + {#if info.meta.profile_image_url} + <img + src={info.meta.profile_image_url} + alt="modelfile profile" + class=" rounded-full size-16 object-cover" + /> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-8" + > + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + </div> + </div> + + <div class="my-2 flex space-x-2"> + <div class="flex-1"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Name')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Name your model')} + bind:value={name} + required + /> + </div> + </div> + + <div class="flex-1"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Model ID')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a model id')} + bind:value={id} + required + /> + </div> + </div> + </div> + + <div class="my-2"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Base Model (From)')}</div> + + <div> + <select + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder="Select a base model (e.g. llama3, gpt-4o)" + bind:value={info.base_model_id} + on:change={(e) => { + addUsage(e.target.value); + }} + required + > + <option value={null} class=" text-gray-900">{$i18n.t('Select a base model')}</option> + {#each $models.filter((m) => !m?.preset) as model} + <option value={model.id} class=" text-gray-900">{model.name}</option> + {/each} + </select> + </div> + </div> + + <div class="my-1"> + <div class="flex w-full justify-between items-center mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Description')}</div> + + <button + class="p-1 text-xs flex rounded transition" + type="button" + on:click={() => { + if (info.meta.description === null) { + info.meta.description = ''; + } else { + info.meta.description = null; + } + }} + > + {#if info.meta.description === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if info.meta.description !== null} + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a short description about what this model does')} + bind:value={info.meta.description} + row="3" + /> + {/if} + </div> + + <hr class=" dark:border-gray-850 my-1" /> + + <div class="my-2"> + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div> + </div> + + <div class="mt-2"> + <div class="my-1"> + <div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div> + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1" + placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`} + rows="4" + bind:value={info.params.system} + /> + </div> + </div> + + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-semibold"> + {$i18n.t('Advanced Params')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + showAdvanced = !showAdvanced; + }} + > + {#if showAdvanced} + <span class="ml-2 self-center">{$i18n.t('Hide')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Show')}</span> + {/if} + </button> + </div> + + {#if showAdvanced} + <div class="my-2"> + <AdvancedParams + admin={true} + bind:params + on:change={(e) => { + info.params = { ...info.params, ...params }; + }} + /> + </div> + {/if} + </div> + </div> + + <hr class=" dark:border-gray-850 my-1" /> + + <div class="my-1"> + <div class="flex w-full justify-between items-center"> + <div class="flex w-full justify-between items-center"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div> + + <button + class="p-1 text-xs flex rounded transition" + type="button" + on:click={() => { + if (info.meta.suggestion_prompts === null) { + info.meta.suggestion_prompts = [{ content: '' }]; + } else { + info.meta.suggestion_prompts = null; + } + }} + > + {#if info.meta.suggestion_prompts === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if info.meta.suggestion_prompts !== null} + <button + class="p-1 px-2 text-xs flex rounded transition" + type="button" + on:click={() => { + if ( + info.meta.suggestion_prompts.length === 0 || + info.meta.suggestion_prompts.at(-1).content !== '' + ) { + info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }]; + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" + /> + </svg> + </button> + {/if} + </div> + + {#if info.meta.suggestion_prompts} + <div class="flex flex-col space-y-1 mt-2"> + {#if info.meta.suggestion_prompts.length > 0} + {#each info.meta.suggestion_prompts as prompt, promptIdx} + <div class=" flex border dark:border-gray-600 rounded-lg"> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" + placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')} + bind:value={prompt.content} + /> + + <button + class="px-2" + type="button" + on:click={() => { + info.meta.suggestion_prompts.splice(promptIdx, 1); + info.meta.suggestion_prompts = info.meta.suggestion_prompts; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + {/each} + {:else} + <div class="text-xs text-center">No suggestion prompts</div> + {/if} + </div> + {/if} + </div> + + <div class="my-2"> + <Knowledge bind:knowledge /> + </div> + + <div class="my-2"> + <ToolsSelector bind:selectedToolIds={toolIds} tools={$tools} /> + </div> + + <div class="my-2"> + <FiltersSelector + bind:selectedFilterIds={filterIds} + filters={$functions.filter((func) => func.type === 'filter')} + /> + </div> + + <div class="my-2"> + <ActionsSelector + bind:selectedActionIds={actionIds} + actions={$functions.filter((func) => func.type === 'action')} + /> + </div> + + <div class="my-1"> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div> + </div> + <div class="flex flex-col"> + {#each Object.keys(capabilities) as capability} + <div class=" flex items-center gap-2"> + <Checkbox + state={capabilities[capability] ? 'checked' : 'unchecked'} + on:change={(e) => { + capabilities[capability] = e.detail === 'checked'; + }} + /> + + <div class=" py-0.5 text-sm w-full capitalize"> + {$i18n.t(capability)} + </div> + </div> + {/each} + </div> + </div> + + <div class="my-1"> + <div class="flex w-full justify-between items-center"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div> + </div> + + <div class="mt-2"> + <Tags + tags={info?.meta?.tags ?? []} + deleteTag={(tagName) => { + info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName); + }} + addTag={(tagName) => { + console.log(tagName); + if (!(info?.meta?.tags ?? null)) { + info.meta.tags = [{ name: tagName }]; + } else { + info.meta.tags = [...info.meta.tags, { name: tagName }]; + } + }} + /> + </div> + </div> + + <div class="my-2 text-gray-300 dark:text-gray-700"> + <div class="flex w-full justify-between mb-2"> + <div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + showPreview = !showPreview; + }} + > + {#if showPreview} + <span class="ml-2 self-center">{$i18n.t('Hide')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Show')}</span> + {/if} + </button> + </div> + + {#if showPreview} + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + rows="10" + value={JSON.stringify(info, null, 2)} + disabled + readonly + /> + </div> + {/if} + </div> + + <div class="my-2 flex justify-end mb-20"> + <button + class=" text-sm px-3 py-2 transition rounded-xl {loading + ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' + : ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" + type="submit" + disabled={loading} + > + <div class=" self-center font-medium">{$i18n.t('Save & Create')}</div> + + {#if loading} + <div class="ml-1.5 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/workspace/models/edit/+page.svelte b/src/routes/(app)/workspace/models/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..d05d9fc6d1b233ca983ebbe7f4b10b4119d39e54 --- /dev/null +++ b/src/routes/(app)/workspace/models/edit/+page.svelte @@ -0,0 +1,697 @@ +<script> + import { v4 as uuidv4 } from 'uuid'; + import { toast } from 'svelte-sonner'; + import { goto } from '$app/navigation'; + + import { onMount, getContext } from 'svelte'; + import { page } from '$app/stores'; + import { settings, user, config, models, tools, functions } from '$lib/stores'; + import { splitStream } from '$lib/utils'; + + import { getModelInfos, updateModelById } from '$lib/apis/models'; + + import AdvancedParams from '$lib/components/chat/Settings/Advanced/AdvancedParams.svelte'; + import { getModels } from '$lib/apis'; + import Checkbox from '$lib/components/common/Checkbox.svelte'; + import Tags from '$lib/components/common/Tags.svelte'; + import Knowledge from '$lib/components/workspace/Models/Knowledge.svelte'; + import ToolsSelector from '$lib/components/workspace/Models/ToolsSelector.svelte'; + import FiltersSelector from '$lib/components/workspace/Models/FiltersSelector.svelte'; + import ActionsSelector from '$lib/components/workspace/Models/ActionsSelector.svelte'; + + const i18n = getContext('i18n'); + + let loading = false; + let success = false; + + let filesInputElement; + let inputFiles; + + let digest = ''; + let pullProgress = null; + + let showAdvanced = false; + let showPreview = false; + + // /////////// + // model + // /////////// + + let model = null; + + let id = ''; + let name = ''; + + let info = { + id: '', + base_model_id: null, + name: '', + meta: { + profile_image_url: '/static/favicon.png', + description: '', + suggestion_prompts: null, + tags: [] + }, + params: { + system: '' + } + }; + + let params = {}; + let capabilities = { + vision: true + }; + + let knowledge = []; + let toolIds = []; + let filterIds = []; + let actionIds = []; + + const updateHandler = async () => { + loading = true; + + info.id = id; + info.name = name; + info.meta.capabilities = capabilities; + + if (knowledge.length > 0) { + info.meta.knowledge = knowledge; + } else { + if (info.meta.knowledge) { + delete info.meta.knowledge; + } + } + + if (toolIds.length > 0) { + info.meta.toolIds = toolIds; + } else { + if (info.meta.toolIds) { + delete info.meta.toolIds; + } + } + + if (filterIds.length > 0) { + info.meta.filterIds = filterIds; + } else { + if (info.meta.filterIds) { + delete info.meta.filterIds; + } + } + + if (actionIds.length > 0) { + info.meta.actionIds = actionIds; + } else { + if (info.meta.actionIds) { + delete info.meta.actionIds; + } + } + + info.params.stop = params.stop ? params.stop.split(',').filter((s) => s.trim()) : null; + Object.keys(info.params).forEach((key) => { + if (info.params[key] === '' || info.params[key] === null) { + delete info.params[key]; + } + }); + + const res = await updateModelById(localStorage.token, info.id, info); + + if (res) { + await models.set(await getModels(localStorage.token)); + toast.success($i18n.t('Model updated successfully')); + await goto('/workspace/models'); + } + + loading = false; + success = false; + }; + + onMount(() => { + const _id = $page.url.searchParams.get('id'); + + if (_id) { + model = $models.find((m) => m.id === _id); + if (model) { + id = model.id; + name = model.name; + + info = { + ...info, + ...JSON.parse( + JSON.stringify( + model?.info + ? model?.info + : { + id: model.id, + name: model.name + } + ) + ) + }; + + if (model.preset && model.owned_by === 'ollama' && !info.base_model_id.includes(':')) { + info.base_model_id = `${info.base_model_id}:latest`; + } + + params = { ...params, ...model?.info?.params }; + params.stop = params?.stop + ? (typeof params.stop === 'string' ? params.stop.split(',') : params?.stop ?? []).join( + ',' + ) + : null; + + if (model?.info?.meta?.knowledge) { + knowledge = [...model?.info?.meta?.knowledge]; + } + + if (model?.info?.meta?.toolIds) { + toolIds = [...model?.info?.meta?.toolIds]; + } + + if (model?.info?.meta?.filterIds) { + filterIds = [...model?.info?.meta?.filterIds]; + } + + if (model?.info?.meta?.actionIds) { + actionIds = [...model?.info?.meta?.actionIds]; + } + + if (model?.owned_by === 'openai') { + capabilities.usage = false; + } + + if (model?.info?.meta?.capabilities) { + capabilities = { ...capabilities, ...model?.info?.meta?.capabilities }; + } + + console.log(model); + } else { + goto('/workspace/models'); + } + } else { + goto('/workspace/models'); + } + }); +</script> + +<div class="w-full max-h-full"> + <input + bind:this={filesInputElement} + bind:files={inputFiles} + type="file" + hidden + accept="image/*" + on:change={() => { + let reader = new FileReader(); + reader.onload = (event) => { + let originalImageUrl = `${event.target.result}`; + + const img = new Image(); + img.src = originalImageUrl; + + img.onload = function () { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + // Calculate the aspect ratio of the image + const aspectRatio = img.width / img.height; + + // Calculate the new width and height to fit within 100x100 + let newWidth, newHeight; + if (aspectRatio > 1) { + newWidth = 250 * aspectRatio; + newHeight = 250; + } else { + newWidth = 250; + newHeight = 250 / aspectRatio; + } + + // Set the canvas size + canvas.width = 250; + canvas.height = 250; + + // Calculate the position to center the image + const offsetX = (250 - newWidth) / 2; + const offsetY = (250 - newHeight) / 2; + + // Draw the image on the canvas + ctx.drawImage(img, offsetX, offsetY, newWidth, newHeight); + + // Get the base64 representation of the compressed image + const compressedSrc = canvas.toDataURL('image/jpeg'); + + // Display the compressed image + info.meta.profile_image_url = compressedSrc; + + inputFiles = null; + }; + }; + + if ( + inputFiles && + inputFiles.length > 0 && + ['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type']) + ) { + reader.readAsDataURL(inputFiles[0]); + } else { + console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`); + inputFiles = null; + } + }} + /> + + <button + class="flex space-x-1" + on:click={() => { + goto('/workspace/models'); + }} + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + + {#if model} + <form + class="flex flex-col max-w-2xl mx-auto mt-4 mb-10" + on:submit|preventDefault={() => { + updateHandler(); + }} + > + <div class="flex justify-center my-4"> + <div class="self-center"> + <button + class=" {info.meta.profile_image_url + ? '' + : 'p-4'} rounded-full dark:bg-gray-700 border border-dashed border-gray-200 flex items-center" + type="button" + on:click={() => { + filesInputElement.click(); + }} + > + {#if info.meta.profile_image_url} + <img + src={info.meta.profile_image_url} + alt="modelfile profile" + class=" rounded-full size-16 object-cover" + /> + {:else} + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 24 24" + fill="currentColor" + class="size-8" + > + <path + fill-rule="evenodd" + d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z" + clip-rule="evenodd" + /> + </svg> + {/if} + </button> + </div> + </div> + + <div class="mt-2 my-1 flex space-x-2"> + <div class="flex-1"> + <div class=" text-sm font-semibold mb-1">{$i18n.t('Name')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Name your model')} + bind:value={name} + required + /> + </div> + </div> + + <div class="flex-1"> + <div class=" text-sm font-semibold mb-1">{$i18n.t('Model ID')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent disabled:text-gray-500 border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a model id')} + value={id} + disabled + required + /> + </div> + </div> + </div> + + {#if model.preset} + <div class="my-1"> + <div class=" text-sm font-semibold mb-1">{$i18n.t('Base Model (From)')}</div> + + <div> + <select + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder="Select a base model (e.g. llama3, gpt-4o)" + bind:value={info.base_model_id} + required + > + <option value={null} class=" text-gray-900">{$i18n.t('Select a base model')}</option> + {#each $models.filter((m) => m.id !== model.id && !m?.preset) as model} + <option value={model.id} class=" text-gray-900">{model.name}</option> + {/each} + </select> + </div> + </div> + {/if} + + <div class="my-1"> + <div class="flex w-full justify-between items-center"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Description')}</div> + + <button + class="p-1 text-xs flex rounded transition" + type="button" + on:click={() => { + if (info.meta.description === null) { + info.meta.description = ''; + } else { + info.meta.description = null; + } + }} + > + {#if info.meta.description === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if info.meta.description !== null} + <textarea + class="mt-1 px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a short description about what this model does')} + bind:value={info.meta.description} + row="3" + /> + {/if} + </div> + + <hr class=" dark:border-gray-850 my-1" /> + + <div class="my-2"> + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Model Params')}</div> + </div> + + <!-- <div class=" text-sm font-semibold mb-2"></div> --> + + <div class="mt-2"> + <div class="my-1"> + <div class=" text-xs font-semibold mb-2">{$i18n.t('System Prompt')}</div> + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg -mb-1" + placeholder={`Write your model system prompt content here\ne.g.) You are Mario from Super Mario Bros, acting as an assistant.`} + rows="4" + bind:value={info.params.system} + /> + </div> + </div> + + <div class="flex w-full justify-between"> + <div class=" self-center text-xs font-semibold"> + {$i18n.t('Advanced Params')} + </div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + showAdvanced = !showAdvanced; + }} + > + {#if showAdvanced} + <span class="ml-2 self-center">{$i18n.t('Hide')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Show')}</span> + {/if} + </button> + </div> + + {#if showAdvanced} + <div class="my-2"> + <AdvancedParams + admin={true} + bind:params + on:change={(e) => { + info.params = { ...info.params, ...params }; + }} + /> + </div> + {/if} + </div> + </div> + + <hr class=" dark:border-gray-850 my-1" /> + + <div class="my-2"> + <div class="flex w-full justify-between items-center"> + <div class="flex w-full justify-between items-center"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt suggestions')}</div> + + <button + class="p-1 text-xs flex rounded transition" + type="button" + on:click={() => { + if ((info?.meta?.suggestion_prompts ?? null) === null) { + info.meta.suggestion_prompts = [{ content: '' }]; + } else { + info.meta.suggestion_prompts = null; + } + }} + > + {#if (info?.meta?.suggestion_prompts ?? null) === null} + <span class="ml-2 self-center">{$i18n.t('Default')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Custom')}</span> + {/if} + </button> + </div> + + {#if (info?.meta?.suggestion_prompts ?? null) !== null} + <button + class="p-1 px-2 text-xs flex rounded transition" + type="button" + on:click={() => { + if ( + info.meta.suggestion_prompts.length === 0 || + info.meta.suggestion_prompts.at(-1).content !== '' + ) { + info.meta.suggestion_prompts = [...info.meta.suggestion_prompts, { content: '' }]; + } + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z" + /> + </svg> + </button> + {/if} + </div> + + {#if info?.meta?.suggestion_prompts} + <div class="flex flex-col space-y-1 mt-2"> + {#if info.meta.suggestion_prompts.length > 0} + {#each info.meta.suggestion_prompts as prompt, promptIdx} + <div class=" flex border dark:border-gray-600 rounded-lg"> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent outline-none border-r dark:border-gray-600" + placeholder={$i18n.t('Write a prompt suggestion (e.g. Who are you?)')} + bind:value={prompt.content} + /> + + <button + class="px-2" + type="button" + on:click={() => { + info.meta.suggestion_prompts.splice(promptIdx, 1); + info.meta.suggestion_prompts = info.meta.suggestion_prompts; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" + /> + </svg> + </button> + </div> + {/each} + {:else} + <div class="text-xs text-center">No suggestion prompts</div> + {/if} + </div> + {/if} + </div> + + <div class="my-2"> + <Knowledge bind:knowledge /> + </div> + + <div class="my-2"> + <ToolsSelector bind:selectedToolIds={toolIds} tools={$tools} /> + </div> + + <div class="my-2"> + <FiltersSelector + bind:selectedFilterIds={filterIds} + filters={$functions.filter((func) => func.type === 'filter')} + /> + </div> + + <div class="my-2"> + <ActionsSelector + bind:selectedActionIds={actionIds} + actions={$functions.filter((func) => func.type === 'action')} + /> + </div> + + <div class="my-2"> + <div class="flex w-full justify-between mb-1"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Capabilities')}</div> + </div> + <div class="flex flex-col"> + {#each Object.keys(capabilities) as capability} + <div class=" flex items-center gap-2"> + <Checkbox + state={capabilities[capability] ? 'checked' : 'unchecked'} + on:change={(e) => { + capabilities[capability] = e.detail === 'checked'; + }} + /> + + <div class=" py-0.5 text-sm w-full capitalize"> + {$i18n.t(capability)} + </div> + </div> + {/each} + </div> + </div> + + <div class="my-1"> + <div class="flex w-full justify-between items-center"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Tags')}</div> + </div> + + <div class="mt-2"> + <Tags + tags={info?.meta?.tags ?? []} + deleteTag={(tagName) => { + info.meta.tags = info.meta.tags.filter((tag) => tag.name !== tagName); + }} + addTag={(tagName) => { + console.log(tagName); + if (!(info?.meta?.tags ?? null)) { + info.meta.tags = [{ name: tagName }]; + } else { + info.meta.tags = [...info.meta.tags, { name: tagName }]; + } + }} + /> + </div> + </div> + + <div class="my-2 text-gray-300 dark:text-gray-700"> + <div class="flex w-full justify-between mb-2"> + <div class=" self-center text-sm font-semibold">{$i18n.t('JSON Preview')}</div> + + <button + class="p-1 px-3 text-xs flex rounded transition" + type="button" + on:click={() => { + showPreview = !showPreview; + }} + > + {#if showPreview} + <span class="ml-2 self-center">{$i18n.t('Hide')}</span> + {:else} + <span class="ml-2 self-center">{$i18n.t('Show')}</span> + {/if} + </button> + </div> + + {#if showPreview} + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + rows="10" + value={JSON.stringify(info, null, 2)} + disabled + readonly + /> + </div> + {/if} + </div> + + <div class="my-2 flex justify-end mb-20"> + <button + class=" text-sm px-3 py-2 transition rounded-xl {loading + ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' + : ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" + type="submit" + disabled={loading} + > + <div class=" self-center font-medium">{$i18n.t('Save & Update')}</div> + + {#if loading} + <div class="ml-1.5 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> + {/if} +</div> diff --git a/src/routes/(app)/workspace/prompts/+page.svelte b/src/routes/(app)/workspace/prompts/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..48c6e65c6eb5b7a326ce0a23f04fd16ab5031a59 --- /dev/null +++ b/src/routes/(app)/workspace/prompts/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Prompts from '$lib/components/workspace/Prompts.svelte'; +</script> + +<Prompts /> diff --git a/src/routes/(app)/workspace/prompts/create/+page.svelte b/src/routes/(app)/workspace/prompts/create/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..eb03d23726221a7fdbcd01dcc03cce6a65eae3c8 --- /dev/null +++ b/src/routes/(app)/workspace/prompts/create/+page.svelte @@ -0,0 +1,241 @@ +<script> + import { toast } from 'svelte-sonner'; + + import { goto } from '$app/navigation'; + import { prompts } from '$lib/stores'; + import { onMount, tick, getContext } from 'svelte'; + + import { createNewPrompt, getPrompts } from '$lib/apis/prompts'; + + const i18n = getContext('i18n'); + + let loading = false; + + // /////////// + // Prompt + // /////////// + + let title = ''; + let command = ''; + let content = ''; + + $: command = title !== '' ? `${title.replace(/\s+/g, '-').toLowerCase()}` : ''; + + const submitHandler = async () => { + loading = true; + + if (validateCommandString(command)) { + const prompt = await createNewPrompt(localStorage.token, command, title, content).catch( + (error) => { + toast.error(error); + + return null; + } + ); + + if (prompt) { + await prompts.set(await getPrompts(localStorage.token)); + await goto('/workspace/prompts'); + } + } else { + toast.error( + $i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.') + ); + } + + loading = false; + }; + + const validateCommandString = (inputString) => { + // Regular expression to match only alphanumeric characters and hyphen + const regex = /^[a-zA-Z0-9-]+$/; + + // Test the input string against the regular expression + return regex.test(inputString); + }; + + onMount(async () => { + window.addEventListener('message', async (event) => { + if ( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:5173'].includes( + event.origin + ) + ) + return; + const prompt = JSON.parse(event.data); + console.log(prompt); + + title = prompt.title; + await tick(); + content = prompt.content; + command = prompt.command; + }); + + if (window.opener ?? false) { + window.opener.postMessage('loaded', '*'); + } + + if (sessionStorage.prompt) { + const prompt = JSON.parse(sessionStorage.prompt); + + console.log(prompt); + title = prompt.title; + await tick(); + content = prompt.content; + command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command; + + sessionStorage.removeItem('prompt'); + } + }); +</script> + +<div class="w-full max-h-full"> + <button + class="flex space-x-1" + on:click={() => { + history.back(); + }} + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + + <form + class="flex flex-col max-w-2xl mx-auto mt-4 mb-10" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="my-2"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Title')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a short title for this prompt')} + bind:value={title} + required + /> + </div> + </div> + + <div class="my-2"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Command')}*</div> + + <div class="flex items-center mb-1"> + <div + class="bg-gray-200 dark:bg-gray-600 font-semibold px-3 py-1 border border-r-0 dark:border-gray-600 rounded-l-lg" + > + / + </div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-r-lg" + placeholder={$i18n.t('short-summary')} + bind:value={command} + required + /> + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Only')} + <span class=" text-gray-600 dark:text-gray-300 font-medium" + >{$i18n.t('alphanumeric characters and hyphens')}</span + > + {$i18n.t('are allowed - Activate this command by typing')} "<span + class=" text-gray-600 dark:text-gray-300 font-medium" + > + /{command} + </span>" + {$i18n.t('to chat input.')} + </div> + </div> + + <div class="my-2"> + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt Content')}*</div> + </div> + + <div class="mt-2"> + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Write a summary in 50 words that summarizes [topic or keyword].')} + rows="6" + bind:value={content} + required + /> + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + ⓘ {$i18n.t('Format your variables using square brackets like this:')} <span + class=" text-gray-600 dark:text-gray-300 font-medium">[{$i18n.t('variable')}]</span + >. + {$i18n.t('Make sure to enclose them with')} + <span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> + {$i18n.t('and')} + <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span>. + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium"> + {` {{CLIPBOARD}}`}</span + > + {$i18n.t('variable to have them replaced with clipboard content.')} + </div> + </div> + </div> + + <div class="my-2 flex justify-end"> + <button + class=" text-sm px-3 py-2 transition rounded-xl {loading + ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' + : ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" + type="submit" + disabled={loading} + > + <div class=" self-center font-medium">{$i18n.t('Save & Create')}</div> + + {#if loading} + <div class="ml-1.5 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/workspace/prompts/edit/+page.svelte b/src/routes/(app)/workspace/prompts/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..a5fb92247279fa1c393ebf8b551806fcc1a0eb7a --- /dev/null +++ b/src/routes/(app)/workspace/prompts/edit/+page.svelte @@ -0,0 +1,228 @@ +<script> + import { toast } from 'svelte-sonner'; + + import { goto } from '$app/navigation'; + import { prompts } from '$lib/stores'; + import { onMount, tick, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + import { getPrompts, updatePromptByCommand } from '$lib/apis/prompts'; + import { page } from '$app/stores'; + + let loading = false; + + // /////////// + // Prompt + // /////////// + + let title = ''; + let command = ''; + let content = ''; + + const updateHandler = async () => { + loading = true; + + if (validateCommandString(command)) { + const prompt = await updatePromptByCommand(localStorage.token, command, title, content).catch( + (error) => { + toast.error(error); + return null; + } + ); + + if (prompt) { + await prompts.set(await getPrompts(localStorage.token)); + await goto('/workspace/prompts'); + } + } else { + toast.error( + $i18n.t('Only alphanumeric characters and hyphens are allowed in the command string.') + ); + } + + loading = false; + }; + + const validateCommandString = (inputString) => { + // Regular expression to match only alphanumeric characters and hyphen + const regex = /^[a-zA-Z0-9-]+$/; + + // Test the input string against the regular expression + return regex.test(inputString); + }; + + onMount(async () => { + command = $page.url.searchParams.get('command'); + if (command) { + const prompt = $prompts.filter((prompt) => prompt.command === command).at(0); + + if (prompt) { + console.log(prompt); + + console.log(prompt.command); + + title = prompt.title; + await tick(); + command = prompt.command.slice(1); + content = prompt.content; + } else { + goto('/workspace/prompts'); + } + } else { + goto('/workspace/prompts'); + } + }); +</script> + +<div class="w-full max-h-full"> + <button + class="flex space-x-1" + on:click={() => { + history.back(); + }} + > + <div class=" self-center"> + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + class="w-4 h-4" + > + <path + fill-rule="evenodd" + d="M17 10a.75.75 0 01-.75.75H5.612l4.158 3.96a.75.75 0 11-1.04 1.08l-5.5-5.25a.75.75 0 010-1.08l5.5-5.25a.75.75 0 111.04 1.08L5.612 9.25H16.25A.75.75 0 0117 10z" + clip-rule="evenodd" + /> + </svg> + </div> + <div class=" self-center font-medium text-sm">{$i18n.t('Back')}</div> + </button> + + <form + class="flex flex-col max-w-2xl mx-auto mt-4 mb-10" + on:submit|preventDefault={() => { + updateHandler(); + }} + > + <div class="my-2"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Title')}*</div> + + <div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t('Add a short title for this prompt')} + bind:value={title} + required + /> + </div> + </div> + + <div class="my-2"> + <div class=" text-sm font-semibold mb-2">{$i18n.t('Command')}*</div> + + <div class="flex items-center mb-1"> + <div + class="bg-gray-200 dark:bg-gray-600 font-semibold px-3 py-1 border border-r-0 dark:border-gray-600 rounded-l-lg" + > + / + </div> + <input + class="px-3 py-1.5 text-sm w-full bg-transparent border disabled:text-gray-500 dark:border-gray-600 outline-none rounded-r-lg" + placeholder="short-summary" + bind:value={command} + disabled + required + /> + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Only')} + <span class=" text-gray-600 dark:text-gray-300 font-medium" + >{$i18n.t('alphanumeric characters and hyphens')}</span + > + {$i18n.t('are allowed - Activate this command by typing')} "<span + class=" text-gray-600 dark:text-gray-300 font-medium" + > + /{command} + </span>" + {$i18n.t('to chat input.')} + </div> + </div> + + <div class="my-2"> + <div class="flex w-full justify-between"> + <div class=" self-center text-sm font-semibold">{$i18n.t('Prompt Content')}*</div> + </div> + + <div class="mt-2"> + <div> + <textarea + class="px-3 py-1.5 text-sm w-full bg-transparent border dark:border-gray-600 outline-none rounded-lg" + placeholder={$i18n.t(`Write a summary in 50 words that summarizes [topic or keyword].`)} + rows="6" + bind:value={content} + required + /> + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + ⓘ {$i18n.t('Format your variables using square brackets like this:')} <span + class=" text-gray-600 dark:text-gray-300 font-medium">[{$i18n.t('variable')}]</span + >. + {$i18n.t('Make sure to enclose them with')} + <span class=" text-gray-600 dark:text-gray-300 font-medium">'['</span> + {$i18n.t('and')} + <span class=" text-gray-600 dark:text-gray-300 font-medium">']'</span>. + </div> + + <div class="text-xs text-gray-400 dark:text-gray-500"> + {$i18n.t('Utilize')}<span class=" text-gray-600 dark:text-gray-300 font-medium"> + {` {{CLIPBOARD}}`}</span + > + {$i18n.t('variable to have them replaced with clipboard content.')} + </div> + </div> + </div> + + <div class="my-2 flex justify-end"> + <button + class=" text-sm px-3 py-2 transition rounded-xl {loading + ? ' cursor-not-allowed bg-gray-100 dark:bg-gray-800' + : ' bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-800'} flex" + type="submit" + disabled={loading} + > + <div class=" self-center font-medium">{$i18n.t('Save & Update')}</div> + + {#if loading} + <div class="ml-1.5 self-center"> + <svg + class=" w-4 h-4" + viewBox="0 0 24 24" + fill="currentColor" + xmlns="http://www.w3.org/2000/svg" + ><style> + .spinner_ajPY { + transform-origin: center; + animation: spinner_AtaB 0.75s infinite linear; + } + @keyframes spinner_AtaB { + 100% { + transform: rotate(360deg); + } + } + </style><path + d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" + opacity=".25" + /><path + d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" + class="spinner_ajPY" + /></svg + > + </div> + {/if} + </button> + </div> + </form> +</div> diff --git a/src/routes/(app)/workspace/tools/+page.svelte b/src/routes/(app)/workspace/tools/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..e745cfa80e984b9816247397b4fb824722a49e9b --- /dev/null +++ b/src/routes/(app)/workspace/tools/+page.svelte @@ -0,0 +1,5 @@ +<script> + import Tools from '$lib/components/workspace/Tools.svelte'; +</script> + +<Tools /> diff --git a/src/routes/(app)/workspace/tools/create/+page.svelte b/src/routes/(app)/workspace/tools/create/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..29a285d2269abe02847f0865a6d5c5e0d05ebdb1 --- /dev/null +++ b/src/routes/(app)/workspace/tools/create/+page.svelte @@ -0,0 +1,95 @@ +<script> + import { goto } from '$app/navigation'; + import { createNewTool, getTools } from '$lib/apis/tools'; + import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte'; + import { WEBUI_VERSION } from '$lib/constants'; + import { tools } from '$lib/stores'; + import { compareVersion, extractFrontmatter } from '$lib/utils'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + let mounted = false; + let clone = false; + let tool = null; + + const saveHandler = async (data) => { + console.log(data); + + const manifest = extractFrontmatter(data.content); + if (compareVersion(manifest?.required_open_webui_version ?? '0.0.0', WEBUI_VERSION)) { + console.log('Version is lower than required'); + toast.error( + $i18n.t( + 'Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})', + { + OPEN_WEBUI_VERSION: WEBUI_VERSION, + REQUIRED_VERSION: manifest?.required_open_webui_version ?? '0.0.0' + } + ) + ); + return; + } + + const res = await createNewTool(localStorage.token, { + id: data.id, + name: data.name, + meta: data.meta, + content: data.content + }).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Tool created successfully')); + tools.set(await getTools(localStorage.token)); + + await goto('/workspace/tools'); + } + }; + + onMount(() => { + window.addEventListener('message', async (event) => { + if ( + !['https://openwebui.com', 'https://www.openwebui.com', 'http://localhost:9999'].includes( + event.origin + ) + ) + return; + + tool = JSON.parse(event.data); + console.log(tool); + }); + + if (window.opener ?? false) { + window.opener.postMessage('loaded', '*'); + } + + if (sessionStorage.tool) { + tool = JSON.parse(sessionStorage.tool); + sessionStorage.removeItem('tool'); + + console.log(tool); + clone = true; + } + + mounted = true; + }); +</script> + +{#if mounted} + {#key tool?.content} + <ToolkitEditor + id={tool?.id ?? ''} + name={tool?.name ?? ''} + meta={tool?.meta ?? { description: '' }} + content={tool?.content ?? ''} + {clone} + on:save={(e) => { + saveHandler(e.detail); + }} + /> + {/key} +{/if} diff --git a/src/routes/(app)/workspace/tools/edit/+page.svelte b/src/routes/(app)/workspace/tools/edit/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..ebd47701e37f119707664022d406c2481aa7fdea --- /dev/null +++ b/src/routes/(app)/workspace/tools/edit/+page.svelte @@ -0,0 +1,86 @@ +<script> + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { getToolById, getTools, updateToolById } from '$lib/apis/tools'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import ToolkitEditor from '$lib/components/workspace/Tools/ToolkitEditor.svelte'; + import { WEBUI_VERSION } from '$lib/constants'; + import { tools } from '$lib/stores'; + import { compareVersion, extractFrontmatter } from '$lib/utils'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + + const i18n = getContext('i18n'); + + let tool = null; + + const saveHandler = async (data) => { + console.log(data); + + const manifest = extractFrontmatter(data.content); + if (compareVersion(manifest?.required_open_webui_version ?? '0.0.0', WEBUI_VERSION)) { + console.log('Version is lower than required'); + toast.error( + $i18n.t( + 'Open WebUI version (v{{OPEN_WEBUI_VERSION}}) is lower than required version (v{{REQUIRED_VERSION}})', + { + OPEN_WEBUI_VERSION: WEBUI_VERSION, + REQUIRED_VERSION: manifest?.required_open_webui_version ?? '0.0.0' + } + ) + ); + return; + } + + const res = await updateToolById(localStorage.token, tool.id, { + id: data.id, + name: data.name, + meta: data.meta, + content: data.content + }).catch((error) => { + toast.error(error); + return null; + }); + + if (res) { + toast.success($i18n.t('Tool updated successfully')); + tools.set(await getTools(localStorage.token)); + + // await goto('/workspace/tools'); + } + }; + + onMount(async () => { + console.log('mounted'); + const id = $page.url.searchParams.get('id'); + + if (id) { + tool = await getToolById(localStorage.token, id).catch((error) => { + toast.error(error); + goto('/workspace/tools'); + return null; + }); + + console.log(tool); + } + }); +</script> + +{#if tool} + <ToolkitEditor + edit={true} + id={tool.id} + name={tool.name} + meta={tool.meta} + content={tool.content} + on:save={(e) => { + saveHandler(e.detail); + }} + /> +{:else} + <div class="flex items-center justify-center h-full"> + <div class=" pb-16"> + <Spinner /> + </div> + </div> +{/if} diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte new file mode 100644 index 0000000000000000000000000000000000000000..76748a1d3139141c43c101cfa5b74758e78233f2 --- /dev/null +++ b/src/routes/+error.svelte @@ -0,0 +1,11 @@ +<script> + import { page } from '$app/stores'; +</script> + +<div class=" bg-white dark:bg-gray-800 min-h-screen"> + <div class=" flex h-full"> + <div class="m-auto my-10 dark:text-gray-300 text-3xl font-semibold"> + {$page.status}: {$page.error.message} + </div> + </div> +</div> diff --git a/src/routes/+layout.js b/src/routes/+layout.js new file mode 100644 index 0000000000000000000000000000000000000000..b49c52809497f1c37096bafc5ea6919e0438c2d9 --- /dev/null +++ b/src/routes/+layout.js @@ -0,0 +1,16 @@ +// if you want to generate a static html file +// for your page. +// Documentation: https://kit.svelte.dev/docs/page-options#prerender +// export const prerender = true; + +// if you want to Generate a SPA +// you have to set ssr to false. +// This is not the case (so set as true or comment the line) +// Documentation: https://kit.svelte.dev/docs/page-options#ssr +export const ssr = false; + +// How to manage the trailing slashes in the URLs +// the URL for about page witll be /about with 'ignore' (default) +// the URL for about page witll be /about/ with 'always' +// https://kit.svelte.dev/docs/page-options#trailingslash +export const trailingSlash = 'ignore'; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b306c57e35d516e5166cc0598314f52278a29680 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,214 @@ +<script> + import { io } from 'socket.io-client'; + import { spring } from 'svelte/motion'; + + let loadingProgress = spring(0, { + stiffness: 0.05 + }); + + import { onMount, tick, setContext } from 'svelte'; + import { + config, + user, + theme, + WEBUI_NAME, + mobile, + socket, + activeUserCount, + USAGE_POOL + } from '$lib/stores'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + import { Toaster, toast } from 'svelte-sonner'; + + import { getBackendConfig } from '$lib/apis'; + import { getSessionUser } from '$lib/apis/auths'; + + import '../tailwind.css'; + import '../app.css'; + + import 'tippy.js/dist/tippy.css'; + + import { WEBUI_BASE_URL, WEBUI_HOSTNAME } from '$lib/constants'; + import i18n, { initI18n, getLanguages } from '$lib/i18n'; + import { bestMatchingLanguage } from '$lib/utils'; + + setContext('i18n', i18n); + + let loaded = false; + const BREAKPOINT = 768; + + let wakeLock = null; + + onMount(async () => { + theme.set(localStorage.theme); + + mobile.set(window.innerWidth < BREAKPOINT); + const onResize = () => { + if (window.innerWidth < BREAKPOINT) { + mobile.set(true); + } else { + mobile.set(false); + } + }; + + window.addEventListener('resize', onResize); + + const setWakeLock = async () => { + try { + wakeLock = await navigator.wakeLock.request('screen'); + } catch (err) { + // The Wake Lock request has failed - usually system related, such as battery. + console.log(err); + } + + if (wakeLock) { + // Add a listener to release the wake lock when the page is unloaded + wakeLock.addEventListener('release', () => { + // the wake lock has been released + console.log('Wake Lock released'); + }); + } + }; + + if ('wakeLock' in navigator) { + await setWakeLock(); + + document.addEventListener('visibilitychange', async () => { + // Re-request the wake lock if the document becomes visible + if (wakeLock !== null && document.visibilityState === 'visible') { + await setWakeLock(); + } + }); + } + + let backendConfig = null; + try { + backendConfig = await getBackendConfig(); + console.log('Backend config:', backendConfig); + } catch (error) { + console.error('Error loading backend config:', error); + } + // Initialize i18n even if we didn't get a backend config, + // so `/error` can show something that's not `undefined`. + + initI18n(); + if (!localStorage.locale) { + const languages = await getLanguages(); + const browserLanguages = navigator.languages + ? navigator.languages + : [navigator.language || navigator.userLanguage]; + const lang = backendConfig.default_locale + ? backendConfig.default_locale + : bestMatchingLanguage(languages, browserLanguages, 'en-US'); + $i18n.changeLanguage(lang); + } + + if (backendConfig) { + // Save Backend Status to Store + await config.set(backendConfig); + await WEBUI_NAME.set(backendConfig.name); + + if ($config) { + const _socket = io(`${WEBUI_BASE_URL}` || undefined, { + path: '/ws/socket.io', + auth: { token: localStorage.token } + }); + + _socket.on('connect', () => { + console.log('connected'); + }); + + await socket.set(_socket); + + _socket.on('user-count', (data) => { + console.log('user-count', data); + activeUserCount.set(data.count); + }); + + _socket.on('usage', (data) => { + console.log('usage', data); + USAGE_POOL.set(data['models']); + }); + + if (localStorage.token) { + // Get Session User Info + const sessionUser = await getSessionUser(localStorage.token).catch((error) => { + toast.error(error); + return null; + }); + + if (sessionUser) { + // Save Session User to Store + await user.set(sessionUser); + } else { + // Redirect Invalid Session User to /auth Page + localStorage.removeItem('token'); + await goto('/auth'); + } + } else { + // Don't redirect if we're already on the auth page + // Needed because we pass in tokens from OAuth logins via URL fragments + if ($page.url.pathname !== '/auth') { + await goto('/auth'); + } + } + } + } else { + // Redirect to /error when Backend Not Detected + await goto(`/error`); + } + + await tick(); + + if ( + document.documentElement.classList.contains('her') && + document.getElementById('progress-bar') + ) { + loadingProgress.subscribe((value) => { + const progressBar = document.getElementById('progress-bar'); + + if (progressBar) { + progressBar.style.width = `${value}%`; + } + }); + + await loadingProgress.set(100); + + document.getElementById('splash-screen')?.remove(); + + const audio = new Audio(`/audio/greeting.mp3`); + const playAudio = () => { + audio.play(); + document.removeEventListener('click', playAudio); + }; + + document.addEventListener('click', playAudio); + + loaded = true; + } else { + document.getElementById('splash-screen')?.remove(); + loaded = true; + } + + return () => { + window.removeEventListener('resize', onResize); + }; + }); +</script> + +<svelte:head> + <title>{$WEBUI_NAME}</title> + <link crossorigin="anonymous" rel="icon" href="{WEBUI_BASE_URL}/static/favicon.png" /> + + <!-- rosepine themes have been disabled as it's not up to date with our latest version. --> + <!-- feel free to make a PR to fix if anyone wants to see it return --> + <!-- <link rel="stylesheet" type="text/css" href="/themes/rosepine.css" /> + <link rel="stylesheet" type="text/css" href="/themes/rosepine-dawn.css" /> --> +</svelte:head> + +{#if loaded} + <slot /> +{/if} + +<Toaster richColors position="top-center" /> diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..b0f13e3864d5ab43c626ab2ce7f88efab61f0f7f --- /dev/null +++ b/src/routes/auth/+page.svelte @@ -0,0 +1,359 @@ +<script> + import { goto } from '$app/navigation'; + import { getSessionUser, userSignIn, userSignUp } from '$lib/apis/auths'; + import Spinner from '$lib/components/common/Spinner.svelte'; + import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; + import { WEBUI_NAME, config, user, socket } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + import { toast } from 'svelte-sonner'; + import { generateInitialsImage, canvasPixelTest } from '$lib/utils'; + import { page } from '$app/stores'; + + const i18n = getContext('i18n'); + + let loaded = false; + let mode = 'signin'; + + let name = ''; + let email = ''; + let password = ''; + + const setSessionUser = async (sessionUser) => { + if (sessionUser) { + console.log(sessionUser); + toast.success($i18n.t(`You're now logged in.`)); + if (sessionUser.token) { + localStorage.token = sessionUser.token; + } + + $socket.emit('user-join', { auth: { token: sessionUser.token } }); + await user.set(sessionUser); + goto('/'); + } + }; + + const signInHandler = async () => { + const sessionUser = await userSignIn(email, password).catch((error) => { + toast.error(error); + return null; + }); + + await setSessionUser(sessionUser); + }; + + const signUpHandler = async () => { + const sessionUser = await userSignUp(name, email, password, generateInitialsImage(name)).catch( + (error) => { + toast.error(error); + return null; + } + ); + + await setSessionUser(sessionUser); + }; + + const submitHandler = async () => { + if (mode === 'signin') { + await signInHandler(); + } else { + await signUpHandler(); + } + }; + + const checkOauthCallback = async () => { + if (!$page.url.hash) { + return; + } + const hash = $page.url.hash.substring(1); + if (!hash) { + return; + } + const params = new URLSearchParams(hash); + const token = params.get('token'); + if (!token) { + return; + } + const sessionUser = await getSessionUser(token).catch((error) => { + toast.error(error); + return null; + }); + if (!sessionUser) { + return; + } + localStorage.token = token; + await setSessionUser(sessionUser); + }; + + onMount(async () => { + if ($user !== undefined) { + await goto('/'); + } + await checkOauthCallback(); + loaded = true; + if (($config?.features.auth_trusted_header ?? false) || $config?.features.auth === false) { + await signInHandler(); + } + }); +</script> + +<svelte:head> + <title> + {`${$WEBUI_NAME}`} + </title> +</svelte:head> + +{#if loaded} + <div class="fixed m-10 z-50"> + <div class="flex space-x-2"> + <div class=" self-center"> + <img + crossorigin="anonymous" + src="{WEBUI_BASE_URL}/static/favicon.png" + class=" w-8 rounded-full" + alt="logo" + /> + </div> + </div> + </div> + + <div class=" bg-white dark:bg-gray-950 min-h-screen w-full flex justify-center font-primary"> + <!-- <div class="hidden lg:flex lg:flex-1 px-10 md:px-16 w-full bg-yellow-50 justify-center"> + <div class=" my-auto pb-16 text-left"> + <div> + <div class=" font-semibold text-yellow-600 text-4xl"> + {$i18n.t('Get up and running with')} <br /> {$i18n.t('large language models, locally.')} + </div> + + <div class="mt-2 text-yellow-600 text-xl"> + {$i18n.t('Run Llama 2, Code Llama, and other models. Customize and create your own.')} + </div> + </div> + </div> + </div> --> + + <div class="w-full sm:max-w-md px-10 min-h-screen flex flex-col text-center"> + {#if ($config?.features.auth_trusted_header ?? false) || $config?.features.auth === false} + <div class=" my-auto pb-10 w-full"> + <div + class="flex items-center justify-center gap-3 text-xl sm:text-2xl text-center font-semibold dark:text-gray-200" + > + <div> + {$i18n.t('Signing in')} + {$i18n.t('to')} + {$WEBUI_NAME} + </div> + + <div> + <Spinner /> + </div> + </div> + </div> + {:else} + <div class=" my-auto pb-10 w-full dark:text-gray-100"> + <form + class=" flex flex-col justify-center" + on:submit|preventDefault={() => { + submitHandler(); + }} + > + <div class="mb-1"> + <div class=" text-2xl font-medium"> + {mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Sign up')} + {$i18n.t('to')} + {$WEBUI_NAME} + </div> + + {#if mode === 'signup'} + <div class=" mt-1 text-xs font-medium text-gray-500"> + ⓘ {$WEBUI_NAME} + {$i18n.t( + 'does not make any external connections, and your data stays securely on your locally hosted server.' + )} + </div> + {/if} + </div> + + {#if $config?.features.enable_login_form} + <div class="flex flex-col mt-4"> + {#if mode === 'signup'} + <div> + <div class=" text-sm font-medium text-left mb-1">{$i18n.t('Name')}</div> + <input + bind:value={name} + type="text" + class=" px-5 py-3 rounded-2xl w-full text-sm outline-none border dark:border-none dark:bg-gray-900" + autocomplete="name" + placeholder={$i18n.t('Enter Your Full Name')} + required + /> + </div> + + <hr class=" my-3 dark:border-gray-900" /> + {/if} + + <div class="mb-2"> + <div class=" text-sm font-medium text-left mb-1">{$i18n.t('Email')}</div> + <input + bind:value={email} + type="email" + class=" px-5 py-3 rounded-2xl w-full text-sm outline-none border dark:border-none dark:bg-gray-900" + autocomplete="email" + placeholder={$i18n.t('Enter Your Email')} + required + /> + </div> + + <div> + <div class=" text-sm font-medium text-left mb-1">{$i18n.t('Password')}</div> + + <input + bind:value={password} + type="password" + class=" px-5 py-3 rounded-2xl w-full text-sm outline-none border dark:border-none dark:bg-gray-900" + placeholder={$i18n.t('Enter Your Password')} + autocomplete="current-password" + required + /> + </div> + </div> + {/if} + + {#if $config?.features.enable_login_form} + <div class="mt-5"> + <button + class=" bg-gray-900 hover:bg-gray-800 w-full rounded-2xl text-white font-medium text-sm py-3 transition" + type="submit" + > + {mode === 'signin' ? $i18n.t('Sign in') : $i18n.t('Create Account')} + </button> + + {#if $config?.features.enable_signup} + <div class=" mt-4 text-sm text-center"> + {mode === 'signin' + ? $i18n.t("Don't have an account?") + : $i18n.t('Already have an account?')} + + <button + class=" font-medium underline" + type="button" + on:click={() => { + if (mode === 'signin') { + mode = 'signup'; + } else { + mode = 'signin'; + } + }} + > + {mode === 'signin' ? $i18n.t('Sign up') : $i18n.t('Sign in')} + </button> + </div> + {/if} + </div> + {/if} + </form> + + {#if Object.keys($config?.oauth?.providers ?? {}).length > 0} + <div class="inline-flex items-center justify-center w-full"> + <hr class="w-64 h-px my-8 bg-gray-200 border-0 dark:bg-gray-700" /> + {#if $config?.features.enable_login_form} + <span + class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 bg-white left-1/2 dark:text-white dark:bg-gray-950" + >{$i18n.t('or')}</span + > + {/if} + </div> + <div class="flex flex-col space-y-2"> + {#if $config?.oauth?.providers?.google} + <button + class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition" + on:click={() => { + window.location.href = `${WEBUI_BASE_URL}/oauth/google/login`; + }} + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" class="size-6 mr-3"> + <path + fill="#EA4335" + d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" + /><path + fill="#4285F4" + d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" + /><path + fill="#FBBC05" + d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" + /><path + fill="#34A853" + d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" + /><path fill="none" d="M0 0h48v48H0z" /> + </svg> + <span>{$i18n.t('Continue with {{provider}}', { provider: 'Google' })}</span> + </button> + {/if} + {#if $config?.oauth?.providers?.microsoft} + <button + class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition" + on:click={() => { + window.location.href = `${WEBUI_BASE_URL}/oauth/microsoft/login`; + }} + > + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21" class="size-6 mr-3"> + <rect x="1" y="1" width="9" height="9" fill="#f25022" /><rect + x="1" + y="11" + width="9" + height="9" + fill="#00a4ef" + /><rect x="11" y="1" width="9" height="9" fill="#7fba00" /><rect + x="11" + y="11" + width="9" + height="9" + fill="#ffb900" + /> + </svg> + <span>{$i18n.t('Continue with {{provider}}', { provider: 'Microsoft' })}</span> + </button> + {/if} + {#if $config?.oauth?.providers?.oidc} + <button + class="flex items-center px-6 border-2 dark:border-gray-800 duration-300 dark:bg-gray-900 hover:bg-gray-100 dark:hover:bg-gray-800 w-full rounded-2xl dark:text-white text-sm py-3 transition" + on:click={() => { + window.location.href = `${WEBUI_BASE_URL}/oauth/oidc/login`; + }} + > + <svg + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke-width="1.5" + stroke="currentColor" + class="size-6 mr-3" + > + <path + stroke-linecap="round" + stroke-linejoin="round" + d="M15.75 5.25a3 3 0 0 1 3 3m3 0a6 6 0 0 1-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1 1 21.75 8.25Z" + /> + </svg> + + <span + >{$i18n.t('Continue with {{provider}}', { + provider: $config?.oauth?.providers?.oidc ?? 'SSO' + })}</span + > + </button> + {/if} + </div> + {/if} + </div> + {/if} + </div> + </div> +{/if} + +<style> + .font-mona { + font-family: 'Mona Sans', -apple-system, 'Inter', ui-sans-serif, system-ui, 'Segoe UI', Roboto, + Ubuntu, Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + } +</style> diff --git a/src/routes/error/+page.svelte b/src/routes/error/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..955186600f72144c1db4666dbe8a6396796e6bfa --- /dev/null +++ b/src/routes/error/+page.svelte @@ -0,0 +1,60 @@ +<script> + import { goto } from '$app/navigation'; + import { WEBUI_NAME, config } from '$lib/stores'; + import { onMount, getContext } from 'svelte'; + + const i18n = getContext('i18n'); + + let loaded = false; + + onMount(async () => { + if ($config) { + await goto('/'); + } + + loaded = true; + }); +</script> + +{#if loaded} + <div class="absolute w-full h-full flex z-50"> + <div class="absolute rounded-xl w-full h-full backdrop-blur flex justify-center"> + <div class="m-auto pb-44 flex flex-col justify-center"> + <div class="max-w-md"> + <div class="text-center text-2xl font-medium z-50"> + {$i18n.t('{{webUIName}} Backend Required', { webUIName: $WEBUI_NAME })} + </div> + + <div class=" mt-4 text-center text-sm w-full"> + {$i18n.t( + "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend." + )} + + <br class=" " /> + <br class=" " /> + <a + class=" font-semibold underline" + href="https://github.com/open-webui/open-webui#how-to-install-" + target="_blank">{$i18n.t('See readme.md for instructions')}</a + > + {$i18n.t('or')} + <a class=" font-semibold underline" href="https://discord.gg/5rJgQTnV4s" target="_blank" + >{$i18n.t('join our Discord for help.')}</a + > + </div> + + <div class=" mt-6 mx-auto relative group w-fit"> + <button + class="relative z-20 flex px-5 py-2 rounded-full bg-gray-100 hover:bg-gray-200 transition font-medium text-sm" + on:click={() => { + location.href = '/'; + }} + > + {$i18n.t('Check Again')} + </button> + </div> + </div> + </div> + </div> + </div> +{/if} diff --git a/src/routes/s/[id]/+page.svelte b/src/routes/s/[id]/+page.svelte new file mode 100644 index 0000000000000000000000000000000000000000..73be10d3f55be95f88f477ffa7e0ebc558226501 --- /dev/null +++ b/src/routes/s/[id]/+page.svelte @@ -0,0 +1,166 @@ +<script lang="ts"> + import { onMount, tick, getContext } from 'svelte'; + import { goto } from '$app/navigation'; + import { page } from '$app/stores'; + + import dayjs from 'dayjs'; + + import { settings, chatId, WEBUI_NAME, models } from '$lib/stores'; + import { convertMessagesToHistory } from '$lib/utils'; + + import { getChatByShareId } from '$lib/apis/chats'; + + import Messages from '$lib/components/chat/Messages.svelte'; + import Navbar from '$lib/components/layout/Navbar.svelte'; + import { getUserById } from '$lib/apis/users'; + import { error } from '@sveltejs/kit'; + import { getModels } from '$lib/apis'; + + const i18n = getContext('i18n'); + + let loaded = false; + + let autoScroll = true; + let processing = ''; + let messagesContainerElement: HTMLDivElement; + + // let chatId = $page.params.id; + let showModelSelector = false; + let selectedModels = ['']; + + let chat = null; + let user = null; + + let title = ''; + let files = []; + + let messages = []; + let history = { + messages: {}, + currentId: null + }; + + $: if (history.currentId !== null) { + let _messages = []; + + let currentMessage = history.messages[history.currentId]; + while (currentMessage !== null) { + _messages.unshift({ ...currentMessage }); + currentMessage = + currentMessage.parentId !== null ? history.messages[currentMessage.parentId] : null; + } + messages = _messages; + } else { + messages = []; + } + + $: if ($page.params.id) { + (async () => { + if (await loadSharedChat()) { + await tick(); + loaded = true; + } else { + await goto('/'); + } + })(); + } + + ////////////////////////// + // Web functions + ////////////////////////// + + const loadSharedChat = async () => { + await models.set(await getModels(localStorage.token)); + await chatId.set($page.params.id); + chat = await getChatByShareId(localStorage.token, $chatId).catch(async (error) => { + await goto('/'); + return null; + }); + + if (chat) { + user = await getUserById(localStorage.token, chat.user_id).catch((error) => { + console.error(error); + return null; + }); + + const chatContent = chat.chat; + + if (chatContent) { + console.log(chatContent); + + selectedModels = + (chatContent?.models ?? undefined) !== undefined + ? chatContent.models + : [chatContent.models ?? '']; + history = + (chatContent?.history ?? undefined) !== undefined + ? chatContent.history + : convertMessagesToHistory(chatContent.messages); + title = chatContent.title; + + autoScroll = true; + await tick(); + + if (messages.length > 0) { + history.messages[messages.at(-1).id].done = true; + } + await tick(); + + return true; + } else { + return null; + } + } + }; +</script> + +<svelte:head> + <title> + {title + ? `${title.length > 30 ? `${title.slice(0, 30)}...` : title} | ${$WEBUI_NAME}` + : `${$WEBUI_NAME}`} + </title> +</svelte:head> + +{#if loaded} + <div + class="min-h-screen max-h-screen w-full flex flex-col text-gray-700 dark:text-gray-100 bg-white dark:bg-gray-900" + > + <div class="flex flex-col flex-auto justify-center py-8"> + <div class="px-3 w-full max-w-5xl mx-auto"> + <div> + <div class=" text-3xl font-semibold line-clamp-1"> + {title} + </div> + + <div class=" mt-1 text-gray-400"> + {dayjs(chat.chat.timestamp).format($i18n.t('MMMM DD, YYYY'))} + </div> + </div> + + <hr class=" dark:border-gray-800 mt-6 mb-2" /> + </div> + + <div class=" flex flex-col w-full flex-auto overflow-auto h-0" id="messages-container"> + <div class=" h-full w-full flex flex-col py-4"> + <div class="py-2"> + <Messages + {user} + chatId={$chatId} + readOnly={true} + {selectedModels} + {processing} + bind:history + bind:messages + bind:autoScroll + bottomPadding={files.length > 0} + sendPrompt={() => {}} + continueGeneration={() => {}} + regenerateResponse={() => {}} + /> + </div> + </div> + </div> + </div> + </div> +{/if} diff --git a/src/tailwind.css b/src/tailwind.css new file mode 100644 index 0000000000000000000000000000000000000000..998ab8433d9a2253069aafbfecb7ba632c01253c --- /dev/null +++ b/src/tailwind.css @@ -0,0 +1,16 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + html, + pre { + font-family: -apple-system, BlinkMacSystemFont, 'Inter', ui-sans-serif, system-ui, 'Segoe UI', + Roboto, Ubuntu, Cantarell, 'Noto Sans', sans-serif, 'Helvetica Neue', Arial, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + } + + pre { + white-space: pre-wrap; + } +} diff --git a/static/assets/fonts/Archivo-Variable.ttf b/static/assets/fonts/Archivo-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..99dc9e5bc7b39e8e989462bef7b624972818da11 --- /dev/null +++ b/static/assets/fonts/Archivo-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed648e6308957e7b2d45b755d8a0461c4d10cd99bc501ee859f91935cb3d8727 +size 652084 diff --git a/static/assets/fonts/Inter-Variable.ttf b/static/assets/fonts/Inter-Variable.ttf new file mode 100644 index 0000000000000000000000000000000000000000..13713530a909f056dd9fa8cfc3dd0d3acf107a92 --- /dev/null +++ b/static/assets/fonts/Inter-Variable.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf3cb43b0366e2dc6df60e1132b1c9a4c15777f0cd8e5a53e0c15124003e9ed4 +size 804612 diff --git a/static/assets/fonts/Mona-Sans.woff2 b/static/assets/fonts/Mona-Sans.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d88d5ff27bea51dfc9954392acd7933d642415c9 Binary files /dev/null and b/static/assets/fonts/Mona-Sans.woff2 differ diff --git a/static/audio/greeting.mp3 b/static/audio/greeting.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8ded2a86a568459c861f280b20064b2c8943a0bf Binary files /dev/null and b/static/audio/greeting.mp3 differ diff --git a/static/doge.png b/static/doge.png new file mode 100644 index 0000000000000000000000000000000000000000..66723c6c1f20b5fbee8fdf52ccbbf91280a763d3 Binary files /dev/null and b/static/doge.png differ diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2074780847581edf9cf2ed0d2e9ebd8ff08c56 Binary files /dev/null and b/static/favicon.png differ diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1 @@ +{} diff --git a/static/opensearch.xml b/static/opensearch.xml new file mode 100644 index 0000000000000000000000000000000000000000..ce47e39ae988ff08b4d47eab9f7af325929d4599 --- /dev/null +++ b/static/opensearch.xml @@ -0,0 +1,8 @@ +<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/"> +<ShortName>Open WebUI</ShortName> +<Description>Search Open WebUI</Description> +<InputEncoding>UTF-8</InputEncoding> +<Image width="16" height="16" type="image/x-icon">http://localhost:5137/favicon.png</Image> +<Url type="text/html" method="get" template="http://localhost:5137/?q={searchTerms}"/> +<moz:SearchForm>http://localhost:5137</moz:SearchForm> +</OpenSearchDescription> \ No newline at end of file diff --git a/static/pyodide/pyodide-lock.json b/static/pyodide/pyodide-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..a651b04459c26ad27ac598e03e5acc802cba867a --- /dev/null +++ b/static/pyodide/pyodide-lock.json @@ -0,0 +1 @@ +{"info": {"abi_version": "2024_0", "arch": "wasm32", "platform": "emscripten_3_1_58", "python": "3.12.1", "version": "0.26.1"}, "packages": {"aiohttp": {"depends": ["aiosignal", "async-timeout", "attrs", "charset-normalizer", "frozenlist", "multidict", "yarl"], "file_name": "aiohttp-3.9.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["aiohttp"], "install_dir": "site", "name": "aiohttp", "package_type": "package", "sha256": "c9a381c45ca9d6f16f6ec269c27a82bcbcaa3075f7f8e5de271ef5c256aa4ea4", "shared_library": false, "unvendored_tests": true, "version": "3.9.5"}, "aiohttp-tests": {"depends": ["aiohttp"], "file_name": "aiohttp-tests.tar", "imports": [], "install_dir": "site", "name": "aiohttp-tests", "package_type": "package", "sha256": "dc42d3bea4ede411be4cd43059f224832438b00f076fbe4d9d1ef516e2eab250", "shared_library": false, "unvendored_tests": false, "version": "3.9.5"}, "aiosignal": {"depends": ["frozenlist"], "file_name": "aiosignal-1.3.1-py3-none-any.whl", "imports": ["aiosignal"], "install_dir": "site", "name": "aiosignal", "package_type": "package", "sha256": "e091282e280a5940e759c5c849dfcd3169f8eea21d5838c872ec1265ef825f1e", "shared_library": false, "unvendored_tests": false, "version": "1.3.1"}, "altair": {"depends": ["typing-extensions", "jinja2", "jsonschema", "numpy", "pandas", "toolz", "packaging"], "file_name": "altair-5.3.0-py3-none-any.whl", "imports": ["altair"], "install_dir": "site", "name": "altair", "package_type": "package", "sha256": "1d2f248506ab81f13292d42464faa60c4940c1ef5da9013dc3a97ed398d6f7f7", "shared_library": false, "unvendored_tests": false, "version": "5.3.0"}, "annotated-types": {"depends": [], "file_name": "annotated_types-0.6.0-py3-none-any.whl", "imports": ["annotated_types"], "install_dir": "site", "name": "annotated-types", "package_type": "package", "sha256": "337d2a3e1b65926cd6560d0c9a33f8be55d6b8a21e846091bc7ab8fdb2f421a3", "shared_library": false, "unvendored_tests": true, "version": "0.6.0"}, "annotated-types-tests": {"depends": ["annotated-types"], "file_name": "annotated-types-tests.tar", "imports": [], "install_dir": "site", "name": "annotated-types-tests", "package_type": "package", "sha256": "0eecd674a295f84758689100739cca7d54f73cd6288c41f2269f78a01e465a8d", "shared_library": false, "unvendored_tests": false, "version": "0.6.0"}, "asciitree": {"depends": [], "file_name": "asciitree-0.3.3-py3-none-any.whl", "imports": ["asciitree"], "install_dir": "site", "name": "asciitree", "package_type": "package", "sha256": "15d47009b9cacdddacbc7ef306e64d103ea96d4fdc0fbb2d579e43c8fe8666bf", "shared_library": false, "unvendored_tests": false, "version": "0.3.3"}, "astropy": {"depends": ["packaging", "numpy", "pyerfa", "pyyaml", "astropy_iers_data"], "file_name": "astropy-6.0.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["astropy"], "install_dir": "site", "name": "astropy", "package_type": "package", "sha256": "94be600bfff4973c962112913bf86b5d1c4ba8b162b6ef409a03f70a0cefb9e9", "shared_library": false, "unvendored_tests": false, "version": "6.0.1"}, "astropy-iers-data": {"depends": [], "file_name": "astropy_iers_data-0.2024.4.22.0.29.50-py3-none-any.whl", "imports": ["astropy_iers_data"], "install_dir": "site", "name": "astropy_iers_data", "package_type": "package", "sha256": "b9989c71b05bc8a550d74d7e49d5e041f192f420bdf8b71553adb225e286f478", "shared_library": false, "unvendored_tests": true, "version": "0.2024.4.22.0.29.50"}, "astropy-iers-data-tests": {"depends": ["astropy_iers_data"], "file_name": "astropy_iers_data-tests.tar", "imports": [], "install_dir": "site", "name": "astropy_iers_data-tests", "package_type": "package", "sha256": "3cbaffbea097e4d5f206a7f357b59871a67594cd3f899ad45a28c293c271d4f1", "shared_library": false, "unvendored_tests": false, "version": "0.2024.4.22.0.29.50"}, "asttokens": {"depends": ["six"], "file_name": "asttokens-2.4.1-py2.py3-none-any.whl", "imports": ["asttokens"], "install_dir": "site", "name": "asttokens", "package_type": "package", "sha256": "4f62a79cfd557b35cd1f1e4809c61eef0b6e54e0dea5270653bbde3fc341d05a", "shared_library": false, "unvendored_tests": false, "version": "2.4.1"}, "async-timeout": {"depends": [], "file_name": "async_timeout-4.0.3-py3-none-any.whl", "imports": ["async_timeout"], "install_dir": "site", "name": "async-timeout", "package_type": "package", "sha256": "39d42f0d92a009c9205d74a01ff194d89ea9f1741af36998bc405c615993779e", "shared_library": false, "unvendored_tests": false, "version": "4.0.3"}, "atomicwrites": {"depends": [], "file_name": "atomicwrites-1.4.1-py2.py3-none-any.whl", "imports": ["atomicwrites"], "install_dir": "site", "name": "atomicwrites", "package_type": "package", "sha256": "7ac6f1fb3dde1c23246b08b6d2a380fb9e1b344e57126f00bd9364ad1104bc82", "shared_library": false, "unvendored_tests": false, "version": "1.4.1"}, "attrs": {"depends": ["six"], "file_name": "attrs-23.2.0-py3-none-any.whl", "imports": ["attr", "attrs"], "install_dir": "site", "name": "attrs", "package_type": "package", "sha256": "8e0e26b04b67bd3f09a60f289a0673aadb45daef63c8a6ceca762159d41bdea8", "shared_library": false, "unvendored_tests": false, "version": "23.2.0"}, "autograd": {"depends": ["numpy", "future"], "file_name": "autograd-1.6.2-py3-none-any.whl", "imports": ["autograd"], "install_dir": "site", "name": "autograd", "package_type": "package", "sha256": "4fd6746b144d95de5e8dceae6d2d4fa2a962810b93e420430df74506e915af67", "shared_library": false, "unvendored_tests": true, "version": "1.6.2"}, "autograd-tests": {"depends": ["autograd"], "file_name": "autograd-tests.tar", "imports": [], "install_dir": "site", "name": "autograd-tests", "package_type": "package", "sha256": "4869f8c5b9bbdedfb84ebe2f866934dd9204f62d65b8e2e1c87feb5f769afec3", "shared_library": false, "unvendored_tests": false, "version": "1.6.2"}, "awkward-cpp": {"depends": ["numpy"], "file_name": "awkward_cpp-33-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["awkward_cpp"], "install_dir": "site", "name": "awkward-cpp", "package_type": "package", "sha256": "6dba5ce80f904ade19758b4e0b1e3bc4681aa0bb5e414d6a243251bc3ff9fd10", "shared_library": false, "unvendored_tests": false, "version": "33"}, "b2d": {"depends": ["numpy", "pydantic", "setuptools", "annotated-types"], "file_name": "b2d-0.7.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["b2d"], "install_dir": "site", "name": "b2d", "package_type": "package", "sha256": "6a2c8d02641ff9a65b19adfbe0f0aeef1ad1a8a8915a91122d554c3335c82faa", "shared_library": false, "unvendored_tests": false, "version": "0.7.4"}, "bcrypt": {"depends": [], "file_name": "bcrypt-4.1.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["bcrypt"], "install_dir": "site", "name": "bcrypt", "package_type": "package", "sha256": "8981e7922af6f1f6e2ad31ef120c67bfee047916e0b764ed5cd2a1ed8eaadbc7", "shared_library": false, "unvendored_tests": false, "version": "4.1.2"}, "beautifulsoup4": {"depends": ["soupsieve"], "file_name": "beautifulsoup4-4.12.3-py3-none-any.whl", "imports": ["bs4"], "install_dir": "site", "name": "beautifulsoup4", "package_type": "package", "sha256": "94a8052bb54628e76229fe6a1d1d16aa360f265c15abbca97497dad5370f7a7b", "shared_library": false, "unvendored_tests": true, "version": "4.12.3"}, "beautifulsoup4-tests": {"depends": ["beautifulsoup4"], "file_name": "beautifulsoup4-tests.tar", "imports": [], "install_dir": "site", "name": "beautifulsoup4-tests", "package_type": "package", "sha256": "55ec265dec8d21577aad92c4f7f2e5b22f3edecdc68fe003e525946bb088c44d", "shared_library": false, "unvendored_tests": false, "version": "4.12.3"}, "biopython": {"depends": ["numpy"], "file_name": "biopython-1.83-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["Bio", "BioSQL"], "install_dir": "site", "name": "biopython", "package_type": "package", "sha256": "4102d8fa77feca014bb2d49bebbae50cb6b0583737aff4ad02a3efaa6accd55c", "shared_library": false, "unvendored_tests": false, "version": "1.83"}, "bitarray": {"depends": [], "file_name": "bitarray-2.9.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["bitarray"], "install_dir": "site", "name": "bitarray", "package_type": "package", "sha256": "c7cfd44b70d8e6d5e26f81dbd7c7ac96cb562f6dceed02d6af3fdd23e6f74888", "shared_library": false, "unvendored_tests": true, "version": "2.9.2"}, "bitarray-tests": {"depends": ["bitarray"], "file_name": "bitarray-tests.tar", "imports": [], "install_dir": "site", "name": "bitarray-tests", "package_type": "package", "sha256": "3378a1981df0a26a423dde6ab332a4965447e35af539ceabfe4e330bed05f933", "shared_library": false, "unvendored_tests": false, "version": "2.9.2"}, "bitstring": {"depends": ["bitarray"], "file_name": "bitstring-4.1.4-py3-none-any.whl", "imports": ["bitstring"], "install_dir": "site", "name": "bitstring", "package_type": "package", "sha256": "fdea8060e5d10fe019b702aba16c3b0a349abca3230640dd70d57e49e824d127", "shared_library": false, "unvendored_tests": false, "version": "4.1.4"}, "bleach": {"depends": ["webencodings", "packaging", "six"], "file_name": "bleach-6.1.0-py3-none-any.whl", "imports": ["bleach"], "install_dir": "site", "name": "bleach", "package_type": "package", "sha256": "5e12c4669d0caeb23584e9fd5115a7c81c7d8cd10915c64e6f3357e549e3179d", "shared_library": false, "unvendored_tests": false, "version": "6.1.0"}, "bokeh": {"depends": ["contourpy", "numpy", "jinja2", "pandas", "pillow", "python-dateutil", "six", "typing-extensions", "pyyaml", "xyzservices"], "file_name": "bokeh-3.4.1-py3-none-any.whl", "imports": ["bokeh"], "install_dir": "site", "name": "bokeh", "package_type": "package", "sha256": "5617ba021385e030d02822fee0f10d2a58e97951472ddd6eaae4fc8682ff276f", "shared_library": false, "unvendored_tests": false, "version": "3.4.1"}, "boost-histogram": {"depends": ["numpy"], "file_name": "boost_histogram-1.4.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["boost_histogram"], "install_dir": "site", "name": "boost-histogram", "package_type": "package", "sha256": "76cb6e74ed49b53043fa55a0896279c3f83a8eb9b749f7a710360d08f2e324a4", "shared_library": false, "unvendored_tests": false, "version": "1.4.1"}, "brotli": {"depends": [], "file_name": "Brotli-1.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["brotli"], "install_dir": "site", "name": "brotli", "package_type": "package", "sha256": "60f96da7ec930a6c71d92f9f4c811ac96748d80c7f83f0664f2b59c08052f173", "shared_library": false, "unvendored_tests": false, "version": "1.1.0"}, "cachetools": {"depends": [], "file_name": "cachetools-5.3.3-py3-none-any.whl", "imports": ["cachetools"], "install_dir": "site", "name": "cachetools", "package_type": "package", "sha256": "89aabb74b24badd4557a1e4b2d6e2ac000089aeb4083b1c62afed95a40c88d90", "shared_library": false, "unvendored_tests": false, "version": "5.3.3"}, "cartopy": {"depends": ["shapely", "pyshp", "pyproj", "geos", "matplotlib", "scipy"], "file_name": "Cartopy-0.23.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cartopy"], "install_dir": "site", "name": "Cartopy", "package_type": "package", "sha256": "d202ba6909b904b13d2013ab791651e24658dae3c7dfb930ae14b2522a6064b9", "shared_library": false, "unvendored_tests": true, "version": "0.23.0"}, "cartopy-tests": {"depends": ["cartopy"], "file_name": "Cartopy-tests.tar", "imports": [], "install_dir": "site", "name": "Cartopy-tests", "package_type": "package", "sha256": "e817f56f9745cbe6a20df6aa41c03967fd82508354e9f5b8ddda9886be3be903", "shared_library": false, "unvendored_tests": false, "version": "0.23.0"}, "cbor-diag": {"depends": [], "file_name": "cbor_diag-1.0.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cbor_diag"], "install_dir": "site", "name": "cbor-diag", "package_type": "package", "sha256": "e4f0f8e870c80e76a50edc2a12883a90b87c8c8a6296444cd32c58157aebef2a", "shared_library": false, "unvendored_tests": false, "version": "1.0.1"}, "certifi": {"depends": [], "file_name": "certifi-2024.2.2-py3-none-any.whl", "imports": ["certifi"], "install_dir": "site", "name": "certifi", "package_type": "package", "sha256": "60307c886a375d40cf3ba444151c347b4271e9dcd7f432517896dcd692dafc62", "shared_library": false, "unvendored_tests": false, "version": "2024.2.2"}, "cffi": {"depends": ["pycparser"], "file_name": "cffi-1.16.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cffi"], "install_dir": "site", "name": "cffi", "package_type": "package", "sha256": "4a993a87e8a955d817656931640ee3a5e2abb887e73397def631613b1ee91273", "shared_library": false, "unvendored_tests": false, "version": "1.16.0"}, "cffi-example": {"depends": ["cffi"], "file_name": "cffi_example-0.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cffi_example"], "install_dir": "site", "name": "cffi_example", "package_type": "package", "sha256": "0419901f15584f393072aabafdaf30b9469fdd515771b15fa0348ad4145303de", "shared_library": false, "unvendored_tests": false, "version": "0.1"}, "cftime": {"depends": ["numpy"], "file_name": "cftime-1.6.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cftime"], "install_dir": "site", "name": "cftime", "package_type": "package", "sha256": "af2ffd030f317b437c9d8b51348d5203c06c6d62363b29780d83c0f0b37f7912", "shared_library": false, "unvendored_tests": false, "version": "1.6.3"}, "charset-normalizer": {"depends": [], "file_name": "charset_normalizer-3.3.2-py3-none-any.whl", "imports": ["charset_normalizer"], "install_dir": "site", "name": "charset-normalizer", "package_type": "package", "sha256": "b7159d1583fd0938540590346a637e8f4661d9a31461c1a7bc57893a71089acb", "shared_library": false, "unvendored_tests": false, "version": "3.3.2"}, "clarabel": {"depends": ["numpy", "scipy"], "file_name": "clarabel-0.7.1-cp37-abi3-pyodide_2024_0_wasm32.whl", "imports": ["clarabel"], "install_dir": "site", "name": "clarabel", "package_type": "package", "sha256": "2065e3eae13ee52440cadd32e373bd748536a572698f2e722315948f1bf1800c", "shared_library": false, "unvendored_tests": false, "version": "0.7.1"}, "click": {"depends": [], "file_name": "click-8.1.7-py3-none-any.whl", "imports": ["click"], "install_dir": "site", "name": "click", "package_type": "package", "sha256": "a91efee37121d94b0a1584676a08fba266acf76ca821274cbd7bf6abbb7984df", "shared_library": false, "unvendored_tests": false, "version": "8.1.7"}, "cligj": {"depends": ["click"], "file_name": "cligj-0.7.2-py3-none-any.whl", "imports": ["cligj"], "install_dir": "site", "name": "cligj", "package_type": "package", "sha256": "b938b3aa1e035663db0077f98b889af0f6e3a1fcba727392ef0fdb999c061a76", "shared_library": false, "unvendored_tests": false, "version": "0.7.2"}, "cloudpickle": {"depends": [], "file_name": "cloudpickle-3.0.0-py3-none-any.whl", "imports": ["cloudpickle"], "install_dir": "site", "name": "cloudpickle", "package_type": "package", "sha256": "c658a09163430185fcc8b091cccec3b7809a66a89ce366ddcea721bae39082f6", "shared_library": false, "unvendored_tests": false, "version": "3.0.0"}, "cmyt": {"depends": ["colorspacious", "matplotlib", "more-itertools", "numpy"], "file_name": "cmyt-2.0.0-py3-none-any.whl", "imports": ["cmyt"], "install_dir": "site", "name": "cmyt", "package_type": "package", "sha256": "5e99f7f3922eebd62e1570a254cd6aba140efadc5f5dab743ad16859f84071d3", "shared_library": false, "unvendored_tests": true, "version": "2.0.0"}, "cmyt-tests": {"depends": ["cmyt"], "file_name": "cmyt-tests.tar", "imports": [], "install_dir": "site", "name": "cmyt-tests", "package_type": "package", "sha256": "8cabd34a691476a4c425459e8ca0cf39c34d552a34e23ced302edc3c3257a671", "shared_library": false, "unvendored_tests": false, "version": "2.0.0"}, "colorspacious": {"depends": ["numpy"], "file_name": "colorspacious-1.1.2-py2.py3-none-any.whl", "imports": ["colorspacious"], "install_dir": "site", "name": "colorspacious", "package_type": "package", "sha256": "acee2628cc1eafc4ef7456563bb3dbe0f1578e95dd96a02d8127b09a552dbd33", "shared_library": false, "unvendored_tests": false, "version": "1.1.2"}, "contourpy": {"depends": ["numpy"], "file_name": "contourpy-1.2.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["contourpy"], "install_dir": "site", "name": "contourpy", "package_type": "package", "sha256": "8cc70108fea11bc60feb5d38fa4546155f00407c8ffcd082600c62bf0278b0a5", "shared_library": false, "unvendored_tests": false, "version": "1.2.1"}, "coolprop": {"depends": ["numpy", "matplotlib"], "file_name": "CoolProp-6.6.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["CoolProp"], "install_dir": "site", "name": "coolprop", "package_type": "package", "sha256": "aa0083a1067adc59aa8741e10ea67b677f2693e0fe95574ca4589169cef2d03b", "shared_library": false, "unvendored_tests": true, "version": "6.6.0"}, "coolprop-tests": {"depends": ["coolprop"], "file_name": "coolprop-tests.tar", "imports": [], "install_dir": "site", "name": "coolprop-tests", "package_type": "package", "sha256": "82b2574e6b4ee184156367481db9b9479f7f3d6cf5880940b0e726f866f3677c", "shared_library": false, "unvendored_tests": false, "version": "6.6.0"}, "coverage": {"depends": ["sqlite3"], "file_name": "coverage-7.4.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["coverage"], "install_dir": "site", "name": "coverage", "package_type": "package", "sha256": "606a1a385c30766260b5b7d2f7ded768a5f91e29b0c090c5bbac7b2bb2b2c2e7", "shared_library": false, "unvendored_tests": false, "version": "7.4.4"}, "cpp-exceptions-test": {"depends": [], "file_name": "cpp-exceptions-test-0.1.zip", "imports": [], "install_dir": "dynlib", "name": "cpp-exceptions-test", "package_type": "shared_library", "sha256": "23ee6f17609635436bc8cced518a3adec8bdf095f0dda8062166e39077cf8005", "shared_library": true, "unvendored_tests": false, "version": "0.1"}, "cpp-exceptions-test2": {"depends": [], "file_name": "cpp_exceptions_test2-1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cpp-exceptions-test2"], "install_dir": "site", "name": "cpp-exceptions-test2", "package_type": "package", "sha256": "5d5c8a58219d050452190b4fbe3a0577c9da2ac557822a977d34c08b1ff4cac9", "shared_library": false, "unvendored_tests": false, "version": "1.0"}, "cramjam": {"depends": [], "file_name": "cramjam-2.8.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cramjam"], "install_dir": "site", "name": "cramjam", "package_type": "package", "sha256": "42c509a8c042b90f8d39b2b428d7512e40110b6bc8bff417ff5f345fba3860bd", "shared_library": false, "unvendored_tests": false, "version": "2.8.3"}, "crc32c": {"depends": [], "file_name": "crc32c-2.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["crc32c"], "install_dir": "site", "name": "crc32c", "package_type": "package", "sha256": "c0703c9b40d31758e7d836a2f0657c343a8a60d2e09bb2826e0d01febe41b6a4", "shared_library": false, "unvendored_tests": false, "version": "2.4"}, "cryptography": {"depends": ["openssl", "six", "cffi"], "file_name": "cryptography-42.0.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cryptography"], "install_dir": "site", "name": "cryptography", "package_type": "package", "sha256": "c05ea45f7aa36950b2f7c4216147e51abd6e6899656c512e106c7043bc7772d9", "shared_library": false, "unvendored_tests": false, "version": "42.0.5"}, "cssselect": {"depends": [], "file_name": "cssselect-1.2.0-py2.py3-none-any.whl", "imports": ["cssselect"], "install_dir": "site", "name": "cssselect", "package_type": "package", "sha256": "a835c843cac791f67be9d6ad2eabe0d2690cab0c147902ffe604018271bc557c", "shared_library": false, "unvendored_tests": false, "version": "1.2.0"}, "cvxpy-base": {"depends": ["numpy", "scipy", "clarabel"], "file_name": "cvxpy_base-1.5.1-py3-none-any.whl", "imports": ["cvxpy"], "install_dir": "site", "name": "cvxpy-base", "package_type": "package", "sha256": "43dcbeee1e7875516cdf6d3a227cc9d6720b91dc0765c2f4a2dfe1a297607324", "shared_library": false, "unvendored_tests": true, "version": "1.5.1"}, "cvxpy-base-tests": {"depends": ["cvxpy-base"], "file_name": "cvxpy-base-tests.tar", "imports": [], "install_dir": "site", "name": "cvxpy-base-tests", "package_type": "package", "sha256": "a18ac6cec0d4a36e5297d9920717bfdab1db27b56ae9aae245c04051b8bf6990", "shared_library": false, "unvendored_tests": false, "version": "1.5.1"}, "cycler": {"depends": ["six"], "file_name": "cycler-0.12.1-py3-none-any.whl", "imports": ["cycler"], "install_dir": "site", "name": "cycler", "package_type": "package", "sha256": "62243ceb501f58ad420395814f88d259d7546d115bd652e67d281b476b17c96e", "shared_library": false, "unvendored_tests": false, "version": "0.12.1"}, "cysignals": {"depends": [], "file_name": "cysignals-1.11.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cysignals"], "install_dir": "site", "name": "cysignals", "package_type": "package", "sha256": "ff6b27611229d4fffd3ff5f9a8d88854e5181441a2e9c2e337e6bc79ee14de5e", "shared_library": false, "unvendored_tests": false, "version": "1.11.4"}, "cytoolz": {"depends": ["toolz"], "file_name": "cytoolz-0.12.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cytoolz"], "install_dir": "site", "name": "cytoolz", "package_type": "package", "sha256": "3db082ceb3c2e49842ad8065d5d9a48cadfa58752d0ee00cf0d9b86cb4b3cf8c", "shared_library": false, "unvendored_tests": true, "version": "0.12.3"}, "cytoolz-tests": {"depends": ["cytoolz"], "file_name": "cytoolz-tests.tar", "imports": [], "install_dir": "site", "name": "cytoolz-tests", "package_type": "package", "sha256": "9b8bae8b847080f2530f764d63b4a87df456f96a63eadb2eefc8884a47beee2b", "shared_library": false, "unvendored_tests": false, "version": "0.12.3"}, "decorator": {"depends": [], "file_name": "decorator-5.1.1-py3-none-any.whl", "imports": ["decorator"], "install_dir": "site", "name": "decorator", "package_type": "package", "sha256": "e7b2576b7ab0f7fab4a8260ce091e610dedeb2f7874c328db3920c5b1d06a57e", "shared_library": false, "unvendored_tests": false, "version": "5.1.1"}, "demes": {"depends": ["attrs", "ruamel.yaml"], "file_name": "demes-0.2.3-py3-none-any.whl", "imports": ["demes"], "install_dir": "site", "name": "demes", "package_type": "package", "sha256": "8be66f0c28a713b8efc6626b4aeb7cdeea70818b2ff3877ebb731a0e6a638fdd", "shared_library": false, "unvendored_tests": false, "version": "0.2.3"}, "deprecation": {"depends": ["packaging"], "file_name": "deprecation-2.1.0-py2.py3-none-any.whl", "imports": ["deprecation"], "install_dir": "site", "name": "deprecation", "package_type": "package", "sha256": "7e29e9de88b1bb7319e512cf74e474e47fe216978df72fe696504c464f564088", "shared_library": false, "unvendored_tests": false, "version": "2.1.0"}, "distlib": {"depends": [], "file_name": "distlib-0.3.8-py2.py3-none-any.whl", "imports": ["distlib"], "install_dir": "site", "name": "distlib", "package_type": "package", "sha256": "c38f21e1668748621aa187e1d77017a072cad344df5b17e855c87d7b29cbeb56", "shared_library": false, "unvendored_tests": false, "version": "0.3.8"}, "docutils": {"depends": [], "file_name": "docutils-0.21.1-py3-none-any.whl", "imports": ["docutils"], "install_dir": "site", "name": "docutils", "package_type": "package", "sha256": "820ce56834fd002d80f0512070f7d71565f076de184287a491cbad6273e5a76c", "shared_library": false, "unvendored_tests": false, "version": "0.21.1"}, "ewah-bool-utils": {"depends": [], "file_name": "ewah_bool_utils-1.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["ewah_bool_utils"], "install_dir": "site", "name": "ewah_bool_utils", "package_type": "package", "sha256": "2d64e4cc33e22bc6e54a3cea53f6b7b156f9b5d24c81a012eaab449edb64b2bd", "shared_library": false, "unvendored_tests": true, "version": "1.2.0"}, "ewah-bool-utils-tests": {"depends": ["ewah_bool_utils"], "file_name": "ewah_bool_utils-tests.tar", "imports": [], "install_dir": "site", "name": "ewah_bool_utils-tests", "package_type": "package", "sha256": "1bb3b4036603e5e7a1209236f78105e7036af3b0b615dd38b2bc2c92b60232e8", "shared_library": false, "unvendored_tests": false, "version": "1.2.0"}, "exceptiongroup": {"depends": [], "file_name": "exceptiongroup-1.2.1-py3-none-any.whl", "imports": ["exceptiongroup"], "install_dir": "site", "name": "exceptiongroup", "package_type": "package", "sha256": "92b9f147b8f6e461cf0caf268db6a6e2ee7efb228d60d80f7dd92d521d59e469", "shared_library": false, "unvendored_tests": false, "version": "1.2.1"}, "executing": {"depends": [], "file_name": "executing-2.0.1-py2.py3-none-any.whl", "imports": ["executing"], "install_dir": "site", "name": "executing", "package_type": "package", "sha256": "9d446583b446b5795bc02ae5e92eebd5ffba061287b3940f45462f2612f47b6a", "shared_library": false, "unvendored_tests": false, "version": "2.0.1"}, "fastparquet": {"depends": ["cramjam", "numpy", "pandas", "fsspec", "packaging"], "file_name": "fastparquet-2024.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["fastparquet"], "install_dir": "site", "name": "fastparquet", "package_type": "package", "sha256": "ea0392fe11e9fbdea79ad8199d13a38798eb9981413e08539e6bbd33d17b85be", "shared_library": false, "unvendored_tests": false, "version": "2024.2.0"}, "fiona": {"depends": ["attrs", "certifi", "setuptools", "six", "click", "cligj"], "file_name": "fiona-1.9.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["fiona"], "install_dir": "site", "name": "fiona", "package_type": "package", "sha256": "0f55d6c5d5b09d420e2b58d010967b5e2e43c1ffdf3aa1d559951ac10d893655", "shared_library": false, "unvendored_tests": true, "version": "1.9.5"}, "fiona-tests": {"depends": ["fiona"], "file_name": "fiona-tests.tar", "imports": [], "install_dir": "site", "name": "fiona-tests", "package_type": "package", "sha256": "6b82496111cea9551fa09174c70c47c12ceb6e5885c8511683e3bbf7b7e29ff5", "shared_library": false, "unvendored_tests": false, "version": "1.9.5"}, "fonttools": {"depends": [], "file_name": "fonttools-4.51.0-py3-none-any.whl", "imports": ["fontTools"], "install_dir": "site", "name": "fonttools", "package_type": "package", "sha256": "e27bd9df5b39f6210c66571b12aa2372310e9e96aee0b605fc5021716b5f3249", "shared_library": false, "unvendored_tests": false, "version": "4.51.0"}, "fpcast-test": {"depends": [], "file_name": "fpcast_test-0.1.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["fpcast_test"], "install_dir": "site", "name": "fpcast-test", "package_type": "package", "sha256": "0f72430fbccb73c6690a931413032f9c1892bd07b7af2ba59f32374ec4970756", "shared_library": false, "unvendored_tests": false, "version": "0.1.1"}, "freesasa": {"depends": [], "file_name": "freesasa-2.2.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["freesasa"], "install_dir": "site", "name": "freesasa", "package_type": "package", "sha256": "d3d478c7aa17ca7b6caf6609cc3152929f4cb43d20fdda7217e648aad96acf0e", "shared_library": false, "unvendored_tests": false, "version": "2.2.1"}, "frozenlist": {"depends": [], "file_name": "frozenlist-1.4.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["frozenlist"], "install_dir": "site", "name": "frozenlist", "package_type": "package", "sha256": "481ab66c7f7b932d8fadfadece1bd9928c0ee471da75cff2baf22845baea2a2c", "shared_library": false, "unvendored_tests": false, "version": "1.4.1"}, "fsspec": {"depends": [], "file_name": "fsspec-2024.3.1-py3-none-any.whl", "imports": ["fsspec"], "install_dir": "site", "name": "fsspec", "package_type": "package", "sha256": "2f1439e76d695b8414d89c7c85cdb0ce0f227be1ef083f07e8c9452e0718d9c8", "shared_library": false, "unvendored_tests": true, "version": "2024.3.1"}, "fsspec-tests": {"depends": ["fsspec"], "file_name": "fsspec-tests.tar", "imports": [], "install_dir": "site", "name": "fsspec-tests", "package_type": "package", "sha256": "0f777d658d32926b150c87c88b5e839545d6ad88320818027908ac83cf7bd8de", "shared_library": false, "unvendored_tests": false, "version": "2024.3.1"}, "future": {"depends": [], "file_name": "future-1.0.0-py3-none-any.whl", "imports": ["future"], "install_dir": "site", "name": "future", "package_type": "package", "sha256": "445e96936c4f8ae76ddb2fa505308eeda42af28db4584037634318c3ebaf9d03", "shared_library": false, "unvendored_tests": true, "version": "1.0.0"}, "future-tests": {"depends": ["future"], "file_name": "future-tests.tar", "imports": [], "install_dir": "site", "name": "future-tests", "package_type": "package", "sha256": "eb8fedb6b9848fa3ab6fa1362f81b7d1c5e8c89773783d923584b95a3b97547f", "shared_library": false, "unvendored_tests": false, "version": "1.0.0"}, "galpy": {"depends": ["numpy", "scipy", "matplotlib", "astropy", "future", "setuptools"], "file_name": "galpy-1.9.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["galpy"], "install_dir": "site", "name": "galpy", "package_type": "package", "sha256": "8f5af7d9d1c7a558fe0ac137ad88f59185bdf30dec8649f291403eceff248b98", "shared_library": false, "unvendored_tests": false, "version": "1.9.2"}, "gdal": {"depends": ["geos"], "file_name": "gdal-3.8.3.zip", "imports": [], "install_dir": "dynlib", "name": "gdal", "package_type": "shared_library", "sha256": "30401f5d6894f73556a95de723c278de64c617dc35a10894b77154c94292b572", "shared_library": true, "unvendored_tests": false, "version": "3.8.3"}, "gensim": {"depends": ["numpy", "scipy", "six", "smart_open", "wrapt"], "file_name": "gensim-4.3.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["gensim"], "install_dir": "site", "name": "gensim", "package_type": "package", "sha256": "3c4b346db7bc0fd0174881be499333eef4aade2aca690beeef4a1a5fd05b6815", "shared_library": false, "unvendored_tests": true, "version": "4.3.2"}, "gensim-tests": {"depends": ["gensim"], "file_name": "gensim-tests.tar", "imports": [], "install_dir": "site", "name": "gensim-tests", "package_type": "package", "sha256": "5ceb546ac41bb973d08be7c44a5732ce29f5720b5f35102149e4f6961223e19d", "shared_library": false, "unvendored_tests": false, "version": "4.3.2"}, "geopandas": {"depends": ["shapely", "fiona", "pyproj", "packaging", "pandas"], "file_name": "geopandas-0.14.3-py3-none-any.whl", "imports": ["geopandas"], "install_dir": "site", "name": "geopandas", "package_type": "package", "sha256": "ad8eade2c6f45ba17c878a302910ebc3d6f7cecd009cdb5d63d140781afb4a21", "shared_library": false, "unvendored_tests": true, "version": "0.14.3"}, "geopandas-tests": {"depends": ["geopandas"], "file_name": "geopandas-tests.tar", "imports": [], "install_dir": "site", "name": "geopandas-tests", "package_type": "package", "sha256": "40703d011365f2763e7dd7514be85e65af8f488671ab4f1aaaacc7ad1a14e23e", "shared_library": false, "unvendored_tests": false, "version": "0.14.3"}, "geos": {"depends": [], "file_name": "geos-3.12.1.zip", "imports": [], "install_dir": "dynlib", "name": "geos", "package_type": "shared_library", "sha256": "150ef1387663f8ddebb9e2b2d49c556f60ccfe3d6a8c3b2334f22bd2e3ad9e52", "shared_library": true, "unvendored_tests": false, "version": "3.12.1"}, "gmpy2": {"depends": [], "file_name": "gmpy2-2.1.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["gmpy2"], "install_dir": "site", "name": "gmpy2", "package_type": "package", "sha256": "60b06b4d6ad774cd76ce94dea287cab3d27fe846477ce5f1e5db58e9ebe7596b", "shared_library": false, "unvendored_tests": false, "version": "2.1.5"}, "gsw": {"depends": ["numpy"], "file_name": "gsw-3.6.17-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["gsw"], "install_dir": "site", "name": "gsw", "package_type": "package", "sha256": "abba76d5a7da89cd64646e7856599f31621ae9f48951cad5eac9ed75ab3ee031", "shared_library": false, "unvendored_tests": true, "version": "3.6.17"}, "gsw-tests": {"depends": ["gsw"], "file_name": "gsw-tests.tar", "imports": [], "install_dir": "site", "name": "gsw-tests", "package_type": "package", "sha256": "c1dcfb0baf3f2891f68ed569ed7f08264bca69cc210e1681d9628b64ce42b02d", "shared_library": false, "unvendored_tests": false, "version": "3.6.17"}, "h5py": {"depends": ["numpy", "pkgconfig"], "file_name": "h5py-3.11.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["h5py"], "install_dir": "site", "name": "h5py", "package_type": "package", "sha256": "5d7604354ff24428521620d75352946656375e7a725e7040c976f7be0a908055", "shared_library": false, "unvendored_tests": true, "version": "3.11.0"}, "h5py-tests": {"depends": ["h5py"], "file_name": "h5py-tests.tar", "imports": [], "install_dir": "site", "name": "h5py-tests", "package_type": "package", "sha256": "6f4d260fb05e96386f5091941bb520c804447436983e24901d4e7ba831ed15e7", "shared_library": false, "unvendored_tests": false, "version": "3.11.0"}, "hashlib": {"depends": ["openssl"], "file_name": "hashlib-1.0.0.zip", "imports": ["_hashlib"], "install_dir": "stdlib", "name": "hashlib", "package_type": "cpython_module", "sha256": "2608deda99041715fec1152b2d2d377f591035fcab44a76e270a898b9f70f580", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "html5lib": {"depends": ["webencodings", "six"], "file_name": "html5lib-1.1-py2.py3-none-any.whl", "imports": ["html5lib"], "install_dir": "site", "name": "html5lib", "package_type": "package", "sha256": "58e4002edf04a42f3ec887c5b68c432b21427603b4d1090e379259f6678a3d58", "shared_library": false, "unvendored_tests": false, "version": "1.1"}, "idna": {"depends": [], "file_name": "idna-3.7-py3-none-any.whl", "imports": ["idna"], "install_dir": "site", "name": "idna", "package_type": "package", "sha256": "9fda8aaea4574988fcd20f729a3b6df5b22488245ef347ec63bfe794325d4def", "shared_library": false, "unvendored_tests": false, "version": "3.7"}, "igraph": {"depends": ["texttable"], "file_name": "igraph-0.11.4-cp39-abi3-pyodide_2024_0_wasm32.whl", "imports": ["igraph"], "install_dir": "site", "name": "igraph", "package_type": "package", "sha256": "d6030d268459626e1db94e26ec6f8d372e808e9a9cdf4d684cbe6bb2e6afa866", "shared_library": false, "unvendored_tests": false, "version": "0.11.4"}, "imageio": {"depends": ["numpy", "pillow"], "file_name": "imageio-2.34.1-py3-none-any.whl", "imports": ["imageio"], "install_dir": "site", "name": "imageio", "package_type": "package", "sha256": "052dc45002387ef8b9fd79693c99e67591bcaaf1ca9a82cb67f0de348325122b", "shared_library": false, "unvendored_tests": false, "version": "2.34.1"}, "iniconfig": {"depends": [], "file_name": "iniconfig-2.0.0-py3-none-any.whl", "imports": ["iniconfig"], "install_dir": "site", "name": "iniconfig", "package_type": "package", "sha256": "f9cab05fb8a8d99890626831d140b0f9c26c44ed38a520b89c42ff07afef929d", "shared_library": false, "unvendored_tests": false, "version": "2.0.0"}, "ipython": {"depends": ["asttokens", "decorator", "executing", "matplotlib-inline", "prompt_toolkit", "pure_eval", "pygments", "six", "stack_data", "traitlets", "sqlite3", "wcwidth"], "file_name": "ipython-8.23.0-py3-none-any.whl", "imports": ["IPython"], "install_dir": "site", "name": "ipython", "package_type": "package", "sha256": "cff64d70b073fb2dcf718ee4abd47767373171b864e3d30f890f1573f0654c22", "shared_library": false, "unvendored_tests": true, "version": "8.23.0"}, "ipython-tests": {"depends": ["ipython"], "file_name": "ipython-tests.tar", "imports": [], "install_dir": "site", "name": "ipython-tests", "package_type": "package", "sha256": "b5a49455fa364805cfd9b765a1c68df0dc12cedd79e25ba4096cbe94f04bf10d", "shared_library": false, "unvendored_tests": false, "version": "8.23.0"}, "jedi": {"depends": ["parso"], "file_name": "jedi-0.19.1-py2.py3-none-any.whl", "imports": ["jedi"], "install_dir": "site", "name": "jedi", "package_type": "package", "sha256": "5ee59883b4346dcc0c88adc8973f1dc635b38779f17595a7abec794cc09e8840", "shared_library": false, "unvendored_tests": true, "version": "0.19.1"}, "jedi-tests": {"depends": ["jedi"], "file_name": "jedi-tests.tar", "imports": [], "install_dir": "site", "name": "jedi-tests", "package_type": "package", "sha256": "ca8d52714ffade10e237eed44cb8d359ade5ae6b92d8c7ba0ff66d00161f9dff", "shared_library": false, "unvendored_tests": false, "version": "0.19.1"}, "jinja2": {"depends": ["markupsafe"], "file_name": "Jinja2-3.1.3-py3-none-any.whl", "imports": ["jinja2"], "install_dir": "site", "name": "Jinja2", "package_type": "package", "sha256": "ed3b38b217a040a4dd6dd5f62fca8d0a18ab68f3d5c5a8fef12dcdfeaa9a4373", "shared_library": false, "unvendored_tests": false, "version": "3.1.3"}, "joblib": {"depends": [], "file_name": "joblib-1.4.0-py3-none-any.whl", "imports": ["joblib"], "install_dir": "site", "name": "joblib", "package_type": "package", "sha256": "e913d6bb8a168be5a89e76764eacbb56f8e93f99c8e19e93197da2a4f0331e3f", "shared_library": false, "unvendored_tests": true, "version": "1.4.0"}, "joblib-tests": {"depends": ["joblib"], "file_name": "joblib-tests.tar", "imports": [], "install_dir": "site", "name": "joblib-tests", "package_type": "package", "sha256": "a0e85b342cc9fdb888eef3328a7e99a0740f6a774953276f0a082793a3e10981", "shared_library": false, "unvendored_tests": false, "version": "1.4.0"}, "jsonschema": {"depends": ["attrs", "pyrsistent", "referencing", "jsonschema_specifications"], "file_name": "jsonschema-4.21.1-py3-none-any.whl", "imports": ["jsonschema"], "install_dir": "site", "name": "jsonschema", "package_type": "package", "sha256": "8e103c1dd50ecd40e385b2167ebb00182ed3aa3b455cd88d1edab5243c521503", "shared_library": false, "unvendored_tests": true, "version": "4.21.1"}, "jsonschema-specifications": {"depends": [], "file_name": "jsonschema_specifications-2023.12.1-py3-none-any.whl", "imports": ["jsonschema_specifications"], "install_dir": "site", "name": "jsonschema_specifications", "package_type": "package", "sha256": "40589d9387803ddaf07c51e0a935f6fd0af653826182dfc2eda0e6171320f83b", "shared_library": false, "unvendored_tests": true, "version": "2023.12.1"}, "jsonschema-specifications-tests": {"depends": ["jsonschema_specifications"], "file_name": "jsonschema_specifications-tests.tar", "imports": [], "install_dir": "site", "name": "jsonschema_specifications-tests", "package_type": "package", "sha256": "1aa663daca70e3b7560afb0097d1bcfd7cbea9f5fcd6cb290a020e66e1726f88", "shared_library": false, "unvendored_tests": false, "version": "2023.12.1"}, "jsonschema-tests": {"depends": ["jsonschema"], "file_name": "jsonschema-tests.tar", "imports": [], "install_dir": "site", "name": "jsonschema-tests", "package_type": "package", "sha256": "4f2b82f097f9379d0029c442fa67ed87fc37e3d53045dff293bb9f8491f96857", "shared_library": false, "unvendored_tests": false, "version": "4.21.1"}, "kiwisolver": {"depends": [], "file_name": "kiwisolver-1.4.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["kiwisolver"], "install_dir": "site", "name": "kiwisolver", "package_type": "package", "sha256": "93324048b98508b7c87c8827f7fb810803f4c19e1216d3f44ea39ebefce5d977", "shared_library": false, "unvendored_tests": false, "version": "1.4.5"}, "lakers-python": {"depends": [], "file_name": "lakers_python-0.3.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["lakers"], "install_dir": "site", "name": "lakers-python", "package_type": "package", "sha256": "a3ff1dd2c6b808ef58c25b889309a1183780faae47bc30a3d0c3fcf565225c46", "shared_library": false, "unvendored_tests": false, "version": "0.3.0"}, "lazy-loader": {"depends": [], "file_name": "lazy_loader-0.4-py3-none-any.whl", "imports": ["lazy_loader"], "install_dir": "site", "name": "lazy_loader", "package_type": "package", "sha256": "791a725032a6950412e9af99ea11e1c33767616f62fff82038dc5a1e011a2a15", "shared_library": false, "unvendored_tests": true, "version": "0.4"}, "lazy-loader-tests": {"depends": ["lazy_loader"], "file_name": "lazy_loader-tests.tar", "imports": [], "install_dir": "site", "name": "lazy_loader-tests", "package_type": "package", "sha256": "bb75854213b518faf51a33cf774b28c62db019a863e0c8b82336e15118402f07", "shared_library": false, "unvendored_tests": false, "version": "0.4"}, "lazy-object-proxy": {"depends": [], "file_name": "lazy_object_proxy-1.10.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["lazy_object_proxy"], "install_dir": "site", "name": "lazy-object-proxy", "package_type": "package", "sha256": "630987c74e6a8715032001b486625f6b5e319299e447b7ca9730bff8a476b49a", "shared_library": false, "unvendored_tests": false, "version": "1.10.0"}, "libcst": {"depends": ["pyyaml"], "file_name": "libcst-1.3.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["libcst"], "install_dir": "site", "name": "libcst", "package_type": "package", "sha256": "f503a45b3855bb1a008caf8f6ac1a71443bf66369cd6168dccc9f5331400a970", "shared_library": false, "unvendored_tests": true, "version": "1.3.1"}, "libcst-tests": {"depends": ["libcst"], "file_name": "libcst-tests.tar", "imports": [], "install_dir": "site", "name": "libcst-tests", "package_type": "package", "sha256": "61555281e0724435cf9ccf1722dfeb75776795c5d07dc143e06f0d8086e42700", "shared_library": false, "unvendored_tests": false, "version": "1.3.1"}, "libhdf5": {"depends": [], "file_name": "libhdf5-1.12.1.zip", "imports": [], "install_dir": "dynlib", "name": "libhdf5", "package_type": "shared_library", "sha256": "7cd1ee833910ded4bca697b2881f49e26d185bf84d5921baecdf84c6f3e78b8d", "shared_library": true, "unvendored_tests": false, "version": "1.12.1"}, "libheif": {"depends": [], "file_name": "libheif-1.12.0.zip", "imports": [], "install_dir": "dynlib", "name": "libheif", "package_type": "shared_library", "sha256": "ed954f71cd78d1fcf4f9aa93e34f598bbc9a87cc6a288c12f43b5dad800a85b2", "shared_library": true, "unvendored_tests": false, "version": "1.12.0"}, "libmagic": {"depends": [], "file_name": "libmagic-5.42.zip", "imports": [], "install_dir": "dynlib", "name": "libmagic", "package_type": "shared_library", "sha256": "b8c3aee549fcdb7484f24b5dd804192efc75f1de315523f44b626662cc807b67", "shared_library": true, "unvendored_tests": false, "version": "5.42"}, "libnetcdf": {"depends": [], "file_name": "libnetcdf-4.9.2.zip", "imports": [], "install_dir": "dynlib", "name": "libnetcdf", "package_type": "shared_library", "sha256": "25e03271a4ccc228f092d03d6e7001b622d87069f82b255781aaea09f793adaa", "shared_library": true, "unvendored_tests": false, "version": "4.9.2"}, "lightgbm": {"depends": ["numpy", "scipy", "scikit-learn"], "file_name": "lightgbm-4.3.0-py3-none-pyodide_2024_0_wasm32.whl", "imports": ["lightgbm"], "install_dir": "site", "name": "lightgbm", "package_type": "package", "sha256": "3bf6ce78f0341aa9f2ba194f61222bc1e91b1a60e2e2c46bc7bbd6fb5e8534b0", "shared_library": false, "unvendored_tests": false, "version": "4.3.0"}, "logbook": {"depends": ["ssl"], "file_name": "Logbook-1.7.0.post0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["logbook"], "install_dir": "site", "name": "logbook", "package_type": "package", "sha256": "ebdfee44251e45c6a3b7804f8742a477d374c732ec7851552e4f25eb267c1aef", "shared_library": false, "unvendored_tests": false, "version": "1.7.0.post0"}, "lxml": {"depends": [], "file_name": "lxml-5.2.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["lxml"], "install_dir": "site", "name": "lxml", "package_type": "package", "sha256": "71420ee4af4188f59c3e7acdf1d7cab4f4e2d10f9ec1bf4b1742f9075d4b36a2", "shared_library": false, "unvendored_tests": false, "version": "5.2.1"}, "lzma": {"depends": [], "file_name": "lzma-1.0.0.zip", "imports": ["lzma", "_lzma"], "install_dir": "stdlib", "name": "lzma", "package_type": "cpython_module", "sha256": "95f2169b9c57112556c5fc907c3429b572a204cf210048a9d65456ad8e3f4c14", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "markupsafe": {"depends": [], "file_name": "MarkupSafe-2.1.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["markupsafe"], "install_dir": "site", "name": "MarkupSafe", "package_type": "package", "sha256": "f2e79b5b66389a43f8381cece4b7dc03df1a28f9d083abe4e964f33bf9e9b72c", "shared_library": false, "unvendored_tests": false, "version": "2.1.5"}, "matplotlib": {"depends": ["cycler", "fonttools", "kiwisolver", "numpy", "packaging", "pillow", "pyparsing", "python-dateutil", "pytz", "matplotlib-pyodide"], "file_name": "matplotlib-3.5.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pylab", "mpl_toolkits", "matplotlib"], "install_dir": "site", "name": "matplotlib", "package_type": "package", "sha256": "670024b6ec06a27fe933dda6cbca5140497ff67d80c70f70b6e6a2d4ccef9b5d", "shared_library": false, "unvendored_tests": true, "version": "3.5.2"}, "matplotlib-inline": {"depends": ["traitlets"], "file_name": "matplotlib_inline-0.1.7-py3-none-any.whl", "imports": ["matplotlib-inline"], "install_dir": "site", "name": "matplotlib-inline", "package_type": "package", "sha256": "01eae0298c3b9d145a69317bbf9622c145130683bd5419b7112f6c2dcaed988a", "shared_library": false, "unvendored_tests": false, "version": "0.1.7"}, "matplotlib-pyodide": {"depends": [], "file_name": "matplotlib_pyodide-0.2.2-py3-none-any.whl", "imports": ["matplotlib_pyodide"], "install_dir": "site", "name": "matplotlib-pyodide", "package_type": "package", "sha256": "07d56729d9625b2b9bb9778a92c88c5c750476abb80f58d71b1386fed434eb8c", "shared_library": false, "unvendored_tests": false, "version": "0.2.2"}, "matplotlib-tests": {"depends": ["matplotlib"], "file_name": "matplotlib-tests.tar", "imports": [], "install_dir": "site", "name": "matplotlib-tests", "package_type": "package", "sha256": "47377f81b9aa221e30824c3edb8cf81268603a578fb6351aab69ec71da799b20", "shared_library": false, "unvendored_tests": false, "version": "3.5.2"}, "memory-allocator": {"depends": [], "file_name": "memory_allocator-0.1.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["memory_allocator"], "install_dir": "site", "name": "memory-allocator", "package_type": "package", "sha256": "b1e90dba5ac8b79c66a9677d52dd2c4ba15320151196c7cc9f30aacafb8752f0", "shared_library": false, "unvendored_tests": false, "version": "0.1.4"}, "micropip": {"depends": ["packaging"], "file_name": "micropip-0.6.0-py3-none-any.whl", "imports": ["micropip"], "install_dir": "site", "name": "micropip", "package_type": "package", "sha256": "1a3c889a69e6b2a15456f183e7711bd54175930cbe7ba09e91bec92cac8c418a", "shared_library": false, "unvendored_tests": false, "version": "0.6.0"}, "mmh3": {"depends": [], "file_name": "mmh3-4.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["mmh3"], "install_dir": "site", "name": "mmh3", "package_type": "package", "sha256": "ebd23d16d23dd0b2471857f6aa239bd68bfd1fe88a35072dce95ad9c0fb377df", "shared_library": false, "unvendored_tests": false, "version": "4.1.0"}, "mne": {"depends": ["numpy", "scipy", "setuptools", "decorator", "lazy_loader", "packaging"], "file_name": "mne-1.7.0-py3-none-any.whl", "imports": ["mne"], "install_dir": "site", "name": "mne", "package_type": "package", "sha256": "8086f8a7cb973332e29c253c9d60f9807c2384fba15b47b8d875786f6191366f", "shared_library": false, "unvendored_tests": true, "version": "1.7.0"}, "mne-tests": {"depends": ["mne"], "file_name": "mne-tests.tar", "imports": [], "install_dir": "site", "name": "mne-tests", "package_type": "package", "sha256": "7e5c8b54e62641797d2dfed22aed6cf8336135cf65008a3df2b7addb0c1e5015", "shared_library": false, "unvendored_tests": false, "version": "1.7.0"}, "more-itertools": {"depends": [], "file_name": "more_itertools-10.2.0-py3-none-any.whl", "imports": ["more_itertools"], "install_dir": "site", "name": "more-itertools", "package_type": "package", "sha256": "31e7d9b869d4cc1507cbd8ab9883dc52f04cfc60edeb867faa8a43199ee2dfd4", "shared_library": false, "unvendored_tests": false, "version": "10.2.0"}, "mpmath": {"depends": [], "file_name": "mpmath-1.3.0-py3-none-any.whl", "imports": ["mpmath"], "install_dir": "site", "name": "mpmath", "package_type": "package", "sha256": "9fd7b1f3a8a915006c2b48071c89ba620780c714d7e60560d9095f38bb440d9d", "shared_library": false, "unvendored_tests": true, "version": "1.3.0"}, "mpmath-tests": {"depends": ["mpmath"], "file_name": "mpmath-tests.tar", "imports": [], "install_dir": "site", "name": "mpmath-tests", "package_type": "package", "sha256": "c05a226349ebd546b5295ff3a43205e2180cfd552a1fa93d10cbea8763ac8053", "shared_library": false, "unvendored_tests": false, "version": "1.3.0"}, "msgpack": {"depends": [], "file_name": "msgpack-1.0.8-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["msgpack"], "install_dir": "site", "name": "msgpack", "package_type": "package", "sha256": "1e1b95c34b429d9ce044703050035fe1afed561fcbafb5615f9d0ce6d0b243d6", "shared_library": false, "unvendored_tests": false, "version": "1.0.8"}, "msgspec": {"depends": [], "file_name": "msgspec-0.18.6-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["msgspec"], "install_dir": "site", "name": "msgspec", "package_type": "package", "sha256": "8a3ada2ad782421c91e46dfe3bdc88279a9f9640874c774c7eb664f49b4f546e", "shared_library": false, "unvendored_tests": false, "version": "0.18.6"}, "msprime": {"depends": ["numpy", "newick", "tskit", "demes", "rpds-py"], "file_name": "msprime-1.3.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["msprime"], "install_dir": "site", "name": "msprime", "package_type": "package", "sha256": "a5f301b280e9821ae0994fb8dfbc547b41e12a6897e51139c4195de3682f922b", "shared_library": false, "unvendored_tests": false, "version": "1.3.1"}, "multidict": {"depends": [], "file_name": "multidict-6.0.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["multidict"], "install_dir": "site", "name": "multidict", "package_type": "package", "sha256": "01a44293972db77a7d74872a37dc32fc57e127ac6c3e92d6c70b039f4656d1f5", "shared_library": false, "unvendored_tests": false, "version": "6.0.5"}, "munch": {"depends": ["setuptools", "six"], "file_name": "munch-4.0.0-py2.py3-none-any.whl", "imports": ["munch"], "install_dir": "site", "name": "munch", "package_type": "package", "sha256": "107c09e48dd9dab92ced1bb625eb714396188b747775ae08f0b289839ac9d08e", "shared_library": false, "unvendored_tests": false, "version": "4.0.0"}, "mypy": {"depends": [], "file_name": "mypy-1.9.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["mypyc", "mypy"], "install_dir": "site", "name": "mypy", "package_type": "package", "sha256": "a0959e7167e1c1d2ca3f015a3b37462c8469a5846960e71db85c6f9700966b09", "shared_library": false, "unvendored_tests": true, "version": "1.9.0"}, "mypy-tests": {"depends": ["mypy"], "file_name": "mypy-tests.tar", "imports": [], "install_dir": "site", "name": "mypy-tests", "package_type": "package", "sha256": "901440be6cfa6a63bd22258515f8997855bf9e358846cbc3424eb630a29f987a", "shared_library": false, "unvendored_tests": false, "version": "1.9.0"}, "netcdf4": {"depends": ["numpy", "packaging", "h5py", "cftime", "certifi"], "file_name": "netCDF4-1.6.5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["netCDF4"], "install_dir": "site", "name": "netcdf4", "package_type": "package", "sha256": "2b7af5d41dded09350659179b4e5e119cb23652952e08eba560629b38dad52c4", "shared_library": false, "unvendored_tests": false, "version": "1.6.5"}, "networkx": {"depends": ["decorator", "setuptools", "matplotlib", "numpy"], "file_name": "networkx-3.3-py3-none-any.whl", "imports": ["networkx"], "install_dir": "site", "name": "networkx", "package_type": "package", "sha256": "0a39e7e4000d95224d4baf7d9e22c4e837fc67da2e0d39a9bb68bfca50860131", "shared_library": false, "unvendored_tests": true, "version": "3.3"}, "networkx-tests": {"depends": ["networkx"], "file_name": "networkx-tests.tar", "imports": [], "install_dir": "site", "name": "networkx-tests", "package_type": "package", "sha256": "d8bfd615ab6db81a3c8db5afabe78c6c61c80a7994b273aa69d26fdabc2675e8", "shared_library": false, "unvendored_tests": false, "version": "3.3"}, "newick": {"depends": [], "file_name": "newick-1.9.0-py2.py3-none-any.whl", "imports": ["newick"], "install_dir": "site", "name": "newick", "package_type": "package", "sha256": "fd7c551780ac51fbf27e0b1b1efada288a79bfc8b66afc5ea21cd42a7906085e", "shared_library": false, "unvendored_tests": false, "version": "1.9.0"}, "nh3": {"depends": [], "file_name": "nh3-0.2.17-cp37-abi3-pyodide_2024_0_wasm32.whl", "imports": ["nh3"], "install_dir": "site", "name": "nh3", "package_type": "package", "sha256": "ed8c980917c2e5cae0e37fd037fee7115119c27d6eab471662efb1ab6660a284", "shared_library": false, "unvendored_tests": false, "version": "0.2.17"}, "nlopt": {"depends": ["numpy"], "file_name": "nlopt-2.7.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["nlopt"], "install_dir": "site", "name": "nlopt", "package_type": "package", "sha256": "d7b47dd14f8a4d64d7f5a23ebe2d92597fa37492886dc91ce3433cf08d0af1d5", "shared_library": false, "unvendored_tests": false, "version": "2.7.0"}, "nltk": {"depends": ["regex", "sqlite3"], "file_name": "nltk-3.8.1-py3-none-any.whl", "imports": ["nltk"], "install_dir": "site", "name": "nltk", "package_type": "package", "sha256": "063ce4e517cf219b990c6ea452cf46f28b798fd46d7641c74d07a6d49ed8d29c", "shared_library": false, "unvendored_tests": true, "version": "3.8.1"}, "nltk-tests": {"depends": ["nltk"], "file_name": "nltk-tests.tar", "imports": [], "install_dir": "site", "name": "nltk-tests", "package_type": "package", "sha256": "83ea5fe1cc7ff21cd0e7ab455eadd2a69e207955640ac7e7274eddf5df01625e", "shared_library": false, "unvendored_tests": false, "version": "3.8.1"}, "numcodecs": {"depends": ["numpy", "msgpack"], "file_name": "numcodecs-0.11.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["numcodecs"], "install_dir": "site", "name": "numcodecs", "package_type": "package", "sha256": "9c7b7254d00fd62b7effd5b31786bb19f1509530df671a090a5913ff5c3e1e44", "shared_library": false, "unvendored_tests": true, "version": "0.11.0"}, "numcodecs-tests": {"depends": ["numcodecs"], "file_name": "numcodecs-tests.tar", "imports": [], "install_dir": "site", "name": "numcodecs-tests", "package_type": "package", "sha256": "ec0ab47c1ac88f3fb08eb7291e2c8bbd90339809dac5066c744fb0a7c2300859", "shared_library": false, "unvendored_tests": false, "version": "0.11.0"}, "numpy": {"depends": [], "file_name": "numpy-1.26.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["numpy"], "install_dir": "site", "name": "numpy", "package_type": "package", "sha256": "4aeba609614f88fbb49d31bdafbc3f8e18e3ae4ba6d2b710c5281e5d22b64f9a", "shared_library": false, "unvendored_tests": true, "version": "1.26.4"}, "numpy-tests": {"depends": ["numpy"], "file_name": "numpy-tests.tar", "imports": [], "install_dir": "site", "name": "numpy-tests", "package_type": "package", "sha256": "95ee5fe4134dc7b045637150374154a2034430e0b48ae522d59496a6c81a16df", "shared_library": false, "unvendored_tests": false, "version": "1.26.4"}, "openblas": {"depends": [], "file_name": "openblas-0.3.26.zip", "imports": [], "install_dir": "dynlib", "name": "openblas", "package_type": "shared_library", "sha256": "bdc0f6c43169c8cd80a6f708d873fd441e6e475f3615476907f8865c3efd5668", "shared_library": true, "unvendored_tests": false, "version": "0.3.26"}, "opencv-python": {"depends": ["numpy"], "file_name": "opencv_python-4.9.0.80-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["cv2"], "install_dir": "site", "name": "opencv-python", "package_type": "package", "sha256": "50b8041c16f52608e17132ca5149bbc88495a030a19d88ce5502a8cf6abbf618", "shared_library": false, "unvendored_tests": false, "version": "4.9.0.80"}, "openssl": {"depends": [], "file_name": "openssl-1.1.1n.zip", "imports": [], "install_dir": "dynlib", "name": "openssl", "package_type": "shared_library", "sha256": "59303d428657ee5466221b3f778fc425149387a89d3330859af53be2796ff95d", "shared_library": true, "unvendored_tests": false, "version": "1.1.1n"}, "optlang": {"depends": ["sympy", "six", "swiglpk"], "file_name": "optlang-1.8.1-py2.py3-none-any.whl", "imports": ["optlang"], "install_dir": "site", "name": "optlang", "package_type": "package", "sha256": "c0f07f5eaf66e64a6c42d0061032f86c670b4e1af96514196f2d2c3f3a71e70a", "shared_library": false, "unvendored_tests": true, "version": "1.8.1"}, "optlang-tests": {"depends": ["optlang"], "file_name": "optlang-tests.tar", "imports": [], "install_dir": "site", "name": "optlang-tests", "package_type": "package", "sha256": "cf7f754efeb1bf3195e16c5cce5eb13c1716db18dda20d4fc88683c9e49427a6", "shared_library": false, "unvendored_tests": false, "version": "1.8.1"}, "orjson": {"depends": [], "file_name": "orjson-3.10.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["orjson"], "install_dir": "site", "name": "orjson", "package_type": "package", "sha256": "b12cded1997656c848609705f31a1d0d91ab4e53000365acec899c7cb9488deb", "shared_library": false, "unvendored_tests": false, "version": "3.10.1"}, "packaging": {"depends": [], "file_name": "packaging-23.2-py3-none-any.whl", "imports": ["packaging"], "install_dir": "site", "name": "packaging", "package_type": "package", "sha256": "34c09d7d17c4b584b10edca9255281c11c4713f77c76945d918d7ffa0455c9fa", "shared_library": false, "unvendored_tests": false, "version": "23.2"}, "pandas": {"depends": ["numpy", "python-dateutil", "pytz"], "file_name": "pandas-2.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pandas"], "install_dir": "site", "name": "pandas", "package_type": "package", "sha256": "57257d1fe41081ec6aa88499a6eab6e06b1cee1e54fbc05fc1365c739017e4d6", "shared_library": false, "unvendored_tests": true, "version": "2.2.0"}, "pandas-tests": {"depends": ["pandas"], "file_name": "pandas-tests.tar", "imports": [], "install_dir": "site", "name": "pandas-tests", "package_type": "package", "sha256": "eba78f89a53a4898e0ae87e7571fbfe7a540d65d53fda17935b115e1c767c444", "shared_library": false, "unvendored_tests": false, "version": "2.2.0"}, "parso": {"depends": [], "file_name": "parso-0.8.4-py2.py3-none-any.whl", "imports": ["parso"], "install_dir": "site", "name": "parso", "package_type": "package", "sha256": "36e589cee5c2aaf94489df8f30f4db7d89b42b260a6d739583fa00a3e42d157a", "shared_library": false, "unvendored_tests": false, "version": "0.8.4"}, "patsy": {"depends": ["numpy", "six"], "file_name": "patsy-0.5.6-py2.py3-none-any.whl", "imports": ["patsy"], "install_dir": "site", "name": "patsy", "package_type": "package", "sha256": "760833a100a66baafd1e4b0a726eea87e6c9edcd7c6ba3e7473ca1dfe593e7f8", "shared_library": false, "unvendored_tests": true, "version": "0.5.6"}, "patsy-tests": {"depends": ["patsy"], "file_name": "patsy-tests.tar", "imports": [], "install_dir": "site", "name": "patsy-tests", "package_type": "package", "sha256": "6a3632284ca095eb87cec8a79d06183399f85c21af4ff986cb7d9a37ebd3e13f", "shared_library": false, "unvendored_tests": false, "version": "0.5.6"}, "peewee": {"depends": ["sqlite3", "cffi"], "file_name": "peewee-3.17.3-py3-none-any.whl", "imports": ["peewee"], "install_dir": "site", "name": "peewee", "package_type": "package", "sha256": "b3aac565a0a5e345fa0c069c41a0b696c9c8292e9970405ce9fb7fded0a3cf34", "shared_library": false, "unvendored_tests": true, "version": "3.17.3"}, "peewee-tests": {"depends": ["peewee"], "file_name": "peewee-tests.tar", "imports": [], "install_dir": "site", "name": "peewee-tests", "package_type": "package", "sha256": "bcda11ebbf1e3b0326845039c48106fd56843a1ff1cf33375b075f9b615ae233", "shared_library": false, "unvendored_tests": false, "version": "3.17.3"}, "pillow": {"depends": [], "file_name": "pillow-10.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["PIL"], "install_dir": "site", "name": "Pillow", "package_type": "package", "sha256": "b1ef215cdd2abc38fab5d821a86d730bc495e0450d34ed99d90c74f319f90f55", "shared_library": false, "unvendored_tests": false, "version": "10.2.0"}, "pillow-heif": {"depends": ["cffi", "pillow", "libheif"], "file_name": "pillow_heif-0.8.0-cp36-abi3-pyodide_2024_0_wasm32.whl", "imports": ["pillow_heif"], "install_dir": "site", "name": "pillow_heif", "package_type": "package", "sha256": "a2adfb50f218b2609b2be0d48d7fa0e4f03f66b7c8b69a80a83fab2bd62c8472", "shared_library": false, "unvendored_tests": false, "version": "0.8.0"}, "pkgconfig": {"depends": [], "file_name": "pkgconfig-1.5.5-py3-none-any.whl", "imports": ["pkgconfig"], "install_dir": "site", "name": "pkgconfig", "package_type": "package", "sha256": "b1772cd7ef35b74e2fae52ee83f70bc1c51c46e962c3ce8d514be3927b57a37b", "shared_library": false, "unvendored_tests": false, "version": "1.5.5"}, "pluggy": {"depends": [], "file_name": "pluggy-1.5.0-py3-none-any.whl", "imports": ["pluggy"], "install_dir": "site", "name": "pluggy", "package_type": "package", "sha256": "41726a2e85006e36c5d1b584b01674aeaba798f7f27eabc68dc1350ebdebd7e1", "shared_library": false, "unvendored_tests": false, "version": "1.5.0"}, "pplpy": {"depends": ["gmpy2", "cysignals"], "file_name": "pplpy-0.8.10-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["ppl"], "install_dir": "site", "name": "pplpy", "package_type": "package", "sha256": "325d38df6ddd86ba3e0b8fc02e77fcb520033c3211589550b1c0f30638644f22", "shared_library": false, "unvendored_tests": false, "version": "0.8.10"}, "primecountpy": {"depends": ["cysignals"], "file_name": "primecountpy-0.1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["primecountpy"], "install_dir": "site", "name": "primecountpy", "package_type": "package", "sha256": "dbbd9dd54cdde6f191cb615875f8a8edf6ec4f548def141d99b4ee5e9804d44e", "shared_library": false, "unvendored_tests": false, "version": "0.1.0"}, "prompt-toolkit": {"depends": [], "file_name": "prompt_toolkit-3.0.43-py3-none-any.whl", "imports": ["prompt_toolkit"], "install_dir": "site", "name": "prompt_toolkit", "package_type": "package", "sha256": "954ac31a47ce598daec1757c13f9d4ae5e3081f5762bcaaadbea62cef593ca3e", "shared_library": false, "unvendored_tests": false, "version": "3.0.43"}, "protobuf": {"depends": [], "file_name": "protobuf-4.24.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["google"], "install_dir": "site", "name": "protobuf", "package_type": "package", "sha256": "fedaad25244cd92d3ae1cce9dc2bfc30186efde7d979146aa6c2e5c322f3f441", "shared_library": false, "unvendored_tests": false, "version": "4.24.4"}, "pure-eval": {"depends": [], "file_name": "pure_eval-0.2.2-py3-none-any.whl", "imports": ["pure_eval"], "install_dir": "site", "name": "pure_eval", "package_type": "package", "sha256": "2a02f1b49cb9b405f7fa300695be174fc4f8b4da220dcace1daa66ab252ba027", "shared_library": false, "unvendored_tests": false, "version": "0.2.2"}, "py": {"depends": [], "file_name": "py-1.11.0-py2.py3-none-any.whl", "imports": ["py"], "install_dir": "site", "name": "py", "package_type": "package", "sha256": "8027d2e090352a65167983ec14b187b0c281dc31e98cb6e17885f57d3cf2b407", "shared_library": false, "unvendored_tests": false, "version": "1.11.0"}, "pyclipper": {"depends": [], "file_name": "pyclipper-1.3.0.post5-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyclipper"], "install_dir": "site", "name": "pyclipper", "package_type": "package", "sha256": "c6f8d4b9749600cb8443076258ccf293465fbe0bd62c448316ef3613c9dd2c05", "shared_library": false, "unvendored_tests": false, "version": "1.3.0.post5"}, "pycparser": {"depends": [], "file_name": "pycparser-2.22-py3-none-any.whl", "imports": ["pycparser"], "install_dir": "site", "name": "pycparser", "package_type": "package", "sha256": "2a807b142ffd51086282d4776576ca61b0fd870ea6babc7212d75607357279be", "shared_library": false, "unvendored_tests": false, "version": "2.22"}, "pycryptodome": {"depends": [], "file_name": "pycryptodome-3.20.0-cp35-abi3-pyodide_2024_0_wasm32.whl", "imports": ["Crypto"], "install_dir": "site", "name": "pycryptodome", "package_type": "package", "sha256": "bea845208b260b9081f5ec1cd9833b111b1642af04ec5824e3c83fd0d2c30a4f", "shared_library": false, "unvendored_tests": true, "version": "3.20.0"}, "pycryptodome-tests": {"depends": ["pycryptodome"], "file_name": "pycryptodome-tests.tar", "imports": [], "install_dir": "site", "name": "pycryptodome-tests", "package_type": "package", "sha256": "8b37e84e9df0bae3d6459d5ac64669d88e73015d11918c6c57adeb052f5d0c59", "shared_library": false, "unvendored_tests": false, "version": "3.20.0"}, "pydantic": {"depends": ["typing-extensions", "pydantic_core", "annotated-types"], "file_name": "pydantic-2.7.0-py3-none-any.whl", "imports": ["pydantic"], "install_dir": "site", "name": "pydantic", "package_type": "package", "sha256": "7c866a4a9071ca25279c06c0e4871d6e5a7c611f2e1bcfadc47ba2b3336016ca", "shared_library": false, "unvendored_tests": false, "version": "2.7.0"}, "pydantic-core": {"depends": [], "file_name": "pydantic_core-2.18.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pydantic_core"], "install_dir": "site", "name": "pydantic_core", "package_type": "package", "sha256": "be7f83ad55493a9ffb94777d30fe3e6d3334cb08e2ebdaf0e7a799eef0174866", "shared_library": false, "unvendored_tests": false, "version": "2.18.1"}, "pydecimal": {"depends": [], "file_name": "pydecimal-1.0.0.zip", "imports": ["_pydecimal"], "install_dir": "stdlib", "name": "pydecimal", "package_type": "cpython_module", "sha256": "c0661bafc675196901c67b4f883abdb0dffb44b97aec91bec0a03bfebeb12668", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "pydoc-data": {"depends": [], "file_name": "pydoc_data-1.0.0.zip", "imports": ["pydoc_data"], "install_dir": "stdlib", "name": "pydoc_data", "package_type": "cpython_module", "sha256": "b15bf763b1158e7757c7265a1340c92ce23c753f983e4d487e2bea8cc749300f", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "pyerfa": {"depends": ["numpy"], "file_name": "pyerfa-2.0.1.4-cp39-abi3-pyodide_2024_0_wasm32.whl", "imports": ["erfa"], "install_dir": "site", "name": "pyerfa", "package_type": "package", "sha256": "423bd62931e5244edd860856e47eb6b5122f1ec369fdd797112b6676ab60922a", "shared_library": false, "unvendored_tests": true, "version": "2.0.1.4"}, "pyerfa-tests": {"depends": ["pyerfa"], "file_name": "pyerfa-tests.tar", "imports": [], "install_dir": "site", "name": "pyerfa-tests", "package_type": "package", "sha256": "8bf7d38d8aa679833a5af53c9729bcd28c69a028fefe7f751d06d2e92f93981f", "shared_library": false, "unvendored_tests": false, "version": "2.0.1.4"}, "pygame-ce": {"depends": [], "file_name": "pygame_ce-2.4.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pygame"], "install_dir": "site", "name": "pygame-ce", "package_type": "package", "sha256": "ffeae5c40f989d0c208711b552fa2e2b41b2df6077a4d3941f208fc97d4bf9a1", "shared_library": false, "unvendored_tests": true, "version": "2.4.1"}, "pygame-ce-tests": {"depends": ["pygame-ce"], "file_name": "pygame-ce-tests.tar", "imports": [], "install_dir": "site", "name": "pygame-ce-tests", "package_type": "package", "sha256": "ece1ba607e541cea41d29ad516327e58853cb1908f9fbf338058ee40bdc683c0", "shared_library": false, "unvendored_tests": false, "version": "2.4.1"}, "pygments": {"depends": [], "file_name": "pygments-2.17.2-py3-none-any.whl", "imports": ["pygments"], "install_dir": "site", "name": "Pygments", "package_type": "package", "sha256": "58dfa90582044080f780d8f1b71e9e52c320fb7b0231c7191d32d456650f6cee", "shared_library": false, "unvendored_tests": false, "version": "2.17.2"}, "pyheif": {"depends": ["cffi"], "file_name": "pyheif-0.7.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyheif"], "install_dir": "site", "name": "pyheif", "package_type": "package", "sha256": "7b1837edd9ae8bb8e1f1d16a65c3f260ca42751db17dd6652a02c92c12201936", "shared_library": false, "unvendored_tests": false, "version": "0.7.1"}, "pyiceberg": {"depends": ["click", "fsspec", "mmh3", "pydantic", "pyparsing", "requests", "rich", "sortedcontainers", "sqlalchemy", "strictyaml"], "file_name": "pyiceberg-0.6.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyiceberg"], "install_dir": "site", "name": "pyiceberg", "package_type": "package", "sha256": "aa291e29362604cc8acf7ae035b7f044ab5660014087a7464f75b9fbf36a566d", "shared_library": false, "unvendored_tests": false, "version": "0.6.0"}, "pyinstrument": {"depends": [], "file_name": "pyinstrument-4.4.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyinstrument"], "install_dir": "site", "name": "pyinstrument", "package_type": "package", "sha256": "1ac73667303af9889b8787b46413084b55cb3b2420825cc68bc6afd529fe1996", "shared_library": false, "unvendored_tests": false, "version": "4.4.0"}, "pynacl": {"depends": ["cffi"], "file_name": "PyNaCl-1.5.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["nacl"], "install_dir": "site", "name": "pynacl", "package_type": "package", "sha256": "c3ab7e374410b93609bc53212a8faf8cd617f45bdfd9cb6cdf8ecce60e3ba9e9", "shared_library": false, "unvendored_tests": false, "version": "1.5.0"}, "pyodide-http": {"depends": [], "file_name": "pyodide_http-0.2.1-py3-none-any.whl", "imports": ["pyodide_http"], "install_dir": "site", "name": "pyodide-http", "package_type": "package", "sha256": "aa2cc7e585e98a84208bc042dd36140cb4213ec6112915a1cc86f7101e38d47a", "shared_library": false, "unvendored_tests": false, "version": "0.2.1"}, "pyparsing": {"depends": [], "file_name": "pyparsing-3.1.2-py3-none-any.whl", "imports": ["pyparsing"], "install_dir": "site", "name": "pyparsing", "package_type": "package", "sha256": "8db11035317ae75a36c2b4dea44eeba5630c8bd2f501e74a243dbbe93f511895", "shared_library": false, "unvendored_tests": false, "version": "3.1.2"}, "pyproj": {"depends": ["certifi", "sqlite3"], "file_name": "pyproj-3.6.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyproj"], "install_dir": "site", "name": "pyproj", "package_type": "package", "sha256": "109d9c5c99442a89c40453571c1337589bcfdeb66d683645973ee8bc5d14ff85", "shared_library": false, "unvendored_tests": false, "version": "3.6.1"}, "pyrsistent": {"depends": [], "file_name": "pyrsistent-0.20.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["_pyrsistent_version", "pyrsistent"], "install_dir": "site", "name": "pyrsistent", "package_type": "package", "sha256": "5d464ec51462568599539a111c431c7612e4acefa8d86a4d18280cfa3bc4d3ec", "shared_library": false, "unvendored_tests": false, "version": "0.20.0"}, "pysam": {"depends": [], "file_name": "pysam-0.22.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pysam"], "install_dir": "site", "name": "pysam", "package_type": "package", "sha256": "c6374ae788806ea721c6588dd8e297c214262e95e42839ef4ade44b649f8a278", "shared_library": false, "unvendored_tests": false, "version": "0.22.0"}, "pyshp": {"depends": [], "file_name": "pyshp-2.3.1-py2.py3-none-any.whl", "imports": ["shapefile"], "install_dir": "site", "name": "pyshp", "package_type": "package", "sha256": "592a39ee27138b454d767f6620c6895f29ae360c4a47ab3fd00ad6875e432566", "shared_library": false, "unvendored_tests": false, "version": "2.3.1"}, "pytest": {"depends": ["atomicwrites", "attrs", "more-itertools", "pluggy", "py", "setuptools", "six", "iniconfig", "exceptiongroup"], "file_name": "pytest-8.1.1-py3-none-any.whl", "imports": ["_pytest", "pytest"], "install_dir": "site", "name": "pytest", "package_type": "package", "sha256": "132f51fe851ed5735e6f6b7729c566e44cb6ac488b7d0040ca247d1cb9638d1a", "shared_library": false, "unvendored_tests": false, "version": "8.1.1"}, "pytest-asyncio": {"depends": ["pytest"], "file_name": "pytest_asyncio-0.23.7-py3-none-any.whl", "imports": ["pytest_asyncio"], "install_dir": "site", "name": "pytest-asyncio", "package_type": "package", "sha256": "80597e5a925462a351645d48af5806fd5ccf95f269c0690eae4df772aa66d424", "shared_library": false, "unvendored_tests": false, "version": "0.23.7"}, "pytest-benchmark": {"depends": [], "file_name": "pytest_benchmark-4.0.0-py3-none-any.whl", "imports": ["pytest_benchmark"], "install_dir": "site", "name": "pytest-benchmark", "package_type": "package", "sha256": "4752b0087dc78a3909a472998646266e834f2af2111ba7cf4a6c9659e737a957", "shared_library": false, "unvendored_tests": false, "version": "4.0.0"}, "python-dateutil": {"depends": ["six"], "file_name": "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", "imports": ["dateutil"], "install_dir": "site", "name": "python-dateutil", "package_type": "package", "sha256": "1adf6847a0ae4a09bef1fc9954089766800ce6d9688a5329d04ad54bcf7d1f2c", "shared_library": false, "unvendored_tests": false, "version": "2.9.0.post0"}, "python-flint": {"depends": [], "file_name": "python_flint-0.6.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["flint"], "install_dir": "site", "name": "python-flint", "package_type": "package", "sha256": "95eac4de7c679cd78071e8d17f4e6ed2f373082f3bddba775d5a017dd9bfdc69", "shared_library": false, "unvendored_tests": false, "version": "0.6.0"}, "python-magic": {"depends": ["libmagic"], "file_name": "python_magic-0.4.27-py2.py3-none-any.whl", "imports": ["magic"], "install_dir": "site", "name": "python-magic", "package_type": "package", "sha256": "7353b45205f8d79530c37367321ae511e84dec5f8a39c441082f3b2f0fff70d3", "shared_library": false, "unvendored_tests": false, "version": "0.4.27"}, "python-sat": {"depends": ["six"], "file_name": "python_sat-1.8.dev13-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pysat"], "install_dir": "site", "name": "python-sat", "package_type": "package", "sha256": "210f18f6d80f9670a9d5d53973b73facfbf2fcc71a87d6583db816862be0ba23", "shared_library": false, "unvendored_tests": false, "version": "1.8.dev13"}, "python-solvespace": {"depends": [], "file_name": "python_solvespace-3.0.8-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["python_solvespace"], "install_dir": "site", "name": "python_solvespace", "package_type": "package", "sha256": "b88c85292a771f365664a9c4d044d87db62c201ee8f7a97bd4fca94ddbee242c", "shared_library": false, "unvendored_tests": false, "version": "3.0.8"}, "pytz": {"depends": [], "file_name": "pytz-2024.1-py2.py3-none-any.whl", "imports": ["pytz"], "install_dir": "site", "name": "pytz", "package_type": "package", "sha256": "62dfa923a4643d5d56371fe08a8aa2ea63854d154cf46c09e7c91c167207eb1a", "shared_library": false, "unvendored_tests": false, "version": "2024.1"}, "pywavelets": {"depends": ["numpy", "matplotlib", "scipy"], "file_name": "pywavelets-1.6.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pywt"], "install_dir": "site", "name": "pywavelets", "package_type": "package", "sha256": "32626f1fd288714f500b961b7358f5dc7b2fabfbd90b458514055dd95f7965d3", "shared_library": false, "unvendored_tests": true, "version": "1.6.0"}, "pywavelets-tests": {"depends": ["pywavelets"], "file_name": "pywavelets-tests.tar", "imports": [], "install_dir": "site", "name": "pywavelets-tests", "package_type": "package", "sha256": "6a6b63b515e6285b7b90bc94dd138a94f1771fa1dfb550816c61c6b1b7bcb4bf", "shared_library": false, "unvendored_tests": false, "version": "1.6.0"}, "pyxel": {"depends": [], "file_name": "pyxel-1.9.10-cp37-abi3-pyodide_2024_0_wasm32.whl", "imports": ["pyxel"], "install_dir": "site", "name": "pyxel", "package_type": "package", "sha256": "46e15678938bb23be6a5e89ee2566bbe6457fa7ede38a0c26e3ea86bdb642b09", "shared_library": false, "unvendored_tests": false, "version": "1.9.10"}, "pyxirr": {"depends": [], "file_name": "pyxirr-0.10.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["pyxirr"], "install_dir": "site", "name": "pyxirr", "package_type": "package", "sha256": "ccb91ae101e3ee001aec040e1423c86b25832deddbaf3efbb6ca4557064a7ced", "shared_library": false, "unvendored_tests": false, "version": "0.10.3"}, "pyyaml": {"depends": [], "file_name": "PyYAML-6.0.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["_yaml", "yaml"], "install_dir": "site", "name": "pyyaml", "package_type": "package", "sha256": "f8e7882442554c1648a62c1f47e4ef3c1c4756834c9d42cd3ea89b01e92738c6", "shared_library": false, "unvendored_tests": false, "version": "6.0.1"}, "rebound": {"depends": ["numpy"], "file_name": "rebound-3.24.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["rebound"], "install_dir": "site", "name": "rebound", "package_type": "package", "sha256": "e85c1670d5a67564fa313e55c176a946db7646fb6ad4faa4bc8c32464e7f30de", "shared_library": false, "unvendored_tests": false, "version": "3.24.2"}, "reboundx": {"depends": ["rebound", "numpy"], "file_name": "reboundx-3.10.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["reboundx"], "install_dir": "site", "name": "reboundx", "package_type": "package", "sha256": "2acea849f2e63eaf09ec8ea3e0cc676f3c7930ecdaf372660a72aed514acdb40", "shared_library": false, "unvendored_tests": false, "version": "3.10.1"}, "referencing": {"depends": ["attrs", "rpds-py"], "file_name": "referencing-0.34.0-py3-none-any.whl", "imports": ["referencing"], "install_dir": "site", "name": "referencing", "package_type": "package", "sha256": "6912d699247bc5c38391859c922308465a6a6beb27b4d6a48ade6cf7f66db561", "shared_library": false, "unvendored_tests": true, "version": "0.34.0"}, "referencing-tests": {"depends": ["referencing"], "file_name": "referencing-tests.tar", "imports": [], "install_dir": "site", "name": "referencing-tests", "package_type": "package", "sha256": "5f7bceb247a09327cf0912eca4d6a3c90cb6b4436a864bc82a333ecf3e11159f", "shared_library": false, "unvendored_tests": false, "version": "0.34.0"}, "regex": {"depends": [], "file_name": "regex-2024.4.16-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["regex"], "install_dir": "site", "name": "regex", "package_type": "package", "sha256": "826a6da49b07843236888dd29c8ce0cab1f588315ccf4c55321ecd135fd21093", "shared_library": false, "unvendored_tests": true, "version": "2024.4.16"}, "regex-tests": {"depends": ["regex"], "file_name": "regex-tests.tar", "imports": [], "install_dir": "site", "name": "regex-tests", "package_type": "package", "sha256": "37d33d6e7fb2d1c605bd72d78236ef4e324665c474aea4db737a6e6444159942", "shared_library": false, "unvendored_tests": false, "version": "2024.4.16"}, "requests": {"depends": ["charset-normalizer", "idna", "urllib3", "certifi"], "file_name": "requests-2.31.0-py3-none-any.whl", "imports": ["requests"], "install_dir": "site", "name": "requests", "package_type": "package", "sha256": "001c81f2678aa7dbd3f6884e53997b26c948d0b52603933d953d6f835c397965", "shared_library": false, "unvendored_tests": false, "version": "2.31.0"}, "retrying": {"depends": ["six"], "file_name": "retrying-1.3.4-py3-none-any.whl", "imports": ["retrying"], "install_dir": "site", "name": "retrying", "package_type": "package", "sha256": "c3dba397227a4f7eb4eb95fe627f4d2d03a4c50adf6f25edc1dbc691ee5622c9", "shared_library": false, "unvendored_tests": false, "version": "1.3.4"}, "rich": {"depends": [], "file_name": "rich-13.7.1-py3-none-any.whl", "imports": ["rich"], "install_dir": "site", "name": "rich", "package_type": "package", "sha256": "ea0e850b047833799b03e521dd8eb7d21118056183cf6957c336bf414c265282", "shared_library": false, "unvendored_tests": false, "version": "13.7.1"}, "river": {"depends": ["numpy", "pandas", "pytest", "scipy"], "file_name": "river-0.19.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["river"], "install_dir": "site", "name": "river", "package_type": "package", "sha256": "aaac213ea3f3176ee84654399e6b397f69f0bf2d1632d72c66a3232499dca938", "shared_library": false, "unvendored_tests": true, "version": "0.19.0"}, "river-tests": {"depends": ["river"], "file_name": "river-tests.tar", "imports": [], "install_dir": "site", "name": "river-tests", "package_type": "package", "sha256": "9b2bee4cfa8e9c91c3587ec24657def30e8a569b329b52d1acb64f078f309e22", "shared_library": false, "unvendored_tests": false, "version": "0.19.0"}, "robotraconteur": {"depends": ["numpy"], "file_name": "RobotRaconteur-1.2.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["RobotRaconteur"], "install_dir": "site", "name": "RobotRaconteur", "package_type": "package", "sha256": "77753ca8a261756c7821f7b2cf7be4db90e2a2281d7d89e2c952f68e69eb3711", "shared_library": false, "unvendored_tests": false, "version": "1.2.0"}, "rpds-py": {"depends": [], "file_name": "rpds_py-0.18.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["rpds"], "install_dir": "site", "name": "rpds-py", "package_type": "package", "sha256": "abbcc63b95769dd804106e191eaf0ac462bd81acc45faeb6e83a8d72f8353b2d", "shared_library": false, "unvendored_tests": false, "version": "0.18.0"}, "ruamel-yaml": {"depends": [], "file_name": "ruamel.yaml-0.18.6-py3-none-any.whl", "imports": ["ruamel"], "install_dir": "site", "name": "ruamel.yaml", "package_type": "package", "sha256": "2ebfdf7175e37b832f191a7e11452e32c900618382b6bba4e9b16265d5e56f57", "shared_library": false, "unvendored_tests": false, "version": "0.18.6"}, "rust-panic-test": {"depends": [], "file_name": "rust_panic_test-1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["rust-panic-test"], "install_dir": "site", "name": "rust-panic-test", "package_type": "package", "sha256": "c97425311fffebd46631b3e0666d4a4a6d4a84b5143e54c58d663b2fe6c122e9", "shared_library": false, "unvendored_tests": false, "version": "1.0"}, "scikit-image": {"depends": ["packaging", "numpy", "scipy", "networkx", "pillow", "imageio", "pywavelets", "lazy_loader"], "file_name": "scikit_image-0.23.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["skimage"], "install_dir": "site", "name": "scikit-image", "package_type": "package", "sha256": "ac530cc5e2cb757079e691415e096fe14b476267ccb0d14be9cd65c714cea96e", "shared_library": false, "unvendored_tests": true, "version": "0.23.2"}, "scikit-image-tests": {"depends": ["scikit-image"], "file_name": "scikit-image-tests.tar", "imports": [], "install_dir": "site", "name": "scikit-image-tests", "package_type": "package", "sha256": "292dda6f83cc184f2332fd05659a55214e9c269ada38472da9338d9e51ee500a", "shared_library": false, "unvendored_tests": false, "version": "0.23.2"}, "scikit-learn": {"depends": ["scipy", "joblib", "threadpoolctl"], "file_name": "scikit_learn-1.4.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["sklearn"], "install_dir": "site", "name": "scikit-learn", "package_type": "package", "sha256": "53302f72d375c6c110e1f50c9d70376884b22ba87eab53e440bd68f2a96016cd", "shared_library": false, "unvendored_tests": true, "version": "1.4.2"}, "scikit-learn-tests": {"depends": ["scikit-learn"], "file_name": "scikit-learn-tests.tar", "imports": [], "install_dir": "site", "name": "scikit-learn-tests", "package_type": "package", "sha256": "e57544bda608c3c48f613197bf1853d6d58d24e56ac70020f7c3c10e905e1b76", "shared_library": false, "unvendored_tests": false, "version": "1.4.2"}, "scipy": {"depends": ["numpy", "openblas"], "file_name": "scipy-1.12.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["scipy"], "install_dir": "site", "name": "scipy", "package_type": "package", "sha256": "3eb24e2939b223bc946db8cd193b33ad2ebbd6c98004cf17f26e2bd4e5bad92c", "shared_library": false, "unvendored_tests": true, "version": "1.12.0"}, "scipy-tests": {"depends": ["scipy"], "file_name": "scipy-tests.tar", "imports": [], "install_dir": "site", "name": "scipy-tests", "package_type": "package", "sha256": "999bf6b9449c64b74a08f4b40840c1f0a9e690cd8714a75554d552fd4cded727", "shared_library": false, "unvendored_tests": false, "version": "1.12.0"}, "screed": {"depends": [], "file_name": "screed-1.1.3-py2.py3-none-any.whl", "imports": ["bigtests", "screed"], "install_dir": "site", "name": "screed", "package_type": "package", "sha256": "1f7bd864f01e97056e0c06a11d0c302726f4d9bcd2cf378961dad6336cedfebf", "shared_library": false, "unvendored_tests": true, "version": "1.1.3"}, "screed-tests": {"depends": ["screed"], "file_name": "screed-tests.tar", "imports": [], "install_dir": "site", "name": "screed-tests", "package_type": "package", "sha256": "8d3ce3a84b6efcd306853e35bffb027b1b21b0cdf8f60be25c7c906193df07bf", "shared_library": false, "unvendored_tests": false, "version": "1.1.3"}, "setuptools": {"depends": ["pyparsing"], "file_name": "setuptools-69.5.1-py3-none-any.whl", "imports": ["_distutils_hack", "pkg_resources", "setuptools"], "install_dir": "site", "name": "setuptools", "package_type": "package", "sha256": "8d109163f2597f5353f0e9089636de4031a310245443e8a1f2af6afe18b8c41c", "shared_library": false, "unvendored_tests": false, "version": "69.5.1"}, "shapely": {"depends": ["numpy"], "file_name": "shapely-2.0.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["shapely"], "install_dir": "site", "name": "shapely", "package_type": "package", "sha256": "2ca8a9a020893e3ce747c006908bd3ff8ca14f5ecf7201bc240d7e2bcdae53a5", "shared_library": false, "unvendored_tests": true, "version": "2.0.2"}, "shapely-tests": {"depends": ["shapely"], "file_name": "shapely-tests.tar", "imports": [], "install_dir": "site", "name": "shapely-tests", "package_type": "package", "sha256": "cd2eca2e5c2aa75fe98a012eebbc52beda9dc13bc9ddfe455955f79d28d1972f", "shared_library": false, "unvendored_tests": false, "version": "2.0.2"}, "sharedlib-test": {"depends": [], "file_name": "sharedlib-test-1.0.zip", "imports": [], "install_dir": "dynlib", "name": "sharedlib-test", "package_type": "shared_library", "sha256": "7478123361942519d05cc921696d597a2ba75806d52ddb5973c0f19b21ee22fd", "shared_library": true, "unvendored_tests": false, "version": "1.0"}, "sharedlib-test-py": {"depends": ["sharedlib-test"], "file_name": "sharedlib_test_py-1.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["sharedlib_test"], "install_dir": "site", "name": "sharedlib-test-py", "package_type": "package", "sha256": "f1e7add98c4d1aebced3e6ccbfe973ff83ec5e8642a00f16975351d0518411cf", "shared_library": false, "unvendored_tests": false, "version": "1.0"}, "simplejson": {"depends": [], "file_name": "simplejson-3.19.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["simplejson"], "install_dir": "site", "name": "simplejson", "package_type": "package", "sha256": "c4f8f531952ab9d402d01ce89106edfbcc243173f968f2e60efdb3618930b3df", "shared_library": false, "unvendored_tests": true, "version": "3.19.2"}, "simplejson-tests": {"depends": ["simplejson"], "file_name": "simplejson-tests.tar", "imports": [], "install_dir": "site", "name": "simplejson-tests", "package_type": "package", "sha256": "4c8ff471020ca4a7d115672f1d2aca34dd7bd7db53b38d45e6b24c3d71f7c2b5", "shared_library": false, "unvendored_tests": false, "version": "3.19.2"}, "sisl": {"depends": ["pyparsing", "numpy", "scipy", "tqdm", "xarray", "pandas", "matplotlib"], "file_name": "sisl-0.14.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["sisl_toolbox", "sisl"], "install_dir": "site", "name": "sisl", "package_type": "package", "sha256": "a026d869059702221c0b093650bbd62b96d1c7d87bc42188b5a4a328a2bdb39d", "shared_library": false, "unvendored_tests": true, "version": "0.14.3"}, "sisl-tests": {"depends": ["sisl"], "file_name": "sisl-tests.tar", "imports": [], "install_dir": "site", "name": "sisl-tests", "package_type": "package", "sha256": "35a35535a2ae2ea220c97042753c7c10e3a76932cbb773bb3a8166a833e03d0b", "shared_library": false, "unvendored_tests": false, "version": "0.14.3"}, "six": {"depends": [], "file_name": "six-1.16.0-py2.py3-none-any.whl", "imports": ["six"], "install_dir": "site", "name": "six", "package_type": "package", "sha256": "176a02f2e3c155246b96ff0a8e8a35c35e74f666923a50e47d719f21e2d3b7f2", "shared_library": false, "unvendored_tests": false, "version": "1.16.0"}, "smart-open": {"depends": [], "file_name": "smart_open-7.0.4-py3-none-any.whl", "imports": ["smart_open"], "install_dir": "site", "name": "smart_open", "package_type": "package", "sha256": "8217625b7117f7fe3bd295a6a41c5a5253b2471a0668e3d28453fd5375920690", "shared_library": false, "unvendored_tests": false, "version": "7.0.4"}, "sortedcontainers": {"depends": [], "file_name": "sortedcontainers-2.4.0-py2.py3-none-any.whl", "imports": ["sortedcontainers"], "install_dir": "site", "name": "sortedcontainers", "package_type": "package", "sha256": "56194229f40e8f5d813b68ce87595483b408ead2c4552439265069545b20d753", "shared_library": false, "unvendored_tests": false, "version": "2.4.0"}, "soupsieve": {"depends": [], "file_name": "soupsieve-2.5-py3-none-any.whl", "imports": ["soupsieve"], "install_dir": "site", "name": "soupsieve", "package_type": "package", "sha256": "cc4fbcb87488b840a835be9f568435dba297c6914f1edfa32b3bbf74a8a348cf", "shared_library": false, "unvendored_tests": false, "version": "2.5"}, "sourmash": {"depends": ["screed", "cffi", "deprecation", "cachetools", "numpy", "matplotlib", "scipy", "sqlite3", "bitstring"], "file_name": "sourmash-4.8.8-py3-none-pyodide_2024_0_wasm32.whl", "imports": ["sourmash"], "install_dir": "site", "name": "sourmash", "package_type": "package", "sha256": "2f1a1b6906816867b95e61dea826e7a5cd9344380324a2a90bae8bb33753927d", "shared_library": false, "unvendored_tests": false, "version": "4.8.8"}, "sparseqr": {"depends": ["pycparser", "cffi", "numpy", "scipy", "suitesparse"], "file_name": "sparseqr-1.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["sparseqr"], "install_dir": "site", "name": "sparseqr", "package_type": "package", "sha256": "88acb940cfadf513b4d72c1ccb0adaf7001f0d7da9ca101d450e0321cef08116", "shared_library": false, "unvendored_tests": false, "version": "1.2"}, "sqlalchemy": {"depends": ["sqlite3", "typing-extensions"], "file_name": "SQLAlchemy-2.0.29-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["sqlalchemy"], "install_dir": "site", "name": "sqlalchemy", "package_type": "package", "sha256": "e3e485820b9782d2ce98bf4fdbaa6dee12314d3fd66751fd9ec6edb13065e75b", "shared_library": false, "unvendored_tests": true, "version": "2.0.29"}, "sqlalchemy-tests": {"depends": ["sqlalchemy"], "file_name": "sqlalchemy-tests.tar", "imports": [], "install_dir": "site", "name": "sqlalchemy-tests", "package_type": "package", "sha256": "a4335234473c85da12709a05d651940564acd344f62bf91cbf13cdac888f9604", "shared_library": false, "unvendored_tests": false, "version": "2.0.29"}, "sqlite3": {"depends": [], "file_name": "sqlite3-1.0.0.zip", "imports": ["sqlite3", "_sqlite3"], "install_dir": "stdlib", "name": "sqlite3", "package_type": "cpython_module", "sha256": "29586a9ec94786c6385d3ef46aa65f484bb3f9f61b1bdbd18a0aa396e551cd9c", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "ssl": {"depends": ["openssl"], "file_name": "ssl-1.0.0.zip", "imports": ["ssl", "_ssl"], "install_dir": "stdlib", "name": "ssl", "package_type": "cpython_module", "sha256": "c57eed8de854b8d1ba4911c98390e5a6810d2b88ce84e030598d9a23c98346bf", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "stack-data": {"depends": [], "file_name": "stack_data-0.6.3-py3-none-any.whl", "imports": ["stack_data"], "install_dir": "site", "name": "stack_data", "package_type": "package", "sha256": "2f71f5dd0878678c74d89dfafe6b6d343b4ad5a1036f444df8b0208b8dd2576a", "shared_library": false, "unvendored_tests": false, "version": "0.6.3"}, "statsmodels": {"depends": ["numpy", "scipy", "pandas", "patsy", "packaging"], "file_name": "statsmodels-0.14.2-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["statsmodels"], "install_dir": "site", "name": "statsmodels", "package_type": "package", "sha256": "663c63c8ec52595437705125b2312c334ade7d805278c4484a64db14d6c0e5cf", "shared_library": false, "unvendored_tests": true, "version": "0.14.2"}, "statsmodels-tests": {"depends": ["statsmodels"], "file_name": "statsmodels-tests.tar", "imports": [], "install_dir": "site", "name": "statsmodels-tests", "package_type": "package", "sha256": "6c90ea3d8b162c1557a557145bc8fcdf3437da3d5be74d710b0824b6f5ad49ec", "shared_library": false, "unvendored_tests": false, "version": "0.14.2"}, "strictyaml": {"depends": ["python-dateutil"], "file_name": "strictyaml-1.7.3-py3-none-any.whl", "imports": ["strictyaml"], "install_dir": "site", "name": "strictyaml", "package_type": "package", "sha256": "7e0306417f802210ada75ea0eb506d7a60b150d366f3144b7e8bc18f5ff75d29", "shared_library": false, "unvendored_tests": false, "version": "1.7.3"}, "suitesparse": {"depends": ["openblas"], "file_name": "suitesparse-5.11.0.zip", "imports": [], "install_dir": "dynlib", "name": "suitesparse", "package_type": "shared_library", "sha256": "c14ddfb6b7c3f5fdc86177b9f96cdceacdad3d970bc53a1b22be5c1dd244db8d", "shared_library": true, "unvendored_tests": false, "version": "5.11.0"}, "svgwrite": {"depends": [], "file_name": "svgwrite-1.4.3-py3-none-any.whl", "imports": ["svgwrite"], "install_dir": "site", "name": "svgwrite", "package_type": "package", "sha256": "2f708f13e85580beff1adb8e1b178ab83c3f41161aec6afbdcc418fc6297daa7", "shared_library": false, "unvendored_tests": false, "version": "1.4.3"}, "swiglpk": {"depends": [], "file_name": "swiglpk-5.0.10-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["swiglpk"], "install_dir": "site", "name": "swiglpk", "package_type": "package", "sha256": "47c0fb4a7692d01155b2b15af095ad3505a2ba2c5a5d31bf46197293533515d9", "shared_library": false, "unvendored_tests": false, "version": "5.0.10"}, "sympy": {"depends": ["mpmath"], "file_name": "sympy-1.12-py3-none-any.whl", "imports": ["isympy", "sympy"], "install_dir": "site", "name": "sympy", "package_type": "package", "sha256": "7a4792ab8d537fa71e1a0ee316e106132212e7ef1473fb6a31b325ad868f1802", "shared_library": false, "unvendored_tests": true, "version": "1.12"}, "sympy-tests": {"depends": ["sympy"], "file_name": "sympy-tests.tar", "imports": [], "install_dir": "site", "name": "sympy-tests", "package_type": "package", "sha256": "5a1619b61ee0615c73cafbf492c5a9875b4af5411264c3eca2201d346e86a093", "shared_library": false, "unvendored_tests": false, "version": "1.12"}, "tblib": {"depends": [], "file_name": "tblib-3.0.0-py3-none-any.whl", "imports": ["tblib"], "install_dir": "site", "name": "tblib", "package_type": "package", "sha256": "ad99c66810817b58e92c3be9ceb9ebc3762f51d7ecb6c06a2c8753a170c4e662", "shared_library": false, "unvendored_tests": false, "version": "3.0.0"}, "termcolor": {"depends": [], "file_name": "termcolor-2.4.0-py3-none-any.whl", "imports": ["termcolor"], "install_dir": "site", "name": "termcolor", "package_type": "package", "sha256": "f7eea924f89bf4f79713eb1253d6f3560247cee070e0c5531256e93d7d21e666", "shared_library": false, "unvendored_tests": false, "version": "2.4.0"}, "test": {"depends": [], "file_name": "test-1.0.0.zip", "imports": ["test"], "install_dir": "stdlib", "name": "test", "package_type": "cpython_module", "sha256": "0e1c1c9a3fab52f55d1ee52242962d3e83c4b25aa33aa4c0a9d3e628cedcb67c", "shared_library": true, "unvendored_tests": false, "version": "1.0.0"}, "texttable": {"depends": [], "file_name": "texttable-1.7.0-py2.py3-none-any.whl", "imports": ["texttable"], "install_dir": "site", "name": "texttable", "package_type": "package", "sha256": "c2c44fd89aff617e8211c9e531ee654de3af7dba3cc8edb8415129555e349199", "shared_library": false, "unvendored_tests": false, "version": "1.7.0"}, "threadpoolctl": {"depends": [], "file_name": "threadpoolctl-3.4.0-py3-none-any.whl", "imports": ["threadpoolctl"], "install_dir": "site", "name": "threadpoolctl", "package_type": "package", "sha256": "42744246ef195dcf1a4e1387ef93b52fdbbb1e838c970d8303a970c9cea886c5", "shared_library": false, "unvendored_tests": false, "version": "3.4.0"}, "tomli": {"depends": [], "file_name": "tomli-2.0.1-py3-none-any.whl", "imports": ["tomli"], "install_dir": "site", "name": "tomli", "package_type": "package", "sha256": "96c7e72ef2b9a75c5a5fe81e1e79f8c0dcb2f95cefe5c2b4406083e41bb922be", "shared_library": false, "unvendored_tests": false, "version": "2.0.1"}, "tomli-w": {"depends": [], "file_name": "tomli_w-1.0.0-py3-none-any.whl", "imports": ["tomli_w"], "install_dir": "site", "name": "tomli-w", "package_type": "package", "sha256": "bc217efbfd5909c997ee2ffcbbedce498b235f89d3f8bd97dc91bce38f8222f6", "shared_library": false, "unvendored_tests": false, "version": "1.0.0"}, "toolz": {"depends": [], "file_name": "toolz-0.12.1-py3-none-any.whl", "imports": ["tlz", "toolz"], "install_dir": "site", "name": "toolz", "package_type": "package", "sha256": "7fdec44c6bf2ec5e200f88d34616ffa37a612d59e79a3c7650e1651f43830ad1", "shared_library": false, "unvendored_tests": true, "version": "0.12.1"}, "toolz-tests": {"depends": ["toolz"], "file_name": "toolz-tests.tar", "imports": [], "install_dir": "site", "name": "toolz-tests", "package_type": "package", "sha256": "f8284c76aa221ae218fc32b9c8e79f2002d762d4b29fc418bf661ed1aaaebbbf", "shared_library": false, "unvendored_tests": false, "version": "0.12.1"}, "tqdm": {"depends": [], "file_name": "tqdm-4.66.2-py3-none-any.whl", "imports": ["tqdm"], "install_dir": "site", "name": "tqdm", "package_type": "package", "sha256": "a320470e3ebe4427ba9d5ebc55ba32e115602768a3c3839ffc3b6734cdc5c332", "shared_library": false, "unvendored_tests": false, "version": "4.66.2"}, "traitlets": {"depends": [], "file_name": "traitlets-5.14.3-py3-none-any.whl", "imports": ["traitlets"], "install_dir": "site", "name": "traitlets", "package_type": "package", "sha256": "eb436d3e1a6fc61c765112cb14ef88a97e17af1c1776530d152031d03d616625", "shared_library": false, "unvendored_tests": true, "version": "5.14.3"}, "traitlets-tests": {"depends": ["traitlets"], "file_name": "traitlets-tests.tar", "imports": [], "install_dir": "site", "name": "traitlets-tests", "package_type": "package", "sha256": "ede0f6fe9beada3195925d17172d7bd6007bb32704528658d858b696b204bb66", "shared_library": false, "unvendored_tests": false, "version": "5.14.3"}, "traits": {"depends": [], "file_name": "traits-6.4.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["traits"], "install_dir": "site", "name": "traits", "package_type": "package", "sha256": "2c44ca2179d175a1321158821ea4c1950a661ea4d68df0c7c00994ac2b8bb1f5", "shared_library": false, "unvendored_tests": true, "version": "6.4.3"}, "traits-tests": {"depends": ["traits"], "file_name": "traits-tests.tar", "imports": [], "install_dir": "site", "name": "traits-tests", "package_type": "package", "sha256": "281021428f44bc41d9b312740eb2c5081434d14be76ab56dc872011156b0ad01", "shared_library": false, "unvendored_tests": false, "version": "6.4.3"}, "tskit": {"depends": ["numpy", "svgwrite", "jsonschema", "rpds-py"], "file_name": "tskit-0.5.6-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["tskit"], "install_dir": "site", "name": "tskit", "package_type": "package", "sha256": "357a4e1bd6e3221f492300564910dea0745c796d4bba3327244e3bd766a900d7", "shared_library": false, "unvendored_tests": false, "version": "0.5.6"}, "typing-extensions": {"depends": [], "file_name": "typing_extensions-4.11.0-py3-none-any.whl", "imports": ["typing_extensions"], "install_dir": "site", "name": "typing-extensions", "package_type": "package", "sha256": "27413666e546c12f1fd74234fcd3ff44b3b11dd82b4becb2dfd697d199a01373", "shared_library": false, "unvendored_tests": false, "version": "4.11.0"}, "tzdata": {"depends": [], "file_name": "tzdata-2024.1-py2.py3-none-any.whl", "imports": ["tzdata"], "install_dir": "site", "name": "tzdata", "package_type": "package", "sha256": "7bcb4600ba97f6aae14e4bdd9d3f03f16e0b8136f9532a9af3f4c0e9d0403bea", "shared_library": false, "unvendored_tests": false, "version": "2024.1"}, "uncertainties": {"depends": ["future"], "file_name": "uncertainties-3.1.7-py2.py3-none-any.whl", "imports": ["uncertainties"], "install_dir": "site", "name": "uncertainties", "package_type": "package", "sha256": "7d976cd8383c7a5494588ef8c753aa3bc0cf58b85c13d35ef32a28258bc51dca", "shared_library": false, "unvendored_tests": true, "version": "3.1.7"}, "uncertainties-tests": {"depends": ["uncertainties"], "file_name": "uncertainties-tests.tar", "imports": [], "install_dir": "site", "name": "uncertainties-tests", "package_type": "package", "sha256": "31b8ae8b62bfac15cd420ddf7aecb67a11ea6c4911b43ac181763fb0c8c21279", "shared_library": false, "unvendored_tests": false, "version": "3.1.7"}, "unyt": {"depends": ["numpy", "packaging", "sympy"], "file_name": "unyt-3.0.2-py3-none-any.whl", "imports": ["unyt"], "install_dir": "site", "name": "unyt", "package_type": "package", "sha256": "0266261f277eb5ee959c6ddc12b912808ede8f4fc65550474fa52345072adca8", "shared_library": false, "unvendored_tests": true, "version": "3.0.2"}, "unyt-tests": {"depends": ["unyt"], "file_name": "unyt-tests.tar", "imports": [], "install_dir": "site", "name": "unyt-tests", "package_type": "package", "sha256": "dc62293a0d6b7548dea6cffce3fc6560c9efd62fd828038ef9e650a3352bc1fb", "shared_library": false, "unvendored_tests": false, "version": "3.0.2"}, "urllib3": {"depends": [], "file_name": "urllib3-2.2.1-py3-none-any.whl", "imports": ["urllib3"], "install_dir": "site", "name": "urllib3", "package_type": "package", "sha256": "db8c61a24d25a4902704fce3b4dfdd369141859cbeb5b8dd553e22634875f912", "shared_library": false, "unvendored_tests": false, "version": "2.2.1"}, "wcwidth": {"depends": [], "file_name": "wcwidth-0.2.13-py2.py3-none-any.whl", "imports": ["wcwidth"], "install_dir": "site", "name": "wcwidth", "package_type": "package", "sha256": "3384f09b43b9618624cb57500076aa54372a1c6160bf5069f6309c9f9e11a914", "shared_library": false, "unvendored_tests": false, "version": "0.2.13"}, "webencodings": {"depends": [], "file_name": "webencodings-0.5.1-py2.py3-none-any.whl", "imports": ["webencodings"], "install_dir": "site", "name": "webencodings", "package_type": "package", "sha256": "bd558d5be03feae048c02a9b5875f7b7dc5eac1e1c8b167fac3a9575bd13178c", "shared_library": false, "unvendored_tests": false, "version": "0.5.1"}, "wordcloud": {"depends": ["matplotlib"], "file_name": "wordcloud-1.9.3-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["wordcloud"], "install_dir": "site", "name": "wordcloud", "package_type": "package", "sha256": "e35767ded44ad7a2565633e60a011cdf63c9c40ce8755127c80ab12bd71c340b", "shared_library": false, "unvendored_tests": false, "version": "1.9.3"}, "wrapt": {"depends": [], "file_name": "wrapt-1.16.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["wrapt"], "install_dir": "site", "name": "wrapt", "package_type": "package", "sha256": "218ecd2503e989f9ff5e0c44b3270f3faed9e0c1cc989dd30e555840833aba7a", "shared_library": false, "unvendored_tests": false, "version": "1.16.0"}, "xarray": {"depends": ["numpy", "packaging", "pandas"], "file_name": "xarray-2024.3.0-py3-none-any.whl", "imports": ["xarray"], "install_dir": "site", "name": "xarray", "package_type": "package", "sha256": "811da0060896ccf83a10b575e3b73519c2ea6c4f1c53a7cc4b7ec7367f8297b2", "shared_library": false, "unvendored_tests": true, "version": "2024.3.0"}, "xarray-tests": {"depends": ["xarray"], "file_name": "xarray-tests.tar", "imports": [], "install_dir": "site", "name": "xarray-tests", "package_type": "package", "sha256": "ad0d1a70e79d8fd8059283d0e3cc56ddbf0282f64f252b84d9aa18bf183c8f5a", "shared_library": false, "unvendored_tests": false, "version": "2024.3.0"}, "xgboost": {"depends": ["numpy", "scipy", "setuptools"], "file_name": "xgboost-2.1.0.dev0-py3-none-pyodide_2024_0_wasm32.whl", "imports": ["xgboost"], "install_dir": "site", "name": "xgboost", "package_type": "package", "sha256": "4628575b6ceb2e986bbba4356c4864fb32bcbcc60786d3f26d5730012a48712b", "shared_library": false, "unvendored_tests": false, "version": "2.1.0.dev0"}, "xlrd": {"depends": [], "file_name": "xlrd-2.0.1-py2.py3-none-any.whl", "imports": ["xlrd"], "install_dir": "site", "name": "xlrd", "package_type": "package", "sha256": "5dce9ae1baf5f1bca143ae6bc664618ed3cd69d15b7be8d59901296dfd5f316d", "shared_library": false, "unvendored_tests": false, "version": "2.0.1"}, "xxhash": {"depends": [], "file_name": "xxhash-3.4.1-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["xxhash"], "install_dir": "site", "name": "xxhash", "package_type": "package", "sha256": "d7552b7b502fed9a814536ef4192f615c72293c03bc3b4f0abb8a910b025d22b", "shared_library": false, "unvendored_tests": false, "version": "3.4.1"}, "xyzservices": {"depends": [], "file_name": "xyzservices-2024.4.0-py3-none-any.whl", "imports": ["xyzservices"], "install_dir": "site", "name": "xyzservices", "package_type": "package", "sha256": "d83a3998a7ebdaf867608ed25ff2225d735ef165d7d637a53eae60a4019be77c", "shared_library": false, "unvendored_tests": true, "version": "2024.4.0"}, "xyzservices-tests": {"depends": ["xyzservices"], "file_name": "xyzservices-tests.tar", "imports": [], "install_dir": "site", "name": "xyzservices-tests", "package_type": "package", "sha256": "5478c9cfc55e20112bd9786d5170e03498715987db905508288723ce5bd015ba", "shared_library": false, "unvendored_tests": false, "version": "2024.4.0"}, "yarl": {"depends": ["multidict", "idna"], "file_name": "yarl-1.9.4-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["yarl"], "install_dir": "site", "name": "yarl", "package_type": "package", "sha256": "970a80d724db012e4832adbe5f7f25d3070dddec20eac496063ec4c615ba0f5b", "shared_library": false, "unvendored_tests": false, "version": "1.9.4"}, "yt": {"depends": ["ewah_bool_utils", "numpy", "matplotlib", "sympy", "setuptools", "packaging", "unyt", "cmyt", "colorspacious", "tqdm", "tomli", "tomli-w"], "file_name": "yt-4.3.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["yt"], "install_dir": "site", "name": "yt", "package_type": "package", "sha256": "8064f02ce5685790bfa3030a538a5808086ffdd84d4591cb2caaf6d959125564", "shared_library": false, "unvendored_tests": false, "version": "4.3.0"}, "zarr": {"depends": ["numpy", "asciitree", "numcodecs"], "file_name": "zarr-2.16.1-py3-none-any.whl", "imports": ["zarr"], "install_dir": "site", "name": "zarr", "package_type": "package", "sha256": "e8d9f3f619de2b6ab029535822e2ac7d215533c175b10dda90744cd0584edc4b", "shared_library": false, "unvendored_tests": true, "version": "2.16.1"}, "zarr-tests": {"depends": ["zarr"], "file_name": "zarr-tests.tar", "imports": [], "install_dir": "site", "name": "zarr-tests", "package_type": "package", "sha256": "559d33fa87d95d2da09bc0f84fa387ee6e9bf193eecb55b103825659065d804f", "shared_library": false, "unvendored_tests": false, "version": "2.16.1"}, "zengl": {"depends": [], "file_name": "zengl-2.4.1-cp311-abi3-pyodide_2024_0_wasm32.whl", "imports": ["zengl", "_zengl"], "install_dir": "site", "name": "zengl", "package_type": "package", "sha256": "84ca8691ab1b7ac6892fb546f1bb954a9cd97f46b39b25f33e3ee5c68ce44cb5", "shared_library": false, "unvendored_tests": false, "version": "2.4.1"}, "zstandard": {"depends": ["cffi"], "file_name": "zstandard-0.22.0-cp312-cp312-pyodide_2024_0_wasm32.whl", "imports": ["zstandard"], "install_dir": "site", "name": "zstandard", "package_type": "package", "sha256": "2132e35bc2775019e64bb1a88ae0f09c4bb46eb6516681ff7e0f27ef38fade54", "shared_library": false, "unvendored_tests": false, "version": "0.22.0"}}} \ No newline at end of file diff --git a/static/static/favicon.png b/static/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..2b2074780847581edf9cf2ed0d2e9ebd8ff08c56 Binary files /dev/null and b/static/static/favicon.png differ diff --git a/static/static/splash-dark.png b/static/static/splash-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..202c03f8e46e189025b204b5bedc0552aec4ac82 Binary files /dev/null and b/static/static/splash-dark.png differ diff --git a/static/static/splash.png b/static/static/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..389196ca6a364b9e4b7daa0fc13be463b914b251 Binary files /dev/null and b/static/static/splash.png differ diff --git a/static/themes/rosepine-dawn.css b/static/themes/rosepine-dawn.css new file mode 100644 index 0000000000000000000000000000000000000000..08445138c4e79f395b620f4c75fd44856350280a --- /dev/null +++ b/static/themes/rosepine-dawn.css @@ -0,0 +1,140 @@ +.rose-pine-dawn * { + color: #575279 !important; + stroke: #d7827e !important; +} + +.rose-pine-dawn .app > * { + background-color: #faf4ed !important; +} + +.rose-pine-dawn #nav { + background-color: #fffaf3; +} + +.rose-pine-dawn .py-2\.5.my-auto.flex.flex-col.justify-between.h-screen { + background: #f2e9e1; +} + +.rose-pine-dawn .bg-white.dark\:bg-gray-800 { + background: #f2e9e1; +} + +.rose-pine-dawn .w-4.h-4 { + fill: #ebbcba; +} + +.rose-pine-dawn #chat-textarea { + background: #cecacd; + margin: 0.3rem; + padding: 0.5rem; +} + +.rose-pine-dawn .bg-gradient-to-t.from-white.dark\:from-gray-800.from-40\%.pb-2 { + background: #f2e9e1 !important; + padding-top: 0.6rem; +} + +.rose-pine-dawn + .text-white.bg-gray-100.dark\:text-gray-800.dark\:bg-gray-600.disabled.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { + background-color: #cecacd; + transition: background-color 0.2s ease-out linear; +} + +.rose-pine-dawn + .bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { + background-color: #286983; + transition: background-color 0.2s ease-out linear; +} + +.rose-pine-dawn + .bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center + > * { + fill: #56949f !important; + transition: fill 0.2s ease-out linear; +} + +.rose-pine-dawn + .w-full.flex.justify-between.rounded-md.px-3.py-2.hover\:bg-gray-900.bg-gray-900.transition.whitespace-nowrap.text-ellipsis { + background-color: #56526e; + font-weight: bold; +} + +.rose-pine-dawn .hover\:bg-gray-900:hover { + --tw-bg-opacity: 1; + background-color: rgb(152 147 165 / var(--tw-bg-opacity)); +} + +.rose-pine-dawn .text-xs.text-gray-700.uppercase.bg-gray-50.dark\:bg-gray-700.dark\:text-gray-400 { + background-color: #403d52; +} + +.rose-pine-dawn .scrollbar-hidden.relative.overflow-x-auto.whitespace-nowrap.svelte-3g4avz { + border-radius: 16px 16px 0 0; +} + +.rose-pine-dawn .base.enter.svelte-ug60r4 { + background-color: #286983; +} + +.rose-pine-dawn .message.svelte-1nauejd { + color: #e0def4 !important; +} + +.rose-pine-dawn #dropdownDots { + background-color: #dfdad9; +} + +.rose-pine-dawn .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { + background: #cecacd; +} + +.rose-pine-dawn #dropdownDots { + background-color: #dfdad9; +} + +.rose-pine-dawn .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { + background: #cecacd; +} + +.rose-pine-dawn + .m-auto.rounded-xl.max-w-full.w-\[40rem\].mx-2.bg-gray-50.dark\:bg-gray-900.shadow-3xl { + background-color: #f2e9e1; +} + +.rose-pine-dawn + .w-full.rounded.p-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.resize-none { + background-color: #cecacd; +} + +.rose-pine-dawn + .w-full.rounded.py-2.px-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.svelte-1vx7r9s { + background-color: #cecacd; +} + +.rose-pine-dawn + .px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.bg-gray-200.dark\:bg-gray-700 { + background-color: #dfdad9; +} + +.rose-pine-dawn + .px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.hover\:bg-gray-300.dark\:hover\:bg-gray-800:hover { + background-color: #cecacd; +} + +.rose-pine-dawn .px-4.py-2.bg-emerald-600.hover\:bg-emerald-700.text-gray-100.transition.rounded { + background-color: #56949f; +} + +.rose-pine-dawn #chat-search > * { + background-color: #dfdad9 !important; +} + +.rose-pine-dawn .svelte-1ee93ns { + --primary: #b4637a !important; + --secondary: #fffaf3 !important; +} + +.rose-pine-dawn .svelte-11kvm4p { + --primary: #56949f !important; + --secondary: #fffaf3 !important; +} diff --git a/static/themes/rosepine.css b/static/themes/rosepine.css new file mode 100644 index 0000000000000000000000000000000000000000..c98ee5c69a530449db64b4a0edb218f5a5ec763c --- /dev/null +++ b/static/themes/rosepine.css @@ -0,0 +1,131 @@ +.rose-pine * { + color: #e0def4 !important; + stroke: #907aa9 !important; +} + +.rose-pine .app > * { + background-color: #1f1d2e !important; +} + +.rose-pine #nav { + background-color: #191724; +} + +.rose-pine .py-2\.5.my-auto.flex.flex-col.justify-between.h-screen { + background: #191724; +} + +.rose-pine .bg-white.dark\:bg-gray-800 { + background: #26233a; +} + +.rose-pine .w-4.h-4 { + fill: #c4a7e7; +} + +.rose-pine #chat-textarea { + background: #393552; + margin: 0.3rem; + padding: 0.5rem; +} + +.rose-pine .bg-gradient-to-t.from-white.dark\:from-gray-800.from-40\%.pb-2 { + background: #26233a !important; + padding-top: 0.6rem; +} + +.rose-pine + .text-white.bg-gray-100.dark\:text-gray-800.dark\:bg-gray-600.disabled.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { + background-color: #6e6a86; + transition: background-color 0.2s ease-out linear; +} + +.rose-pine + .bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center { + background-color: #286983; + transition: background-color 0.2s ease-out linear; +} + +.rose-pine + .bg-black.text-white.hover\:bg-gray-900.dark\:bg-white.dark\:text-black.dark\:hover\:bg-gray-100.transition.rounded-lg.p-1.mr-0\.5.w-7.h-7.self-center + > * { + fill: #9ccfd8 !important; + transition: fill 0.2s ease-out linear; +} + +.rose-pine + .w-full.flex.justify-between.rounded-md.px-3.py-2.hover\:bg-gray-900.bg-gray-900.transition.whitespace-nowrap.text-ellipsis { + background-color: #56526e; + font-weight: bold; +} + +.rose-pine .hover\:bg-gray-900:hover { + --tw-bg-opacity: 1; + background-color: rgb(57 53 82 / var(--tw-bg-opacity)); +} + +.rose-pine .text-xs.text-gray-700.uppercase.bg-gray-50.dark\:bg-gray-700.dark\:text-gray-400 { + background-color: #403d52; +} + +.rose-pine .scrollbar-hidden.relative.overflow-x-auto.whitespace-nowrap.svelte-3g4avz { + border-radius: 16px 16px 0 0; +} + +.rose-pine .base.enter.svelte-ug60r4 { + background-color: #393552; +} + +.rose-pine .message.svelte-1nauejd { + color: #e0def4 !important; +} + +.rose-pine #dropdownDots { + background-color: #403d52; +} + +.rose-pine .flex.py-2\.5.px-3\.5.w-full.hover\:bg-gray-800.transition:hover { + background: #524f67; +} + +.rose-pine .m-auto.rounded-xl.max-w-full.w-\[40rem\].mx-2.bg-gray-50.dark\:bg-gray-900.shadow-3xl { + background-color: #26233a; +} + +.rose-pine + .w-full.rounded.p-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.resize-none { + background-color: #524f67; +} + +.rose-pine + .w-full.rounded.py-2.px-4.text-sm.dark\:text-gray-300.dark\:bg-gray-800.outline-none.svelte-1vx7r9s { + background-color: #524f67; +} + +.rose-pine + .px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.bg-gray-200.dark\:bg-gray-700 { + background-color: #403d52; +} + +.rose-pine + .px-2\.5.py-2\.5.min-w-fit.rounded-lg.flex-1.md\:flex-none.flex.text-right.transition.hover\:bg-gray-300.dark\:hover\:bg-gray-800:hover { + background-color: #524f67; +} + +.rose-pine .px-4.py-2.bg-emerald-600.hover\:bg-emerald-700.text-gray-100.transition.rounded { + background-color: #31748f; +} + +.rose-pine #chat-search > * { + background-color: #403d52 !important; +} + +.rose-pine .svelte-1ee93ns { + --primary: #eb6f92 !important; + --secondary: #e0def4 !important; +} + +.rose-pine .svelte-11kvm4p { + --primary: #9ccfd8 !important; + --secondary: #1f1d2e !important; +} diff --git a/static/user.png b/static/user.png new file mode 100644 index 0000000000000000000000000000000000000000..2771e7286684e0935f952ef671dfe1f75f422256 Binary files /dev/null and b/static/user.png differ diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000000000000000000000000000000000..1ff7ceeb7476d2a0cfd222e38bdb466f95d9a459 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,27 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/kit/vite'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. + // If your environment is not supported or you settled on a specific environment, switch out the adapter. + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html' + }) + }, + onwarn: (warning, handler) => { + const { code, _ } = warning; + if (code === 'css-unused-selector') return; + + handler(warning); + } +}; + +export default config; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000000000000000000000000000000000000..893cda83d66113d7324992a45a6094586a504657 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,37 @@ +/** @type {import('tailwindcss').Config} */ +export default { + darkMode: 'class', + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: { + colors: { + gray: { + 50: '#f9f9f9', + 100: '#ececec', + 200: '#e3e3e3', + 300: '#cdcdcd', + 400: '#b4b4b4', + 500: '#9b9b9b', + 600: '#676767', + 700: '#4e4e4e', + 800: 'var(--color-gray-800, #333)', + 850: 'var(--color-gray-850, #262626)', + 900: 'var(--color-gray-900, #171717)', + 950: 'var(--color-gray-950, #0d0d0d)' + } + }, + typography: { + DEFAULT: { + css: { + pre: false, + code: false, + 'pre code': false, + 'code::before': false, + 'code::after': false + } + } + } + } + }, + plugins: [require('@tailwindcss/typography')] +}; diff --git a/test/test_files/image_gen/sd-empty.pt b/test/test_files/image_gen/sd-empty.pt new file mode 100644 index 0000000000000000000000000000000000000000..c6ac59eb01fcb778290a85f12bdb7867de3dfdd1 Binary files /dev/null and b/test/test_files/image_gen/sd-empty.pt differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..6ae0c8c44d08a78140c9c62c1b0f745edd05e804 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/update_ollama_models.sh b/update_ollama_models.sh new file mode 100644 index 0000000000000000000000000000000000000000..bde11b4b248694fcb1a224f7c193e75a88ee156f --- /dev/null +++ b/update_ollama_models.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# update_llm.sh + +# Retrieves the list of LLMs installed in the Docker container +llm_list=$(docker exec ollama ollama list | tail -n +2 | awk '{print $1}') + +# Loop over each LLM to update it +for llm in $llm_list; do + docker exec ollama ollama pull $llm +done diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6af4bba559bece5432370df7712b7ea69a0aa66 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,30 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +// /** @type {import('vite').Plugin} */ +// const viteServerConfig = { +// name: 'log-request-middleware', +// configureServer(server) { +// server.middlewares.use((req, res, next) => { +// res.setHeader('Access-Control-Allow-Origin', '*'); +// res.setHeader('Access-Control-Allow-Methods', 'GET'); +// res.setHeader('Cross-Origin-Opener-Policy', 'same-origin'); +// res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp'); +// next(); +// }); +// } +// }; + +export default defineConfig({ + plugins: [sveltekit()], + define: { + APP_VERSION: JSON.stringify(process.env.npm_package_version), + APP_BUILD_HASH: JSON.stringify(process.env.APP_BUILD_HASH || 'dev-build') + }, + build: { + sourcemap: true + }, + worker: { + format: 'es' + } +});