mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-19 01:40:15 +01:00
Merge branch 'main' into feat/webauthn
This commit is contained in:
commit
8173f5fca1
132 changed files with 5513 additions and 769 deletions
24
.env.example
24
.env.example
|
|
@ -20,6 +20,11 @@ DOMAIN_CLIENT=http://localhost:3080
|
||||||
DOMAIN_SERVER=http://localhost:3080
|
DOMAIN_SERVER=http://localhost:3080
|
||||||
|
|
||||||
NO_INDEX=true
|
NO_INDEX=true
|
||||||
|
# Use the address that is at most n number of hops away from the Express application.
|
||||||
|
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
|
||||||
|
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
|
||||||
|
# Defaulted to 1.
|
||||||
|
TRUST_PROXY=1
|
||||||
|
|
||||||
#===============#
|
#===============#
|
||||||
# JSON Logging #
|
# JSON Logging #
|
||||||
|
|
@ -292,6 +297,10 @@ MEILI_NO_ANALYTICS=true
|
||||||
MEILI_HOST=http://0.0.0.0:7700
|
MEILI_HOST=http://0.0.0.0:7700
|
||||||
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
||||||
|
|
||||||
|
# Optional: Disable indexing, useful in a multi-node setup
|
||||||
|
# where only one instance should perform an index sync.
|
||||||
|
# MEILI_NO_SYNC=true
|
||||||
|
|
||||||
#==================================================#
|
#==================================================#
|
||||||
# Speech to Text & Text to Speech #
|
# Speech to Text & Text to Speech #
|
||||||
#==================================================#
|
#==================================================#
|
||||||
|
|
@ -389,7 +398,7 @@ FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
|
||||||
GITHUB_CLIENT_ID=
|
GITHUB_CLIENT_ID=
|
||||||
GITHUB_CLIENT_SECRET=
|
GITHUB_CLIENT_SECRET=
|
||||||
GITHUB_CALLBACK_URL=/oauth/github/callback
|
GITHUB_CALLBACK_URL=/oauth/github/callback
|
||||||
# GitHub Eenterprise
|
# GitHub Enterprise
|
||||||
# GITHUB_ENTERPRISE_BASE_URL=
|
# GITHUB_ENTERPRISE_BASE_URL=
|
||||||
# GITHUB_ENTERPRISE_USER_AGENT=
|
# GITHUB_ENTERPRISE_USER_AGENT=
|
||||||
|
|
||||||
|
|
@ -499,6 +508,16 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
||||||
# Google tag manager id
|
# Google tag manager id
|
||||||
#ANALYTICS_GTM_ID=user provided google tag manager id
|
#ANALYTICS_GTM_ID=user provided google tag manager id
|
||||||
|
|
||||||
|
#===============#
|
||||||
|
# REDIS Options #
|
||||||
|
#===============#
|
||||||
|
|
||||||
|
# REDIS_URI=10.10.10.10:6379
|
||||||
|
# USE_REDIS=true
|
||||||
|
|
||||||
|
# USE_REDIS_CLUSTER=true
|
||||||
|
# REDIS_CA=/path/to/ca.crt
|
||||||
|
|
||||||
#==================================================#
|
#==================================================#
|
||||||
# Others #
|
# Others #
|
||||||
#==================================================#
|
#==================================================#
|
||||||
|
|
@ -506,9 +525,6 @@ HELP_AND_FAQ_URL=https://librechat.ai
|
||||||
|
|
||||||
# NODE_ENV=
|
# NODE_ENV=
|
||||||
|
|
||||||
# REDIS_URI=
|
|
||||||
# USE_REDIS=
|
|
||||||
|
|
||||||
# E2E_USER_EMAIL=
|
# E2E_USER_EMAIL=
|
||||||
# E2E_USER_PASSWORD=
|
# E2E_USER_PASSWORD=
|
||||||
|
|
||||||
|
|
|
||||||
42
.github/ISSUE_TEMPLATE/LOCIZE_TRANSLATION_ACCESS_REQUEST.yml
vendored
Normal file
42
.github/ISSUE_TEMPLATE/LOCIZE_TRANSLATION_ACCESS_REQUEST.yml
vendored
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
name: Locize Translation Access Request
|
||||||
|
description: Request access to an additional language in Locize for LibreChat translations.
|
||||||
|
title: "Locize Access Request: "
|
||||||
|
labels: ["🌍 i18n", "🔑 access request"]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thank you for your interest in contributing to LibreChat translations!
|
||||||
|
Please fill out the form below to request access to an additional language in **Locize**.
|
||||||
|
|
||||||
|
**🔗 Available Languages:** [View the list here](https://www.librechat.ai/docs/translation)
|
||||||
|
|
||||||
|
**📌 Note:** Ensure that the requested language is supported before submitting your request.
|
||||||
|
- type: input
|
||||||
|
id: account_name
|
||||||
|
attributes:
|
||||||
|
label: Locize Account Name
|
||||||
|
description: Please provide your Locize account name (e.g., John Doe).
|
||||||
|
placeholder: e.g., John Doe
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: language_requested
|
||||||
|
attributes:
|
||||||
|
label: Language Code (ISO 639-1)
|
||||||
|
description: |
|
||||||
|
Enter the **ISO 639-1** language code for the language you want to translate into.
|
||||||
|
Example: `es` for Spanish, `zh-Hant` for Traditional Chinese.
|
||||||
|
|
||||||
|
**🔗 Reference:** [Available Languages](https://www.librechat.ai/docs/translation)
|
||||||
|
placeholder: e.g., es
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: checkboxes
|
||||||
|
id: agreement
|
||||||
|
attributes:
|
||||||
|
label: Agreement
|
||||||
|
description: By submitting this request, you confirm that you will contribute responsibly and adhere to the project guidelines.
|
||||||
|
options:
|
||||||
|
- label: I agree to use my access solely for contributing to LibreChat translations.
|
||||||
|
required: true
|
||||||
50
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
50
.github/ISSUE_TEMPLATE/QUESTION.yml
vendored
|
|
@ -1,50 +0,0 @@
|
||||||
name: Question
|
|
||||||
description: Ask your question
|
|
||||||
title: "[Question]: "
|
|
||||||
labels: ["❓ question"]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thanks for taking the time to fill this!
|
|
||||||
- type: textarea
|
|
||||||
id: what-is-your-question
|
|
||||||
attributes:
|
|
||||||
label: What is your question?
|
|
||||||
description: Please give as many details as possible
|
|
||||||
placeholder: Please give as many details as possible
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: more-details
|
|
||||||
attributes:
|
|
||||||
label: More Details
|
|
||||||
description: Please provide more details if needed.
|
|
||||||
placeholder: Please provide more details if needed.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: dropdown
|
|
||||||
id: browsers
|
|
||||||
attributes:
|
|
||||||
label: What is the main subject of your question?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Documentation
|
|
||||||
- Installation
|
|
||||||
- UI
|
|
||||||
- Endpoints
|
|
||||||
- User System/OAuth
|
|
||||||
- Other
|
|
||||||
- type: textarea
|
|
||||||
id: screenshots
|
|
||||||
attributes:
|
|
||||||
label: Screenshots
|
|
||||||
description: If applicable, add screenshots to help explain your problem. You can drag and drop, paste images directly here or link to them.
|
|
||||||
- type: checkboxes
|
|
||||||
id: terms
|
|
||||||
attributes:
|
|
||||||
label: Code of Conduct
|
|
||||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/danny-avila/LibreChat/blob/main/.github/CODE_OF_CONDUCT.md)
|
|
||||||
options:
|
|
||||||
- label: I agree to follow this project's Code of Conduct
|
|
||||||
required: true
|
|
||||||
60
.github/configuration-release.json
vendored
Normal file
60
.github/configuration-release.json
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"title": "### ✨ New Features",
|
||||||
|
"labels": ["feat"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 🌍 Internationalization",
|
||||||
|
"labels": ["i18n"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 👐 Accessibility",
|
||||||
|
"labels": ["a11y"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 🔧 Fixes",
|
||||||
|
"labels": ["Fix", "fix"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### ⚙️ Other Changes",
|
||||||
|
"labels": ["ci", "style", "docs", "refactor", "chore"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignore_labels": [
|
||||||
|
"🔁 duplicate",
|
||||||
|
"📊 analytics",
|
||||||
|
"🌱 good first issue",
|
||||||
|
"🔍 investigation",
|
||||||
|
"🙏 help wanted",
|
||||||
|
"❌ invalid",
|
||||||
|
"❓ question",
|
||||||
|
"🚫 wontfix",
|
||||||
|
"🚀 release",
|
||||||
|
"version"
|
||||||
|
],
|
||||||
|
"base_branches": ["main"],
|
||||||
|
"sort": {
|
||||||
|
"order": "ASC",
|
||||||
|
"on_property": "mergedAt"
|
||||||
|
},
|
||||||
|
"label_extractor": [
|
||||||
|
{
|
||||||
|
"pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:",
|
||||||
|
"target": "$1",
|
||||||
|
"flags": "i",
|
||||||
|
"on_property": "title",
|
||||||
|
"method": "match"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*",
|
||||||
|
"target": "version",
|
||||||
|
"flags": "i",
|
||||||
|
"on_property": "title",
|
||||||
|
"method": "match"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"template": "## [#{{TO_TAG}}] - #{{TO_TAG_DATE}}\n\nChanges from #{{FROM_TAG}} to #{{TO_TAG}}.\n\n#{{CHANGELOG}}\n\n[See full release details][release-#{{TO_TAG}}]\n\n[release-#{{TO_TAG}}]: https://github.com/#{{OWNER}}/#{{REPO}}/releases/tag/#{{TO_TAG}}\n\n---",
|
||||||
|
"pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})",
|
||||||
|
"empty_template": "- no changes"
|
||||||
|
}
|
||||||
68
.github/configuration-unreleased.json
vendored
Normal file
68
.github/configuration-unreleased.json
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"title": "### ✨ New Features",
|
||||||
|
"labels": ["feat"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 🌍 Internationalization",
|
||||||
|
"labels": ["i18n"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 👐 Accessibility",
|
||||||
|
"labels": ["a11y"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### 🔧 Fixes",
|
||||||
|
"labels": ["Fix", "fix"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "### ⚙️ Other Changes",
|
||||||
|
"labels": ["ci", "style", "docs", "refactor", "chore"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ignore_labels": [
|
||||||
|
"🔁 duplicate",
|
||||||
|
"📊 analytics",
|
||||||
|
"🌱 good first issue",
|
||||||
|
"🔍 investigation",
|
||||||
|
"🙏 help wanted",
|
||||||
|
"❌ invalid",
|
||||||
|
"❓ question",
|
||||||
|
"🚫 wontfix",
|
||||||
|
"🚀 release",
|
||||||
|
"version",
|
||||||
|
"action"
|
||||||
|
],
|
||||||
|
"base_branches": ["main"],
|
||||||
|
"sort": {
|
||||||
|
"order": "ASC",
|
||||||
|
"on_property": "mergedAt"
|
||||||
|
},
|
||||||
|
"label_extractor": [
|
||||||
|
{
|
||||||
|
"pattern": "^(?:[^A-Za-z0-9]*)(feat|fix|chore|docs|refactor|ci|style|a11y|i18n)\\s*:",
|
||||||
|
"target": "$1",
|
||||||
|
"flags": "i",
|
||||||
|
"on_property": "title",
|
||||||
|
"method": "match"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^(?:[^A-Za-z0-9]*)(v\\d+\\.\\d+\\.\\d+(?:-rc\\d+)?).*",
|
||||||
|
"target": "version",
|
||||||
|
"flags": "i",
|
||||||
|
"on_property": "title",
|
||||||
|
"method": "match"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pattern": "^(?:[^A-Za-z0-9]*)(action)\\b.*",
|
||||||
|
"target": "action",
|
||||||
|
"flags": "i",
|
||||||
|
"on_property": "title",
|
||||||
|
"method": "match"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"template": "## [Unreleased]\n\n#{{CHANGELOG}}\n\n---",
|
||||||
|
"pr_template": "- #{{TITLE}} by **@#{{AUTHOR}}** in [##{{NUMBER}}](#{{URL}})",
|
||||||
|
"empty_template": "- no changes"
|
||||||
|
}
|
||||||
94
.github/workflows/generate-release-changelog-pr.yml
vendored
Normal file
94
.github/workflows/generate-release-changelog-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
name: Generate Release Changelog PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate-release-changelog-pr:
|
||||||
|
permissions:
|
||||||
|
contents: write # Needed for pushing commits and creating branches.
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# 1. Checkout the repository (with full history).
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# 2. Generate the release changelog using our custom configuration.
|
||||||
|
- name: Generate Release Changelog
|
||||||
|
id: generate_release
|
||||||
|
uses: mikepenz/release-changelog-builder-action@v5.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
configuration: ".github/configuration-release.json"
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repo: ${{ github.event.repository.name }}
|
||||||
|
outputFile: CHANGELOG-release.md
|
||||||
|
|
||||||
|
# 3. Update the main CHANGELOG.md:
|
||||||
|
# - If it doesn't exist, create it with a basic header.
|
||||||
|
# - Remove the "Unreleased" section (if present).
|
||||||
|
# - Prepend the new release changelog above previous releases.
|
||||||
|
# - Remove all temporary files before committing.
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
# Determine the release tag, e.g. "v1.2.3"
|
||||||
|
TAG=${GITHUB_REF##*/}
|
||||||
|
echo "Using release tag: $TAG"
|
||||||
|
|
||||||
|
# Ensure CHANGELOG.md exists; if not, create a basic header.
|
||||||
|
if [ ! -f CHANGELOG.md ]; then
|
||||||
|
echo "# Changelog" > CHANGELOG.md
|
||||||
|
echo "" >> CHANGELOG.md
|
||||||
|
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
|
||||||
|
echo "" >> CHANGELOG.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating CHANGELOG.md…"
|
||||||
|
|
||||||
|
# Remove the "Unreleased" section (from "## [Unreleased]" until the first occurrence of '---') if it exists.
|
||||||
|
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
awk '/^## \[Unreleased\]/{flag=1} flag && /^---/{flag=0; next} !flag' CHANGELOG.md > CHANGELOG.cleaned
|
||||||
|
else
|
||||||
|
cp CHANGELOG.md CHANGELOG.cleaned
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Split the cleaned file into:
|
||||||
|
# - header.md: content before the first release header ("## [v...").
|
||||||
|
# - tail.md: content from the first release header onward.
|
||||||
|
awk '/^## \[v/{exit} {print}' CHANGELOG.cleaned > header.md
|
||||||
|
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.cleaned > tail.md
|
||||||
|
|
||||||
|
# Combine header, the new release changelog, and the tail.
|
||||||
|
echo "Combining updated changelog parts..."
|
||||||
|
cat header.md CHANGELOG-release.md > CHANGELOG.md.new
|
||||||
|
echo "" >> CHANGELOG.md.new
|
||||||
|
cat tail.md >> CHANGELOG.md.new
|
||||||
|
|
||||||
|
mv CHANGELOG.md.new CHANGELOG.md
|
||||||
|
|
||||||
|
# Remove temporary files.
|
||||||
|
rm -f CHANGELOG.cleaned header.md tail.md CHANGELOG-release.md
|
||||||
|
|
||||||
|
echo "Final CHANGELOG.md content:"
|
||||||
|
cat CHANGELOG.md
|
||||||
|
|
||||||
|
# 4. Create (or update) the Pull Request with the updated CHANGELOG.md.
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
sign-commits: true
|
||||||
|
commit-message: "chore: update CHANGELOG for release ${GITHUB_REF##*/}"
|
||||||
|
base: main
|
||||||
|
branch: "changelog/${GITHUB_REF##*/}"
|
||||||
|
reviewers: danny-avila
|
||||||
|
title: "chore: update CHANGELOG for release ${GITHUB_REF##*/}"
|
||||||
|
body: |
|
||||||
|
**Description**:
|
||||||
|
- This PR updates the CHANGELOG.md by removing the "Unreleased" section and adding new release notes for release ${GITHUB_REF##*/} above previous releases.
|
||||||
106
.github/workflows/generate-unreleased-changelog-pr.yml
vendored
Normal file
106
.github/workflows/generate-unreleased-changelog-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
name: Generate Unreleased Changelog PR
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * 1" # Runs every Monday at 00:00 UTC
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate-unreleased-changelog-pr:
|
||||||
|
permissions:
|
||||||
|
contents: write # Needed for pushing commits and creating branches.
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# 1. Checkout the repository on main.
|
||||||
|
- name: Checkout Repository on Main
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# 4. Get the latest version tag.
|
||||||
|
- name: Get Latest Tag
|
||||||
|
id: get_latest_tag
|
||||||
|
run: |
|
||||||
|
LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1) || echo "none")
|
||||||
|
echo "Latest tag: $LATEST_TAG"
|
||||||
|
echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 5. Generate the Unreleased changelog.
|
||||||
|
- name: Generate Unreleased Changelog
|
||||||
|
id: generate_unreleased
|
||||||
|
uses: mikepenz/release-changelog-builder-action@v5.1.0
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
configuration: ".github/configuration-unreleased.json"
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repo: ${{ github.event.repository.name }}
|
||||||
|
outputFile: CHANGELOG-unreleased.md
|
||||||
|
fromTag: ${{ steps.get_latest_tag.outputs.tag }}
|
||||||
|
toTag: main
|
||||||
|
|
||||||
|
# 7. Update CHANGELOG.md with the new Unreleased section.
|
||||||
|
- name: Update CHANGELOG.md
|
||||||
|
id: update_changelog
|
||||||
|
run: |
|
||||||
|
# Create CHANGELOG.md if it doesn't exist.
|
||||||
|
if [ ! -f CHANGELOG.md ]; then
|
||||||
|
echo "# Changelog" > CHANGELOG.md
|
||||||
|
echo "" >> CHANGELOG.md
|
||||||
|
echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md
|
||||||
|
echo "" >> CHANGELOG.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Updating CHANGELOG.md…"
|
||||||
|
|
||||||
|
# Extract content before the "## [Unreleased]" (or first version header if missing).
|
||||||
|
if grep -q "^## \[Unreleased\]" CHANGELOG.md; then
|
||||||
|
awk '/^## \[Unreleased\]/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
|
||||||
|
else
|
||||||
|
awk '/^## \[v/{exit} {print}' CHANGELOG.md > CHANGELOG_TMP.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Append the generated Unreleased changelog.
|
||||||
|
echo "" >> CHANGELOG_TMP.md
|
||||||
|
cat CHANGELOG-unreleased.md >> CHANGELOG_TMP.md
|
||||||
|
echo "" >> CHANGELOG_TMP.md
|
||||||
|
|
||||||
|
# Append the remainder of the original changelog (starting from the first version header).
|
||||||
|
awk 'f{print} /^## \[v/{f=1; print}' CHANGELOG.md >> CHANGELOG_TMP.md
|
||||||
|
|
||||||
|
# Replace the old file with the updated file.
|
||||||
|
mv CHANGELOG_TMP.md CHANGELOG.md
|
||||||
|
|
||||||
|
# Remove the temporary generated file.
|
||||||
|
rm -f CHANGELOG-unreleased.md
|
||||||
|
|
||||||
|
echo "Final CHANGELOG.md:"
|
||||||
|
cat CHANGELOG.md
|
||||||
|
|
||||||
|
# 8. Check if CHANGELOG.md has any updates.
|
||||||
|
- name: Check for CHANGELOG.md changes
|
||||||
|
id: changelog_changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet CHANGELOG.md; then
|
||||||
|
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 9. Create (or update) the Pull Request only if there are changes.
|
||||||
|
- name: Create Pull Request
|
||||||
|
if: steps.changelog_changes.outputs.has_changes == 'true'
|
||||||
|
uses: peter-evans/create-pull-request@v7
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
base: main
|
||||||
|
branch: "changelog/unreleased-update"
|
||||||
|
sign-commits: true
|
||||||
|
commit-message: "action: update Unreleased changelog"
|
||||||
|
title: "action: update Unreleased changelog"
|
||||||
|
body: |
|
||||||
|
**Description**:
|
||||||
|
- This PR updates the Unreleased section in CHANGELOG.md.
|
||||||
|
- It compares the current main branch with the latest version tag (determined as ${{ steps.get_latest_tag.outputs.tag }}),
|
||||||
|
regenerates the Unreleased changelog, removes any old Unreleased block, and inserts the new content.
|
||||||
13
.github/workflows/i18n-unused-keys.yml
vendored
13
.github/workflows/i18n-unused-keys.yml
vendored
|
|
@ -4,6 +4,7 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- "client/src/**"
|
- "client/src/**"
|
||||||
|
- "api/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
detect-unused-i18n-keys:
|
detect-unused-i18n-keys:
|
||||||
|
|
@ -21,7 +22,7 @@ jobs:
|
||||||
|
|
||||||
# Define paths
|
# Define paths
|
||||||
I18N_FILE="client/src/locales/en/translation.json"
|
I18N_FILE="client/src/locales/en/translation.json"
|
||||||
SOURCE_DIR="client/src"
|
SOURCE_DIRS=("client/src" "api")
|
||||||
|
|
||||||
# Check if translation file exists
|
# Check if translation file exists
|
||||||
if [[ ! -f "$I18N_FILE" ]]; then
|
if [[ ! -f "$I18N_FILE" ]]; then
|
||||||
|
|
@ -37,7 +38,15 @@ jobs:
|
||||||
|
|
||||||
# Check if each key is used in the source code
|
# Check if each key is used in the source code
|
||||||
for KEY in $KEYS; do
|
for KEY in $KEYS; do
|
||||||
if ! grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$SOURCE_DIR"; then
|
FOUND=false
|
||||||
|
for DIR in "${SOURCE_DIRS[@]}"; do
|
||||||
|
if grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$DIR"; then
|
||||||
|
FOUND=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$FOUND" == false ]]; then
|
||||||
UNUSED_KEYS+=("$KEY")
|
UNUSED_KEYS+=("$KEY")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
||||||
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024 LibreChat
|
Copyright (c) 2025 LibreChat
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ class GoogleClient extends BaseClient {
|
||||||
|
|
||||||
const serviceKey = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
|
const serviceKey = creds[AuthKeys.GOOGLE_SERVICE_KEY] ?? {};
|
||||||
this.serviceKey =
|
this.serviceKey =
|
||||||
serviceKey && typeof serviceKey === 'string' ? JSON.parse(serviceKey) : serviceKey ?? {};
|
serviceKey && typeof serviceKey === 'string' ? JSON.parse(serviceKey) : (serviceKey ?? {});
|
||||||
/** @type {string | null | undefined} */
|
/** @type {string | null | undefined} */
|
||||||
this.project_id = this.serviceKey.project_id;
|
this.project_id = this.serviceKey.project_id;
|
||||||
this.client_email = this.serviceKey.client_email;
|
this.client_email = this.serviceKey.client_email;
|
||||||
|
|
@ -73,6 +73,8 @@ class GoogleClient extends BaseClient {
|
||||||
* @type {string} */
|
* @type {string} */
|
||||||
this.outputTokensKey = 'output_tokens';
|
this.outputTokensKey = 'output_tokens';
|
||||||
this.visionMode = VisionModes.generative;
|
this.visionMode = VisionModes.generative;
|
||||||
|
/** @type {string} */
|
||||||
|
this.systemMessage;
|
||||||
if (options.skipSetOptions) {
|
if (options.skipSetOptions) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +186,7 @@ class GoogleClient extends BaseClient {
|
||||||
if (typeof this.options.artifactsPrompt === 'string' && this.options.artifactsPrompt) {
|
if (typeof this.options.artifactsPrompt === 'string' && this.options.artifactsPrompt) {
|
||||||
promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim();
|
promptPrefix = `${promptPrefix ?? ''}\n${this.options.artifactsPrompt}`.trim();
|
||||||
}
|
}
|
||||||
this.options.promptPrefix = promptPrefix;
|
this.systemMessage = promptPrefix;
|
||||||
this.initializeClient();
|
this.initializeClient();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
@ -314,7 +316,7 @@ class GoogleClient extends BaseClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.augmentedPrompt = await this.contextHandlers.createContext();
|
this.augmentedPrompt = await this.contextHandlers.createContext();
|
||||||
this.options.promptPrefix = this.augmentedPrompt + this.options.promptPrefix;
|
this.systemMessage = this.augmentedPrompt + this.systemMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,8 +363,8 @@ class GoogleClient extends BaseClient {
|
||||||
throw new Error('[GoogleClient] PaLM 2 and Codey models are no longer supported.');
|
throw new Error('[GoogleClient] PaLM 2 and Codey models are no longer supported.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.promptPrefix) {
|
if (this.systemMessage) {
|
||||||
const instructionsTokenCount = this.getTokenCount(this.options.promptPrefix);
|
const instructionsTokenCount = this.getTokenCount(this.systemMessage);
|
||||||
|
|
||||||
this.maxContextTokens = this.maxContextTokens - instructionsTokenCount;
|
this.maxContextTokens = this.maxContextTokens - instructionsTokenCount;
|
||||||
if (this.maxContextTokens < 0) {
|
if (this.maxContextTokens < 0) {
|
||||||
|
|
@ -417,8 +419,8 @@ class GoogleClient extends BaseClient {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.options.promptPrefix) {
|
if (this.systemMessage) {
|
||||||
payload.instances[0].context = this.options.promptPrefix;
|
payload.instances[0].context = this.systemMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('[GoogleClient] buildMessages', payload);
|
logger.debug('[GoogleClient] buildMessages', payload);
|
||||||
|
|
@ -464,7 +466,7 @@ class GoogleClient extends BaseClient {
|
||||||
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
identityPrefix = `${identityPrefix}\nYou are ${this.options.modelLabel}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let promptPrefix = (this.options.promptPrefix ?? '').trim();
|
let promptPrefix = (this.systemMessage ?? '').trim();
|
||||||
|
|
||||||
if (identityPrefix) {
|
if (identityPrefix) {
|
||||||
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
promptPrefix = `${identityPrefix}${promptPrefix}`;
|
||||||
|
|
@ -639,7 +641,7 @@ class GoogleClient extends BaseClient {
|
||||||
let error;
|
let error;
|
||||||
try {
|
try {
|
||||||
if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) {
|
if (!EXCLUDED_GENAI_MODELS.test(modelName) && !this.project_id) {
|
||||||
/** @type {GenAI} */
|
/** @type {GenerativeModel} */
|
||||||
const client = this.client;
|
const client = this.client;
|
||||||
/** @type {GenerateContentRequest} */
|
/** @type {GenerateContentRequest} */
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
|
|
@ -648,7 +650,7 @@ class GoogleClient extends BaseClient {
|
||||||
generationConfig: googleGenConfigSchema.parse(this.modelOptions),
|
generationConfig: googleGenConfigSchema.parse(this.modelOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
const promptPrefix = (this.options.promptPrefix ?? '').trim();
|
const promptPrefix = (this.systemMessage ?? '').trim();
|
||||||
if (promptPrefix.length) {
|
if (promptPrefix.length) {
|
||||||
requestOptions.systemInstruction = {
|
requestOptions.systemInstruction = {
|
||||||
parts: [
|
parts: [
|
||||||
|
|
@ -663,7 +665,17 @@ class GoogleClient extends BaseClient {
|
||||||
/** @type {GenAIUsageMetadata} */
|
/** @type {GenAIUsageMetadata} */
|
||||||
let usageMetadata;
|
let usageMetadata;
|
||||||
|
|
||||||
const result = await client.generateContentStream(requestOptions);
|
abortController.signal.addEventListener(
|
||||||
|
'abort',
|
||||||
|
() => {
|
||||||
|
logger.warn('[GoogleClient] Request was aborted', abortController.signal.reason);
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await client.generateContentStream(requestOptions, {
|
||||||
|
signal: abortController.signal,
|
||||||
|
});
|
||||||
for await (const chunk of result.stream) {
|
for await (const chunk of result.stream) {
|
||||||
usageMetadata = !usageMetadata
|
usageMetadata = !usageMetadata
|
||||||
? chunk?.usageMetadata
|
? chunk?.usageMetadata
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ const { z } = require('zod');
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const { Ollama } = require('ollama');
|
const { Ollama } = require('ollama');
|
||||||
const { Constants } = require('librechat-data-provider');
|
const { Constants } = require('librechat-data-provider');
|
||||||
const { deriveBaseURL } = require('~/utils');
|
const { deriveBaseURL, logAxiosError } = require('~/utils');
|
||||||
const { sleep } = require('~/server/utils');
|
const { sleep } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@ class OllamaClient {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const logMessage =
|
const logMessage =
|
||||||
'Failed to fetch models from Ollama API. If you are not using Ollama directly, and instead, through some aggregator or reverse proxy that handles fetching via OpenAI spec, ensure the name of the endpoint doesn\'t start with `ollama` (case-insensitive).';
|
'Failed to fetch models from Ollama API. If you are not using Ollama directly, and instead, through some aggregator or reverse proxy that handles fetching via OpenAI spec, ensure the name of the endpoint doesn\'t start with `ollama` (case-insensitive).';
|
||||||
logger.error(logMessage, error);
|
logAxiosError({ message: logMessage, error });
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const {
|
||||||
ImageDetail,
|
ImageDetail,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
resolveHeaders,
|
resolveHeaders,
|
||||||
|
KnownEndpoints,
|
||||||
openAISettings,
|
openAISettings,
|
||||||
ImageDetailCost,
|
ImageDetailCost,
|
||||||
CohereConstants,
|
CohereConstants,
|
||||||
|
|
@ -116,11 +117,7 @@ class OpenAIClient extends BaseClient {
|
||||||
|
|
||||||
const { reverseProxyUrl: reverseProxy } = this.options;
|
const { reverseProxyUrl: reverseProxy } = this.options;
|
||||||
|
|
||||||
if (
|
if (!this.useOpenRouter && reverseProxy && reverseProxy.includes(KnownEndpoints.openrouter)) {
|
||||||
!this.useOpenRouter &&
|
|
||||||
reverseProxy &&
|
|
||||||
reverseProxy.includes('https://openrouter.ai/api/v1')
|
|
||||||
) {
|
|
||||||
this.useOpenRouter = true;
|
this.useOpenRouter = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -282,4 +282,47 @@ describe('formatAgentMessages', () => {
|
||||||
// Additional check to ensure the consecutive assistant messages were combined
|
// Additional check to ensure the consecutive assistant messages were combined
|
||||||
expect(result[1].content).toHaveLength(2);
|
expect(result[1].content).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip THINK type content parts', () => {
|
||||||
|
const payload = [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Initial response' },
|
||||||
|
{ type: ContentTypes.THINK, [ContentTypes.THINK]: 'Reasoning about the problem...' },
|
||||||
|
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final answer' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatAgentMessages(payload);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toBeInstanceOf(AIMessage);
|
||||||
|
expect(result[0].content).toEqual('Initial response\nFinal answer');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should join TEXT content as string when THINK content type is present', () => {
|
||||||
|
const payload = [
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: [
|
||||||
|
{ type: ContentTypes.THINK, [ContentTypes.THINK]: 'Analyzing the problem...' },
|
||||||
|
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'First part of response' },
|
||||||
|
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Second part of response' },
|
||||||
|
{ type: ContentTypes.TEXT, [ContentTypes.TEXT]: 'Final part of response' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatAgentMessages(payload);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0]).toBeInstanceOf(AIMessage);
|
||||||
|
expect(typeof result[0].content).toBe('string');
|
||||||
|
expect(result[0].content).toBe(
|
||||||
|
'First part of response\nSecond part of response\nFinal part of response',
|
||||||
|
);
|
||||||
|
expect(result[0].content).not.toContain('Analyzing the problem...');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ const formatAgentMessages = (payload) => {
|
||||||
let currentContent = [];
|
let currentContent = [];
|
||||||
let lastAIMessage = null;
|
let lastAIMessage = null;
|
||||||
|
|
||||||
|
let hasReasoning = false;
|
||||||
for (const part of message.content) {
|
for (const part of message.content) {
|
||||||
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
|
if (part.type === ContentTypes.TEXT && part.tool_call_ids) {
|
||||||
/*
|
/*
|
||||||
|
|
@ -207,11 +208,25 @@ const formatAgentMessages = (payload) => {
|
||||||
content: output || '',
|
content: output || '',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
} else if (part.type === ContentTypes.THINK) {
|
||||||
|
hasReasoning = true;
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
currentContent.push(part);
|
currentContent.push(part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasReasoning) {
|
||||||
|
currentContent = currentContent
|
||||||
|
.reduce((acc, curr) => {
|
||||||
|
if (curr.type === ContentTypes.TEXT) {
|
||||||
|
return `${acc}${curr[ContentTypes.TEXT]}\n`;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentContent.length > 0) {
|
if (currentContent.length > 0) {
|
||||||
messages.push(new AIMessage({ content: currentContent }));
|
messages.push(new AIMessage({ content: currentContent }));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,18 +106,21 @@ const createFileSearchTool = async ({ req, files, entity_id }) => {
|
||||||
|
|
||||||
const formattedResults = validResults
|
const formattedResults = validResults
|
||||||
.flatMap((result) =>
|
.flatMap((result) =>
|
||||||
result.data.map(([docInfo, relevanceScore]) => ({
|
result.data.map(([docInfo, distance]) => ({
|
||||||
filename: docInfo.metadata.source.split('/').pop(),
|
filename: docInfo.metadata.source.split('/').pop(),
|
||||||
content: docInfo.page_content,
|
content: docInfo.page_content,
|
||||||
relevanceScore,
|
distance,
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
// TODO: results should be sorted by relevance, not distance
|
||||||
|
.sort((a, b) => a.distance - b.distance)
|
||||||
|
// TODO: make this configurable
|
||||||
|
.slice(0, 10);
|
||||||
|
|
||||||
const formattedString = formattedResults
|
const formattedString = formattedResults
|
||||||
.map(
|
.map(
|
||||||
(result) =>
|
(result) =>
|
||||||
`File: ${result.filename}\nRelevance: ${result.relevanceScore.toFixed(4)}\nContent: ${
|
`File: ${result.filename}\nRelevance: ${1.0 - result.distance.toFixed(4)}\nContent: ${
|
||||||
result.content
|
result.content
|
||||||
}\n`,
|
}\n`,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
72
api/cache/keyvRedis.js
vendored
72
api/cache/keyvRedis.js
vendored
|
|
@ -1,15 +1,81 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const ioredis = require('ioredis');
|
||||||
const KeyvRedis = require('@keyv/redis');
|
const KeyvRedis = require('@keyv/redis');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
const logger = require('~/config/winston');
|
const logger = require('~/config/winston');
|
||||||
|
|
||||||
const { REDIS_URI, USE_REDIS } = process.env;
|
const { REDIS_URI, USE_REDIS, USE_REDIS_CLUSTER, REDIS_CA, REDIS_KEY_PREFIX, REDIS_MAX_LISTENERS } =
|
||||||
|
process.env;
|
||||||
|
|
||||||
let keyvRedis;
|
let keyvRedis;
|
||||||
|
const redis_prefix = REDIS_KEY_PREFIX || '';
|
||||||
|
const redis_max_listeners = REDIS_MAX_LISTENERS || 10;
|
||||||
|
|
||||||
|
function mapURI(uri) {
|
||||||
|
const regex =
|
||||||
|
/^(?:(?<scheme>\w+):\/\/)?(?:(?<user>[^:@]+)(?::(?<password>[^@]+))?@)?(?<host>[\w.-]+)(?::(?<port>\d{1,5}))?$/;
|
||||||
|
const match = uri.match(regex);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const { scheme, user, password, host, port } = match.groups;
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheme: scheme || 'none',
|
||||||
|
user: user || null,
|
||||||
|
password: password || null,
|
||||||
|
host: host || null,
|
||||||
|
port: port || null,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const parts = uri.split(':');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return {
|
||||||
|
scheme: 'none',
|
||||||
|
user: null,
|
||||||
|
password: null,
|
||||||
|
host: parts[0],
|
||||||
|
port: parts[1],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
scheme: 'none',
|
||||||
|
user: null,
|
||||||
|
password: null,
|
||||||
|
host: uri,
|
||||||
|
port: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (REDIS_URI && isEnabled(USE_REDIS)) {
|
if (REDIS_URI && isEnabled(USE_REDIS)) {
|
||||||
keyvRedis = new KeyvRedis(REDIS_URI, { useRedisSets: false });
|
let redisOptions = null;
|
||||||
|
let keyvOpts = {
|
||||||
|
useRedisSets: false,
|
||||||
|
keyPrefix: redis_prefix,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (REDIS_CA) {
|
||||||
|
const ca = fs.readFileSync(REDIS_CA);
|
||||||
|
redisOptions = { tls: { ca } };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnabled(USE_REDIS_CLUSTER)) {
|
||||||
|
const hosts = REDIS_URI.split(',').map((item) => {
|
||||||
|
var value = mapURI(item);
|
||||||
|
|
||||||
|
return {
|
||||||
|
host: value.host,
|
||||||
|
port: value.port,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const cluster = new ioredis.Cluster(hosts, { redisOptions });
|
||||||
|
keyvRedis = new KeyvRedis(cluster, keyvOpts);
|
||||||
|
} else {
|
||||||
|
keyvRedis = new KeyvRedis(REDIS_URI, keyvOpts);
|
||||||
|
}
|
||||||
keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err));
|
keyvRedis.on('error', (err) => logger.error('KeyvRedis connection error:', err));
|
||||||
keyvRedis.setMaxListeners(20);
|
keyvRedis.setMaxListeners(redis_max_listeners);
|
||||||
logger.info(
|
logger.info(
|
||||||
'[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.',
|
'[Optional] Redis initialized. Note: Redis support is experimental. If you have issues, disable it. Cache needs to be flushed for values to refresh.',
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
const { MeiliSearch } = require('meilisearch');
|
const { MeiliSearch } = require('meilisearch');
|
||||||
const Conversation = require('~/models/schema/convoSchema');
|
const Conversation = require('~/models/schema/convoSchema');
|
||||||
const Message = require('~/models/schema/messageSchema');
|
const Message = require('~/models/schema/messageSchema');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const searchEnabled = process.env?.SEARCH?.toLowerCase() === 'true';
|
const searchEnabled = isEnabled(process.env.SEARCH);
|
||||||
|
const indexingDisabled = isEnabled(process.env.MEILI_NO_SYNC);
|
||||||
let currentTimeout = null;
|
let currentTimeout = null;
|
||||||
|
|
||||||
class MeiliSearchClient {
|
class MeiliSearchClient {
|
||||||
|
|
@ -23,8 +25,7 @@ class MeiliSearchClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
async function indexSync() {
|
||||||
async function indexSync(req, res, next) {
|
|
||||||
if (!searchEnabled) {
|
if (!searchEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -33,10 +34,15 @@ async function indexSync(req, res, next) {
|
||||||
const client = MeiliSearchClient.getInstance();
|
const client = MeiliSearchClient.getInstance();
|
||||||
|
|
||||||
const { status } = await client.health();
|
const { status } = await client.health();
|
||||||
if (status !== 'available' || !process.env.SEARCH) {
|
if (status !== 'available') {
|
||||||
throw new Error('Meilisearch not available');
|
throw new Error('Meilisearch not available');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (indexingDisabled === true) {
|
||||||
|
logger.info('[indexSync] Indexing is disabled, skipping...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const messageCount = await Message.countDocuments();
|
const messageCount = await Message.countDocuments();
|
||||||
const convoCount = await Conversation.countDocuments();
|
const convoCount = await Conversation.countDocuments();
|
||||||
const messages = await client.index('messages').getStats();
|
const messages = await client.index('messages').getStats();
|
||||||
|
|
@ -71,7 +77,6 @@ async function indexSync(req, res, next) {
|
||||||
logger.info('[indexSync] Meilisearch not configured, search will be disabled.');
|
logger.info('[indexSync] Meilisearch not configured, search will be disabled.');
|
||||||
} else {
|
} else {
|
||||||
logger.error('[indexSync] error', err);
|
logger.error('[indexSync] error', err);
|
||||||
// res.status(500).json({ error: 'Server error' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,11 +97,22 @@ const updateAgent = async (searchParameter, updateData) => {
|
||||||
const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
|
const addAgentResourceFile = async ({ agent_id, tool_resource, file_id }) => {
|
||||||
const searchParameter = { id: agent_id };
|
const searchParameter = { id: agent_id };
|
||||||
|
|
||||||
// build the update to push or create the file ids set
|
|
||||||
const fileIdsPath = `tool_resources.${tool_resource}.file_ids`;
|
const fileIdsPath = `tool_resources.${tool_resource}.file_ids`;
|
||||||
|
|
||||||
|
await Agent.updateOne(
|
||||||
|
{
|
||||||
|
id: agent_id,
|
||||||
|
[`${fileIdsPath}`]: { $exists: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
[`${fileIdsPath}`]: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const updateData = { $addToSet: { [fileIdsPath]: file_id } };
|
const updateData = { $addToSet: { [fileIdsPath]: file_id } };
|
||||||
|
|
||||||
// return the updated agent or throw if no agent matches
|
|
||||||
const updatedAgent = await updateAgent(searchParameter, updateData);
|
const updatedAgent = await updateAgent(searchParameter, updateData);
|
||||||
if (updatedAgent) {
|
if (updatedAgent) {
|
||||||
return updatedAgent;
|
return updatedAgent;
|
||||||
|
|
@ -290,6 +301,7 @@ const updateAgentProjects = async ({ user, agentId, projectIds, removeProjectIds
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Agent,
|
||||||
getAgent,
|
getAgent,
|
||||||
loadAgent,
|
loadAgent,
|
||||||
createAgent,
|
createAgent,
|
||||||
|
|
|
||||||
160
api/models/Agent.spec.js
Normal file
160
api/models/Agent.spec.js
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
const mongoose = require('mongoose');
|
||||||
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
||||||
|
const { Agent, addAgentResourceFile, removeAgentResourceFiles } = require('./Agent');
|
||||||
|
|
||||||
|
describe('Agent Resource File Operations', () => {
|
||||||
|
let mongoServer;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
mongoServer = await MongoMemoryServer.create();
|
||||||
|
const mongoUri = mongoServer.getUri();
|
||||||
|
await mongoose.connect(mongoUri);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await mongoose.disconnect();
|
||||||
|
await mongoServer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await Agent.deleteMany({});
|
||||||
|
});
|
||||||
|
|
||||||
|
const createBasicAgent = async () => {
|
||||||
|
const agentId = `agent_${uuidv4()}`;
|
||||||
|
const agent = await Agent.create({
|
||||||
|
id: agentId,
|
||||||
|
name: 'Test Agent',
|
||||||
|
provider: 'test',
|
||||||
|
model: 'test-model',
|
||||||
|
author: new mongoose.Types.ObjectId(),
|
||||||
|
});
|
||||||
|
return agent;
|
||||||
|
};
|
||||||
|
|
||||||
|
test('should handle concurrent file additions', async () => {
|
||||||
|
const agent = await createBasicAgent();
|
||||||
|
const fileIds = Array.from({ length: 10 }, () => uuidv4());
|
||||||
|
|
||||||
|
// Concurrent additions
|
||||||
|
const additionPromises = fileIds.map((fileId) =>
|
||||||
|
addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: 'test_tool',
|
||||||
|
file_id: fileId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(additionPromises);
|
||||||
|
|
||||||
|
const updatedAgent = await Agent.findOne({ id: agent.id });
|
||||||
|
expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined();
|
||||||
|
expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(10);
|
||||||
|
expect(new Set(updatedAgent.tool_resources.test_tool.file_ids).size).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle concurrent additions and removals', async () => {
|
||||||
|
const agent = await createBasicAgent();
|
||||||
|
const initialFileIds = Array.from({ length: 5 }, () => uuidv4());
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
initialFileIds.map((fileId) =>
|
||||||
|
addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: 'test_tool',
|
||||||
|
file_id: fileId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFileIds = Array.from({ length: 5 }, () => uuidv4());
|
||||||
|
const operations = [
|
||||||
|
...newFileIds.map((fileId) =>
|
||||||
|
addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: 'test_tool',
|
||||||
|
file_id: fileId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
...initialFileIds.map((fileId) =>
|
||||||
|
removeAgentResourceFiles({
|
||||||
|
agent_id: agent.id,
|
||||||
|
files: [{ tool_resource: 'test_tool', file_id: fileId }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await Promise.all(operations);
|
||||||
|
|
||||||
|
const updatedAgent = await Agent.findOne({ id: agent.id });
|
||||||
|
expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined();
|
||||||
|
expect(updatedAgent.tool_resources.test_tool.file_ids).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should initialize array when adding to non-existent tool resource', async () => {
|
||||||
|
const agent = await createBasicAgent();
|
||||||
|
const fileId = uuidv4();
|
||||||
|
|
||||||
|
const updatedAgent = await addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: 'new_tool',
|
||||||
|
file_id: fileId,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedAgent.tool_resources.new_tool.file_ids).toBeDefined();
|
||||||
|
expect(updatedAgent.tool_resources.new_tool.file_ids).toHaveLength(1);
|
||||||
|
expect(updatedAgent.tool_resources.new_tool.file_ids[0]).toBe(fileId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle rapid sequential modifications to same tool resource', async () => {
|
||||||
|
const agent = await createBasicAgent();
|
||||||
|
const fileId = uuidv4();
|
||||||
|
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: 'test_tool',
|
||||||
|
file_id: `${fileId}_${i}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
await removeAgentResourceFiles({
|
||||||
|
agent_id: agent.id,
|
||||||
|
files: [{ tool_resource: 'test_tool', file_id: `${fileId}_${i}` }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedAgent = await Agent.findOne({ id: agent.id });
|
||||||
|
expect(updatedAgent.tool_resources.test_tool.file_ids).toBeDefined();
|
||||||
|
expect(Array.isArray(updatedAgent.tool_resources.test_tool.file_ids)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle multiple tool resources concurrently', async () => {
|
||||||
|
const agent = await createBasicAgent();
|
||||||
|
const toolResources = ['tool1', 'tool2', 'tool3'];
|
||||||
|
const operations = [];
|
||||||
|
|
||||||
|
toolResources.forEach((tool) => {
|
||||||
|
const fileIds = Array.from({ length: 5 }, () => uuidv4());
|
||||||
|
fileIds.forEach((fileId) => {
|
||||||
|
operations.push(
|
||||||
|
addAgentResourceFile({
|
||||||
|
agent_id: agent.id,
|
||||||
|
tool_resource: tool,
|
||||||
|
file_id: fileId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(operations);
|
||||||
|
|
||||||
|
const updatedAgent = await Agent.findOne({ id: agent.id });
|
||||||
|
toolResources.forEach((tool) => {
|
||||||
|
expect(updatedAgent.tool_resources[tool].file_ids).toBeDefined();
|
||||||
|
expect(updatedAgent.tool_resources[tool].file_ids).toHaveLength(5);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,40 +1,41 @@
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
// const { Categories } = require('./schema/categories');
|
// const { Categories } = require('./schema/categories');
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
label: 'idea',
|
label: 'com_ui_idea',
|
||||||
value: 'idea',
|
value: 'idea',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'travel',
|
label: 'com_ui_travel',
|
||||||
value: 'travel',
|
value: 'travel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'teach_or_explain',
|
label: 'com_ui_teach_or_explain',
|
||||||
value: 'teach_or_explain',
|
value: 'teach_or_explain',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'write',
|
label: 'com_ui_write',
|
||||||
value: 'write',
|
value: 'write',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'shop',
|
label: 'com_ui_shop',
|
||||||
value: 'shop',
|
value: 'shop',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'code',
|
label: 'com_ui_code',
|
||||||
value: 'code',
|
value: 'code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'misc',
|
label: 'com_ui_misc',
|
||||||
value: 'misc',
|
value: 'misc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'roleplay',
|
label: 'com_ui_roleplay',
|
||||||
value: 'roleplay',
|
value: 'roleplay',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'finance',
|
label: 'com_ui_finance',
|
||||||
value: 'finance',
|
value: 'finance',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,18 @@ const Session = mongoose.Schema({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const backupCodeSchema = mongoose.Schema({
|
||||||
|
codeHash: { type: String, required: true },
|
||||||
|
used: { type: Boolean, default: false },
|
||||||
|
usedAt: { type: Date, default: null },
|
||||||
|
});
|
||||||
|
|
||||||
const passkeySchema = mongoose.Schema({
|
const passkeySchema = mongoose.Schema({
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
publicKey: { type: Buffer, required: true },
|
publicKey: { type: Buffer, required: true },
|
||||||
counter: { type: Number, default: 0 },
|
counter: { type: Number, default: 0 },
|
||||||
transports: { type: [String], default: [] },
|
transports: { type: [String], default: [] },
|
||||||
});
|
});
|
||||||
|
|
||||||
/** @type {MongooseSchema<MongoUser>} */
|
/** @type {MongooseSchema<MongoUser>} */
|
||||||
const userSchema = mongoose.Schema(
|
const userSchema = mongoose.Schema(
|
||||||
|
|
@ -130,7 +136,12 @@ const userSchema = mongoose.Schema(
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: [],
|
},
|
||||||
|
totpSecret: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
backupCodes: {
|
||||||
|
type: [backupCodeSchema],
|
||||||
},
|
},
|
||||||
refreshToken: {
|
refreshToken: {
|
||||||
type: [Session],
|
type: [Session],
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
"@langchain/google-genai": "^0.1.7",
|
"@langchain/google-genai": "^0.1.7",
|
||||||
"@langchain/google-vertexai": "^0.1.8",
|
"@langchain/google-vertexai": "^0.1.8",
|
||||||
"@langchain/textsplitters": "^0.1.0",
|
"@langchain/textsplitters": "^0.1.0",
|
||||||
"@librechat/agents": "^2.0.5",
|
"@librechat/agents": "^2.1.2",
|
||||||
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
"@waylaidwanderer/fetch-event-source": "^3.0.1",
|
||||||
"axios": "1.7.8",
|
"axios": "1.7.8",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
"firebase": "^11.0.2",
|
"firebase": "^11.0.2",
|
||||||
"googleapis": "^126.0.1",
|
"googleapis": "^126.0.1",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ const refreshController = async (req, res) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
|
||||||
const user = await getUserById(payload.id, '-password -__v');
|
const user = await getUserById(payload.id, '-password -__v -totpSecret');
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).redirect('/login');
|
return res.status(401).redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
119
api/server/controllers/TwoFactorController.js
Normal file
119
api/server/controllers/TwoFactorController.js
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
const {
|
||||||
|
verifyTOTP,
|
||||||
|
verifyBackupCode,
|
||||||
|
generateTOTPSecret,
|
||||||
|
generateBackupCodes,
|
||||||
|
getTOTPSecret,
|
||||||
|
} = require('~/server/services/twoFactorService');
|
||||||
|
const { updateUser, getUserById } = require('~/models');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
const { encryptV2 } = require('~/server/utils/crypto');
|
||||||
|
|
||||||
|
const enable2FAController = async (req, res) => {
|
||||||
|
const safeAppTitle = (process.env.APP_TITLE || 'LibreChat').replace(/\s+/g, '');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const secret = generateTOTPSecret();
|
||||||
|
const { plainCodes, codeObjects } = await generateBackupCodes();
|
||||||
|
|
||||||
|
const encryptedSecret = await encryptV2(secret);
|
||||||
|
const user = await updateUser(userId, { totpSecret: encryptedSecret, backupCodes: codeObjects });
|
||||||
|
|
||||||
|
const otpauthUrl = `otpauth://totp/${safeAppTitle}:${user.email}?secret=${secret}&issuer=${safeAppTitle}`;
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
otpauthUrl,
|
||||||
|
backupCodes: plainCodes,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[enable2FAController]', err);
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const verify2FAController = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { token, backupCode } = req.body;
|
||||||
|
const user = await getUserById(userId);
|
||||||
|
if (!user || !user.totpSecret) {
|
||||||
|
return res.status(400).json({ message: '2FA not initiated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the plain TOTP secret using getTOTPSecret.
|
||||||
|
const secret = await getTOTPSecret(user.totpSecret);
|
||||||
|
|
||||||
|
if (token && (await verifyTOTP(secret, token))) {
|
||||||
|
return res.status(200).json();
|
||||||
|
} else if (backupCode) {
|
||||||
|
const verified = await verifyBackupCode({ user, backupCode });
|
||||||
|
if (verified) {
|
||||||
|
return res.status(200).json();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ message: 'Invalid token.' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[verify2FAController]', err);
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirm2FAController = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { token } = req.body;
|
||||||
|
const user = await getUserById(userId);
|
||||||
|
|
||||||
|
if (!user || !user.totpSecret) {
|
||||||
|
return res.status(400).json({ message: '2FA not initiated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the plain TOTP secret using getTOTPSecret.
|
||||||
|
const secret = await getTOTPSecret(user.totpSecret);
|
||||||
|
|
||||||
|
if (await verifyTOTP(secret, token)) {
|
||||||
|
return res.status(200).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ message: 'Invalid token.' });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[confirm2FAController]', err);
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const disable2FAController = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
await updateUser(userId, { totpSecret: null, backupCodes: [] });
|
||||||
|
res.status(200).json();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[disable2FAController]', err);
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const regenerateBackupCodesController = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { plainCodes, codeObjects } = await generateBackupCodes();
|
||||||
|
await updateUser(userId, { backupCodes: codeObjects });
|
||||||
|
res.status(200).json({
|
||||||
|
backupCodes: plainCodes,
|
||||||
|
backupCodesHash: codeObjects,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[regenerateBackupCodesController]', err);
|
||||||
|
res.status(500).json({ message: err.message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
enable2FAController,
|
||||||
|
verify2FAController,
|
||||||
|
confirm2FAController,
|
||||||
|
disable2FAController,
|
||||||
|
regenerateBackupCodesController,
|
||||||
|
};
|
||||||
|
|
@ -19,7 +19,9 @@ const { Transaction } = require('~/models/Transaction');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const getUserController = async (req, res) => {
|
const getUserController = async (req, res) => {
|
||||||
res.status(200).send(req.user);
|
const userData = req.user.toObject != null ? req.user.toObject() : { ...req.user };
|
||||||
|
delete userData.totpSecret;
|
||||||
|
res.status(200).send(userData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTermsStatusController = async (req, res) => {
|
const getTermsStatusController = async (req, res) => {
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,22 @@ function getDefaultHandlers({ res, aggregateContent, toolEndCallback, collectedU
|
||||||
aggregateContent({ event, data });
|
aggregateContent({ event, data });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[GraphEvents.ON_REASONING_DELTA]: {
|
||||||
|
/**
|
||||||
|
* Handle ON_REASONING_DELTA event.
|
||||||
|
* @param {string} event - The event name.
|
||||||
|
* @param {StreamEventData} data - The event data.
|
||||||
|
* @param {GraphRunnableConfig['configurable']} [metadata] The runnable metadata.
|
||||||
|
*/
|
||||||
|
handle: (event, data, metadata) => {
|
||||||
|
if (metadata?.last_agent_index === metadata?.agent_index) {
|
||||||
|
sendEvent(res, { event, data });
|
||||||
|
} else if (!metadata?.hide_sequential_outputs) {
|
||||||
|
sendEvent(res, { event, data });
|
||||||
|
}
|
||||||
|
aggregateContent({ event, data });
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return handlers;
|
return handlers;
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,6 @@ const {
|
||||||
bedrockOutputParser,
|
bedrockOutputParser,
|
||||||
removeNullishValues,
|
removeNullishValues,
|
||||||
} = require('librechat-data-provider');
|
} = require('librechat-data-provider');
|
||||||
const {
|
|
||||||
extractBaseURL,
|
|
||||||
// constructAzureURL,
|
|
||||||
// genAzureChatCompletion,
|
|
||||||
} = require('~/utils');
|
|
||||||
const {
|
const {
|
||||||
formatMessage,
|
formatMessage,
|
||||||
formatAgentMessages,
|
formatAgentMessages,
|
||||||
|
|
@ -477,19 +472,6 @@ class AgentClient extends BaseClient {
|
||||||
abortController = new AbortController();
|
abortController = new AbortController();
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseURL = extractBaseURL(this.completionsUrl);
|
|
||||||
logger.debug('[api/server/controllers/agents/client.js] chatCompletion', {
|
|
||||||
baseURL,
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
// if (this.useOpenRouter) {
|
|
||||||
// opts.defaultHeaders = {
|
|
||||||
// 'HTTP-Referer': 'https://librechat.ai',
|
|
||||||
// 'X-Title': 'LibreChat',
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (this.options.headers) {
|
// if (this.options.headers) {
|
||||||
// opts.defaultHeaders = { ...opts.defaultHeaders, ...this.options.headers };
|
// opts.defaultHeaders = { ...opts.defaultHeaders, ...this.options.headers };
|
||||||
// }
|
// }
|
||||||
|
|
@ -626,7 +608,7 @@ class AgentClient extends BaseClient {
|
||||||
let systemContent = [
|
let systemContent = [
|
||||||
systemMessage,
|
systemMessage,
|
||||||
agent.instructions ?? '',
|
agent.instructions ?? '',
|
||||||
i !== 0 ? agent.additional_instructions ?? '' : '',
|
i !== 0 ? (agent.additional_instructions ?? '') : '',
|
||||||
]
|
]
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { Run, Providers } = require('@librechat/agents');
|
const { Run, Providers } = require('@librechat/agents');
|
||||||
const { providerEndpointMap } = require('librechat-data-provider');
|
const { providerEndpointMap, KnownEndpoints } = require('librechat-data-provider');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@librechat/agents').t} t
|
* @typedef {import('@librechat/agents').t} t
|
||||||
|
|
@ -7,6 +7,7 @@ const { providerEndpointMap } = require('librechat-data-provider');
|
||||||
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
|
* @typedef {import('@librechat/agents').StreamEventData} StreamEventData
|
||||||
* @typedef {import('@librechat/agents').EventHandler} EventHandler
|
* @typedef {import('@librechat/agents').EventHandler} EventHandler
|
||||||
* @typedef {import('@librechat/agents').GraphEvents} GraphEvents
|
* @typedef {import('@librechat/agents').GraphEvents} GraphEvents
|
||||||
|
* @typedef {import('@librechat/agents').LLMConfig} LLMConfig
|
||||||
* @typedef {import('@librechat/agents').IState} IState
|
* @typedef {import('@librechat/agents').IState} IState
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -32,6 +33,7 @@ async function createRun({
|
||||||
streamUsage = true,
|
streamUsage = true,
|
||||||
}) {
|
}) {
|
||||||
const provider = providerEndpointMap[agent.provider] ?? agent.provider;
|
const provider = providerEndpointMap[agent.provider] ?? agent.provider;
|
||||||
|
/** @type {LLMConfig} */
|
||||||
const llmConfig = Object.assign(
|
const llmConfig = Object.assign(
|
||||||
{
|
{
|
||||||
provider,
|
provider,
|
||||||
|
|
@ -41,6 +43,11 @@ async function createRun({
|
||||||
agent.model_parameters,
|
agent.model_parameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @type {'reasoning_content' | 'reasoning'} */
|
||||||
|
let reasoningKey;
|
||||||
|
if (llmConfig.configuration?.baseURL?.includes(KnownEndpoints.openrouter)) {
|
||||||
|
reasoningKey = 'reasoning';
|
||||||
|
}
|
||||||
if (/o1(?!-(?:mini|preview)).*$/.test(llmConfig.model)) {
|
if (/o1(?!-(?:mini|preview)).*$/.test(llmConfig.model)) {
|
||||||
llmConfig.streaming = false;
|
llmConfig.streaming = false;
|
||||||
llmConfig.disableStreaming = true;
|
llmConfig.disableStreaming = true;
|
||||||
|
|
@ -50,6 +57,7 @@ async function createRun({
|
||||||
const graphConfig = {
|
const graphConfig = {
|
||||||
signal,
|
signal,
|
||||||
llmConfig,
|
llmConfig,
|
||||||
|
reasoningKey,
|
||||||
tools: agent.tools,
|
tools: agent.tools,
|
||||||
instructions: agent.instructions,
|
instructions: agent.instructions,
|
||||||
additional_instructions: agent.additional_instructions,
|
additional_instructions: agent.additional_instructions,
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { generate2FATempToken } = require('~/server/services/twoFactorService');
|
||||||
const { setAuthTokens } = require('~/server/services/AuthService');
|
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
|
@ -7,7 +8,12 @@ const loginController = async (req, res) => {
|
||||||
return res.status(400).json({ message: 'Invalid credentials' });
|
return res.status(400).json({ message: 'Invalid credentials' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { password: _, __v, ...user } = req.user;
|
if (req.user.backupCodes != null && req.user.backupCodes.length > 0) {
|
||||||
|
const tempToken = generate2FATempToken(req.user._id);
|
||||||
|
return res.status(200).json({ twoFAPending: true, tempToken });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { password: _p, totpSecret: _t, __v, ...user } = req.user;
|
||||||
user.id = user._id.toString();
|
user.id = user._id.toString();
|
||||||
|
|
||||||
const token = await setAuthTokens(req.user._id, res);
|
const token = await setAuthTokens(req.user._id, res);
|
||||||
|
|
|
||||||
58
api/server/controllers/auth/TwoFactorAuthController.js
Normal file
58
api/server/controllers/auth/TwoFactorAuthController.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const { verifyTOTP, verifyBackupCode, getTOTPSecret } = require('~/server/services/twoFactorService');
|
||||||
|
const { setAuthTokens } = require('~/server/services/AuthService');
|
||||||
|
const { getUserById } = require('~/models/userMethods');
|
||||||
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
|
const verify2FA = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { tempToken, token, backupCode } = req.body;
|
||||||
|
if (!tempToken) {
|
||||||
|
return res.status(400).json({ message: 'Missing temporary token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload;
|
||||||
|
try {
|
||||||
|
payload = jwt.verify(tempToken, process.env.JWT_SECRET);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(401).json({ message: 'Invalid or expired temporary token' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUserById(payload.userId);
|
||||||
|
// Ensure that the user exists and has backup codes (i.e. 2FA enabled)
|
||||||
|
if (!user || !(user.backupCodes && user.backupCodes.length > 0)) {
|
||||||
|
return res.status(400).json({ message: '2FA is not enabled for this user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the new getTOTPSecret function to retrieve (and decrypt if necessary) the TOTP secret.
|
||||||
|
const secret = await getTOTPSecret(user.totpSecret);
|
||||||
|
|
||||||
|
let verified = false;
|
||||||
|
if (token && (await verifyTOTP(secret, token))) {
|
||||||
|
verified = true;
|
||||||
|
} else if (backupCode) {
|
||||||
|
verified = await verifyBackupCode({ user, backupCode });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verified) {
|
||||||
|
return res.status(401).json({ message: 'Invalid 2FA code or backup code' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare user data for response.
|
||||||
|
// If the user is a plain object (from lean queries), we create a shallow copy.
|
||||||
|
const userData = user.toObject ? user.toObject() : { ...user };
|
||||||
|
// Remove sensitive fields.
|
||||||
|
delete userData.password;
|
||||||
|
delete userData.__v;
|
||||||
|
delete userData.totpSecret;
|
||||||
|
userData.id = user._id.toString();
|
||||||
|
|
||||||
|
const authToken = await setAuthTokens(user._id, res);
|
||||||
|
return res.status(200).json({ token: authToken, user: userData });
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('[verify2FA]', err);
|
||||||
|
return res.status(500).json({ message: 'Something went wrong' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { verify2FA };
|
||||||
|
|
@ -24,10 +24,11 @@ const routes = require('./routes');
|
||||||
const { mongoUserStore, mongoChallengeStore } = require('~/cache');
|
const { mongoUserStore, mongoChallengeStore } = require('~/cache');
|
||||||
const { WebAuthnStrategy } = require('passport-simple-webauthn2');
|
const { WebAuthnStrategy } = require('passport-simple-webauthn2');
|
||||||
|
|
||||||
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION } = process.env ?? {};
|
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION, TRUST_PROXY } = process.env ?? {};
|
||||||
|
|
||||||
const port = Number(PORT) || 3080;
|
const port = Number(PORT) || 3080;
|
||||||
const host = HOST || 'localhost';
|
const host = HOST || 'localhost';
|
||||||
|
const trusted_proxy = Number(TRUST_PROXY) || 1; /* trust first proxy by default */
|
||||||
|
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
if (typeof Bun !== 'undefined') {
|
if (typeof Bun !== 'undefined') {
|
||||||
|
|
@ -55,7 +56,7 @@ const startServer = async () => {
|
||||||
app.use(staticCache(app.locals.paths.dist));
|
app.use(staticCache(app.locals.paths.dist));
|
||||||
app.use(staticCache(app.locals.paths.fonts));
|
app.use(staticCache(app.locals.paths.fonts));
|
||||||
app.use(staticCache(app.locals.paths.assets));
|
app.use(staticCache(app.locals.paths.assets));
|
||||||
app.set('trust proxy', 1); /* trust first proxy */
|
app.set('trust proxy', trusted_proxy);
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
|
||||||
|
|
@ -165,6 +166,18 @@ process.on('uncaughtException', (err) => {
|
||||||
logger.error('There was an uncaught error:', err);
|
logger.error('There was an uncaught error:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (err.message.includes('abort')) {
|
||||||
|
logger.warn('There was an uncatchable AbortController error.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.message.includes('GoogleGenerativeAI')) {
|
||||||
|
logger.warn(
|
||||||
|
'\n\n`GoogleGenerativeAI` errors cannot be caught due to an upstream issue, see: https://github.com/google-gemini/generative-ai-js/issues/303',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (err.message.includes('fetch failed')) {
|
if (err.message.includes('fetch failed')) {
|
||||||
if (messageCount === 0) {
|
if (messageCount === 0) {
|
||||||
logger.warn('Meilisearch error, search will be disabled');
|
logger.warn('Meilisearch error, search will be disabled');
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,13 @@ const {
|
||||||
} = require('~/server/controllers/AuthController');
|
} = require('~/server/controllers/AuthController');
|
||||||
const { loginController } = require('~/server/controllers/auth/LoginController');
|
const { loginController } = require('~/server/controllers/auth/LoginController');
|
||||||
const { logoutController } = require('~/server/controllers/auth/LogoutController');
|
const { logoutController } = require('~/server/controllers/auth/LogoutController');
|
||||||
|
const { verify2FA } = require('~/server/controllers/auth/TwoFactorAuthController');
|
||||||
|
const {
|
||||||
|
enable2FAController,
|
||||||
|
verify2FAController,
|
||||||
|
disable2FAController,
|
||||||
|
regenerateBackupCodesController, confirm2FAController,
|
||||||
|
} = require('~/server/controllers/TwoFactorController');
|
||||||
const {
|
const {
|
||||||
checkBan,
|
checkBan,
|
||||||
loginLimiter,
|
loginLimiter,
|
||||||
|
|
@ -50,4 +57,11 @@ router.post(
|
||||||
);
|
);
|
||||||
router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController);
|
router.post('/resetPassword', checkBan, validatePasswordReset, resetPasswordController);
|
||||||
|
|
||||||
|
router.get('/2fa/enable', requireJwtAuth, enable2FAController);
|
||||||
|
router.post('/2fa/verify', requireJwtAuth, verify2FAController);
|
||||||
|
router.post('/2fa/verify-temp', checkBan, verify2FA);
|
||||||
|
router.post('/2fa/confirm', requireJwtAuth, confirm2FAController);
|
||||||
|
router.post('/2fa/disable', requireJwtAuth, disable2FAController);
|
||||||
|
router.post('/2fa/backup/regenerate', requireJwtAuth, regenerateBackupCodesController);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,14 @@ const { getAgent } = require('~/models/Agent');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const providerConfigMap = {
|
const providerConfigMap = {
|
||||||
|
[Providers.OLLAMA]: initCustom,
|
||||||
|
[Providers.DEEPSEEK]: initCustom,
|
||||||
|
[Providers.OPENROUTER]: initCustom,
|
||||||
[EModelEndpoint.openAI]: initOpenAI,
|
[EModelEndpoint.openAI]: initOpenAI,
|
||||||
|
[EModelEndpoint.google]: initGoogle,
|
||||||
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
[EModelEndpoint.azureOpenAI]: initOpenAI,
|
||||||
[EModelEndpoint.anthropic]: initAnthropic,
|
[EModelEndpoint.anthropic]: initAnthropic,
|
||||||
[EModelEndpoint.bedrock]: getBedrockOptions,
|
[EModelEndpoint.bedrock]: getBedrockOptions,
|
||||||
[EModelEndpoint.google]: initGoogle,
|
|
||||||
[Providers.OLLAMA]: initCustom,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,8 +102,10 @@ const initializeAgentOptions = async ({
|
||||||
|
|
||||||
const provider = agent.provider;
|
const provider = agent.provider;
|
||||||
let getOptions = providerConfigMap[provider];
|
let getOptions = providerConfigMap[provider];
|
||||||
|
if (!getOptions && providerConfigMap[provider.toLowerCase()] != null) {
|
||||||
if (!getOptions) {
|
agent.provider = provider.toLowerCase();
|
||||||
|
getOptions = providerConfigMap[agent.provider];
|
||||||
|
} else if (!getOptions) {
|
||||||
const customEndpointConfig = await getCustomEndpointConfig(provider);
|
const customEndpointConfig = await getCustomEndpointConfig(provider);
|
||||||
if (!customEndpointConfig) {
|
if (!customEndpointConfig) {
|
||||||
throw new Error(`Provider ${provider} not supported`);
|
throw new Error(`Provider ${provider} not supported`);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
|
const { KnownEndpoints } = require('librechat-data-provider');
|
||||||
const { sanitizeModelName, constructAzureURL } = require('~/utils');
|
const { sanitizeModelName, constructAzureURL } = require('~/utils');
|
||||||
const { isEnabled } = require('~/server/utils');
|
const { isEnabled } = require('~/server/utils');
|
||||||
|
|
||||||
|
|
@ -57,10 +58,9 @@ function getLLMConfig(apiKey, options = {}) {
|
||||||
|
|
||||||
/** @type {OpenAIClientOptions['configuration']} */
|
/** @type {OpenAIClientOptions['configuration']} */
|
||||||
const configOptions = {};
|
const configOptions = {};
|
||||||
|
if (useOpenRouter || (reverseProxyUrl && reverseProxyUrl.includes(KnownEndpoints.openrouter))) {
|
||||||
// Handle OpenRouter or custom reverse proxy
|
llmConfig.include_reasoning = true;
|
||||||
if (useOpenRouter || reverseProxyUrl === 'https://openrouter.ai/api/v1') {
|
configOptions.baseURL = reverseProxyUrl;
|
||||||
configOptions.baseURL = 'https://openrouter.ai/api/v1';
|
|
||||||
configOptions.defaultHeaders = Object.assign(
|
configOptions.defaultHeaders = Object.assign(
|
||||||
{
|
{
|
||||||
'HTTP-Referer': 'https://librechat.ai',
|
'HTTP-Referer': 'https://librechat.ai',
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
const FormData = require('form-data');
|
const FormData = require('form-data');
|
||||||
const { getCodeBaseURL } = require('@librechat/agents');
|
const { getCodeBaseURL } = require('@librechat/agents');
|
||||||
|
const { logAxiosError } = require('~/utils');
|
||||||
|
|
||||||
const MAX_FILE_SIZE = 150 * 1024 * 1024;
|
const MAX_FILE_SIZE = 150 * 1024 * 1024;
|
||||||
|
|
||||||
|
|
@ -78,7 +79,11 @@ async function uploadCodeEnvFile({ req, stream, filename, apiKey, entity_id = ''
|
||||||
|
|
||||||
return `${fileIdentifier}?entity_id=${entity_id}`;
|
return `${fileIdentifier}?entity_id=${entity_id}`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Error uploading file: ${error.message}`);
|
logAxiosError({
|
||||||
|
message: `Error uploading code environment file: ${error.message}`,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
throw new Error(`Error uploading code environment file: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const {
|
||||||
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
const { getStrategyFunctions } = require('~/server/services/Files/strategies');
|
||||||
const { convertImage } = require('~/server/services/Files/images/convert');
|
const { convertImage } = require('~/server/services/Files/images/convert');
|
||||||
const { createFile, getFiles, updateFile } = require('~/models/File');
|
const { createFile, getFiles, updateFile } = require('~/models/File');
|
||||||
|
const { logAxiosError } = require('~/utils');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -85,7 +86,10 @@ const processCodeOutput = async ({
|
||||||
/** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */
|
/** Note: `messageId` & `toolCallId` are not part of file DB schema; message object records associated file ID */
|
||||||
return Object.assign(file, { messageId, toolCallId });
|
return Object.assign(file, { messageId, toolCallId });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error downloading file:', error);
|
logAxiosError({
|
||||||
|
message: 'Error downloading code environment file',
|
||||||
|
error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -135,7 +139,10 @@ async function getSessionInfo(fileIdentifier, apiKey) {
|
||||||
|
|
||||||
return response.data.find((file) => file.name.startsWith(path))?.lastModified;
|
return response.data.find((file) => file.name.startsWith(path))?.lastModified;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error fetching session info: ${error.message}`, error);
|
logAxiosError({
|
||||||
|
message: `Error fetching session info: ${error.message}`,
|
||||||
|
error,
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,7 +209,7 @@ const primeFiles = async (options, apiKey) => {
|
||||||
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(
|
const { handleFileUpload: uploadCodeEnvFile } = getStrategyFunctions(
|
||||||
FileSources.execute_code,
|
FileSources.execute_code,
|
||||||
);
|
);
|
||||||
const stream = await getDownloadStream(file.filepath);
|
const stream = await getDownloadStream(options.req, file.filepath);
|
||||||
const fileIdentifier = await uploadCodeEnvFile({
|
const fileIdentifier = await uploadCodeEnvFile({
|
||||||
req: options.req,
|
req: options.req,
|
||||||
stream,
|
stream,
|
||||||
|
|
|
||||||
|
|
@ -224,10 +224,11 @@ async function uploadFileToFirebase({ req, file, file_id }) {
|
||||||
/**
|
/**
|
||||||
* Retrieves a readable stream for a file from Firebase storage.
|
* Retrieves a readable stream for a file from Firebase storage.
|
||||||
*
|
*
|
||||||
|
* @param {ServerRequest} _req
|
||||||
* @param {string} filepath - The filepath.
|
* @param {string} filepath - The filepath.
|
||||||
* @returns {Promise<ReadableStream>} A readable stream of the file.
|
* @returns {Promise<ReadableStream>} A readable stream of the file.
|
||||||
*/
|
*/
|
||||||
async function getFirebaseFileStream(filepath) {
|
async function getFirebaseFileStream(_req, filepath) {
|
||||||
try {
|
try {
|
||||||
const storage = getFirebaseStorage();
|
const storage = getFirebaseStorage();
|
||||||
if (!storage) {
|
if (!storage) {
|
||||||
|
|
|
||||||
|
|
@ -175,6 +175,17 @@ const isValidPath = (req, base, subfolder, filepath) => {
|
||||||
return normalizedFilepath.startsWith(normalizedBase);
|
return normalizedFilepath.startsWith(normalizedBase);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} filepath
|
||||||
|
*/
|
||||||
|
const unlinkFile = async (filepath) => {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(filepath);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error deleting file:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the filesystem. This function takes a file object, constructs the full path, and
|
* Deletes a file from the filesystem. This function takes a file object, constructs the full path, and
|
||||||
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
* verifies the path's validity before deleting the file. If the path is invalid, an error is thrown.
|
||||||
|
|
@ -217,7 +228,7 @@ const deleteLocalFile = async (req, file) => {
|
||||||
throw new Error(`Invalid file path: ${file.filepath}`);
|
throw new Error(`Invalid file path: ${file.filepath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.unlink(filepath);
|
await unlinkFile(filepath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +244,7 @@ const deleteLocalFile = async (req, file) => {
|
||||||
throw new Error('Invalid file path');
|
throw new Error('Invalid file path');
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.promises.unlink(filepath);
|
await unlinkFile(filepath);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -275,11 +286,31 @@ async function uploadLocalFile({ req, file, file_id }) {
|
||||||
/**
|
/**
|
||||||
* Retrieves a readable stream for a file from local storage.
|
* Retrieves a readable stream for a file from local storage.
|
||||||
*
|
*
|
||||||
|
* @param {ServerRequest} req - The request object from Express
|
||||||
* @param {string} filepath - The filepath.
|
* @param {string} filepath - The filepath.
|
||||||
* @returns {ReadableStream} A readable stream of the file.
|
* @returns {ReadableStream} A readable stream of the file.
|
||||||
*/
|
*/
|
||||||
function getLocalFileStream(filepath) {
|
function getLocalFileStream(req, filepath) {
|
||||||
try {
|
try {
|
||||||
|
if (filepath.includes('/uploads/')) {
|
||||||
|
const basePath = filepath.split('/uploads/')[1];
|
||||||
|
|
||||||
|
if (!basePath) {
|
||||||
|
logger.warn(`Invalid base path: ${filepath}`);
|
||||||
|
throw new Error(`Invalid file path: ${filepath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullPath = path.join(req.app.locals.paths.uploads, basePath);
|
||||||
|
const uploadsDir = req.app.locals.paths.uploads;
|
||||||
|
|
||||||
|
const rel = path.relative(uploadsDir, fullPath);
|
||||||
|
if (rel.startsWith('..') || path.isAbsolute(rel) || rel.includes(`..${path.sep}`)) {
|
||||||
|
logger.warn(`Invalid relative file path: ${filepath}`);
|
||||||
|
throw new Error(`Invalid file path: ${filepath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.createReadStream(fullPath);
|
||||||
|
}
|
||||||
return fs.createReadStream(filepath);
|
return fs.createReadStream(filepath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting local file stream:', error);
|
logger.error('Error getting local file stream:', error);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,14 @@ const deleteVectors = async (req, file) => {
|
||||||
error,
|
error,
|
||||||
message: 'Error deleting vectors',
|
message: 'Error deleting vectors',
|
||||||
});
|
});
|
||||||
throw new Error(error.message || 'An error occurred during file deletion.');
|
if (
|
||||||
|
error.response &&
|
||||||
|
error.response.status !== 404 &&
|
||||||
|
(error.response.status < 200 || error.response.status >= 300)
|
||||||
|
) {
|
||||||
|
logger.warn('Error deleting vectors, file will not be deleted');
|
||||||
|
throw new Error(error.message || 'An error occurred during file deletion.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,8 +347,8 @@ const uploadImageBuffer = async ({ req, context, metadata = {}, resize = true })
|
||||||
req.app.locals.imageOutputType
|
req.app.locals.imageOutputType
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
const fileName = `${file_id}-${filename}`;
|
||||||
const filepath = await saveBuffer({ userId: req.user.id, fileName: filename, buffer });
|
const filepath = await saveBuffer({ userId: req.user.id, fileName, buffer });
|
||||||
return await createFile(
|
return await createFile(
|
||||||
{
|
{
|
||||||
user: req.user.id,
|
user: req.user.id,
|
||||||
|
|
@ -801,8 +801,7 @@ async function saveBase64Image(
|
||||||
{ req, file_id: _file_id, filename: _filename, endpoint, context, resolution = 'high' },
|
{ req, file_id: _file_id, filename: _filename, endpoint, context, resolution = 'high' },
|
||||||
) {
|
) {
|
||||||
const file_id = _file_id ?? v4();
|
const file_id = _file_id ?? v4();
|
||||||
|
let filename = `${file_id}-${_filename}`;
|
||||||
let filename = _filename;
|
|
||||||
const { buffer: inputBuffer, type } = base64ToBuffer(url);
|
const { buffer: inputBuffer, type } = base64ToBuffer(url);
|
||||||
if (!path.extname(_filename)) {
|
if (!path.extname(_filename)) {
|
||||||
const extension = mime.getExtension(type);
|
const extension = mime.getExtension(type);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const axios = require('axios');
|
const axios = require('axios');
|
||||||
|
const { Providers } = require('@librechat/agents');
|
||||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||||
const { EModelEndpoint, defaultModels, CacheKeys } = require('librechat-data-provider');
|
const { EModelEndpoint, defaultModels, CacheKeys } = require('librechat-data-provider');
|
||||||
const { inputSchema, logAxiosError, extractBaseURL, processModelData } = require('~/utils');
|
const { inputSchema, logAxiosError, extractBaseURL, processModelData } = require('~/utils');
|
||||||
|
|
@ -57,7 +58,7 @@ const fetchModels = async ({
|
||||||
return models;
|
return models;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name && name.toLowerCase().startsWith('ollama')) {
|
if (name && name.toLowerCase().startsWith(Providers.OLLAMA)) {
|
||||||
return await OllamaClient.fetchModels(baseURL);
|
return await OllamaClient.fetchModels(baseURL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
238
api/server/services/twoFactorService.js
Normal file
238
api/server/services/twoFactorService.js
Normal file
|
|
@ -0,0 +1,238 @@
|
||||||
|
const { sign } = require('jsonwebtoken');
|
||||||
|
const { webcrypto } = require('node:crypto');
|
||||||
|
const { hashBackupCode, decryptV2 } = require('~/server/utils/crypto');
|
||||||
|
const { updateUser } = require('~/models/userMethods');
|
||||||
|
|
||||||
|
const BASE32_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a Buffer into a Base32 string using the RFC 4648 alphabet.
|
||||||
|
*
|
||||||
|
* @param {Buffer} buffer - The buffer to encode.
|
||||||
|
* @returns {string} The Base32 encoded string.
|
||||||
|
*/
|
||||||
|
const encodeBase32 = (buffer) => {
|
||||||
|
let bits = 0;
|
||||||
|
let value = 0;
|
||||||
|
let output = '';
|
||||||
|
for (const byte of buffer) {
|
||||||
|
value = (value << 8) | byte;
|
||||||
|
bits += 8;
|
||||||
|
while (bits >= 5) {
|
||||||
|
output += BASE32_ALPHABET[(value >>> (bits - 5)) & 31];
|
||||||
|
bits -= 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bits > 0) {
|
||||||
|
output += BASE32_ALPHABET[(value << (5 - bits)) & 31];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes a Base32-encoded string back into a Buffer.
|
||||||
|
*
|
||||||
|
* @param {string} base32Str - The Base32-encoded string.
|
||||||
|
* @returns {Buffer} The decoded buffer.
|
||||||
|
*/
|
||||||
|
const decodeBase32 = (base32Str) => {
|
||||||
|
const cleaned = base32Str.replace(/=+$/, '').toUpperCase();
|
||||||
|
let bits = 0;
|
||||||
|
let value = 0;
|
||||||
|
const output = [];
|
||||||
|
for (const char of cleaned) {
|
||||||
|
const idx = BASE32_ALPHABET.indexOf(char);
|
||||||
|
if (idx === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
value = (value << 5) | idx;
|
||||||
|
bits += 5;
|
||||||
|
if (bits >= 8) {
|
||||||
|
output.push((value >>> (bits - 8)) & 0xff);
|
||||||
|
bits -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Buffer.from(output);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a temporary token for 2FA verification.
|
||||||
|
* The token is signed with the JWT_SECRET and expires in 5 minutes.
|
||||||
|
*
|
||||||
|
* @param {string} userId - The unique identifier of the user.
|
||||||
|
* @returns {string} The signed JWT token.
|
||||||
|
*/
|
||||||
|
const generate2FATempToken = (userId) =>
|
||||||
|
sign({ userId, twoFAPending: true }, process.env.JWT_SECRET, { expiresIn: '5m' });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a TOTP secret.
|
||||||
|
* Creates 10 random bytes using WebCrypto and encodes them into a Base32 string.
|
||||||
|
*
|
||||||
|
* @returns {string} A Base32-encoded secret for TOTP.
|
||||||
|
*/
|
||||||
|
const generateTOTPSecret = () => {
|
||||||
|
const randomArray = new Uint8Array(10);
|
||||||
|
webcrypto.getRandomValues(randomArray);
|
||||||
|
return encodeBase32(Buffer.from(randomArray));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a Time-based One-Time Password (TOTP) based on the provided secret and time.
|
||||||
|
* This implementation uses a 30-second time step and produces a 6-digit code.
|
||||||
|
*
|
||||||
|
* @param {string} secret - The Base32-encoded TOTP secret.
|
||||||
|
* @param {number} [forTime=Date.now()] - The time (in milliseconds) for which to generate the TOTP.
|
||||||
|
* @returns {Promise<string>} A promise that resolves to the 6-digit TOTP code.
|
||||||
|
*/
|
||||||
|
const generateTOTP = async (secret, forTime = Date.now()) => {
|
||||||
|
const timeStep = 30; // seconds
|
||||||
|
const counter = Math.floor(forTime / 1000 / timeStep);
|
||||||
|
const counterBuffer = new ArrayBuffer(8);
|
||||||
|
const counterView = new DataView(counterBuffer);
|
||||||
|
// Write counter into the last 4 bytes (big-endian)
|
||||||
|
counterView.setUint32(4, counter, false);
|
||||||
|
|
||||||
|
// Decode the secret into an ArrayBuffer
|
||||||
|
const keyBuffer = decodeBase32(secret);
|
||||||
|
const keyArrayBuffer = keyBuffer.buffer.slice(
|
||||||
|
keyBuffer.byteOffset,
|
||||||
|
keyBuffer.byteOffset + keyBuffer.byteLength,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import the key for HMAC-SHA1 signing
|
||||||
|
const cryptoKey = await webcrypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
keyArrayBuffer,
|
||||||
|
{ name: 'HMAC', hash: 'SHA-1' },
|
||||||
|
false,
|
||||||
|
['sign'],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate HMAC signature
|
||||||
|
const signatureBuffer = await webcrypto.subtle.sign('HMAC', cryptoKey, counterBuffer);
|
||||||
|
const hmac = new Uint8Array(signatureBuffer);
|
||||||
|
|
||||||
|
// Dynamic truncation as per RFC 4226
|
||||||
|
const offset = hmac[hmac.length - 1] & 0xf;
|
||||||
|
const slice = hmac.slice(offset, offset + 4);
|
||||||
|
const view = new DataView(slice.buffer, slice.byteOffset, slice.byteLength);
|
||||||
|
const binaryCode = view.getUint32(0, false) & 0x7fffffff;
|
||||||
|
const code = (binaryCode % 1000000).toString().padStart(6, '0');
|
||||||
|
return code;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a provided TOTP token against the secret.
|
||||||
|
* It allows for a ±1 time-step window to account for slight clock discrepancies.
|
||||||
|
*
|
||||||
|
* @param {string} secret - The Base32-encoded TOTP secret.
|
||||||
|
* @param {string} token - The TOTP token provided by the user.
|
||||||
|
* @returns {Promise<boolean>} A promise that resolves to true if the token is valid; otherwise, false.
|
||||||
|
*/
|
||||||
|
const verifyTOTP = async (secret, token) => {
|
||||||
|
const timeStepMS = 30 * 1000;
|
||||||
|
const currentTime = Date.now();
|
||||||
|
for (let offset = -1; offset <= 1; offset++) {
|
||||||
|
const expected = await generateTOTP(secret, currentTime + offset * timeStepMS);
|
||||||
|
if (expected === token) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates backup codes for two-factor authentication.
|
||||||
|
* Each backup code is an 8-character hexadecimal string along with its SHA-256 hash.
|
||||||
|
* The plain codes are returned for one-time download, while the hashed objects are meant for secure storage.
|
||||||
|
*
|
||||||
|
* @param {number} [count=10] - The number of backup codes to generate.
|
||||||
|
* @returns {Promise<{ plainCodes: string[], codeObjects: Array<{ codeHash: string, used: boolean, usedAt: Date | null }> }>}
|
||||||
|
* A promise that resolves to an object containing both plain backup codes and their corresponding code objects.
|
||||||
|
*/
|
||||||
|
const generateBackupCodes = async (count = 10) => {
|
||||||
|
const plainCodes = [];
|
||||||
|
const codeObjects = [];
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const randomArray = new Uint8Array(4);
|
||||||
|
webcrypto.getRandomValues(randomArray);
|
||||||
|
const code = Array.from(randomArray)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join(''); // 8-character hex code
|
||||||
|
plainCodes.push(code);
|
||||||
|
|
||||||
|
// Compute SHA-256 hash of the code using WebCrypto
|
||||||
|
const codeBuffer = encoder.encode(code);
|
||||||
|
const hashBuffer = await webcrypto.subtle.digest('SHA-256', codeBuffer);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const codeHash = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
codeObjects.push({ codeHash, used: false, usedAt: null });
|
||||||
|
}
|
||||||
|
return { plainCodes, codeObjects };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies a backup code for a user and updates its status as used if valid.
|
||||||
|
*
|
||||||
|
* @param {Object} params - The parameters object.
|
||||||
|
* @param {TUser | undefined} [params.user] - The user object containing backup codes.
|
||||||
|
* @param {string | undefined} [params.backupCode] - The backup code to verify.
|
||||||
|
* @returns {Promise<boolean>} A promise that resolves to true if the backup code is valid and updated; otherwise, false.
|
||||||
|
*/
|
||||||
|
const verifyBackupCode = async ({ user, backupCode }) => {
|
||||||
|
if (!backupCode || !user || !Array.isArray(user.backupCodes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedInput = await hashBackupCode(backupCode.trim());
|
||||||
|
const matchingCode = user.backupCodes.find(
|
||||||
|
(codeObj) => codeObj.codeHash === hashedInput && !codeObj.used,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingCode) {
|
||||||
|
const updatedBackupCodes = user.backupCodes.map((codeObj) =>
|
||||||
|
codeObj.codeHash === hashedInput && !codeObj.used
|
||||||
|
? { ...codeObj, used: true, usedAt: new Date() }
|
||||||
|
: codeObj,
|
||||||
|
);
|
||||||
|
|
||||||
|
await updateUser(user._id, { backupCodes: updatedBackupCodes });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and, if necessary, decrypts a stored TOTP secret.
|
||||||
|
* If the secret contains a colon, it is assumed to be in the format "iv:encryptedData" and will be decrypted.
|
||||||
|
* If the secret is exactly 16 characters long, it is assumed to be a legacy plain secret.
|
||||||
|
*
|
||||||
|
* @param {string|null} storedSecret - The stored TOTP secret (which may be encrypted).
|
||||||
|
* @returns {Promise<string|null>} A promise that resolves to the plain TOTP secret, or null if none is provided.
|
||||||
|
*/
|
||||||
|
const getTOTPSecret = async (storedSecret) => {
|
||||||
|
if (!storedSecret) { return null; }
|
||||||
|
// Check for a colon marker (encrypted secrets are stored as "iv:encryptedData")
|
||||||
|
if (storedSecret.includes(':')) {
|
||||||
|
return await decryptV2(storedSecret);
|
||||||
|
}
|
||||||
|
// If it's exactly 16 characters, assume it's already plain (legacy secret)
|
||||||
|
if (storedSecret.length === 16) {
|
||||||
|
return storedSecret;
|
||||||
|
}
|
||||||
|
// Fallback in case it doesn't meet our criteria.
|
||||||
|
return storedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyTOTP,
|
||||||
|
generateTOTP,
|
||||||
|
getTOTPSecret,
|
||||||
|
verifyBackupCode,
|
||||||
|
generateTOTPSecret,
|
||||||
|
generateBackupCodes,
|
||||||
|
generate2FATempToken,
|
||||||
|
};
|
||||||
|
|
@ -112,4 +112,25 @@ async function getRandomValues(length) {
|
||||||
return Buffer.from(randomValues).toString('hex');
|
return Buffer.from(randomValues).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { encrypt, decrypt, encryptV2, decryptV2, hashToken, getRandomValues };
|
/**
|
||||||
|
* Computes SHA-256 hash for the given input using WebCrypto
|
||||||
|
* @param {string} input
|
||||||
|
* @returns {Promise<string>} - Hex hash string
|
||||||
|
*/
|
||||||
|
const hashBackupCode = async (input) => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const data = encoder.encode(input);
|
||||||
|
const hashBuffer = await webcrypto.subtle.digest('SHA-256', data);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
encryptV2,
|
||||||
|
decryptV2,
|
||||||
|
hashToken,
|
||||||
|
hashBackupCode,
|
||||||
|
getRandomValues,
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const getProfileDetails = ({ profile }) => ({
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
avatarUrl: profile.photos[0].value,
|
avatarUrl: profile.photos[0].value,
|
||||||
username: profile.name.givenName,
|
username: profile.name.givenName,
|
||||||
name: `${profile.name.givenName} ${profile.name.familyName}`,
|
name: `${profile.name.givenName}${profile.name.familyName ? ` ${profile.name.familyName}` : ''}`,
|
||||||
emailVerified: profile.emails[0].verified,
|
emailVerified: profile.emails[0].verified,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ const jwtLogin = async () =>
|
||||||
},
|
},
|
||||||
async (payload, done) => {
|
async (payload, done) => {
|
||||||
try {
|
try {
|
||||||
const user = await getUserById(payload?.id, '-password -__v');
|
const user = await getUserById(payload?.id, '-password -__v -totpSecret');
|
||||||
if (user) {
|
if (user) {
|
||||||
user.id = user._id.toString();
|
user.id = user._id.toString();
|
||||||
if (!user.role) {
|
if (!user.role) {
|
||||||
|
|
|
||||||
|
|
@ -5,40 +5,32 @@ const { logger } = require('~/config');
|
||||||
*
|
*
|
||||||
* @param {Object} options - The options object.
|
* @param {Object} options - The options object.
|
||||||
* @param {string} options.message - The custom message to be logged.
|
* @param {string} options.message - The custom message to be logged.
|
||||||
* @param {Error} options.error - The Axios error object.
|
* @param {import('axios').AxiosError} options.error - The Axios error object.
|
||||||
*/
|
*/
|
||||||
const logAxiosError = ({ message, error }) => {
|
const logAxiosError = ({ message, error }) => {
|
||||||
const timedOutMessage = 'Cannot read properties of undefined (reading \'status\')';
|
try {
|
||||||
if (error.response) {
|
if (error.response?.status) {
|
||||||
logger.error(
|
const { status, headers, data } = error.response;
|
||||||
`${message} The request was made and the server responded with a status code that falls out of the range of 2xx: ${
|
logger.error(`${message} The server responded with status ${status}: ${error.message}`, {
|
||||||
error.message ? error.message : ''
|
status,
|
||||||
}. Error response data:\n`,
|
headers,
|
||||||
{
|
data,
|
||||||
headers: error.response?.headers,
|
});
|
||||||
status: error.response?.status,
|
} else if (error.request) {
|
||||||
data: error.response?.data,
|
const { method, url } = error.config || {};
|
||||||
},
|
logger.error(
|
||||||
);
|
`${message} No response received for ${method ? method.toUpperCase() : ''} ${url || ''}: ${error.message}`,
|
||||||
} else if (error.request) {
|
{ requestInfo: { method, url } },
|
||||||
logger.error(
|
);
|
||||||
`${message} The request was made but no response was received: ${
|
} else if (error?.message?.includes('Cannot read properties of undefined (reading \'status\')')) {
|
||||||
error.message ? error.message : ''
|
logger.error(
|
||||||
}. Error Request:\n`,
|
`${message} It appears the request timed out or was unsuccessful: ${error.message}`,
|
||||||
{
|
);
|
||||||
request: error.request,
|
} else {
|
||||||
},
|
logger.error(`${message} An error occurred while setting up the request: ${error.message}`);
|
||||||
);
|
}
|
||||||
} else if (error?.message?.includes(timedOutMessage)) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(`Error in logAxiosError: ${err.message}`);
|
||||||
`${message}\nThe request either timed out or was unsuccessful. Error message:\n`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`${message}\nSomething happened in setting up the request. Error message:\n`,
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://librechat.ai",
|
"homepage": "https://librechat.ai",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.4.11",
|
"@ariakit/react": "^0.4.15",
|
||||||
|
"@ariakit/react-core": "^0.4.15",
|
||||||
"@codesandbox/sandpack-react": "^2.19.10",
|
"@codesandbox/sandpack-react": "^2.19.10",
|
||||||
"@dicebear/collection": "^7.0.4",
|
"@dicebear/collection": "^7.0.4",
|
||||||
"@dicebear/core": "^7.0.4",
|
"@dicebear/core": "^7.0.4",
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.0",
|
"@radix-ui/react-label": "^2.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
"@radix-ui/react-progress": "^1.1.2",
|
||||||
"@radix-ui/react-radio-group": "^1.1.3",
|
"@radix-ui/react-radio-group": "^1.1.3",
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
|
|
@ -63,6 +65,8 @@
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"html-to-image": "^1.11.11",
|
"html-to-image": "^1.11.11",
|
||||||
"i18next": "^24.2.2",
|
"i18next": "^24.2.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.3",
|
||||||
|
"input-otp": "^1.4.2",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"librechat-data-provider": "*",
|
"librechat-data-provider": "*",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
|
@ -82,7 +86,7 @@
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-lazy-load-image-component": "^1.6.0",
|
"react-lazy-load-image-component": "^1.6.0",
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-resizable-panels": "^2.1.1",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"react-router-dom": "^6.11.2",
|
"react-router-dom": "^6.11.2",
|
||||||
"react-speech-recognition": "^3.10.0",
|
"react-speech-recognition": "^3.10.0",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ function AuthLayout({
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{(pathname.includes('login') || pathname.includes('register')) && (
|
{!pathname.includes('2fa') && (pathname.includes('login') || pathname.includes('register')) && (
|
||||||
<SocialLoginRender
|
<SocialLoginRender
|
||||||
startupConfig={startupConfig}
|
startupConfig={startupConfig}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|
|
||||||
|
|
@ -166,9 +166,7 @@ const LoginForm: React.FC<TLoginFormProps> = ({ onSubmit, startupConfig, error,
|
||||||
type="submit"
|
type="submit"
|
||||||
className="
|
className="
|
||||||
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white
|
||||||
transition-colors hover:bg-green-700 focus:outline-none focus:ring-2
|
transition-colors hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700
|
||||||
focus:ring-green-500 focus:ring-offset-2 disabled:opacity-50
|
|
||||||
disabled:hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700
|
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{localize('com_auth_continue')}
|
{localize('com_auth_continue')}
|
||||||
|
|
|
||||||
176
client/src/components/Auth/TwoFactorScreen.tsx
Normal file
176
client/src/components/Auth/TwoFactorScreen.tsx
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
|
||||||
|
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Label } from '~/components';
|
||||||
|
import { useVerifyTwoFactorTempMutation } from '~/data-provider';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface VerifyPayload {
|
||||||
|
tempToken: string;
|
||||||
|
token?: string;
|
||||||
|
backupCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TwoFactorFormInputs = {
|
||||||
|
token?: string;
|
||||||
|
backupCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TwoFactorScreen: React.FC = React.memo(() => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const tempTokenRaw = searchParams.get('tempToken');
|
||||||
|
const tempToken = tempTokenRaw !== null && tempTokenRaw !== '' ? tempTokenRaw : '';
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<TwoFactorFormInputs>();
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const [useBackup, setUseBackup] = useState<boolean>(false);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const { mutate: verifyTempMutate } = useVerifyTwoFactorTempMutation({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
if (result.token != null && result.token !== '') {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
setIsLoading(false);
|
||||||
|
const err = error as { response?: { data?: { message?: unknown } } };
|
||||||
|
const errorMsg =
|
||||||
|
typeof err.response?.data?.message === 'string'
|
||||||
|
? err.response.data.message
|
||||||
|
: 'Error verifying 2FA';
|
||||||
|
showToast({ message: errorMsg, status: 'error' });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: TwoFactorFormInputs) => {
|
||||||
|
const payload: VerifyPayload = { tempToken };
|
||||||
|
if (useBackup && data.backupCode != null && data.backupCode !== '') {
|
||||||
|
payload.backupCode = data.backupCode;
|
||||||
|
} else if (data.token != null && data.token !== '') {
|
||||||
|
payload.token = data.token;
|
||||||
|
}
|
||||||
|
verifyTempMutate(payload);
|
||||||
|
},
|
||||||
|
[tempToken, useBackup, verifyTempMutate],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleBackupOn = useCallback(() => {
|
||||||
|
setUseBackup(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleBackupOff = useCallback(() => {
|
||||||
|
setUseBackup(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Label className="flex justify-center break-keep text-center text-sm text-text-primary">
|
||||||
|
{localize('com_auth_two_factor')}
|
||||||
|
</Label>
|
||||||
|
{!useBackup && (
|
||||||
|
<div className="my-4 flex justify-center text-text-primary">
|
||||||
|
<Controller
|
||||||
|
name="token"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputOTP
|
||||||
|
maxLength={6}
|
||||||
|
value={value != null ? value : ''}
|
||||||
|
onChange={onChange}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
<InputOTPSeparator />
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.token && <span className="text-sm text-red-500">{errors.token.message}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{useBackup && (
|
||||||
|
<div className="my-4 flex justify-center text-text-primary">
|
||||||
|
<Controller
|
||||||
|
name="backupCode"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<InputOTP
|
||||||
|
maxLength={8}
|
||||||
|
value={value != null ? value : ''}
|
||||||
|
onChange={onChange}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
<InputOTPSlot index={6} />
|
||||||
|
<InputOTPSlot index={7} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{errors.backupCode && (
|
||||||
|
<span className="text-sm text-red-500">{errors.backupCode.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
aria-label={localize('com_auth_continue')}
|
||||||
|
data-testid="login-button"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full rounded-2xl bg-green-600 px-4 py-3 text-sm font-medium text-white transition-colors hover:bg-green-700 disabled:opacity-80 dark:bg-green-600 dark:hover:bg-green-700"
|
||||||
|
>
|
||||||
|
{isLoading ? localize('com_auth_email_verifying_ellipsis') : localize('com_ui_verify')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex justify-center">
|
||||||
|
{!useBackup ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleBackupOn}
|
||||||
|
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
|
{localize('com_ui_use_backup_code')}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={toggleBackupOff}
|
||||||
|
className="inline-flex p-1 text-sm font-medium text-green-600 transition-colors hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
|
||||||
|
>
|
||||||
|
{localize('com_ui_use_2fa_code')}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TwoFactorScreen;
|
||||||
|
|
@ -4,3 +4,4 @@ export { default as ResetPassword } from './ResetPassword';
|
||||||
export { default as VerifyEmail } from './VerifyEmail';
|
export { default as VerifyEmail } from './VerifyEmail';
|
||||||
export { default as ApiErrorWatcher } from './ApiErrorWatcher';
|
export { default as ApiErrorWatcher } from './ApiErrorWatcher';
|
||||||
export { default as RequestPasswordReset } from './RequestPasswordReset';
|
export { default as RequestPasswordReset } from './RequestPasswordReset';
|
||||||
|
export { default as TwoFactorScreen } from './TwoFactorScreen';
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
|
|
||||||
let statusText: string;
|
let statusText: string;
|
||||||
if (!status) {
|
if (!status) {
|
||||||
statusText = text ?? localize('com_endpoint_import');
|
statusText = text ?? localize('com_ui_import');
|
||||||
} else if (status === 'success') {
|
} else if (status === 'success') {
|
||||||
statusText = successText ?? localize('com_ui_upload_success');
|
statusText = successText ?? localize('com_ui_upload_success');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -72,12 +72,12 @@ const FileUpload: React.FC<FileUploadProps> = ({
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
|
<FileUp className="mr-1 flex w-[22px] items-center stroke-1" />
|
||||||
<span className="flex text-xs ">{statusText}</span>
|
<span className="flex text-xs">{statusText}</span>
|
||||||
<input
|
<input
|
||||||
id={`file-upload-${id}`}
|
id={`file-upload-${id}`}
|
||||||
value=""
|
value=""
|
||||||
type="file"
|
type="file"
|
||||||
className={cn('hidden ', className)}
|
className={cn('hidden', className)}
|
||||||
accept=".json"
|
accept=".json"
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { Settings2 } from 'lucide-react';
|
import { Settings2 } from 'lucide-react';
|
||||||
import { Root, Anchor } from '@radix-ui/react-popover';
|
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { tConvoUpdateSchema, EModelEndpoint, isParamEndpoint } from 'librechat-data-provider';
|
import { Root, Anchor } from '@radix-ui/react-popover';
|
||||||
|
import {
|
||||||
|
EModelEndpoint,
|
||||||
|
isParamEndpoint,
|
||||||
|
isAgentsEndpoint,
|
||||||
|
tConvoUpdateSchema,
|
||||||
|
} from 'librechat-data-provider';
|
||||||
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
|
||||||
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
|
||||||
import { PluginStoreDialog, TooltipAnchor } from '~/components';
|
import { PluginStoreDialog, TooltipAnchor } from '~/components';
|
||||||
|
|
@ -42,7 +47,6 @@ export default function HeaderOptions({
|
||||||
if (endpoint && noSettings[endpoint]) {
|
if (endpoint && noSettings[endpoint]) {
|
||||||
setShowPopover(false);
|
setShowPopover(false);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [endpoint, noSettings]);
|
}, [endpoint, noSettings]);
|
||||||
|
|
||||||
const saveAsPreset = () => {
|
const saveAsPreset = () => {
|
||||||
|
|
@ -67,7 +71,7 @@ export default function HeaderOptions({
|
||||||
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
|
<div className="my-auto lg:max-w-2xl xl:max-w-3xl">
|
||||||
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
<span className="flex w-full flex-col items-center justify-center gap-0 md:order-none md:m-auto md:gap-2">
|
||||||
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
<div className="z-[61] flex w-full items-center justify-center gap-2">
|
||||||
{interfaceConfig?.modelSelect === true && (
|
{interfaceConfig?.modelSelect === true && !isAgentsEndpoint(endpoint) && (
|
||||||
<ModelSelect
|
<ModelSelect
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
setOption={setOption}
|
setOption={setOption}
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ function AccountSettings() {
|
||||||
!isNaN(parseFloat(balanceQuery.data)) && (
|
!isNaN(parseFloat(balanceQuery.data)) && (
|
||||||
<>
|
<>
|
||||||
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
<div className="text-token-text-secondary ml-3 mr-2 py-2 text-sm" role="note">
|
||||||
{localize('com_nav_balance')}: ${parseFloat(balanceQuery.data).toFixed(2)}
|
{localize('com_nav_balance')}: {parseFloat(balanceQuery.data).toFixed(2)}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,36 @@ import DisplayUsernameMessages from './DisplayUsernameMessages';
|
||||||
import DeleteAccount from './DeleteAccount';
|
import DeleteAccount from './DeleteAccount';
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import PassKeys from './PassKeys';
|
import PassKeys from './PassKeys';
|
||||||
|
import EnableTwoFactorItem from './TwoFactorAuthentication';
|
||||||
|
import BackupCodesItem from './BackupCodesItem';
|
||||||
|
import { useAuthContext } from '~/hooks';
|
||||||
|
|
||||||
function Account() {
|
function Account() {
|
||||||
|
const user = useAuthContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
|
<div className="pb-3">
|
||||||
|
<DisplayUsernameMessages />
|
||||||
|
</div>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</div>
|
</div>
|
||||||
|
{user?.user?.provider === 'local' && (
|
||||||
|
<>
|
||||||
|
<div className="pb-3">
|
||||||
|
<EnableTwoFactorItem />
|
||||||
|
</div>
|
||||||
|
{Array.isArray(user.user?.backupCodes) && user.user?.backupCodes.length > 0 && (
|
||||||
|
<div className="pb-3">
|
||||||
|
<BackupCodesItem />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<DeleteAccount />
|
<DeleteAccount />
|
||||||
</div>
|
</div>
|
||||||
<div className="pb-3">
|
|
||||||
<DisplayUsernameMessages />
|
|
||||||
</div>
|
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<PassKeys />
|
<PassKeys />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ function Avatar() {
|
||||||
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
|
const { mutate: uploadAvatar, isLoading: isUploading } = useUploadAvatarMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
showToast({ message: localize('com_ui_upload_success') });
|
showToast({ message: localize('com_ui_upload_success') });
|
||||||
setUser((prev) => ({ ...prev, avatar: data.url } as TUser));
|
setUser((prev) => ({ ...prev, avatar: data.url }) as TUser);
|
||||||
openButtonRef.current?.click();
|
openButtonRef.current?.click();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
|
|
@ -133,9 +133,11 @@ function Avatar() {
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{localize('com_nav_profile_picture')}</span>
|
<span>{localize('com_nav_profile_picture')}</span>
|
||||||
<OGDialogTrigger ref={openButtonRef} className="btn btn-neutral relative">
|
<OGDialogTrigger ref={openButtonRef}>
|
||||||
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
|
<Button variant="outline">
|
||||||
<span>{localize('com_nav_change_picture')}</span>
|
<FileImage className="mr-2 flex w-[22px] items-center stroke-1" />
|
||||||
|
<span>{localize('com_nav_change_picture')}</span>
|
||||||
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { RefreshCcw, ShieldX } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import { TBackupCode, TRegenerateBackupCodesResponse, type TUser } from 'librechat-data-provider';
|
||||||
|
import {
|
||||||
|
OGDialog,
|
||||||
|
OGDialogContent,
|
||||||
|
OGDialogTitle,
|
||||||
|
OGDialogTrigger,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Spinner,
|
||||||
|
TooltipAnchor,
|
||||||
|
} from '~/components';
|
||||||
|
import { useRegenerateBackupCodesMutation } from '~/data-provider';
|
||||||
|
import { useAuthContext, useLocalize } from '~/hooks';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
|
const BackupCodesItem: React.FC = () => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
const setUser = useSetRecoilState(store.user);
|
||||||
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { mutate: regenerateBackupCodes, isLoading } = useRegenerateBackupCodesMutation();
|
||||||
|
|
||||||
|
const fetchBackupCodes = (auto: boolean = false) => {
|
||||||
|
regenerateBackupCodes(undefined, {
|
||||||
|
onSuccess: (data: TRegenerateBackupCodesResponse) => {
|
||||||
|
const newBackupCodes: TBackupCode[] = data.backupCodesHash.map((codeHash) => ({
|
||||||
|
codeHash,
|
||||||
|
used: false,
|
||||||
|
usedAt: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setUser((prev) => ({ ...prev, backupCodes: newBackupCodes }) as TUser);
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_backup_codes_regenerated'),
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger file download only when user explicitly clicks the button.
|
||||||
|
if (!auto && newBackupCodes.length) {
|
||||||
|
const codesString = data.backupCodes.join('\n');
|
||||||
|
const blob = new Blob([codesString], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'backup-codes.txt';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: () =>
|
||||||
|
showToast({
|
||||||
|
message: localize('com_ui_backup_codes_regenerate_error'),
|
||||||
|
status: 'error',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
fetchBackupCodes(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog open={isDialogOpen} onOpenChange={setDialogOpen}>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Label className="font-light">{localize('com_ui_backup_codes')}</Label>
|
||||||
|
</div>
|
||||||
|
<OGDialogTrigger asChild>
|
||||||
|
<Button aria-label="Show Backup Codes" variant="outline">
|
||||||
|
{localize('com_ui_show')}
|
||||||
|
</Button>
|
||||||
|
</OGDialogTrigger>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OGDialogContent className="w-11/12 max-w-lg">
|
||||||
|
<OGDialogTitle className="mb-6 text-2xl font-semibold">
|
||||||
|
{localize('com_ui_backup_codes')}
|
||||||
|
</OGDialogTitle>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{user?.backupCodes.map((code, index) => {
|
||||||
|
const isUsed = code.used;
|
||||||
|
const description = `Backup code number ${index + 1}, ${
|
||||||
|
isUsed
|
||||||
|
? `used on ${code.usedAt ? new Date(code.usedAt).toLocaleDateString() : 'an unknown date'}`
|
||||||
|
: 'not used yet'
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={code.codeHash}
|
||||||
|
role="listitem"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={description}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
onFocus={() => {
|
||||||
|
const announcement = new CustomEvent('announce', {
|
||||||
|
detail: { message: description },
|
||||||
|
});
|
||||||
|
document.dispatchEvent(announcement);
|
||||||
|
}}
|
||||||
|
className={`flex flex-col rounded-xl border p-4 backdrop-blur-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${
|
||||||
|
isUsed
|
||||||
|
? 'border-red-200 bg-red-50/80 dark:border-red-800 dark:bg-red-900/20'
|
||||||
|
: 'border-green-200 bg-green-50/80 dark:border-green-800 dark:bg-green-900/20'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between" aria-hidden="true">
|
||||||
|
<span className="text-sm font-medium text-text-secondary">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<TooltipAnchor
|
||||||
|
description={
|
||||||
|
code.usedAt ? new Date(code.usedAt).toLocaleDateString() : ''
|
||||||
|
}
|
||||||
|
disabled={!isUsed}
|
||||||
|
focusable={false}
|
||||||
|
className={isUsed ? 'cursor-pointer' : 'cursor-default'}
|
||||||
|
render={
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-3 py-1 text-sm font-medium ${
|
||||||
|
isUsed
|
||||||
|
? 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||||
|
: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isUsed ? localize('com_ui_used') : localize('com_ui_not_used')}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-12 flex justify-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="default"
|
||||||
|
className="px-8 py-3 transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner className="mr-2" />
|
||||||
|
) : (
|
||||||
|
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{isLoading
|
||||||
|
? localize('com_ui_regenerating')
|
||||||
|
: localize('com_ui_regenerate_backup')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-4 p-6 text-center">
|
||||||
|
<ShieldX className="h-12 w-12 text-text-primary" />
|
||||||
|
<p className="text-lg text-text-secondary">{localize('com_ui_no_backup_codes')}</p>
|
||||||
|
<Button
|
||||||
|
onClick={handleRegenerate}
|
||||||
|
disabled={isLoading}
|
||||||
|
variant="default"
|
||||||
|
className="px-8 py-3 transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading && <Spinner className="mr-2" />}
|
||||||
|
{localize('com_ui_generate_backup')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(BackupCodesItem);
|
||||||
|
|
@ -57,7 +57,7 @@ const DeleteAccount = ({ disabled = false }: { title?: string; disabled?: boolea
|
||||||
</Button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
</div>
|
</div>
|
||||||
<OGDialogContent className="w-11/12 max-w-2xl">
|
<OGDialogContent className="w-11/12 max-w-md">
|
||||||
<OGDialogHeader>
|
<OGDialogHeader>
|
||||||
<OGDialogTitle className="text-lg font-medium leading-6">
|
<OGDialogTitle className="text-lg font-medium leading-6">
|
||||||
{localize('com_nav_delete_account_confirm')}
|
{localize('com_nav_delete_account_confirm')}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { LockIcon, UnlockIcon } from 'lucide-react';
|
||||||
|
import { Label, Button } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
interface DisableTwoFactorToggleProps {
|
||||||
|
enabled: boolean;
|
||||||
|
onChange: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisableTwoFactorToggle: React.FC<DisableTwoFactorToggleProps> = ({
|
||||||
|
enabled,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Label className="font-light"> {localize('com_nav_2fa')}</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button
|
||||||
|
variant={enabled ? 'destructive' : 'outline'}
|
||||||
|
onClick={onChange}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{enabled ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_enable')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { SmartphoneIcon } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import type { TUser, TVerify2FARequest } from 'librechat-data-provider';
|
||||||
|
import { OGDialog, OGDialogContent, OGDialogHeader, OGDialogTitle, Progress } from '~/components';
|
||||||
|
import { SetupPhase, QRPhase, VerifyPhase, BackupPhase, DisablePhase } from './TwoFactorPhases';
|
||||||
|
import { DisableTwoFactorToggle } from './DisableTwoFactorToggle';
|
||||||
|
import { useAuthContext, useLocalize } from '~/hooks';
|
||||||
|
import { useToastContext } from '~/Providers';
|
||||||
|
import store from '~/store';
|
||||||
|
import {
|
||||||
|
useConfirmTwoFactorMutation,
|
||||||
|
useDisableTwoFactorMutation,
|
||||||
|
useEnableTwoFactorMutation,
|
||||||
|
useVerifyTwoFactorMutation,
|
||||||
|
} from '~/data-provider';
|
||||||
|
|
||||||
|
export type Phase = 'setup' | 'qr' | 'verify' | 'backup' | 'disable';
|
||||||
|
|
||||||
|
const phaseVariants = {
|
||||||
|
initial: { opacity: 0, scale: 0.95 },
|
||||||
|
animate: { opacity: 1, scale: 1, transition: { duration: 0.3, ease: 'easeOut' } },
|
||||||
|
exit: { opacity: 0, scale: 0.95, transition: { duration: 0.3, ease: 'easeIn' } },
|
||||||
|
};
|
||||||
|
|
||||||
|
const TwoFactorAuthentication: React.FC = () => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const { user } = useAuthContext();
|
||||||
|
const setUser = useSetRecoilState(store.user);
|
||||||
|
const { showToast } = useToastContext();
|
||||||
|
|
||||||
|
const [secret, setSecret] = useState<string>('');
|
||||||
|
const [otpauthUrl, setOtpauthUrl] = useState<string>('');
|
||||||
|
const [downloaded, setDownloaded] = useState<boolean>(false);
|
||||||
|
const [disableToken, setDisableToken] = useState<string>('');
|
||||||
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
const [verificationToken, setVerificationToken] = useState<string>('');
|
||||||
|
const [phase, setPhase] = useState<Phase>(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');
|
||||||
|
|
||||||
|
const { mutate: confirm2FAMutate } = useConfirmTwoFactorMutation();
|
||||||
|
const { mutate: enable2FAMutate, isLoading: isGenerating } = useEnableTwoFactorMutation();
|
||||||
|
const { mutate: verify2FAMutate, isLoading: isVerifying } = useVerifyTwoFactorMutation();
|
||||||
|
const { mutate: disable2FAMutate, isLoading: isDisabling } = useDisableTwoFactorMutation();
|
||||||
|
|
||||||
|
const steps = ['Setup', 'Scan QR', 'Verify', 'Backup'];
|
||||||
|
const phasesLabel: Record<Phase, string> = {
|
||||||
|
setup: 'Setup',
|
||||||
|
qr: 'Scan QR',
|
||||||
|
verify: 'Verify',
|
||||||
|
backup: 'Backup',
|
||||||
|
disable: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentStep = steps.indexOf(phasesLabel[phase]);
|
||||||
|
|
||||||
|
const resetState = useCallback(() => {
|
||||||
|
if (Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && otpauthUrl) {
|
||||||
|
disable2FAMutate(undefined, {
|
||||||
|
onError: () =>
|
||||||
|
showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setOtpauthUrl('');
|
||||||
|
setSecret('');
|
||||||
|
setBackupCodes([]);
|
||||||
|
setVerificationToken('');
|
||||||
|
setDisableToken('');
|
||||||
|
setPhase(Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? 'disable' : 'setup');
|
||||||
|
setDownloaded(false);
|
||||||
|
}, [user, otpauthUrl, disable2FAMutate, localize, showToast]);
|
||||||
|
|
||||||
|
const handleGenerateQRCode = useCallback(() => {
|
||||||
|
enable2FAMutate(undefined, {
|
||||||
|
onSuccess: ({ otpauthUrl, backupCodes }) => {
|
||||||
|
setOtpauthUrl(otpauthUrl);
|
||||||
|
setSecret(otpauthUrl.split('secret=')[1].split('&')[0]);
|
||||||
|
setBackupCodes(backupCodes);
|
||||||
|
setPhase('qr');
|
||||||
|
},
|
||||||
|
onError: () => showToast({ message: localize('com_ui_2fa_generate_error'), status: 'error' }),
|
||||||
|
});
|
||||||
|
}, [enable2FAMutate, localize, showToast]);
|
||||||
|
|
||||||
|
const handleVerify = useCallback(() => {
|
||||||
|
if (!verificationToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
verify2FAMutate(
|
||||||
|
{ token: verificationToken },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({ message: localize('com_ui_2fa_verified') });
|
||||||
|
confirm2FAMutate(
|
||||||
|
{ token: verificationToken },
|
||||||
|
{
|
||||||
|
onSuccess: () => setPhase('backup'),
|
||||||
|
onError: () =>
|
||||||
|
showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [verificationToken, verify2FAMutate, confirm2FAMutate, localize, showToast]);
|
||||||
|
|
||||||
|
const handleDownload = useCallback(() => {
|
||||||
|
if (!backupCodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blob = new Blob([backupCodes.join('\n')], { type: 'text/plain;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'backup-codes.txt';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
setDownloaded(true);
|
||||||
|
}, [backupCodes]);
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
setDialogOpen(false);
|
||||||
|
setPhase('disable');
|
||||||
|
showToast({ message: localize('com_ui_2fa_enabled') });
|
||||||
|
setUser(
|
||||||
|
(prev) =>
|
||||||
|
({
|
||||||
|
...prev,
|
||||||
|
backupCodes: backupCodes.map((code) => ({
|
||||||
|
code,
|
||||||
|
codeHash: code,
|
||||||
|
used: false,
|
||||||
|
usedAt: null,
|
||||||
|
})),
|
||||||
|
}) as TUser,
|
||||||
|
);
|
||||||
|
}, [setUser, localize, showToast, backupCodes]);
|
||||||
|
|
||||||
|
const handleDisableVerify = useCallback(
|
||||||
|
(token: string, useBackup: boolean) => {
|
||||||
|
// Validate: if not using backup, ensure token has at least 6 digits;
|
||||||
|
// if using backup, ensure backup code has at least 8 characters.
|
||||||
|
if (!useBackup && token.trim().length < 6) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useBackup && token.trim().length < 8) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: TVerify2FARequest = {};
|
||||||
|
if (useBackup) {
|
||||||
|
payload.backupCode = token.trim();
|
||||||
|
} else {
|
||||||
|
payload.token = token.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
verify2FAMutate(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
disable2FAMutate(undefined, {
|
||||||
|
onSuccess: () => {
|
||||||
|
showToast({ message: localize('com_ui_2fa_disabled') });
|
||||||
|
setDialogOpen(false);
|
||||||
|
setUser(
|
||||||
|
(prev) =>
|
||||||
|
({
|
||||||
|
...prev,
|
||||||
|
totpSecret: '',
|
||||||
|
backupCodes: [],
|
||||||
|
}) as TUser,
|
||||||
|
);
|
||||||
|
setPhase('setup');
|
||||||
|
setOtpauthUrl('');
|
||||||
|
},
|
||||||
|
onError: () =>
|
||||||
|
showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[disableToken, verify2FAMutate, disable2FAMutate, showToast, localize, setUser],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OGDialog
|
||||||
|
open={isDialogOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
resetState();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DisableTwoFactorToggle
|
||||||
|
enabled={Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0}
|
||||||
|
onChange={() => setDialogOpen(true)}
|
||||||
|
disabled={isVerifying || isDisabling || isGenerating}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OGDialogContent className="w-11/12 max-w-lg p-6">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={phase}
|
||||||
|
variants={phaseVariants}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit="exit"
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<OGDialogHeader>
|
||||||
|
<OGDialogTitle className="mb-2 flex items-center gap-3 text-2xl font-bold">
|
||||||
|
<SmartphoneIcon className="h-6 w-6 text-primary" />
|
||||||
|
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 ? localize('com_ui_2fa_disable') : localize('com_ui_2fa_setup')}
|
||||||
|
</OGDialogTitle>
|
||||||
|
{Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0 && phase !== 'disable' && (
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
<Progress
|
||||||
|
value={(steps.indexOf(phasesLabel[phase]) / (steps.length - 1)) * 100}
|
||||||
|
className="h-2 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
{steps.map((step, index) => (
|
||||||
|
<motion.span
|
||||||
|
key={step}
|
||||||
|
animate={{
|
||||||
|
color:
|
||||||
|
currentStep >= index ? 'var(--text-primary)' : 'var(--text-tertiary)',
|
||||||
|
}}
|
||||||
|
className="font-medium"
|
||||||
|
>
|
||||||
|
{step}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</OGDialogHeader>
|
||||||
|
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{phase === 'setup' && (
|
||||||
|
<SetupPhase
|
||||||
|
isGenerating={isGenerating}
|
||||||
|
onGenerate={handleGenerateQRCode}
|
||||||
|
onNext={() => setPhase('qr')}
|
||||||
|
onError={(error) => showToast({ message: error.message, status: 'error' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'qr' && (
|
||||||
|
<QRPhase
|
||||||
|
secret={secret}
|
||||||
|
otpauthUrl={otpauthUrl}
|
||||||
|
onNext={() => setPhase('verify')}
|
||||||
|
onError={(error) => showToast({ message: error.message, status: 'error' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'verify' && (
|
||||||
|
<VerifyPhase
|
||||||
|
token={verificationToken}
|
||||||
|
onTokenChange={setVerificationToken}
|
||||||
|
isVerifying={isVerifying}
|
||||||
|
onNext={handleVerify}
|
||||||
|
onError={(error) => showToast({ message: error.message, status: 'error' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'backup' && (
|
||||||
|
<BackupPhase
|
||||||
|
backupCodes={backupCodes}
|
||||||
|
onDownload={handleDownload}
|
||||||
|
downloaded={downloaded}
|
||||||
|
onNext={handleConfirm}
|
||||||
|
onError={(error) => showToast({ message: error.message, status: 'error' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'disable' && (
|
||||||
|
<DisablePhase
|
||||||
|
onDisable={handleDisableVerify}
|
||||||
|
isDisabling={isDisabling}
|
||||||
|
onError={(error) => showToast({ message: error.message, status: 'error' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</OGDialogContent>
|
||||||
|
</OGDialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(TwoFactorAuthentication);
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Download } from 'lucide-react';
|
||||||
|
import { Button, Label } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const fadeAnimation = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -20 },
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface BackupPhaseProps {
|
||||||
|
onNext: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
backupCodes: string[];
|
||||||
|
onDownload: () => void;
|
||||||
|
downloaded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackupPhase: React.FC<BackupPhaseProps> = ({
|
||||||
|
backupCodes,
|
||||||
|
onDownload,
|
||||||
|
downloaded,
|
||||||
|
onNext,
|
||||||
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div {...fadeAnimation} className="space-y-6">
|
||||||
|
<Label className="break-keep text-sm">{localize('com_ui_download_backup_tooltip')}</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-4 rounded-xl bg-surface-secondary p-6">
|
||||||
|
{backupCodes.map((code, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={code}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: index * 0.1 }}
|
||||||
|
className="rounded-lg bg-surface-tertiary p-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="hidden text-sm text-text-secondary sm:inline">#{index + 1}</span>
|
||||||
|
<span className="font-mono text-lg">{code}</span>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button variant="outline" onClick={onDownload} className="flex-1 gap-2">
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">{localize('com_ui_download_backup')}</span>
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onNext} disabled={!downloaded} className="flex-1">
|
||||||
|
{localize('com_ui_complete_setup')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { REGEXP_ONLY_DIGITS, REGEXP_ONLY_DIGITS_AND_CHARS } from 'input-otp';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
InputOTP,
|
||||||
|
InputOTPGroup,
|
||||||
|
InputOTPSlot,
|
||||||
|
InputOTPSeparator,
|
||||||
|
Spinner,
|
||||||
|
} from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const fadeAnimation = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -20 },
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DisablePhaseProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
onDisable: (token: string, useBackup: boolean) => void;
|
||||||
|
isDisabling: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DisablePhase: React.FC<DisablePhaseProps> = ({ onDisable, isDisabling }) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [token, setToken] = useState('');
|
||||||
|
const [useBackup, setUseBackup] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div {...fadeAnimation} className="space-y-8">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<InputOTP
|
||||||
|
value={token}
|
||||||
|
onChange={setToken}
|
||||||
|
maxLength={useBackup ? 8 : 6}
|
||||||
|
pattern={useBackup ? REGEXP_ONLY_DIGITS_AND_CHARS : REGEXP_ONLY_DIGITS}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{useBackup ? (
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
<InputOTPSlot index={6} />
|
||||||
|
<InputOTPSlot index={7} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={0} />
|
||||||
|
<InputOTPSlot index={1} />
|
||||||
|
<InputOTPSlot index={2} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
<InputOTPSeparator />
|
||||||
|
<InputOTPGroup>
|
||||||
|
<InputOTPSlot index={3} />
|
||||||
|
<InputOTPSlot index={4} />
|
||||||
|
<InputOTPSlot index={5} />
|
||||||
|
</InputOTPGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</InputOTP>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => onDisable(token, useBackup)}
|
||||||
|
disabled={isDisabling || token.length !== (useBackup ? 8 : 6)}
|
||||||
|
className="w-full rounded-xl px-6 py-3 transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isDisabling === true && <Spinner className="mr-2" />}
|
||||||
|
{isDisabling ? localize('com_ui_disabling') : localize('com_ui_2fa_disable')}
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
onClick={() => setUseBackup(!useBackup)}
|
||||||
|
className="text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
|
{useBackup ? localize('com_ui_use_2fa_code') : localize('com_ui_use_backup_code')}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { Copy, Check } from 'lucide-react';
|
||||||
|
import { Input, Button, Label } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
const fadeAnimation = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -20 },
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface QRPhaseProps {
|
||||||
|
secret: string;
|
||||||
|
otpauthUrl: string;
|
||||||
|
onNext: () => void;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QRPhase: React.FC<QRPhaseProps> = ({ secret, otpauthUrl, onNext }) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
const [isCopying, setIsCopying] = useState(false);
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
await navigator.clipboard.writeText(secret);
|
||||||
|
setIsCopying(true);
|
||||||
|
setTimeout(() => setIsCopying(false), 2000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div {...fadeAnimation} className="space-y-6">
|
||||||
|
<div className="flex flex-col items-center space-y-6">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
className="rounded-2xl bg-white p-4 shadow-lg"
|
||||||
|
>
|
||||||
|
<QRCodeSVG value={otpauthUrl} size={240} />
|
||||||
|
</motion.div>
|
||||||
|
<div className="w-full space-y-3">
|
||||||
|
<Label className="text-sm font-medium text-text-secondary">
|
||||||
|
{localize('com_ui_secret_key')}
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input value={secret} readOnly className="font-mono text-lg tracking-wider" />
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCopy}
|
||||||
|
className={cn('h-auto shrink-0', isCopying ? 'cursor-default' : '')}
|
||||||
|
>
|
||||||
|
{isCopying ? <Check className="size-4" /> : <Copy className="size-4" />}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={onNext} className="w-full">
|
||||||
|
{localize('com_ui_continue')}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { QrCode } from 'lucide-react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Button, Spinner } from '~/components';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const fadeAnimation = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -20 },
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SetupPhaseProps {
|
||||||
|
onNext: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
isGenerating: boolean;
|
||||||
|
onGenerate: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SetupPhase: React.FC<SetupPhaseProps> = ({ isGenerating, onGenerate, onNext }) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div {...fadeAnimation} className="space-y-6">
|
||||||
|
<div className="rounded-xl bg-surface-secondary p-6">
|
||||||
|
<h3 className="mb-4 flex justify-center text-lg font-medium">
|
||||||
|
{localize('com_ui_2fa_account_security')}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={onGenerate}
|
||||||
|
className="flex w-full"
|
||||||
|
disabled={isGenerating}
|
||||||
|
>
|
||||||
|
{isGenerating ? <Spinner className="size-5" /> : <QrCode className="size-5" />}
|
||||||
|
{isGenerating ? localize('com_ui_generating') : localize('com_ui_generate_qrcode')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import { Button, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '~/components';
|
||||||
|
import { REGEXP_ONLY_DIGITS } from 'input-otp';
|
||||||
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
|
const fadeAnimation = {
|
||||||
|
initial: { opacity: 0, y: 20 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
exit: { opacity: 0, y: -20 },
|
||||||
|
transition: { duration: 0.2 },
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VerifyPhaseProps {
|
||||||
|
token: string;
|
||||||
|
onTokenChange: (value: string) => void;
|
||||||
|
isVerifying: boolean;
|
||||||
|
onNext: () => void;
|
||||||
|
onError: (error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VerifyPhase: React.FC<VerifyPhaseProps> = ({
|
||||||
|
token,
|
||||||
|
onTokenChange,
|
||||||
|
isVerifying,
|
||||||
|
onNext,
|
||||||
|
}) => {
|
||||||
|
const localize = useLocalize();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div {...fadeAnimation} className="space-y-8">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<InputOTP
|
||||||
|
value={token}
|
||||||
|
onChange={onTokenChange}
|
||||||
|
maxLength={6}
|
||||||
|
pattern={REGEXP_ONLY_DIGITS}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<InputOTPGroup>
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<InputOTPSlot key={i} index={i} />
|
||||||
|
))}
|
||||||
|
</InputOTPGroup>
|
||||||
|
<InputOTPSeparator />
|
||||||
|
<InputOTPGroup>
|
||||||
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<InputOTPSlot key={i + 3} index={i + 3} />
|
||||||
|
))}
|
||||||
|
</InputOTPGroup>
|
||||||
|
</InputOTP>
|
||||||
|
</div>
|
||||||
|
<Button onClick={onNext} disabled={isVerifying || token.length !== 6} className="w-full">
|
||||||
|
{localize('com_ui_verify')}
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './BackupPhase';
|
||||||
|
export * from './QRPhase';
|
||||||
|
export * from './VerifyPhase';
|
||||||
|
export * from './SetupPhase';
|
||||||
|
export * from './DisablePhase';
|
||||||
|
|
@ -82,7 +82,7 @@ function ImportConversations() {
|
||||||
onClick={handleImportClick}
|
onClick={handleImportClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
disabled={!allowImport}
|
disabled={!allowImport}
|
||||||
aria-label={localize('com_ui_import_conversation')}
|
aria-label={localize('com_ui_import')}
|
||||||
className="btn btn-neutral relative"
|
className="btn btn-neutral relative"
|
||||||
>
|
>
|
||||||
{allowImport ? (
|
{allowImport ? (
|
||||||
|
|
@ -90,7 +90,7 @@ function ImportConversations() {
|
||||||
) : (
|
) : (
|
||||||
<Spinner className="mr-1 w-4" />
|
<Spinner className="mr-1 w-4" />
|
||||||
)}
|
)}
|
||||||
<span>{localize('com_ui_import_conversation')}</span>
|
<span>{localize('com_ui_import')}</span>
|
||||||
</button>
|
</button>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
|
|
|
||||||
|
|
@ -270,9 +270,7 @@ export default function SharedLinks() {
|
||||||
|
|
||||||
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
<OGDialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
<OGDialogTrigger asChild onClick={() => setIsOpen(true)}>
|
||||||
<button className="btn btn-neutral relative">
|
<Button variant="outline">{localize('com_ui_manage')}</Button>
|
||||||
{localize('com_nav_shared_links_manage')}
|
|
||||||
</button>
|
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
|
|
||||||
<OGDialogContent
|
<OGDialogContent
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default function ArchivedChats() {
|
||||||
<OGDialog>
|
<OGDialog>
|
||||||
<OGDialogTrigger asChild>
|
<OGDialogTrigger asChild>
|
||||||
<Button variant="outline" aria-label="Archived chats">
|
<Button variant="outline" aria-label="Archived chats">
|
||||||
{localize('com_nav_archived_chats_manage')}
|
{localize('com_ui_manage')}
|
||||||
</Button>
|
</Button>
|
||||||
</OGDialogTrigger>
|
</OGDialogTrigger>
|
||||||
<OGDialogTemplate
|
<OGDialogTemplate
|
||||||
|
|
|
||||||
|
|
@ -51,15 +51,17 @@ export const LangSelector = ({
|
||||||
const languageOptions = [
|
const languageOptions = [
|
||||||
{ value: 'auto', label: localize('com_nav_lang_auto') },
|
{ value: 'auto', label: localize('com_nav_lang_auto') },
|
||||||
{ value: 'en-US', label: localize('com_nav_lang_english') },
|
{ value: 'en-US', label: localize('com_nav_lang_english') },
|
||||||
{ value: 'zh-CN', label: localize('com_nav_lang_chinese') },
|
{ value: 'zh-Hans', label: localize('com_nav_lang_chinese') },
|
||||||
{ value: 'zh-TW', label: localize('com_nav_lang_traditionalchinese') },
|
{ value: 'zh-Hant', label: localize('com_nav_lang_traditional_chinese') },
|
||||||
{ value: 'ar-EG', label: localize('com_nav_lang_arabic') },
|
{ value: 'ar-EG', label: localize('com_nav_lang_arabic') },
|
||||||
{ value: 'de-DE', label: localize('com_nav_lang_german') },
|
{ value: 'de-DE', label: localize('com_nav_lang_german') },
|
||||||
{ value: 'es-ES', label: localize('com_nav_lang_spanish') },
|
{ value: 'es-ES', label: localize('com_nav_lang_spanish') },
|
||||||
|
{ value: 'et-EE', label: localize('com_nav_lang_estonian') },
|
||||||
{ value: 'fr-FR', label: localize('com_nav_lang_french') },
|
{ value: 'fr-FR', label: localize('com_nav_lang_french') },
|
||||||
{ value: 'it-IT', label: localize('com_nav_lang_italian') },
|
{ value: 'it-IT', label: localize('com_nav_lang_italian') },
|
||||||
{ value: 'pl-PL', label: localize('com_nav_lang_polish') },
|
{ value: 'pl-PL', label: localize('com_nav_lang_polish') },
|
||||||
{ value: 'pt-BR', label: localize('com_nav_lang_brazilian_portuguese') },
|
{ value: 'pt-BR', label: localize('com_nav_lang_brazilian_portuguese') },
|
||||||
|
{ value: 'pt-PT', label: localize('com_nav_lang_portuguese') },
|
||||||
{ value: 'ru-RU', label: localize('com_nav_lang_russian') },
|
{ value: 'ru-RU', label: localize('com_nav_lang_russian') },
|
||||||
{ value: 'ja-JP', label: localize('com_nav_lang_japanese') },
|
{ value: 'ja-JP', label: localize('com_nav_lang_japanese') },
|
||||||
{ value: 'sv-SE', label: localize('com_nav_lang_swedish') },
|
{ value: 'sv-SE', label: localize('com_nav_lang_swedish') },
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ const Command = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light shadow-md">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<SquareSlash className="icon-sm" aria-hidden="true" />
|
<SquareSlash className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const Description = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light">
|
<div className="rounded-xl border border-border-light shadow-md">
|
||||||
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
<h3 className="flex h-10 items-center gap-1 pl-4 text-sm text-text-secondary">
|
||||||
<Info className="icon-sm" aria-hidden="true" />
|
<Info className="icon-sm" aria-hidden="true" />
|
||||||
<Input
|
<Input
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export default function List({
|
||||||
<div className="flex w-full justify-end">
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full bg-transparent px-3"
|
className={`w-full bg-transparent ${isChatRoute ? '' : 'mx-2'}`}
|
||||||
onClick={() => navigate('/d/prompts/new')}
|
onClick={() => navigate('/d/prompts/new')}
|
||||||
>
|
>
|
||||||
<Plus className="size-4" aria-hidden />
|
<Plus className="size-4" aria-hidden />
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 transition-all duration-150 sm:p-4',
|
'w-full flex-1 overflow-auto rounded-b-xl border border-border-light p-2 shadow-md transition-all duration-150 sm:p-4',
|
||||||
{
|
{
|
||||||
'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary':
|
'cursor-pointer bg-surface-primary hover:bg-surface-secondary active:bg-surface-tertiary':
|
||||||
!isEditing,
|
!isEditing,
|
||||||
|
|
@ -105,6 +105,7 @@ const PromptEditor: React.FC<Props> = ({ name, isEditing, setIsEditing }) => {
|
||||||
isEditing ? (
|
isEditing ? (
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
{...field}
|
{...field}
|
||||||
|
autoFocus
|
||||||
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
className="w-full resize-none overflow-y-auto rounded bg-transparent text-sm text-text-primary focus:outline-none sm:text-base"
|
||||||
minRows={3}
|
minRows={3}
|
||||||
maxRows={14}
|
maxRows={14}
|
||||||
|
|
|
||||||
|
|
@ -237,7 +237,6 @@ const PromptForm = () => {
|
||||||
payload: { name: groupName, category: value },
|
payload: { name: groupName, category: value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full"
|
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
<div className="mt-2 flex flex-row items-center justify-center gap-x-2 lg:mt-0">
|
||||||
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
{hasShareAccess && <SharePrompt group={group} disabled={isLoadingGroup} />}
|
||||||
|
|
@ -349,7 +348,7 @@ const PromptForm = () => {
|
||||||
{isLoadingPrompts ? (
|
{isLoadingPrompts ? (
|
||||||
<Skeleton className="h-96" aria-live="polite" />
|
<Skeleton className="h-96" aria-live="polite" />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full flex-col gap-4">
|
<div className="mb-2 flex h-full flex-col gap-4">
|
||||||
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
<PromptEditor name="prompt" isEditing={isEditing} setIsEditing={setIsEditing} />
|
||||||
<PromptVariables promptText={promptText} />
|
<PromptVariables promptText={promptText} />
|
||||||
<Description
|
<Description
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ const PromptVariables = ({
|
||||||
}, [promptText]);
|
}, [promptText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md ">
|
<div className="rounded-xl border border-border-light bg-transparent p-4 shadow-md">
|
||||||
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
<h3 className="flex items-center gap-2 py-2 text-lg font-semibold text-text-primary">
|
||||||
<Variable className="icon-sm" aria-hidden="true" />
|
<Variable className="icon-sm" aria-hidden="true" />
|
||||||
{localize('com_ui_variables')}
|
{localize('com_ui_variables')}
|
||||||
|
|
@ -71,7 +71,7 @@ const PromptVariables = ({
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-text-text-primary text-sm font-medium">
|
<span className="text-sm font-medium text-text-primary">
|
||||||
{localize('com_ui_dropdown_variables')}
|
{localize('com_ui_dropdown_variables')}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-text-secondary">
|
<span className="text-sm text-text-secondary">
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ export default function AgentSwitcher({ isCollapsed }: SwitcherProps) {
|
||||||
ariaLabel={'agent'}
|
ariaLabel={'agent'}
|
||||||
setValue={onSelect}
|
setValue={onSelect}
|
||||||
items={agentOptions}
|
items={agentOptions}
|
||||||
|
iconClassName="assistant-item"
|
||||||
SelectIcon={
|
SelectIcon={
|
||||||
<Icon
|
<Icon
|
||||||
isCreatedByUser={false}
|
isCreatedByUser={false}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
import React, { useMemo, useCallback } from 'react';
|
import React, { useMemo, useCallback } from 'react';
|
||||||
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
|
||||||
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
|
import { Controller, useWatch, useForm, FormProvider } from 'react-hook-form';
|
||||||
|
|
@ -211,34 +212,54 @@ export default function AgentPanel({
|
||||||
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
|
className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden"
|
||||||
aria-label="Agent configuration form"
|
aria-label="Agent configuration form"
|
||||||
>
|
>
|
||||||
<div className="mt-2 flex w-full flex-wrap gap-2">
|
<div className="mx-1 mt-2 flex w-full flex-wrap gap-2">
|
||||||
<Controller
|
<div className="w-full">
|
||||||
name="agent"
|
<Controller
|
||||||
control={control}
|
name="agent"
|
||||||
render={({ field }) => (
|
control={control}
|
||||||
<AgentSelect
|
render={({ field }) => (
|
||||||
reset={reset}
|
<AgentSelect
|
||||||
value={field.value}
|
reset={reset}
|
||||||
agentQuery={agentQuery}
|
value={field.value}
|
||||||
setCurrentAgentId={setCurrentAgentId}
|
agentQuery={agentQuery}
|
||||||
selectedAgentId={current_agent_id ?? null}
|
setCurrentAgentId={setCurrentAgentId}
|
||||||
createMutation={create}
|
selectedAgentId={current_agent_id ?? null}
|
||||||
/>
|
createMutation={create}
|
||||||
)}
|
/>
|
||||||
/>
|
)}
|
||||||
{/* Select Button */}
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Create + Select Button */}
|
||||||
{agent_id && (
|
{agent_id && (
|
||||||
<Button
|
<div className="flex w-full gap-2">
|
||||||
variant="submit"
|
<Button
|
||||||
disabled={!agent_id}
|
type="button"
|
||||||
onClick={(e) => {
|
variant="outline"
|
||||||
e.preventDefault();
|
className="w-full justify-center"
|
||||||
handleSelectAgent();
|
onClick={() => {
|
||||||
}}
|
reset(defaultAgentFormValues);
|
||||||
aria-label="Select agent"
|
setCurrentAgentId(undefined);
|
||||||
>
|
}}
|
||||||
{localize('com_ui_select')}
|
>
|
||||||
</Button>
|
<Plus className="mr-1 h-4 w-4" />
|
||||||
|
{localize('com_ui_create') +
|
||||||
|
' ' +
|
||||||
|
localize('com_ui_new') +
|
||||||
|
' ' +
|
||||||
|
localize('com_ui_agent')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="submit"
|
||||||
|
disabled={!agent_id}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelectAgent();
|
||||||
|
}}
|
||||||
|
aria-label={localize('com_ui_select') + ' ' + localize('com_ui_agent')}
|
||||||
|
>
|
||||||
|
{localize('com_ui_select')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!canEditAgent && (
|
{!canEditAgent && (
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,16 @@ import { Skeleton } from '~/components/ui';
|
||||||
export default function AgentPanelSkeleton() {
|
export default function AgentPanelSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
|
<div className="scrollbar-gutter-stable h-auto w-full flex-shrink-0 overflow-x-hidden">
|
||||||
{/* Agent Select and Button */}
|
<div className="mx-1 mt-2 flex w-full flex-wrap gap-2">
|
||||||
<div className="mt-1 flex w-full gap-2">
|
{/* Agent Select Dropdown */}
|
||||||
<Skeleton className="h-[40px] w-4/5 rounded-lg" />
|
<div className="w-full">
|
||||||
<Skeleton className="h-[40px] w-1/5 rounded-lg" />
|
<Skeleton className="h-[40px] w-full rounded-md" />
|
||||||
|
</div>
|
||||||
|
{/* Create and Select Buttons */}
|
||||||
|
<div className="flex w-full gap-2">
|
||||||
|
<Skeleton className="h-[40px] w-3/4 rounded-md" /> {/* Create Button */}
|
||||||
|
<Skeleton className="h-[40px] w-1/4 rounded-md" /> {/* Select Button */}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
|
<div className="h-auto bg-white px-4 pb-8 pt-3 dark:bg-transparent">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { Plus, EarthIcon } from 'lucide-react';
|
import { EarthIcon } from 'lucide-react';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
|
||||||
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
|
||||||
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
|
||||||
import type { UseFormReset } from 'react-hook-form';
|
import type { UseFormReset } from 'react-hook-form';
|
||||||
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
|
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
|
||||||
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
|
|
||||||
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
|
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
|
||||||
import SelectDropDown from '~/components/ui/SelectDropDown';
|
import { cn, createProviderOption, processAgentOption } from '~/utils';
|
||||||
|
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
|
|
||||||
const keys = new Set(Object.keys(defaultAgentFormValues));
|
const keys = new Set(Object.keys(defaultAgentFormValues));
|
||||||
|
const SELECT_ID = 'agent-builder-combobox';
|
||||||
|
|
||||||
export default function AgentSelect({
|
export default function AgentSelect({
|
||||||
reset,
|
reset,
|
||||||
|
|
@ -120,6 +121,9 @@ export default function AgentSelect({
|
||||||
}
|
}
|
||||||
|
|
||||||
resetAgentForm(agent);
|
resetAgentForm(agent);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById(SELECT_ID)?.focus();
|
||||||
|
}, 5);
|
||||||
},
|
},
|
||||||
[agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset],
|
[agents, createMutation, setCurrentAgentId, agentQuery.data, resetAgentForm, reset],
|
||||||
);
|
);
|
||||||
|
|
@ -152,51 +156,36 @@ export default function AgentSelect({
|
||||||
}, [selectedAgentId, agents, onSelect]);
|
}, [selectedAgentId, agents, onSelect]);
|
||||||
|
|
||||||
const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent');
|
const createAgent = localize('com_ui_create') + ' ' + localize('com_ui_agent');
|
||||||
const hasAgentValue = !!(typeof currentAgentValue === 'object'
|
|
||||||
? currentAgentValue.value != null && currentAgentValue.value !== ''
|
|
||||||
: typeof currentAgentValue !== 'undefined');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectDropDown
|
<ControlCombobox
|
||||||
value={!hasAgentValue ? createAgent : (currentAgentValue as TAgentOption)}
|
selectId={SELECT_ID}
|
||||||
setValue={createDropdownSetter(onSelect)}
|
containerClassName="px-0"
|
||||||
availableValues={
|
selectedValue={(currentAgentValue?.value ?? '') + ''}
|
||||||
agents ?? [
|
displayValue={currentAgentValue?.label ?? ''}
|
||||||
|
selectPlaceholder={createAgent}
|
||||||
|
iconSide="right"
|
||||||
|
searchPlaceholder={localize('com_agents_search_name')}
|
||||||
|
SelectIcon={currentAgentValue?.icon}
|
||||||
|
setValue={onSelect}
|
||||||
|
items={
|
||||||
|
agents?.map((agent) => ({
|
||||||
|
label: agent.name ?? '',
|
||||||
|
value: agent.id ?? '',
|
||||||
|
icon: agent.icon,
|
||||||
|
})) ?? [
|
||||||
{
|
{
|
||||||
label: 'Loading...',
|
label: 'Loading...',
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
iconSide="left"
|
|
||||||
optionIconSide="right"
|
|
||||||
showAbove={false}
|
|
||||||
showLabel={false}
|
|
||||||
emptyTitle={true}
|
|
||||||
showOptionIcon={true}
|
|
||||||
containerClassName="flex-grow"
|
|
||||||
searchClassName="dark:from-gray-850"
|
|
||||||
searchPlaceholder={localize('com_agents_search_name')}
|
|
||||||
optionsClass="hover:bg-gray-20/50 dark:border-gray-700"
|
|
||||||
optionsListClass="rounded-lg shadow-lg dark:bg-gray-850 dark:border-gray-700 dark:last:border"
|
|
||||||
currentValueClass={cn(
|
|
||||||
'text-md font-semibold text-gray-900 dark:text-white',
|
|
||||||
hasAgentValue ? 'text-gray-500' : '',
|
|
||||||
)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-md dark:border-gray-700 dark:bg-gray-850',
|
'z-50 flex h-[40px] w-full flex-none items-center justify-center truncate rounded-md bg-transparent font-bold',
|
||||||
'z-50 flex h-[40px] w-full flex-none items-center justify-center truncate px-4 hover:cursor-pointer hover:border-green-500 focus:border-gray-400',
|
|
||||||
)}
|
|
||||||
renderOption={() => (
|
|
||||||
<span className="flex items-center gap-1.5 truncate">
|
|
||||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2 text-gray-800 dark:text-gray-100">
|
|
||||||
<Plus className="w-[16px]" />
|
|
||||||
</span>
|
|
||||||
<span className={cn('ml-4 flex h-6 items-center gap-1 text-gray-800 dark:text-gray-100')}>
|
|
||||||
{createAgent}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
|
ariaLabel={localize('com_ui_agent')}
|
||||||
|
isCollapsed={false}
|
||||||
|
showCarat={true}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
import { ChevronLeft, RotateCcw } from 'lucide-react';
|
||||||
import { getSettingsKeys } from 'librechat-data-provider';
|
|
||||||
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
import { useFormContext, useWatch, Controller } from 'react-hook-form';
|
||||||
|
import { getSettingsKeys, alternateName } from 'librechat-data-provider';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
|
||||||
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
import { componentMapping } from '~/components/SidePanel/Parameters/components';
|
||||||
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
|
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
|
||||||
import { getEndpointField, cn, cardStyle } from '~/utils';
|
import ControlCombobox from '~/components/ui/ControlCombobox';
|
||||||
import { useGetEndpointsQuery } from '~/data-provider';
|
import { useGetEndpointsQuery } from '~/data-provider';
|
||||||
import { SelectDropDown } from '~/components/ui';
|
import { getEndpointField, cn } from '~/utils';
|
||||||
import { useLocalize } from '~/hooks';
|
import { useLocalize } from '~/hooks';
|
||||||
import { Panel } from '~/common';
|
import { Panel } from '~/common';
|
||||||
|
|
||||||
|
|
@ -33,7 +33,7 @@ export default function Parameters({
|
||||||
return value ?? '';
|
return value ?? '';
|
||||||
}, [providerOption]);
|
}, [providerOption]);
|
||||||
const models = useMemo(
|
const models = useMemo(
|
||||||
() => (provider ? modelsData[provider] ?? [] : []),
|
() => (provider ? (modelsData[provider] ?? []) : []),
|
||||||
[modelsData, provider],
|
[modelsData, provider],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -78,8 +78,8 @@ export default function Parameters({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
|
<div className="scrollbar-gutter-stable h-full min-h-[50vh] overflow-auto pb-12 text-sm">
|
||||||
<div className="model-panel relative flex flex-col items-center px-16 py-6 text-center">
|
<div className="model-panel relative flex flex-col items-center px-16 py-4 text-center">
|
||||||
<div className="absolute left-0 top-6">
|
<div className="absolute left-0 top-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-neutral relative"
|
className="btn btn-neutral relative"
|
||||||
|
|
@ -99,6 +99,7 @@ export default function Parameters({
|
||||||
{/* Endpoint aka Provider for Agents */}
|
{/* Endpoint aka Provider for Agents */}
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label
|
<label
|
||||||
|
id="provider-label"
|
||||||
className="text-token-text-primary model-panel-label mb-2 block font-medium"
|
className="text-token-text-primary model-panel-label mb-2 block font-medium"
|
||||||
htmlFor="provider"
|
htmlFor="provider"
|
||||||
>
|
>
|
||||||
|
|
@ -108,38 +109,47 @@ export default function Parameters({
|
||||||
name="provider"
|
name="provider"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true, minLength: 1 }}
|
rules={{ required: true, minLength: 1 }}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<>
|
const value =
|
||||||
<SelectDropDown
|
typeof field.value === 'string'
|
||||||
emptyTitle={true}
|
? field.value
|
||||||
value={field.value ?? ''}
|
: ((field.value as StringOption)?.value ?? '');
|
||||||
title={localize('com_ui_provider')}
|
const display =
|
||||||
placeholder={localize('com_ui_select_provider')}
|
typeof field.value === 'string'
|
||||||
searchPlaceholder={localize('com_ui_select_search_provider')}
|
? field.value
|
||||||
setValue={field.onChange}
|
: ((field.value as StringOption)?.label ?? '');
|
||||||
availableValues={providers}
|
|
||||||
showAbove={false}
|
return (
|
||||||
showLabel={false}
|
<>
|
||||||
className={cn(
|
<ControlCombobox
|
||||||
cardStyle,
|
selectedValue={value}
|
||||||
'flex h-9 w-full flex-none items-center justify-center border-none px-4 hover:cursor-pointer',
|
displayValue={alternateName[display] ?? display}
|
||||||
(field.value === undefined || field.value === '') &&
|
selectPlaceholder={localize('com_ui_select_provider')}
|
||||||
'border-2 border-yellow-400',
|
searchPlaceholder={localize('com_ui_select_search_provider')}
|
||||||
|
setValue={field.onChange}
|
||||||
|
items={providers.map((provider) => ({
|
||||||
|
label: typeof provider === 'string' ? provider : provider.label,
|
||||||
|
value: typeof provider === 'string' ? provider : provider.value,
|
||||||
|
}))}
|
||||||
|
className={cn(error ? 'border-2 border-red-500' : '')}
|
||||||
|
ariaLabel={localize('com_ui_provider')}
|
||||||
|
isCollapsed={false}
|
||||||
|
showCarat={true}
|
||||||
|
/>
|
||||||
|
{error && (
|
||||||
|
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
|
</>
|
||||||
/>
|
);
|
||||||
{error && (
|
}}
|
||||||
<span className="model-panel-error text-sm text-red-500 transition duration-300 ease-in-out">
|
|
||||||
{localize('com_ui_field_required')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Model */}
|
{/* Model */}
|
||||||
<div className="model-panel-section mb-4">
|
<div className="model-panel-section mb-4">
|
||||||
<label
|
<label
|
||||||
|
id="model-label"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-token-text-primary model-panel-label mb-2 block font-medium',
|
'text-token-text-primary model-panel-label mb-2 block font-medium',
|
||||||
!provider && 'text-gray-500 dark:text-gray-400',
|
!provider && 'text-gray-500 dark:text-gray-400',
|
||||||
|
|
@ -152,35 +162,36 @@ export default function Parameters({
|
||||||
name="model"
|
name="model"
|
||||||
control={control}
|
control={control}
|
||||||
rules={{ required: true, minLength: 1 }}
|
rules={{ required: true, minLength: 1 }}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => {
|
||||||
<>
|
return (
|
||||||
<SelectDropDown
|
<>
|
||||||
emptyTitle={true}
|
<ControlCombobox
|
||||||
placeholder={
|
selectedValue={field.value || ''}
|
||||||
provider
|
selectPlaceholder={
|
||||||
? localize('com_ui_select_model')
|
provider
|
||||||
: localize('com_ui_select_provider_first')
|
? localize('com_ui_select_model')
|
||||||
}
|
: localize('com_ui_select_provider_first')
|
||||||
value={field.value}
|
}
|
||||||
setValue={field.onChange}
|
searchPlaceholder={localize('com_ui_select_model')}
|
||||||
availableValues={models}
|
setValue={field.onChange}
|
||||||
showAbove={false}
|
items={models.map((model) => ({
|
||||||
showLabel={false}
|
label: model,
|
||||||
disabled={!provider}
|
value: model,
|
||||||
className={cn(
|
}))}
|
||||||
cardStyle,
|
disabled={!provider}
|
||||||
'flex h-[40px] w-full flex-none items-center justify-center border-none px-4',
|
className={cn('disabled:opacity-50', error ? 'border-2 border-red-500' : '')}
|
||||||
!provider ? 'cursor-not-allowed bg-gray-200' : 'hover:cursor-pointer',
|
ariaLabel={localize('com_ui_model')}
|
||||||
|
isCollapsed={false}
|
||||||
|
showCarat={true}
|
||||||
|
/>
|
||||||
|
{provider && error && (
|
||||||
|
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
||||||
|
{localize('com_ui_field_required')}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
containerClassName={cn('rounded-md', error ? 'border-red-500 border-2' : '')}
|
</>
|
||||||
/>
|
);
|
||||||
{provider && error && (
|
}}
|
||||||
<span className="text-sm text-red-500 transition duration-300 ease-in-out">
|
|
||||||
{localize('com_ui_field_required')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -188,7 +199,6 @@ export default function Parameters({
|
||||||
{parameters && (
|
{parameters && (
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
<div className="h-auto max-w-full overflow-x-hidden p-2">
|
||||||
<div className="grid grid-cols-4 gap-6">
|
<div className="grid grid-cols-4 gap-6">
|
||||||
{' '}
|
|
||||||
{/* This is the parent element containing all settings */}
|
{/* This is the parent element containing all settings */}
|
||||||
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
{/* Below is an example of an applied dynamic setting, each be contained by a div with the column span specified */}
|
||||||
{parameters.map((setting) => {
|
{parameters.map((setting) => {
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ export default function AssistantSwitcher({ isCollapsed }: SwitcherProps) {
|
||||||
ariaLabel={'assistant'}
|
ariaLabel={'assistant'}
|
||||||
setValue={onSelect}
|
setValue={onSelect}
|
||||||
items={assistantOptions}
|
items={assistantOptions}
|
||||||
|
iconClassName="assistant-item"
|
||||||
SelectIcon={
|
SelectIcon={
|
||||||
<Icon
|
<Icon
|
||||||
isCreatedByUser={false}
|
isCreatedByUser={false}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import * as Ariakit from '@ariakit/react';
|
import * as Ariakit from '@ariakit/react';
|
||||||
import { matchSorter } from 'match-sorter';
|
import { matchSorter } from 'match-sorter';
|
||||||
import { AutoSizer, List } from 'react-virtualized';
|
import { Search, ChevronDown } from 'lucide-react';
|
||||||
import { startTransition, useMemo, useState, useEffect, useRef, memo } from 'react';
|
import { useMemo, useState, useRef, memo, useEffect } from 'react';
|
||||||
import { cn } from '~/utils';
|
import { SelectRenderer } from '@ariakit/react-core/select/select-renderer';
|
||||||
import type { OptionWithIcon } from '~/common';
|
import type { OptionWithIcon } from '~/common';
|
||||||
import { Search } from 'lucide-react';
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
interface ControlComboboxProps {
|
interface ControlComboboxProps {
|
||||||
selectedValue: string;
|
selectedValue: string;
|
||||||
|
|
@ -16,6 +16,13 @@ interface ControlComboboxProps {
|
||||||
selectPlaceholder?: string;
|
selectPlaceholder?: string;
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
SelectIcon?: React.ReactNode;
|
SelectIcon?: React.ReactNode;
|
||||||
|
containerClassName?: string;
|
||||||
|
iconClassName?: string;
|
||||||
|
showCarat?: boolean;
|
||||||
|
className?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
iconSide?: 'left' | 'right';
|
||||||
|
selectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROW_HEIGHT = 36;
|
const ROW_HEIGHT = 36;
|
||||||
|
|
@ -28,18 +35,47 @@ function ControlCombobox({
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
selectPlaceholder,
|
selectPlaceholder,
|
||||||
|
containerClassName,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
SelectIcon,
|
SelectIcon,
|
||||||
|
showCarat,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
iconClassName,
|
||||||
|
iconSide = 'left',
|
||||||
|
selectId,
|
||||||
}: ControlComboboxProps) {
|
}: ControlComboboxProps) {
|
||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
const [buttonWidth, setButtonWidth] = useState<number | null>(null);
|
const [buttonWidth, setButtonWidth] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const getItem = (option: OptionWithIcon) => ({
|
||||||
|
id: `item-${option.value}`,
|
||||||
|
value: option.value as string | undefined,
|
||||||
|
label: option.label,
|
||||||
|
icon: option.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
const combobox = Ariakit.useComboboxStore({
|
||||||
|
defaultItems: items.map(getItem),
|
||||||
|
resetValueOnHide: true,
|
||||||
|
value: searchValue,
|
||||||
|
setValue: setSearchValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const select = Ariakit.useSelectStore({
|
||||||
|
combobox,
|
||||||
|
defaultItems: items.map(getItem),
|
||||||
|
value: selectedValue,
|
||||||
|
setValue,
|
||||||
|
});
|
||||||
|
|
||||||
const matches = useMemo(() => {
|
const matches = useMemo(() => {
|
||||||
return matchSorter(items, searchValue, {
|
const filteredItems = matchSorter(items, searchValue, {
|
||||||
keys: ['value', 'label'],
|
keys: ['value', 'label'],
|
||||||
baseSort: (a, b) => (a.index < b.index ? -1 : 1),
|
baseSort: (a, b) => (a.index < b.index ? -1 : 1),
|
||||||
});
|
});
|
||||||
|
return filteredItems.map(getItem);
|
||||||
}, [searchValue, items]);
|
}, [searchValue, items]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -48,104 +84,95 @@ function ControlCombobox({
|
||||||
}
|
}
|
||||||
}, [isCollapsed]);
|
}, [isCollapsed]);
|
||||||
|
|
||||||
const rowRenderer = ({
|
const selectIconClassName = cn(
|
||||||
index,
|
'flex h-5 w-5 items-center justify-center overflow-hidden rounded-full',
|
||||||
key,
|
iconClassName,
|
||||||
style,
|
);
|
||||||
}: {
|
const optionIconClassName = cn(
|
||||||
index: number;
|
'mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full',
|
||||||
key: string;
|
iconClassName,
|
||||||
style: React.CSSProperties;
|
);
|
||||||
}) => {
|
|
||||||
const item = matches[index];
|
|
||||||
return (
|
|
||||||
<Ariakit.SelectItem
|
|
||||||
key={key}
|
|
||||||
value={`${item.value ?? ''}`}
|
|
||||||
aria-label={`${item.label ?? item.value ?? ''}`}
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-pointer items-center px-3 text-sm',
|
|
||||||
'text-text-primary hover:bg-surface-tertiary',
|
|
||||||
'data-[active-item]:bg-surface-tertiary',
|
|
||||||
)}
|
|
||||||
render={<Ariakit.ComboboxItem />}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
{item.icon != null && (
|
|
||||||
<div className="assistant-item mr-2 flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
|
|
||||||
{item.icon}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="flex-grow truncate text-left">{item.label}</span>
|
|
||||||
</Ariakit.SelectItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-center px-1">
|
<div className={cn('flex w-full items-center justify-center px-1', containerClassName)}>
|
||||||
<Ariakit.ComboboxProvider
|
<Ariakit.SelectLabel store={select} className="sr-only">
|
||||||
resetValueOnHide
|
{ariaLabel}
|
||||||
setValue={(value) => {
|
</Ariakit.SelectLabel>
|
||||||
startTransition(() => {
|
<Ariakit.Select
|
||||||
setSearchValue(value);
|
ref={buttonRef}
|
||||||
});
|
store={select}
|
||||||
}}
|
id={selectId}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center justify-center gap-2 rounded-full bg-surface-secondary',
|
||||||
|
'text-text-primary hover:bg-surface-tertiary',
|
||||||
|
'border border-border-light',
|
||||||
|
isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Ariakit.SelectProvider value={selectedValue} setValue={setValue}>
|
{SelectIcon != null && iconSide === 'left' && (
|
||||||
<Ariakit.SelectLabel className="sr-only">{ariaLabel}</Ariakit.SelectLabel>
|
<div className={selectIconClassName}>{SelectIcon}</div>
|
||||||
<Ariakit.Select
|
)}
|
||||||
ref={buttonRef}
|
{!isCollapsed && (
|
||||||
className={cn(
|
<>
|
||||||
'flex items-center justify-center gap-2 rounded-full bg-surface-secondary',
|
<span className="flex-grow truncate text-left">
|
||||||
'text-text-primary hover:bg-surface-tertiary',
|
{displayValue != null
|
||||||
'border border-border-light',
|
? displayValue || selectPlaceholder
|
||||||
isCollapsed ? 'h-10 w-10' : 'h-10 w-full rounded-md px-3 py-2 text-sm',
|
: selectedValue || selectPlaceholder}
|
||||||
|
</span>
|
||||||
|
{SelectIcon != null && iconSide === 'right' && (
|
||||||
|
<div className={selectIconClassName}>{SelectIcon}</div>
|
||||||
)}
|
)}
|
||||||
>
|
{showCarat && <ChevronDown className="h-4 w-4 text-text-secondary" />}
|
||||||
{SelectIcon != null && (
|
</>
|
||||||
<div className="assistant-item flex h-5 w-5 items-center justify-center overflow-hidden rounded-full">
|
)}
|
||||||
{SelectIcon}
|
</Ariakit.Select>
|
||||||
</div>
|
<Ariakit.SelectPopover
|
||||||
)}
|
store={select}
|
||||||
{!isCollapsed && (
|
gutter={4}
|
||||||
<span className="flex-grow truncate text-left">
|
portal
|
||||||
{displayValue ?? selectPlaceholder}
|
className="z-50 overflow-hidden rounded-md border border-border-light bg-surface-secondary shadow-lg"
|
||||||
</span>
|
style={{ width: isCollapsed ? '300px' : (buttonWidth ?? '300px') }}
|
||||||
)}
|
>
|
||||||
</Ariakit.Select>
|
<div className="p-2">
|
||||||
<Ariakit.SelectPopover
|
<div className="relative">
|
||||||
gutter={4}
|
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
|
||||||
portal
|
<Ariakit.Combobox
|
||||||
className="z-50 overflow-hidden rounded-md border border-border-light bg-surface-secondary shadow-lg"
|
store={combobox}
|
||||||
style={{ width: isCollapsed ? '300px' : buttonWidth ?? '300px' }}
|
autoSelect
|
||||||
>
|
placeholder={searchPlaceholder}
|
||||||
<div className="p-2">
|
className="w-full rounded-md border border-border-light bg-surface-tertiary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
|
||||||
<div className="relative">
|
/>
|
||||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-primary" />
|
</div>
|
||||||
<Ariakit.Combobox
|
</div>
|
||||||
autoSelect
|
<div className="max-h-[300px] overflow-auto">
|
||||||
placeholder={searchPlaceholder}
|
<Ariakit.ComboboxList store={combobox}>
|
||||||
className="w-full rounded-md border border-border-light bg-surface-tertiary py-2 pl-9 pr-3 text-sm text-text-primary focus:outline-none"
|
<SelectRenderer store={select} items={matches} itemSize={ROW_HEIGHT} overscan={5}>
|
||||||
/>
|
{({ value, icon, label, ...item }) => (
|
||||||
</div>
|
<Ariakit.ComboboxItem
|
||||||
</div>
|
key={item.id}
|
||||||
<div className="max-h-[50vh]">
|
{...item}
|
||||||
<AutoSizer disableHeight>
|
className={cn(
|
||||||
{({ width }) => (
|
'flex w-full cursor-pointer items-center px-3 text-sm',
|
||||||
<List
|
'text-text-primary hover:bg-surface-tertiary',
|
||||||
width={width}
|
'data-[active-item]:bg-surface-tertiary',
|
||||||
height={Math.min(matches.length * ROW_HEIGHT, 300)}
|
)}
|
||||||
rowCount={matches.length}
|
render={<Ariakit.SelectItem value={value} />}
|
||||||
rowHeight={ROW_HEIGHT}
|
>
|
||||||
rowRenderer={rowRenderer}
|
{icon != null && iconSide === 'left' && (
|
||||||
overscanRowCount={5}
|
<div className={optionIconClassName}>{icon}</div>
|
||||||
/>
|
)}
|
||||||
)}
|
<span className="flex-grow truncate text-left">{label}</span>
|
||||||
</AutoSizer>
|
{icon != null && iconSide === 'right' && (
|
||||||
</div>
|
<div className={optionIconClassName}>{icon}</div>
|
||||||
</Ariakit.SelectPopover>
|
)}
|
||||||
</Ariakit.SelectProvider>
|
</Ariakit.ComboboxItem>
|
||||||
</Ariakit.ComboboxProvider>
|
)}
|
||||||
|
</SelectRenderer>
|
||||||
|
</Ariakit.ComboboxList>
|
||||||
|
</div>
|
||||||
|
</Ariakit.SelectPopover>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
client/src/components/ui/InputOTP.tsx
Normal file
68
client/src/components/ui/InputOTP.tsx
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { OTPInput, OTPInputContext } from 'input-otp';
|
||||||
|
import { Minus } from 'lucide-react';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
const InputOTP = React.forwardRef<
|
||||||
|
React.ElementRef<typeof OTPInput>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||||
|
>(({ className, containerClassName, ...props }, ref) => (
|
||||||
|
<OTPInput
|
||||||
|
ref={ref}
|
||||||
|
containerClassName={cn(
|
||||||
|
'flex items-center gap-2 has-[:disabled]:opacity-50',
|
||||||
|
containerClassName,
|
||||||
|
)}
|
||||||
|
className={cn('disabled:cursor-not-allowed', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
InputOTP.displayName = 'InputOTP';
|
||||||
|
|
||||||
|
const InputOTPGroup = React.forwardRef<
|
||||||
|
React.ElementRef<'div'>,
|
||||||
|
React.ComponentPropsWithoutRef<'div'>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn('flex items-center', className)} {...props} />
|
||||||
|
));
|
||||||
|
InputOTPGroup.displayName = 'InputOTPGroup';
|
||||||
|
|
||||||
|
const InputOTPSlot = React.forwardRef<
|
||||||
|
React.ElementRef<'div'>,
|
||||||
|
React.ComponentPropsWithoutRef<'div'> & { index: number }
|
||||||
|
>(({ index, className, ...props }, ref) => {
|
||||||
|
const inputOTPContext = React.useContext(OTPInputContext);
|
||||||
|
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'text-md relative flex h-11 w-11 items-center justify-center border-y border-r border-input shadow-sm transition-all first:rounded-l-xl first:border-l last:rounded-r-xl',
|
||||||
|
isActive && 'z-10 ring-1 ring-ring',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{char}
|
||||||
|
{hasFakeCaret && (
|
||||||
|
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="animate-caret-blink h-4 w-px bg-foreground duration-1000" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
InputOTPSlot.displayName = 'InputOTPSlot';
|
||||||
|
|
||||||
|
const InputOTPSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<'div'>,
|
||||||
|
React.ComponentPropsWithoutRef<'div'>
|
||||||
|
>(({ ...props }, ref) => (
|
||||||
|
<div ref={ref} role="separator" {...props}>
|
||||||
|
<Minus />
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
InputOTPSeparator.displayName = 'InputOTPSeparator';
|
||||||
|
|
||||||
|
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||||
22
client/src/components/ui/Progress.tsx
Normal file
22
client/src/components/ui/Progress.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||||
|
import { cn } from '~/utils';
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Label,
|
Label,
|
||||||
Listbox,
|
Listbox,
|
||||||
|
|
@ -82,18 +82,14 @@ function SelectDropDown({
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = _title;
|
let title = _title;
|
||||||
|
|
||||||
if (emptyTitle) {
|
if (emptyTitle) {
|
||||||
title = '';
|
title = '';
|
||||||
} else if (!(title ?? '')) {
|
} else if (!(title ?? '')) {
|
||||||
title = localize('com_ui_model');
|
title = localize('com_ui_model');
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = availableValues ?? [];
|
const values = availableValues ?? [];
|
||||||
|
|
||||||
// Detemine if we should to convert this component into a searchable select. If we have enough elements, a search
|
// Enable searchable select if enough items are provided.
|
||||||
// input will appear near the top of the menu, allowing correct filtering of different model menu items. This will
|
|
||||||
// reset once the component is unmounted (as per a normal search)
|
|
||||||
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
|
const [filteredValues, searchRender] = useMultiSearch<string[] | Option[]>({
|
||||||
availableOptions: values,
|
availableOptions: values,
|
||||||
placeholder: searchPlaceholder,
|
placeholder: searchPlaceholder,
|
||||||
|
|
@ -103,26 +99,35 @@ function SelectDropDown({
|
||||||
});
|
});
|
||||||
const hasSearchRender = searchRender != null;
|
const hasSearchRender = searchRender != null;
|
||||||
const options = hasSearchRender ? filteredValues : values;
|
const options = hasSearchRender ? filteredValues : values;
|
||||||
|
|
||||||
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
|
const renderIcon = showOptionIcon && value != null && (value as OptionWithIcon).icon != null;
|
||||||
|
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-center gap-2 ', containerClassName ?? '')}>
|
<div className={cn('flex items-center justify-center gap-2', containerClassName ?? '')}>
|
||||||
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
<div className={cn('relative w-full', subContainerClassName ?? '')}>
|
||||||
<Listbox value={value} onChange={setValue} disabled={disabled}>
|
<Listbox value={value} onChange={setValue} disabled={disabled}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<ListboxButton
|
<ListboxButton
|
||||||
|
ref={buttonRef}
|
||||||
data-testid="select-dropdown-button"
|
data-testid="select-dropdown-button"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!open && buttonRef.current) {
|
||||||
|
buttonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left disabled:bg-white dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
|
'relative flex w-full cursor-default flex-col rounded-md border border-black/10 bg-white py-2 pl-3 pr-10 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:bg-white dark:border-gray-600 dark:bg-gray-700 sm:text-sm',
|
||||||
className ?? '',
|
className ?? '',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{' '}
|
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<Label
|
<Label
|
||||||
className="block text-xs text-gray-700 dark:text-gray-500 "
|
className="block text-xs text-gray-700 dark:text-gray-500"
|
||||||
id="headlessui-listbox-label-:r1:"
|
id="headlessui-listbox-label-:r1:"
|
||||||
data-headlessui-state=""
|
data-headlessui-state=""
|
||||||
>
|
>
|
||||||
|
|
@ -154,11 +159,9 @@ function SelectDropDown({
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return <span className="text-text-secondary">{placeholder}</span>;
|
return <span className="text-text-secondary">{placeholder}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
return value.label ?? '';
|
return value.label ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
})()}
|
})()}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -171,7 +174,7 @@ function SelectDropDown({
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
className="h-4 w-4 text-gray-400"
|
className="h-4 w-4 text-gray-400"
|
||||||
height="1em"
|
height="1em"
|
||||||
width="1em"
|
width="1em"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
@ -212,17 +215,17 @@ function SelectDropDown({
|
||||||
if (!option) {
|
if (!option) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentLabel =
|
const currentLabel =
|
||||||
typeof option === 'string' ? option : option.label ?? option.value ?? '';
|
typeof option === 'string' ? option : (option.label ?? option.value ?? '');
|
||||||
const currentValue = typeof option === 'string' ? option : option.value ?? '';
|
const currentValue = typeof option === 'string' ? option : (option.value ?? '');
|
||||||
const currentIcon =
|
const currentIcon =
|
||||||
typeof option === 'string' ? null : (option.icon as React.ReactNode) ?? null;
|
typeof option === 'string'
|
||||||
|
? null
|
||||||
|
: ((option.icon as React.ReactNode) ?? null);
|
||||||
let activeValue: string | number | null | Option = value;
|
let activeValue: string | number | null | Option = value;
|
||||||
if (typeof activeValue !== 'string') {
|
if (typeof activeValue !== 'string') {
|
||||||
activeValue = activeValue?.value ?? '';
|
activeValue = activeValue?.value ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListboxOption
|
<ListboxOption
|
||||||
key={i}
|
key={i}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ export * from './Textarea';
|
||||||
export * from './TextareaAutosize';
|
export * from './TextareaAutosize';
|
||||||
export * from './Tooltip';
|
export * from './Tooltip';
|
||||||
export * from './Pagination';
|
export * from './Pagination';
|
||||||
|
export * from './Progress';
|
||||||
|
export * from './InputOTP';
|
||||||
export { default as Combobox } from './Combobox';
|
export { default as Combobox } from './Combobox';
|
||||||
export { default as Dropdown } from './Dropdown';
|
export { default as Dropdown } from './Dropdown';
|
||||||
export { default as FileUpload } from './FileUpload';
|
export { default as FileUpload } from './FileUpload';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useResetRecoilState, useSetRecoilState } from 'recoil';
|
import { useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { MutationKeys, dataService, request } from 'librechat-data-provider';
|
import { MutationKeys, QueryKeys, dataService, request } from 'librechat-data-provider';
|
||||||
import type { UseMutationResult } from '@tanstack/react-query';
|
import type { UseMutationResult } from '@tanstack/react-query';
|
||||||
import type * as t from 'librechat-data-provider';
|
import type * as t from 'librechat-data-provider';
|
||||||
import useClearStates from '~/hooks/Config/useClearStates';
|
import useClearStates from '~/hooks/Config/useClearStates';
|
||||||
|
|
@ -84,3 +84,91 @@ export const useDeleteUserMutation = (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Array.isArray(user?.backupCodes) && user?.backupCodes.length > 0
|
||||||
|
|
||||||
|
export const useEnableTwoFactorMutation = (): UseMutationResult<
|
||||||
|
t.TEnable2FAResponse,
|
||||||
|
unknown,
|
||||||
|
void,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(() => dataService.enableTwoFactor(), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVerifyTwoFactorMutation = (): UseMutationResult<
|
||||||
|
t.TVerify2FAResponse,
|
||||||
|
unknown,
|
||||||
|
t.TVerify2FARequest,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation((payload: t.TVerify2FARequest) => dataService.verifyTwoFactor(payload), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useConfirmTwoFactorMutation = (): UseMutationResult<
|
||||||
|
t.TVerify2FAResponse,
|
||||||
|
unknown,
|
||||||
|
t.TVerify2FARequest,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation((payload: t.TVerify2FARequest) => dataService.confirmTwoFactor(payload), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDisableTwoFactorMutation = (): UseMutationResult<
|
||||||
|
t.TDisable2FAResponse,
|
||||||
|
unknown,
|
||||||
|
void,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(() => dataService.disableTwoFactor(), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa'], null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRegenerateBackupCodesMutation = (): UseMutationResult<
|
||||||
|
t.TRegenerateBackupCodesResponse,
|
||||||
|
unknown,
|
||||||
|
void,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(() => dataService.regenerateBackupCodes(), {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa', 'backup'], data);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useVerifyTwoFactorTempMutation = (
|
||||||
|
options?: t.MutationOptions<t.TVerify2FATempResponse, t.TVerify2FATempRequest, unknown, unknown>,
|
||||||
|
): UseMutationResult<t.TVerify2FATempResponse, unknown, t.TVerify2FATempRequest, unknown> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation(
|
||||||
|
(payload: t.TVerify2FATempRequest) => dataService.verifyTwoFactorTemp(payload),
|
||||||
|
{
|
||||||
|
...(options || {}),
|
||||||
|
onSuccess: (data, ...args) => {
|
||||||
|
queryClient.setQueryData([QueryKeys.user, '2fa'], data);
|
||||||
|
options?.onSuccess?.(data, ...args);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,12 @@ const AuthContextProvider = ({
|
||||||
|
|
||||||
const loginUser = useLoginUserMutation({
|
const loginUser = useLoginUserMutation({
|
||||||
onSuccess: (data: t.TLoginResponse) => {
|
onSuccess: (data: t.TLoginResponse) => {
|
||||||
const { user, token } = data;
|
const { user, token, twoFAPending, tempToken } = data;
|
||||||
|
if (twoFAPending) {
|
||||||
|
// Redirect to the two-factor authentication route.
|
||||||
|
navigate(`/login/2fa?tempToken=${tempToken}`, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
setError(undefined);
|
setError(undefined);
|
||||||
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
|
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { QueryKeys, EModelEndpoint, LocalStorageKeys, Constants } from 'librechat-data-provider';
|
import { QueryKeys, EModelEndpoint, LocalStorageKeys, Constants } from 'librechat-data-provider';
|
||||||
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
|
||||||
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
|
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const useNavigateToConvo = (index = 0) => {
|
const useNavigateToConvo = (index = 0) => {
|
||||||
|
|
@ -20,7 +20,7 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
invalidateMessages = false,
|
invalidateMessages = false,
|
||||||
) => {
|
) => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
console.log('Conversation not provided');
|
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hasSetConversation.current = true;
|
hasSetConversation.current = true;
|
||||||
|
|
@ -34,10 +34,10 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let convo = { ...conversation };
|
let convo = { ...conversation };
|
||||||
if (!convo.endpoint) {
|
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
||||||
/* undefined endpoint edge case */
|
if (!convo.endpoint || !endpointsConfig?.[convo.endpoint]) {
|
||||||
|
/* undefined/removed endpoint edge case */
|
||||||
const modelsConfig = queryClient.getQueryData<TModelsConfig>([QueryKeys.models]);
|
const modelsConfig = queryClient.getQueryData<TModelsConfig>([QueryKeys.models]);
|
||||||
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
|
|
||||||
const defaultEndpoint = getDefaultEndpoint({
|
const defaultEndpoint = getDefaultEndpoint({
|
||||||
convoSetup: conversation,
|
convoSetup: conversation,
|
||||||
endpointsConfig,
|
endpointsConfig,
|
||||||
|
|
@ -51,10 +51,10 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
const models = modelsConfig?.[defaultEndpoint ?? ''] ?? [];
|
const models = modelsConfig?.[defaultEndpoint ?? ''] ?? [];
|
||||||
|
|
||||||
convo = buildDefaultConvo({
|
convo = buildDefaultConvo({
|
||||||
|
models,
|
||||||
conversation,
|
conversation,
|
||||||
endpoint: defaultEndpoint,
|
endpoint: defaultEndpoint,
|
||||||
lastConversationSetup: conversation,
|
lastConversationSetup: conversation,
|
||||||
models,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
clearAllConversations(true);
|
clearAllConversations(true);
|
||||||
|
|
@ -68,7 +68,7 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
invalidateMessages?: boolean,
|
invalidateMessages?: boolean,
|
||||||
) => {
|
) => {
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
console.log('Conversation not provided');
|
logger.warn('conversation', 'Conversation not provided to `navigateToConvo`');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// set conversation to the new conversation
|
// set conversation to the new conversation
|
||||||
|
|
@ -78,7 +78,7 @@ const useNavigateToConvo = (index = 0) => {
|
||||||
lastSelectedTools =
|
lastSelectedTools =
|
||||||
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
JSON.parse(localStorage.getItem(LocalStorageKeys.LAST_TOOLS) ?? '') ?? [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error(e);
|
logger.error('conversation', 'Error parsing last selected tools', e);
|
||||||
}
|
}
|
||||||
const hasTools = (conversation.tools?.length ?? 0) > 0;
|
const hasTools = (conversation.tools?.length ?? 0) > 0;
|
||||||
navigateToConvo(
|
navigateToConvo(
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
megabyte,
|
|
||||||
QueryKeys,
|
QueryKeys,
|
||||||
EModelEndpoint,
|
EModelEndpoint,
|
||||||
codeTypeMapping,
|
|
||||||
mergeFileConfig,
|
mergeFileConfig,
|
||||||
isAgentsEndpoint,
|
isAgentsEndpoint,
|
||||||
isAssistantsEndpoint,
|
isAssistantsEndpoint,
|
||||||
|
|
@ -16,14 +14,12 @@ import {
|
||||||
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
import type { TEndpointsConfig, TError } from 'librechat-data-provider';
|
||||||
import type { ExtendedFile, FileSetter } from '~/common';
|
import type { ExtendedFile, FileSetter } from '~/common';
|
||||||
import { useUploadFileMutation, useGetFileConfig } from '~/data-provider';
|
import { useUploadFileMutation, useGetFileConfig } from '~/data-provider';
|
||||||
|
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
||||||
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
import { useDelayedUploadToast } from './useDelayedUploadToast';
|
||||||
import { useToastContext } from '~/Providers/ToastContext';
|
import { useToastContext } from '~/Providers/ToastContext';
|
||||||
import { useChatContext } from '~/Providers/ChatContext';
|
import { useChatContext } from '~/Providers/ChatContext';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import { logger, validateFiles } from '~/utils';
|
||||||
import useUpdateFiles from './useUpdateFiles';
|
import useUpdateFiles from './useUpdateFiles';
|
||||||
import { logger } from '~/utils';
|
|
||||||
|
|
||||||
const { checkType } = defaultFileConfig;
|
|
||||||
|
|
||||||
type UseFileHandling = {
|
type UseFileHandling = {
|
||||||
overrideEndpoint?: EModelEndpoint;
|
overrideEndpoint?: EModelEndpoint;
|
||||||
|
|
@ -58,20 +54,11 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
[params?.overrideEndpoint, conversation?.endpointType, conversation?.endpoint],
|
[params?.overrideEndpoint, conversation?.endpointType, conversation?.endpoint],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes } = useMemo(
|
|
||||||
() =>
|
|
||||||
fileConfig?.endpoints[endpoint] ??
|
|
||||||
fileConfig?.endpoints.default ??
|
|
||||||
defaultFileConfig.endpoints[endpoint] ??
|
|
||||||
defaultFileConfig.endpoints.default,
|
|
||||||
[fileConfig, endpoint],
|
|
||||||
);
|
|
||||||
|
|
||||||
const displayToast = useCallback(() => {
|
const displayToast = useCallback(() => {
|
||||||
if (errors.length > 1) {
|
if (errors.length > 1) {
|
||||||
// TODO: this should not be a dynamic localize input!!
|
// TODO: this should not be a dynamic localize input!!
|
||||||
const errorList = Array.from(new Set(errors))
|
const errorList = Array.from(new Set(errors))
|
||||||
.map((e, i) => `${i > 0 ? '• ' : ''}${localize(e) || e}\n`)
|
.map((e, i) => `${i > 0 ? '• ' : ''}${localize(e as TranslationKeys) || e}\n`)
|
||||||
.join('');
|
.join('');
|
||||||
showToast({
|
showToast({
|
||||||
message: errorList,
|
message: errorList,
|
||||||
|
|
@ -80,7 +67,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
});
|
});
|
||||||
} else if (errors.length === 1) {
|
} else if (errors.length === 1) {
|
||||||
// TODO: this should not be a dynamic localize input!!
|
// TODO: this should not be a dynamic localize input!!
|
||||||
const message = localize(errors[0]) || errors[0];
|
const message = localize(errors[0] as TranslationKeys) || errors[0];
|
||||||
showToast({
|
showToast({
|
||||||
message,
|
message,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
|
|
@ -147,7 +134,7 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.code === 'ERR_CANCELED'
|
error?.code === 'ERR_CANCELED'
|
||||||
? 'com_error_files_upload_canceled'
|
? 'com_error_files_upload_canceled'
|
||||||
: error?.response?.data?.message ?? 'com_error_files_upload';
|
: (error?.response?.data?.message ?? 'com_error_files_upload');
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -228,87 +215,6 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
uploadFile.mutate(formData);
|
uploadFile.mutate(formData);
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateFiles = useCallback(
|
|
||||||
(fileList: File[]) => {
|
|
||||||
const existingFiles = Array.from(files.values());
|
|
||||||
const incomingTotalSize = fileList.reduce((total, file) => total + file.size, 0);
|
|
||||||
if (incomingTotalSize === 0) {
|
|
||||||
setError('com_error_files_empty');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const currentTotalSize = existingFiles.reduce((total, file) => total + file.size, 0);
|
|
||||||
|
|
||||||
if (fileList.length + files.size > fileLimit) {
|
|
||||||
setError(`You can only upload up to ${fileLimit} files at a time.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < fileList.length; i++) {
|
|
||||||
let originalFile = fileList[i];
|
|
||||||
let fileType = originalFile.type;
|
|
||||||
const extension = originalFile.name.split('.').pop() ?? '';
|
|
||||||
const knownCodeType = codeTypeMapping[extension];
|
|
||||||
|
|
||||||
// Infer MIME type for Known Code files when the type is empty or a mismatch
|
|
||||||
if (knownCodeType && (!fileType || fileType !== knownCodeType)) {
|
|
||||||
fileType = knownCodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the file type is still empty after the extension check
|
|
||||||
if (!fileType) {
|
|
||||||
setError('Unable to determine file type for: ' + originalFile.name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace empty type with inferred type
|
|
||||||
if (originalFile.type !== fileType) {
|
|
||||||
const newFile = new File([originalFile], originalFile.name, { type: fileType });
|
|
||||||
originalFile = newFile;
|
|
||||||
fileList[i] = newFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!checkType(originalFile.type, supportedMimeTypes)) {
|
|
||||||
console.log(originalFile);
|
|
||||||
setError('Currently, unsupported file type: ' + originalFile.type);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalFile.size >= fileSizeLimit) {
|
|
||||||
setError(`File size exceeds ${fileSizeLimit / megabyte} MB.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTotalSize + incomingTotalSize > totalSizeLimit) {
|
|
||||||
setError(`The total size of the files cannot exceed ${totalSizeLimit / megabyte} MB.`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedFilesInfo = [
|
|
||||||
...existingFiles.map(
|
|
||||||
(file) =>
|
|
||||||
`${file.file?.name ?? file.filename}-${file.size}-${
|
|
||||||
file.type?.split('/')[0] ?? 'file'
|
|
||||||
}`,
|
|
||||||
),
|
|
||||||
...fileList.map(
|
|
||||||
(file: File | undefined) =>
|
|
||||||
`${file?.name}-${file?.size}-${file?.type.split('/')[0] ?? 'file'}`,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const uniqueFilesSet = new Set(combinedFilesInfo);
|
|
||||||
|
|
||||||
if (uniqueFilesSet.size !== combinedFilesInfo.length) {
|
|
||||||
setError('com_error_files_dupe');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
[files, fileLimit, fileSizeLimit, totalSizeLimit, supportedMimeTypes],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadImage = (extendedFile: ExtendedFile, preview: string) => {
|
const loadImage = (extendedFile: ExtendedFile, preview: string) => {
|
||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
|
|
@ -332,7 +238,16 @@ const useFileHandling = (params?: UseFileHandling) => {
|
||||||
/* Validate files */
|
/* Validate files */
|
||||||
let filesAreValid: boolean;
|
let filesAreValid: boolean;
|
||||||
try {
|
try {
|
||||||
filesAreValid = validateFiles(fileList);
|
filesAreValid = validateFiles({
|
||||||
|
files,
|
||||||
|
fileList,
|
||||||
|
setError,
|
||||||
|
endpointFileConfig:
|
||||||
|
fileConfig?.endpoints[endpoint] ??
|
||||||
|
fileConfig?.endpoints.default ??
|
||||||
|
defaultFileConfig.endpoints[endpoint] ??
|
||||||
|
defaultFileConfig.endpoints.default,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('file validation error', error);
|
console.error('file validation error', error);
|
||||||
setError('com_error_files_validation');
|
setError('com_error_files_validation');
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
import { useGetCategories } from '~/data-provider';
|
import { useGetCategories } from '~/data-provider';
|
||||||
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
|
||||||
import useLocalize from '~/hooks/useLocalize';
|
import useLocalize, { TranslationKeys } from '~/hooks/useLocalize';
|
||||||
|
|
||||||
const loadingCategories = [
|
const loadingCategories: { label: TranslationKeys; value: string }[] = [
|
||||||
{
|
{
|
||||||
label: 'Loading...',
|
label: 'com_ui_loading',
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
] as undefined | { label: string; value: string }[];
|
];
|
||||||
|
|
||||||
const emptyCategory = {
|
const emptyCategory: { label: TranslationKeys; value: string } = {
|
||||||
label: '-',
|
label: 'com_ui_empty_category',
|
||||||
value: '',
|
value: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ const useCategories = (className = '') => {
|
||||||
const { data: categories = loadingCategories } = useGetCategories({
|
const { data: categories = loadingCategories } = useGetCategories({
|
||||||
select: (data) =>
|
select: (data) =>
|
||||||
data.map((category) => ({
|
data.map((category) => ({
|
||||||
label: localize(`com_ui_${category.label}`) || category.label,
|
label: localize(category.label as TranslationKeys),
|
||||||
value: category.value,
|
value: category.value,
|
||||||
icon: category.value ? (
|
icon: category.value ? (
|
||||||
<CategoryIcon category={category.value} className={className} />
|
<CategoryIcon category={category.value} className={className} />
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { TOptions } from 'i18next';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TOptions } from 'i18next';
|
|
||||||
import store from '~/store';
|
|
||||||
import { resources } from '~/locales/i18n';
|
import { resources } from '~/locales/i18n';
|
||||||
|
import store from '~/store';
|
||||||
|
|
||||||
export type TranslationKeys = keyof typeof resources.en.translation;
|
export type TranslationKeys = keyof typeof resources.en.translation;
|
||||||
|
|
||||||
|
|
@ -17,10 +17,5 @@ export default function useLocalize() {
|
||||||
}
|
}
|
||||||
}, [lang, i18n]);
|
}, [lang, i18n]);
|
||||||
|
|
||||||
const memoizedLocalize = useCallback(
|
return (phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options);
|
||||||
(phraseKey: TranslationKeys, options?: TOptions) => t(phraseKey, options),
|
|
||||||
[t],
|
|
||||||
);
|
|
||||||
|
|
||||||
return memoizedLocalize;
|
|
||||||
}
|
}
|
||||||
|
|
@ -34,10 +34,9 @@ describe('i18next translation tests', () => {
|
||||||
expect(i18n.t('com_ui_examples')).toBe(English.com_ui_examples);
|
expect(i18n.t('com_ui_examples')).toBe(English.com_ui_examples);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an empty string for an invalid key', () => {
|
it('should return the key itself for an invalid key', () => {
|
||||||
i18n.changeLanguage('en');
|
i18n.changeLanguage('en');
|
||||||
// @ts-ignore
|
expect(i18n.t('invalid-key')).toBe('invalid-key'); // Returns the key itself
|
||||||
expect(i18n.t('invalid-key')).toBe('');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly format placeholders in the translation', () => {
|
it('should correctly format placeholders in the translation', () => {
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,6 @@
|
||||||
"com_endpoint_google_temp": "القيم الأعلى = أكثر عشوائية، بينما القيم الأقل = أكثر تركيزًا وحتمية. نوصي بتغيير هذا أو Top P ولكن ليس كلاهما.",
|
"com_endpoint_google_temp": "القيم الأعلى = أكثر عشوائية، بينما القيم الأقل = أكثر تركيزًا وحتمية. نوصي بتغيير هذا أو Top P ولكن ليس كلاهما.",
|
||||||
"com_endpoint_google_topk": "Top-k يغير كيفية اختيار النموذج للرموز للإخراج. top-k من 1 يعني أن الرمز المحدد هو الأكثر احتمالية بين جميع الرموز في مفردات النموذج (يسمى أيضًا الترميز الجشعي)، بينما top-k من 3 يعني أن الرمز التالي يتم اختياره من بين الرموز الثلاثة الأكثر احتمالية (باستخدام الحرارة).",
|
"com_endpoint_google_topk": "Top-k يغير كيفية اختيار النموذج للرموز للإخراج. top-k من 1 يعني أن الرمز المحدد هو الأكثر احتمالية بين جميع الرموز في مفردات النموذج (يسمى أيضًا الترميز الجشعي)، بينما top-k من 3 يعني أن الرمز التالي يتم اختياره من بين الرموز الثلاثة الأكثر احتمالية (باستخدام الحرارة).",
|
||||||
"com_endpoint_google_topp": "Top-p يغير كيفية اختيار النموذج للرموز للإخراج. يتم اختيار الرموز من الأكثر K (انظر معلمة topK) احتمالًا إلى الأقل حتى يصبح مجموع احتمالاتهم يساوي قيمة top-p.",
|
"com_endpoint_google_topp": "Top-p يغير كيفية اختيار النموذج للرموز للإخراج. يتم اختيار الرموز من الأكثر K (انظر معلمة topK) احتمالًا إلى الأقل حتى يصبح مجموع احتمالاتهم يساوي قيمة top-p.",
|
||||||
"com_endpoint_import": "استيراد",
|
|
||||||
"com_endpoint_instructions_assistants": "تعليمات التجاوز",
|
"com_endpoint_instructions_assistants": "تعليمات التجاوز",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "يتجاوز التعليمات الخاصة بالمساعد. هذا مفيد لتعديل السلوك على أساس كل مرة.",
|
"com_endpoint_instructions_assistants_placeholder": "يتجاوز التعليمات الخاصة بالمساعد. هذا مفيد لتعديل السلوك على أساس كل مرة.",
|
||||||
"com_endpoint_max_output_tokens": "الحد الأقصى لعدد الرموز المنتجة",
|
"com_endpoint_max_output_tokens": "الحد الأقصى لعدد الرموز المنتجة",
|
||||||
|
|
@ -262,7 +261,6 @@
|
||||||
"com_nav_archive_name": "الاسم",
|
"com_nav_archive_name": "الاسم",
|
||||||
"com_nav_archived_chats": "الدردشات المؤرشفة",
|
"com_nav_archived_chats": "الدردشات المؤرشفة",
|
||||||
"com_nav_archived_chats_empty": "ليس لديك أي دردشات مؤرشفة.",
|
"com_nav_archived_chats_empty": "ليس لديك أي دردشات مؤرشفة.",
|
||||||
"com_nav_archived_chats_manage": "إدارة",
|
|
||||||
"com_nav_at_command": "أمر-@",
|
"com_nav_at_command": "أمر-@",
|
||||||
"com_nav_at_command_description": "تبديل الأمر \"@\" للتنقل بين نقاط النهاية والنماذج والإعدادات المسبقة وغيرها",
|
"com_nav_at_command_description": "تبديل الأمر \"@\" للتنقل بين نقاط النهاية والنماذج والإعدادات المسبقة وغيرها",
|
||||||
"com_nav_audio_play_error": "خطأ في تشغيل الصوت: {{0}}",
|
"com_nav_audio_play_error": "خطأ في تشغيل الصوت: {{0}}",
|
||||||
|
|
@ -336,6 +334,7 @@
|
||||||
"com_nav_lang_chinese": "中文",
|
"com_nav_lang_chinese": "中文",
|
||||||
"com_nav_lang_dutch": "Nederlands",
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
"com_nav_lang_english": "English",
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
"com_nav_lang_finnish": "Suomi",
|
"com_nav_lang_finnish": "Suomi",
|
||||||
"com_nav_lang_french": "Français ",
|
"com_nav_lang_french": "Français ",
|
||||||
"com_nav_lang_german": "Deutsch",
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
|
@ -345,10 +344,11 @@
|
||||||
"com_nav_lang_japanese": "日本語",
|
"com_nav_lang_japanese": "日本語",
|
||||||
"com_nav_lang_korean": "한국어",
|
"com_nav_lang_korean": "한국어",
|
||||||
"com_nav_lang_polish": "Polski",
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
"com_nav_lang_russian": "Русский",
|
"com_nav_lang_russian": "Русский",
|
||||||
"com_nav_lang_spanish": "Español",
|
"com_nav_lang_spanish": "Español",
|
||||||
"com_nav_lang_swedish": "Svenska",
|
"com_nav_lang_swedish": "Svenska",
|
||||||
"com_nav_lang_traditionalchinese": "繁體中文",
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
"com_nav_lang_turkish": "Türkçe",
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
"com_nav_language": "اللغة",
|
"com_nav_language": "اللغة",
|
||||||
|
|
@ -381,7 +381,6 @@
|
||||||
"com_nav_setting_speech": "الكلام",
|
"com_nav_setting_speech": "الكلام",
|
||||||
"com_nav_settings": "الإعدادات",
|
"com_nav_settings": "الإعدادات",
|
||||||
"com_nav_shared_links": "روابط مشتركة",
|
"com_nav_shared_links": "روابط مشتركة",
|
||||||
"com_nav_shared_links_manage": "الإدارة",
|
|
||||||
"com_nav_show_code": "إظهار الشفرة دائمًا عند استخدام مفسر الشفرة",
|
"com_nav_show_code": "إظهار الشفرة دائمًا عند استخدام مفسر الشفرة",
|
||||||
"com_nav_slash_command": "/-الأمر",
|
"com_nav_slash_command": "/-الأمر",
|
||||||
"com_nav_slash_command_description": "تبديل الأمر \"/\" لاختيار موجه عبر لوحة المفاتيح",
|
"com_nav_slash_command_description": "تبديل الأمر \"/\" لاختيار موجه عبر لوحة المفاتيح",
|
||||||
|
|
@ -585,7 +584,6 @@
|
||||||
"com_ui_happy_birthday": "إنه عيد ميلادي الأول!",
|
"com_ui_happy_birthday": "إنه عيد ميلادي الأول!",
|
||||||
"com_ui_host": "مُضيف",
|
"com_ui_host": "مُضيف",
|
||||||
"com_ui_image_gen": "توليد الصور",
|
"com_ui_image_gen": "توليد الصور",
|
||||||
"com_ui_import_conversation": "استيراد",
|
|
||||||
"com_ui_import_conversation_error": "حدث خطأ أثناء استيراد محادثاتك",
|
"com_ui_import_conversation_error": "حدث خطأ أثناء استيراد محادثاتك",
|
||||||
"com_ui_import_conversation_file_type_error": "نوع الملف غير مدعوم للاستيراد",
|
"com_ui_import_conversation_file_type_error": "نوع الملف غير مدعوم للاستيراد",
|
||||||
"com_ui_import_conversation_info": "استيراد محادثات من ملف JSON",
|
"com_ui_import_conversation_info": "استيراد محادثات من ملف JSON",
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,16 @@
|
||||||
"com_agents_not_available": "Agent nicht verfügbar",
|
"com_agents_not_available": "Agent nicht verfügbar",
|
||||||
"com_agents_search_name": "Agenten nach Namen suchen",
|
"com_agents_search_name": "Agenten nach Namen suchen",
|
||||||
"com_agents_update_error": "Beim Aktualisieren deines Agenten ist ein Fehler aufgetreten.",
|
"com_agents_update_error": "Beim Aktualisieren deines Agenten ist ein Fehler aufgetreten.",
|
||||||
|
"com_assistants_action_attempt": "Assistent möchte kommunizieren mit {{0}}",
|
||||||
"com_assistants_actions": "Aktionen",
|
"com_assistants_actions": "Aktionen",
|
||||||
"com_assistants_actions_disabled": "Du müsst einen Assistenten erstellen, bevor du Aktionen hinzufügen kannst.",
|
"com_assistants_actions_disabled": "Du müsst einen Assistenten erstellen, bevor du Aktionen hinzufügen kannst.",
|
||||||
"com_assistants_actions_info": "Lasse deinen Assistenten Informationen abrufen oder Aktionen über APIs ausführen",
|
"com_assistants_actions_info": "Lasse deinen Assistenten Informationen abrufen oder Aktionen über APIs ausführen",
|
||||||
"com_assistants_add_actions": "Aktionen hinzufügen",
|
"com_assistants_add_actions": "Aktionen hinzufügen",
|
||||||
"com_assistants_add_tools": "Werkzeuge hinzufügen",
|
"com_assistants_add_tools": "Werkzeuge hinzufügen",
|
||||||
|
"com_assistants_allow_sites_you_trust": "Erlaube nur Webseiten, denen du vertraust.",
|
||||||
"com_assistants_append_date": "Aktuelles Datum & Uhrzeit anhängen",
|
"com_assistants_append_date": "Aktuelles Datum & Uhrzeit anhängen",
|
||||||
"com_assistants_append_date_tooltip": "Wenn aktiviert, werden das aktuelle Client-Datum und die Uhrzeit an die Systemanweisungen des Assistenten angehängt.",
|
"com_assistants_append_date_tooltip": "Wenn aktiviert, werden das aktuelle Client-Datum und die Uhrzeit an die Systemanweisungen des Assistenten angehängt.",
|
||||||
|
"com_assistants_attempt_info": "Assistent möchte Folgendes senden:",
|
||||||
"com_assistants_available_actions": "Verfügbare Aktionen",
|
"com_assistants_available_actions": "Verfügbare Aktionen",
|
||||||
"com_assistants_capabilities": "Fähigkeiten",
|
"com_assistants_capabilities": "Fähigkeiten",
|
||||||
"com_assistants_code_interpreter": "Code-Interpreter",
|
"com_assistants_code_interpreter": "Code-Interpreter",
|
||||||
|
|
@ -59,6 +62,7 @@
|
||||||
"com_assistants_update_error": "Bei der Aktualisierung deines Assistenten ist ein Fehler aufgetreten.",
|
"com_assistants_update_error": "Bei der Aktualisierung deines Assistenten ist ein Fehler aufgetreten.",
|
||||||
"com_assistants_update_success": "Erfolgreich aktualisiert",
|
"com_assistants_update_success": "Erfolgreich aktualisiert",
|
||||||
"com_auth_already_have_account": "Hast du bereits ein Konto?",
|
"com_auth_already_have_account": "Hast du bereits ein Konto?",
|
||||||
|
"com_auth_apple_login": "Mit Apple anmelden",
|
||||||
"com_auth_back_to_login": "Zurück zur Anmeldung",
|
"com_auth_back_to_login": "Zurück zur Anmeldung",
|
||||||
"com_auth_click": "Klicke",
|
"com_auth_click": "Klicke",
|
||||||
"com_auth_click_here": "Klicke hier",
|
"com_auth_click_here": "Klicke hier",
|
||||||
|
|
@ -81,6 +85,7 @@
|
||||||
"com_auth_email_verification_redirecting": "Weiterleitung in {{0}} Sekunden...",
|
"com_auth_email_verification_redirecting": "Weiterleitung in {{0}} Sekunden...",
|
||||||
"com_auth_email_verification_resend_prompt": "Keine E-Mail erhalten?",
|
"com_auth_email_verification_resend_prompt": "Keine E-Mail erhalten?",
|
||||||
"com_auth_email_verification_success": "E-Mail erfolgreich verifiziert",
|
"com_auth_email_verification_success": "E-Mail erfolgreich verifiziert",
|
||||||
|
"com_auth_email_verifying_ellipsis": "Überprüfe …",
|
||||||
"com_auth_error_create": "Bei der Registrierung deines Kontos ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
"com_auth_error_create": "Bei der Registrierung deines Kontos ist ein Fehler aufgetreten. Bitte versuche es erneut.",
|
||||||
"com_auth_error_invalid_reset_token": "Dieser Passwort-Reset-Token ist nicht mehr gültig.",
|
"com_auth_error_invalid_reset_token": "Dieser Passwort-Reset-Token ist nicht mehr gültig.",
|
||||||
"com_auth_error_login": "Anmeldung mit den angegebenen Informationen nicht möglich. Bitte überprüfe deine Anmeldedaten und versuche es erneut.",
|
"com_auth_error_login": "Anmeldung mit den angegebenen Informationen nicht möglich. Bitte überprüfe deine Anmeldedaten und versuche es erneut.",
|
||||||
|
|
@ -117,9 +122,11 @@
|
||||||
"com_auth_submit_registration": "Registrierung absenden",
|
"com_auth_submit_registration": "Registrierung absenden",
|
||||||
"com_auth_to_reset_your_password": "um Ihr Passwort zurückzusetzen.",
|
"com_auth_to_reset_your_password": "um Ihr Passwort zurückzusetzen.",
|
||||||
"com_auth_to_try_again": "um es erneut zu versuchen.",
|
"com_auth_to_try_again": "um es erneut zu versuchen.",
|
||||||
|
"com_auth_two_factor": "Prüfe deine bevorzugte Einmalkennwort-App auf einen Code.",
|
||||||
"com_auth_username": "Benutzername (optional)",
|
"com_auth_username": "Benutzername (optional)",
|
||||||
"com_auth_username_max_length": "Benutzername darf nicht länger als 20 Zeichen sein",
|
"com_auth_username_max_length": "Benutzername darf nicht länger als 20 Zeichen sein",
|
||||||
"com_auth_username_min_length": "Benutzername muss mindestens 2 Zeichen lang sein",
|
"com_auth_username_min_length": "Benutzername muss mindestens 2 Zeichen lang sein",
|
||||||
|
"com_auth_verify_your_identity": "Bestätige deine Identität",
|
||||||
"com_auth_welcome_back": "Willkommen zurück",
|
"com_auth_welcome_back": "Willkommen zurück",
|
||||||
"com_click_to_download": "(hier klicken zum Herunterladen)",
|
"com_click_to_download": "(hier klicken zum Herunterladen)",
|
||||||
"com_download_expired": "Download abgelaufen",
|
"com_download_expired": "Download abgelaufen",
|
||||||
|
|
@ -178,7 +185,6 @@
|
||||||
"com_endpoint_google_temp": "Höhere Werte = zufälliger, während niedrigere Werte = fokussierter und deterministischer. Wir empfehlen, entweder dies oder Top P zu ändern, aber nicht beides.",
|
"com_endpoint_google_temp": "Höhere Werte = zufälliger, während niedrigere Werte = fokussierter und deterministischer. Wir empfehlen, entweder dies oder Top P zu ändern, aber nicht beides.",
|
||||||
"com_endpoint_google_topk": "Top-k ändert, wie das Modell Token für die Antwort auswählt. Ein Top-k von 1 bedeutet, dass das ausgewählte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch Greedy-Decoding genannt), während ein Top-k von 3 bedeutet, dass das nächste Token aus den 3 wahrscheinlichsten Token ausgewählt wird (unter Verwendung der Temperatur).",
|
"com_endpoint_google_topk": "Top-k ändert, wie das Modell Token für die Antwort auswählt. Ein Top-k von 1 bedeutet, dass das ausgewählte Token das wahrscheinlichste unter allen Token im Vokabular des Modells ist (auch Greedy-Decoding genannt), während ein Top-k von 3 bedeutet, dass das nächste Token aus den 3 wahrscheinlichsten Token ausgewählt wird (unter Verwendung der Temperatur).",
|
||||||
"com_endpoint_google_topp": "Top-p ändert, wie das Modell Token für die Antwort auswählt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewählt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.",
|
"com_endpoint_google_topp": "Top-p ändert, wie das Modell Token für die Antwort auswählt. Token werden von den wahrscheinlichsten K (siehe topK-Parameter) bis zu den am wenigsten wahrscheinlichen ausgewählt, bis die Summe ihrer Wahrscheinlichkeiten dem Top-p-Wert entspricht.",
|
||||||
"com_endpoint_import": "Importieren",
|
|
||||||
"com_endpoint_instructions_assistants": "Anweisungen überschreiben",
|
"com_endpoint_instructions_assistants": "Anweisungen überschreiben",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "Überschreibt die Anweisungen des Assistenten. Dies ist nützlich, um das Verhalten auf Basis einzelner Ausführungen zu modifizieren.",
|
"com_endpoint_instructions_assistants_placeholder": "Überschreibt die Anweisungen des Assistenten. Dies ist nützlich, um das Verhalten auf Basis einzelner Ausführungen zu modifizieren.",
|
||||||
"com_endpoint_max_output_tokens": "Max. Antwort-Token",
|
"com_endpoint_max_output_tokens": "Max. Antwort-Token",
|
||||||
|
|
@ -195,6 +201,7 @@
|
||||||
"com_endpoint_openai_max_tokens": "Optionales 'max_tokens'-Feld, das die maximale Anzahl von Token darstellt, die in der Chat-Vervollständigung generiert werden können. Die Gesamtlänge der Eingabe-Token und der generierten Token ist durch die Kontextlänge des Modells begrenzt. Du kannst Fehler erleben, wenn diese Zahl die maximalen Kontext-Token überschreitet.",
|
"com_endpoint_openai_max_tokens": "Optionales 'max_tokens'-Feld, das die maximale Anzahl von Token darstellt, die in der Chat-Vervollständigung generiert werden können. Die Gesamtlänge der Eingabe-Token und der generierten Token ist durch die Kontextlänge des Modells begrenzt. Du kannst Fehler erleben, wenn diese Zahl die maximalen Kontext-Token überschreitet.",
|
||||||
"com_endpoint_openai_pres": "Zahl zwischen -2,0 und 2,0. Positive Werte bestrafen neue Token basierend darauf, ob sie im bisherigen Text vorkommen, wodurch die Wahrscheinlichkeit des Modells erhöht wird, über neue Themen zu sprechen.",
|
"com_endpoint_openai_pres": "Zahl zwischen -2,0 und 2,0. Positive Werte bestrafen neue Token basierend darauf, ob sie im bisherigen Text vorkommen, wodurch die Wahrscheinlichkeit des Modells erhöht wird, über neue Themen zu sprechen.",
|
||||||
"com_endpoint_openai_prompt_prefix_placeholder": "Lege benutzerdefinierte Anweisungen fest, die in die Systemnachricht an die KI aufgenommen werden sollen. Standard: keine",
|
"com_endpoint_openai_prompt_prefix_placeholder": "Lege benutzerdefinierte Anweisungen fest, die in die Systemnachricht an die KI aufgenommen werden sollen. Standard: keine",
|
||||||
|
"com_endpoint_openai_reasoning_effort": "Nur für o1-Modelle: Begrenzt den Aufwand des Nachdenkens bei Schlussfolgerungsmodellen. Die Reduzierung des Nachdenkeaufwands kann zu schnelleren Antworten und weniger Token führen, die für das Überlegen vor einer Antwort verwendet werden.",
|
||||||
"com_endpoint_openai_resend": "Alle im Chat zuvor angehängten Bilder mit jeder neuen Nachricht erneut senden. Hinweis: Dies kann die Kosten der Anfrage aufgrund höherer Token-Anzahl erheblich erhöhen und du kannst bei vielen Bildanhängen Fehler erleben.",
|
"com_endpoint_openai_resend": "Alle im Chat zuvor angehängten Bilder mit jeder neuen Nachricht erneut senden. Hinweis: Dies kann die Kosten der Anfrage aufgrund höherer Token-Anzahl erheblich erhöhen und du kannst bei vielen Bildanhängen Fehler erleben.",
|
||||||
"com_endpoint_openai_resend_files": "Alle im Chat zuvor angehängten Dateien mit jeder neuen Nachricht erneut senden. Hinweis: Dies wird die Kosten der Anfrage aufgrund höherer Token-Anzahl erheblich erhöhen und du kannst bei vielen Anhängen Fehler erleben.",
|
"com_endpoint_openai_resend_files": "Alle im Chat zuvor angehängten Dateien mit jeder neuen Nachricht erneut senden. Hinweis: Dies wird die Kosten der Anfrage aufgrund höherer Token-Anzahl erheblich erhöhen und du kannst bei vielen Anhängen Fehler erleben.",
|
||||||
"com_endpoint_openai_stop": "Bis zu 4 Sequenzen, bei denen die API keine weiteren Token generiert.",
|
"com_endpoint_openai_stop": "Bis zu 4 Sequenzen, bei denen die API keine weiteren Token generiert.",
|
||||||
|
|
@ -228,6 +235,7 @@
|
||||||
"com_endpoint_prompt_prefix_assistants": "Zusätzliche Anweisungen",
|
"com_endpoint_prompt_prefix_assistants": "Zusätzliche Anweisungen",
|
||||||
"com_endpoint_prompt_prefix_assistants_placeholder": "Lege zusätzliche Anweisungen oder Kontext zusätzlich zu den Hauptanweisungen des Assistenten fest. Wird ignoriert, wenn leer.",
|
"com_endpoint_prompt_prefix_assistants_placeholder": "Lege zusätzliche Anweisungen oder Kontext zusätzlich zu den Hauptanweisungen des Assistenten fest. Wird ignoriert, wenn leer.",
|
||||||
"com_endpoint_prompt_prefix_placeholder": "Lege benutzerdefinierte Anweisungen oder Kontext fest. Wird ignoriert, wenn leer.",
|
"com_endpoint_prompt_prefix_placeholder": "Lege benutzerdefinierte Anweisungen oder Kontext fest. Wird ignoriert, wenn leer.",
|
||||||
|
"com_endpoint_reasoning_effort": "Denkaufwand",
|
||||||
"com_endpoint_save_as_preset": "Voreinstellung speichern",
|
"com_endpoint_save_as_preset": "Voreinstellung speichern",
|
||||||
"com_endpoint_search": "Endpunkt nach Namen suchen",
|
"com_endpoint_search": "Endpunkt nach Namen suchen",
|
||||||
"com_endpoint_set_custom_name": "Lege einen benutzerdefinierten Namen fest, falls du diese Voreinstellung wiederfinden möchtest",
|
"com_endpoint_set_custom_name": "Lege einen benutzerdefinierten Namen fest, falls du diese Voreinstellung wiederfinden möchtest",
|
||||||
|
|
@ -256,13 +264,13 @@
|
||||||
"com_files_number_selected": "{{0}} von {{1}} Datei(en) ausgewählt",
|
"com_files_number_selected": "{{0}} von {{1}} Datei(en) ausgewählt",
|
||||||
"com_generated_files": "Generierte Dateien:",
|
"com_generated_files": "Generierte Dateien:",
|
||||||
"com_hide_examples": "Beispiele ausblenden",
|
"com_hide_examples": "Beispiele ausblenden",
|
||||||
|
"com_nav_2fa": "Zwei-Faktor-Authentifizierung (2FA)",
|
||||||
"com_nav_account_settings": "Kontoeinstellungen",
|
"com_nav_account_settings": "Kontoeinstellungen",
|
||||||
"com_nav_always_make_prod": "Neue Versionen direkt produktiv nehmen",
|
"com_nav_always_make_prod": "Neue Versionen direkt produktiv nehmen",
|
||||||
"com_nav_archive_created_at": "Archivierungsdatum",
|
"com_nav_archive_created_at": "Archivierungsdatum",
|
||||||
"com_nav_archive_name": "Name",
|
"com_nav_archive_name": "Name",
|
||||||
"com_nav_archived_chats": "Archivierte Chats",
|
"com_nav_archived_chats": "Archivierte Chats",
|
||||||
"com_nav_archived_chats_empty": "Du hast keine archivierten Chats.",
|
"com_nav_archived_chats_empty": "Du hast keine archivierten Chats.",
|
||||||
"com_nav_archived_chats_manage": "Verwalten",
|
|
||||||
"com_nav_at_command": "@-Befehl",
|
"com_nav_at_command": "@-Befehl",
|
||||||
"com_nav_at_command_description": "Schaltet den Befehl \"@\" zum Wechseln von Endpunkten, Modellen, Voreinstellungen usw. um.",
|
"com_nav_at_command_description": "Schaltet den Befehl \"@\" zum Wechseln von Endpunkten, Modellen, Voreinstellungen usw. um.",
|
||||||
"com_nav_audio_play_error": "Fehler beim Abspielen des Audios: {{0}}",
|
"com_nav_audio_play_error": "Fehler beim Abspielen des Audios: {{0}}",
|
||||||
|
|
@ -322,6 +330,7 @@
|
||||||
"com_nav_help_faq": "Hilfe & FAQ",
|
"com_nav_help_faq": "Hilfe & FAQ",
|
||||||
"com_nav_hide_panel": "Rechte Seitenleiste verstecken",
|
"com_nav_hide_panel": "Rechte Seitenleiste verstecken",
|
||||||
"com_nav_info_code_artifacts": "Aktiviert die Anzeige experimenteller Code-Artefakte neben dem Chat",
|
"com_nav_info_code_artifacts": "Aktiviert die Anzeige experimenteller Code-Artefakte neben dem Chat",
|
||||||
|
"com_nav_info_code_artifacts_agent": "Aktiviert die Verwendung von Code-Artefakten für diesen Agenten. Standardmäßig werden zusätzliche, spezielle Anweisungen für die Nutzung von Artefakten hinzugefügt, es sei denn, der \"Benutzerdefinierte Prompt-Modus\" ist aktiviert.",
|
||||||
"com_nav_info_custom_prompt_mode": "Wenn aktiviert, wird die Standard-Systemaufforderung für Artefakte nicht eingeschlossen. Alle Anweisungen zur Erzeugung von Artefakten müssen in diesem Modus manuell bereitgestellt werden.",
|
"com_nav_info_custom_prompt_mode": "Wenn aktiviert, wird die Standard-Systemaufforderung für Artefakte nicht eingeschlossen. Alle Anweisungen zur Erzeugung von Artefakten müssen in diesem Modus manuell bereitgestellt werden.",
|
||||||
"com_nav_info_enter_to_send": "Wenn aktiviert, sendet das Drücken von `ENTER` Ihre Nachricht. Wenn deaktiviert, fügt das Drücken von Enter eine neue Zeile hinzu, und du musst `STRG + ENTER` drücken, um deine Nachricht zu senden.",
|
"com_nav_info_enter_to_send": "Wenn aktiviert, sendet das Drücken von `ENTER` Ihre Nachricht. Wenn deaktiviert, fügt das Drücken von Enter eine neue Zeile hinzu, und du musst `STRG + ENTER` drücken, um deine Nachricht zu senden.",
|
||||||
"com_nav_info_fork_change_default": "`Nur sichtbare Nachrichten` umfasst nur den direkten Pfad zur ausgewählten Nachricht. `Zugehörige Verzweigungen einbeziehen` fügt Verzweigungen entlang des Pfades hinzu. `Alle bis/von hier einbeziehen` umfasst alle verbundenen Nachrichten und Verzweigungen.",
|
"com_nav_info_fork_change_default": "`Nur sichtbare Nachrichten` umfasst nur den direkten Pfad zur ausgewählten Nachricht. `Zugehörige Verzweigungen einbeziehen` fügt Verzweigungen entlang des Pfades hinzu. `Alle bis/von hier einbeziehen` umfasst alle verbundenen Nachrichten und Verzweigungen.",
|
||||||
|
|
@ -329,6 +338,7 @@
|
||||||
"com_nav_info_include_shadcnui": "Wenn aktiviert, werden Anweisungen zur Verwendung von shadcn/ui-Komponenten eingeschlossen. shadcn/ui ist eine Sammlung wiederverwendbarer Komponenten, die mit Radix UI und Tailwind CSS erstellt wurden. Hinweis: Dies sind umfangreiche Anweisungen, die Sie nur aktivieren sollten, wenn es Ihnen wichtig ist, das KI-Modell über die korrekten Importe und Komponenten zu informieren. Weitere Informationen zu diesen Komponenten finden Sie unter: https://ui.shadcn.com/",
|
"com_nav_info_include_shadcnui": "Wenn aktiviert, werden Anweisungen zur Verwendung von shadcn/ui-Komponenten eingeschlossen. shadcn/ui ist eine Sammlung wiederverwendbarer Komponenten, die mit Radix UI und Tailwind CSS erstellt wurden. Hinweis: Dies sind umfangreiche Anweisungen, die Sie nur aktivieren sollten, wenn es Ihnen wichtig ist, das KI-Modell über die korrekten Importe und Komponenten zu informieren. Weitere Informationen zu diesen Komponenten finden Sie unter: https://ui.shadcn.com/",
|
||||||
"com_nav_info_latex_parsing": "Wenn aktiviert, wird LaTeX-Code in Nachrichten als mathematische Gleichungen gerendert. Das Deaktivieren kann die Leistung verbessern, wenn du keine LaTeX-Darstellung benötigst.",
|
"com_nav_info_latex_parsing": "Wenn aktiviert, wird LaTeX-Code in Nachrichten als mathematische Gleichungen gerendert. Das Deaktivieren kann die Leistung verbessern, wenn du keine LaTeX-Darstellung benötigst.",
|
||||||
"com_nav_info_save_draft": "Wenn aktiviert, werden der Text und die Anhänge, die du in das Chat-Formular eingibst, automatisch lokal als Entwürfe gespeichert. Diese Entwürfe sind auch verfügbar, wenn du die Seite neu lädst oder zu einer anderen Konversation wechseln. Entwürfe werden lokal auf deinem Gerät gespeichert und werden gelöscht, sobald die Nachricht gesendet wird.",
|
"com_nav_info_save_draft": "Wenn aktiviert, werden der Text und die Anhänge, die du in das Chat-Formular eingibst, automatisch lokal als Entwürfe gespeichert. Diese Entwürfe sind auch verfügbar, wenn du die Seite neu lädst oder zu einer anderen Konversation wechseln. Entwürfe werden lokal auf deinem Gerät gespeichert und werden gelöscht, sobald die Nachricht gesendet wird.",
|
||||||
|
"com_nav_info_show_thinking": "Wenn aktiviert, sind die Denkprozess-Dropdowns standardmäßig geöffnet, sodass du die Gedankengänge der KI in Echtzeit sehen kannst. Wenn deaktiviert, bleiben sie standardmäßig geschlossen, für eine übersichtlichere Oberfläche.",
|
||||||
"com_nav_info_user_name_display": "Wenn aktiviert, wird der Benutzername des Absenders über jeder Nachricht angezeigt, die du sendest. Wenn deaktiviert, siehst du nur \"Du\" über deinen Nachrichten.",
|
"com_nav_info_user_name_display": "Wenn aktiviert, wird der Benutzername des Absenders über jeder Nachricht angezeigt, die du sendest. Wenn deaktiviert, siehst du nur \"Du\" über deinen Nachrichten.",
|
||||||
"com_nav_lang_arabic": "العربية",
|
"com_nav_lang_arabic": "العربية",
|
||||||
"com_nav_lang_auto": "Automatisch erkennen",
|
"com_nav_lang_auto": "Automatisch erkennen",
|
||||||
|
|
@ -336,6 +346,7 @@
|
||||||
"com_nav_lang_chinese": "中文",
|
"com_nav_lang_chinese": "中文",
|
||||||
"com_nav_lang_dutch": "Nederlands",
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
"com_nav_lang_english": "English",
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
"com_nav_lang_finnish": "Suomi",
|
"com_nav_lang_finnish": "Suomi",
|
||||||
"com_nav_lang_french": "Français ",
|
"com_nav_lang_french": "Français ",
|
||||||
"com_nav_lang_german": "Deutsch",
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
|
@ -345,10 +356,11 @@
|
||||||
"com_nav_lang_japanese": "日本語",
|
"com_nav_lang_japanese": "日本語",
|
||||||
"com_nav_lang_korean": "한국어",
|
"com_nav_lang_korean": "한국어",
|
||||||
"com_nav_lang_polish": "Polski",
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
"com_nav_lang_russian": "Русский",
|
"com_nav_lang_russian": "Русский",
|
||||||
"com_nav_lang_spanish": "Español",
|
"com_nav_lang_spanish": "Español",
|
||||||
"com_nav_lang_swedish": "Svenska",
|
"com_nav_lang_swedish": "Svenska",
|
||||||
"com_nav_lang_traditionalchinese": "繁體中文",
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
"com_nav_lang_turkish": "Türkçe",
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
"com_nav_language": "Sprache",
|
"com_nav_language": "Sprache",
|
||||||
|
|
@ -371,6 +383,7 @@
|
||||||
"com_nav_plus_command_description": "Schaltet den Befehl \"+\" zum Hinzufügen einer Mehrfachantwort-Einstellung um",
|
"com_nav_plus_command_description": "Schaltet den Befehl \"+\" zum Hinzufügen einer Mehrfachantwort-Einstellung um",
|
||||||
"com_nav_profile_picture": "Profilbild",
|
"com_nav_profile_picture": "Profilbild",
|
||||||
"com_nav_save_drafts": "Entwürfe lokal speichern",
|
"com_nav_save_drafts": "Entwürfe lokal speichern",
|
||||||
|
"com_nav_scroll_button": "Zum Ende scrollen Button",
|
||||||
"com_nav_search_placeholder": "Nachrichten durchsuchen",
|
"com_nav_search_placeholder": "Nachrichten durchsuchen",
|
||||||
"com_nav_send_message": "Nachricht senden",
|
"com_nav_send_message": "Nachricht senden",
|
||||||
"com_nav_setting_account": "Konto",
|
"com_nav_setting_account": "Konto",
|
||||||
|
|
@ -381,8 +394,8 @@
|
||||||
"com_nav_setting_speech": "Sprache",
|
"com_nav_setting_speech": "Sprache",
|
||||||
"com_nav_settings": "Einstellungen",
|
"com_nav_settings": "Einstellungen",
|
||||||
"com_nav_shared_links": "Geteilte Links",
|
"com_nav_shared_links": "Geteilte Links",
|
||||||
"com_nav_shared_links_manage": "Verwalten",
|
|
||||||
"com_nav_show_code": "Code immer anzeigen, wenn der Code-Interpreter verwendet wird",
|
"com_nav_show_code": "Code immer anzeigen, wenn der Code-Interpreter verwendet wird",
|
||||||
|
"com_nav_show_thinking": "Denkprozess-Dropdowns standardmäßig öffnen",
|
||||||
"com_nav_slash_command": "/-Befehl",
|
"com_nav_slash_command": "/-Befehl",
|
||||||
"com_nav_slash_command_description": "Schaltet den Befehl \"/\" zur Auswahl einer Eingabeaufforderung über die Tastatur um",
|
"com_nav_slash_command_description": "Schaltet den Befehl \"/\" zur Auswahl einer Eingabeaufforderung über die Tastatur um",
|
||||||
"com_nav_source_buffer_error": "Fehler beim Einrichten der Audiowiedergabe. Bitte lade die Seite neu.",
|
"com_nav_source_buffer_error": "Fehler beim Einrichten der Audiowiedergabe. Bitte lade die Seite neu.",
|
||||||
|
|
@ -418,9 +431,19 @@
|
||||||
"com_sidepanel_conversation_tags": "Lesezeichen",
|
"com_sidepanel_conversation_tags": "Lesezeichen",
|
||||||
"com_sidepanel_hide_panel": "Seitenleiste ausblenden",
|
"com_sidepanel_hide_panel": "Seitenleiste ausblenden",
|
||||||
"com_sidepanel_manage_files": "Dateien verwalten",
|
"com_sidepanel_manage_files": "Dateien verwalten",
|
||||||
"com_sidepanel_parameters": "Parameter",
|
"com_sidepanel_parameters": "KI-Einstellungen",
|
||||||
"com_sidepanel_select_agent": "Wähle einen Agenten",
|
"com_sidepanel_select_agent": "Wähle einen Agenten",
|
||||||
"com_sidepanel_select_assistant": "Assistenten auswählen",
|
"com_sidepanel_select_assistant": "Assistenten auswählen",
|
||||||
|
"com_ui_2fa_account_security": "Die Zwei-Faktor-Authentifizierung bietet deinem Konto eine zusätzliche Sicherheitsebene.",
|
||||||
|
"com_ui_2fa_disable": "2FA deaktivieren",
|
||||||
|
"com_ui_2fa_disable_error": "Beim Deaktivieren der Zwei-Faktor-Authentifizierung ist ein Fehler aufgetreten.",
|
||||||
|
"com_ui_2fa_disabled": "2FA wurde deaktiviert.",
|
||||||
|
"com_ui_2fa_enable": "2FA aktivieren",
|
||||||
|
"com_ui_2fa_enabled": "2FA wurde aktiviert.",
|
||||||
|
"com_ui_2fa_generate_error": "Beim Erstellen der Einstellungen für die Zwei-Faktor-Authentifizierung ist ein Fehler aufgetreten.",
|
||||||
|
"com_ui_2fa_invalid": "Ungültiger Zwei-Faktor-Authentifizierungscode.",
|
||||||
|
"com_ui_2fa_setup": "2FA einrichten",
|
||||||
|
"com_ui_2fa_verified": "Die Zwei-Faktor-Authentifizierung wurde erfolgreich verifiziert.",
|
||||||
"com_ui_accept": "Ich akzeptiere",
|
"com_ui_accept": "Ich akzeptiere",
|
||||||
"com_ui_add": "Hinzufügen",
|
"com_ui_add": "Hinzufügen",
|
||||||
"com_ui_add_model_preset": "Ein KI-Modell oder eine Voreinstellung für eine zusätzliche Antwort hinzufügen",
|
"com_ui_add_model_preset": "Ein KI-Modell oder eine Voreinstellung für eine zusätzliche Antwort hinzufügen",
|
||||||
|
|
@ -435,17 +458,22 @@
|
||||||
"com_ui_agent_duplicate_error": "Beim Duplizieren des Assistenten ist ein Fehler aufgetreten",
|
"com_ui_agent_duplicate_error": "Beim Duplizieren des Assistenten ist ein Fehler aufgetreten",
|
||||||
"com_ui_agent_duplicated": "Agent wurde erfolgreich dupliziert",
|
"com_ui_agent_duplicated": "Agent wurde erfolgreich dupliziert",
|
||||||
"com_ui_agent_editing_allowed": "Andere Nutzende können diesen Agenten bereits bearbeiten",
|
"com_ui_agent_editing_allowed": "Andere Nutzende können diesen Agenten bereits bearbeiten",
|
||||||
|
"com_ui_agent_shared_to_all": "Hier muss etwas eingegeben werden. War leer.",
|
||||||
"com_ui_agents": "Agenten",
|
"com_ui_agents": "Agenten",
|
||||||
"com_ui_agents_allow_create": "Erstellung von Assistenten erlauben",
|
"com_ui_agents_allow_create": "Erstellung von Assistenten erlauben",
|
||||||
"com_ui_agents_allow_share_global": "Assistenten für alle Nutzenden freigeben",
|
"com_ui_agents_allow_share_global": "Assistenten für alle Nutzenden freigeben",
|
||||||
"com_ui_agents_allow_use": "Verwendung von Agenten erlauben",
|
"com_ui_agents_allow_use": "Verwendung von Agenten erlauben",
|
||||||
"com_ui_all": "alle",
|
"com_ui_all": "alle",
|
||||||
"com_ui_all_proper": "Alle",
|
"com_ui_all_proper": "Alle",
|
||||||
|
"com_ui_analyzing": "Analyse läuft",
|
||||||
|
"com_ui_analyzing_finished": "Analyse abgeschlossen",
|
||||||
|
"com_ui_api_key": "API-Schlüssel",
|
||||||
"com_ui_archive": "Archivieren",
|
"com_ui_archive": "Archivieren",
|
||||||
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
|
"com_ui_archive_error": "Konversation konnte nicht archiviert werden",
|
||||||
"com_ui_artifact_click": "Zum Öffnen klicken",
|
"com_ui_artifact_click": "Zum Öffnen klicken",
|
||||||
"com_ui_artifacts": "Artefakte",
|
"com_ui_artifacts": "Artefakte",
|
||||||
"com_ui_artifacts_toggle": "Artefakte-Funktion einschalten",
|
"com_ui_artifacts_toggle": "Artefakte-Funktion einschalten",
|
||||||
|
"com_ui_artifacts_toggle_agent": "Artefakte aktivieren",
|
||||||
"com_ui_ascending": "Aufsteigend",
|
"com_ui_ascending": "Aufsteigend",
|
||||||
"com_ui_assistant": "Assistent",
|
"com_ui_assistant": "Assistent",
|
||||||
"com_ui_assistant_delete_error": "Beim Löschen des Assistenten ist ein Fehler aufgetreten",
|
"com_ui_assistant_delete_error": "Beim Löschen des Assistenten ist ein Fehler aufgetreten",
|
||||||
|
|
@ -458,10 +486,20 @@
|
||||||
"com_ui_attach_error_type": "Nicht unterstützter Dateityp für Endpunkt:",
|
"com_ui_attach_error_type": "Nicht unterstützter Dateityp für Endpunkt:",
|
||||||
"com_ui_attach_warn_endpoint": "Nicht-Assistentendateien werden möglicherweise ohne kompatibles Werkzeug ignoriert",
|
"com_ui_attach_warn_endpoint": "Nicht-Assistentendateien werden möglicherweise ohne kompatibles Werkzeug ignoriert",
|
||||||
"com_ui_attachment": "Anhang",
|
"com_ui_attachment": "Anhang",
|
||||||
|
"com_ui_auth_type": "Authentifizierungstyp",
|
||||||
|
"com_ui_auth_url": "Autorisierungs-URL",
|
||||||
"com_ui_authentication": "Authentifizierung",
|
"com_ui_authentication": "Authentifizierung",
|
||||||
|
"com_ui_authentication_type": "Authentifizierungstyp",
|
||||||
"com_ui_avatar": "Avatar",
|
"com_ui_avatar": "Avatar",
|
||||||
|
"com_ui_azure": "Azure",
|
||||||
"com_ui_back_to_chat": "Zurück zum Chat",
|
"com_ui_back_to_chat": "Zurück zum Chat",
|
||||||
"com_ui_back_to_prompts": "Zurück zu den Prompts",
|
"com_ui_back_to_prompts": "Zurück zu den Prompts",
|
||||||
|
"com_ui_backup_codes": "Backup-Codes",
|
||||||
|
"com_ui_backup_codes_regenerate_error": "Beim Neuerstellen der Backup-Codes ist ein Fehler aufgetreten.",
|
||||||
|
"com_ui_backup_codes_regenerated": "Backup-Codes wurden erfolgreich neu erstellt.",
|
||||||
|
"com_ui_basic": "Basic",
|
||||||
|
"com_ui_basic_auth_header": "Basic-Authentifizierungsheader",
|
||||||
|
"com_ui_bearer": "Bearer",
|
||||||
"com_ui_bookmark_delete_confirm": "Bist du sicher, dass du dieses Lesezeichen löschen möchtest?",
|
"com_ui_bookmark_delete_confirm": "Bist du sicher, dass du dieses Lesezeichen löschen möchtest?",
|
||||||
"com_ui_bookmarks": "Lesezeichen",
|
"com_ui_bookmarks": "Lesezeichen",
|
||||||
"com_ui_bookmarks_add": "Lesezeichen hinzufügen",
|
"com_ui_bookmarks_add": "Lesezeichen hinzufügen",
|
||||||
|
|
@ -480,17 +518,24 @@
|
||||||
"com_ui_bookmarks_title": "Titel",
|
"com_ui_bookmarks_title": "Titel",
|
||||||
"com_ui_bookmarks_update_error": "Beim Aktualisieren des Lesezeichens ist ein Fehler aufgetreten",
|
"com_ui_bookmarks_update_error": "Beim Aktualisieren des Lesezeichens ist ein Fehler aufgetreten",
|
||||||
"com_ui_bookmarks_update_success": "Lesezeichen erfolgreich aktualisiert",
|
"com_ui_bookmarks_update_success": "Lesezeichen erfolgreich aktualisiert",
|
||||||
|
"com_ui_bulk_delete_error": "Geteilte Links konnten nicht gelöscht werden.",
|
||||||
|
"com_ui_callback_url": "Callback-URL",
|
||||||
"com_ui_cancel": "Abbrechen",
|
"com_ui_cancel": "Abbrechen",
|
||||||
"com_ui_chat": "Chat",
|
"com_ui_chat": "Chat",
|
||||||
"com_ui_chat_history": "Chatverlauf",
|
"com_ui_chat_history": "Chatverlauf",
|
||||||
"com_ui_clear": "Löschen",
|
"com_ui_clear": "Löschen",
|
||||||
"com_ui_clear_all": "Auswahl löschen",
|
"com_ui_clear_all": "Auswahl löschen",
|
||||||
|
"com_ui_client_secret": "Client Secret",
|
||||||
"com_ui_close": "Schließen",
|
"com_ui_close": "Schließen",
|
||||||
|
"com_ui_close_menu": "Menü schließen",
|
||||||
"com_ui_code": "Code",
|
"com_ui_code": "Code",
|
||||||
"com_ui_collapse_chat": "Chat einklappen",
|
"com_ui_collapse_chat": "Chat einklappen",
|
||||||
"com_ui_command_placeholder": "Optional: Gib einen Promptbefehl ein oder den Namen.",
|
"com_ui_command_placeholder": "Optional: Gib einen Promptbefehl ein oder den Namen.",
|
||||||
"com_ui_command_usage_placeholder": "Wähle einen Prompt nach Befehl oder Name aus",
|
"com_ui_command_usage_placeholder": "Wähle einen Prompt nach Befehl oder Name aus",
|
||||||
|
"com_ui_complete_setup": "Einrichtung abschließen",
|
||||||
"com_ui_confirm_action": "Aktion bestätigen",
|
"com_ui_confirm_action": "Aktion bestätigen",
|
||||||
|
"com_ui_confirm_admin_use_change": "Wenn du diese Einstellung änderst, wird der Zugriff für Administratoren, einschließlich dir selbst, gesperrt. Bist du sicher, dass du fortfahren möchtest?",
|
||||||
|
"com_ui_confirm_change": "Änderung bestätigen",
|
||||||
"com_ui_context": "Kontext",
|
"com_ui_context": "Kontext",
|
||||||
"com_ui_continue": "Fortfahren",
|
"com_ui_continue": "Fortfahren",
|
||||||
"com_ui_controls": "Steuerung",
|
"com_ui_controls": "Steuerung",
|
||||||
|
|
@ -502,6 +547,9 @@
|
||||||
"com_ui_create": "Erstellen",
|
"com_ui_create": "Erstellen",
|
||||||
"com_ui_create_link": "Link erstellen",
|
"com_ui_create_link": "Link erstellen",
|
||||||
"com_ui_create_prompt": "Prompt erstellen",
|
"com_ui_create_prompt": "Prompt erstellen",
|
||||||
|
"com_ui_currently_production": "Aktuell im Produktivbetrieb",
|
||||||
|
"com_ui_custom": "Benutzerdefiniert",
|
||||||
|
"com_ui_custom_header_name": "Benutzerdefinierter Headername",
|
||||||
"com_ui_custom_prompt_mode": "Benutzerdefinierter Promptmodus für Artefakte",
|
"com_ui_custom_prompt_mode": "Benutzerdefinierter Promptmodus für Artefakte",
|
||||||
"com_ui_dashboard": "Dashboard",
|
"com_ui_dashboard": "Dashboard",
|
||||||
"com_ui_date": "Datum",
|
"com_ui_date": "Datum",
|
||||||
|
|
@ -522,6 +570,7 @@
|
||||||
"com_ui_date_today": "Heute",
|
"com_ui_date_today": "Heute",
|
||||||
"com_ui_date_yesterday": "Gestern",
|
"com_ui_date_yesterday": "Gestern",
|
||||||
"com_ui_decline": "Ich akzeptiere nicht",
|
"com_ui_decline": "Ich akzeptiere nicht",
|
||||||
|
"com_ui_default_post_request": "Standard (POST-Anfrage)",
|
||||||
"com_ui_delete": "Löschen",
|
"com_ui_delete": "Löschen",
|
||||||
"com_ui_delete_action": "Aktion löschen",
|
"com_ui_delete_action": "Aktion löschen",
|
||||||
"com_ui_delete_action_confirm": "Bist du sicher, dass du diese Aktion löschen möchtest?",
|
"com_ui_delete_action_confirm": "Bist du sicher, dass du diese Aktion löschen möchtest?",
|
||||||
|
|
@ -537,6 +586,11 @@
|
||||||
"com_ui_descending": "Absteigend",
|
"com_ui_descending": "Absteigend",
|
||||||
"com_ui_description": "Beschreibung",
|
"com_ui_description": "Beschreibung",
|
||||||
"com_ui_description_placeholder": "Optional: Gib eine Beschreibung für den Prompt ein",
|
"com_ui_description_placeholder": "Optional: Gib eine Beschreibung für den Prompt ein",
|
||||||
|
"com_ui_disabling": "Deaktiviere …",
|
||||||
|
"com_ui_download": "Herunterladen",
|
||||||
|
"com_ui_download_artifact": "Artefakt herunterladen",
|
||||||
|
"com_ui_download_backup": "Backup-Codes herunterladen",
|
||||||
|
"com_ui_download_backup_tooltip": "Bevor du fortfährst, lade bitte deine Backup-Codes herunter. Du benötigst sie, um den Zugang wiederherzustellen, falls du dein Authentifizierungsgerät verlierst.",
|
||||||
"com_ui_download_error": "Fehler beim Herunterladen der Datei. Die Datei wurde möglicherweise gelöscht.",
|
"com_ui_download_error": "Fehler beim Herunterladen der Datei. Die Datei wurde möglicherweise gelöscht.",
|
||||||
"com_ui_dropdown_variables": "Dropdown-Variablen:",
|
"com_ui_dropdown_variables": "Dropdown-Variablen:",
|
||||||
"com_ui_dropdown_variables_info": "Erstellen Sie benutzerdefinierte Dropdown-Menüs für Ihre Eingabeaufforderungen: `{{variable_name:option1|option2|option3}}`",
|
"com_ui_dropdown_variables_info": "Erstellen Sie benutzerdefinierte Dropdown-Menüs für Ihre Eingabeaufforderungen: `{{variable_name:option1|option2|option3}}`",
|
||||||
|
|
@ -558,7 +612,9 @@
|
||||||
"com_ui_examples": "Beispiele",
|
"com_ui_examples": "Beispiele",
|
||||||
"com_ui_export_convo_modal": "Konversation exportieren",
|
"com_ui_export_convo_modal": "Konversation exportieren",
|
||||||
"com_ui_field_required": "Dieses Feld ist erforderlich",
|
"com_ui_field_required": "Dieses Feld ist erforderlich",
|
||||||
|
"com_ui_filter_prompts": "Prompts filtern",
|
||||||
"com_ui_filter_prompts_name": "Prompts nach Namen filtern",
|
"com_ui_filter_prompts_name": "Prompts nach Namen filtern",
|
||||||
|
"com_ui_finance": "Finanzen",
|
||||||
"com_ui_fork": "Abzweigen",
|
"com_ui_fork": "Abzweigen",
|
||||||
"com_ui_fork_all_target": "Alle bis/von hier einbeziehen",
|
"com_ui_fork_all_target": "Alle bis/von hier einbeziehen",
|
||||||
"com_ui_fork_branches": "Zugehörige Verzweigungen einbeziehen",
|
"com_ui_fork_branches": "Zugehörige Verzweigungen einbeziehen",
|
||||||
|
|
@ -581,30 +637,41 @@
|
||||||
"com_ui_fork_split_target_setting": "Abzweigung standardmäßig von der Zielnachricht beginnen",
|
"com_ui_fork_split_target_setting": "Abzweigung standardmäßig von der Zielnachricht beginnen",
|
||||||
"com_ui_fork_success": "Konversation erfolgreich abgezweigt",
|
"com_ui_fork_success": "Konversation erfolgreich abgezweigt",
|
||||||
"com_ui_fork_visible": "Nur sichtbare Nachrichten",
|
"com_ui_fork_visible": "Nur sichtbare Nachrichten",
|
||||||
|
"com_ui_generate_backup": "Backup-Codes generieren",
|
||||||
|
"com_ui_generate_qrcode": "QR-Code generieren",
|
||||||
|
"com_ui_generating": "Generiere …",
|
||||||
|
"com_ui_go_back": "Zurück",
|
||||||
"com_ui_go_to_conversation": "Zur Konversation gehen",
|
"com_ui_go_to_conversation": "Zur Konversation gehen",
|
||||||
"com_ui_happy_birthday": "Es ist mein 1. Geburtstag!",
|
"com_ui_happy_birthday": "Es ist mein 1. Geburtstag!",
|
||||||
|
"com_ui_hide_qr": "QR-Code ausblenden",
|
||||||
"com_ui_host": "Host",
|
"com_ui_host": "Host",
|
||||||
|
"com_ui_idea": "Ideen",
|
||||||
"com_ui_image_gen": "Bildgenerierung",
|
"com_ui_image_gen": "Bildgenerierung",
|
||||||
"com_ui_import_conversation": "Importieren",
|
"com_ui_import": "Importieren",
|
||||||
"com_ui_import_conversation_error": "Beim Importieren Ihrer Konversationen ist ein Fehler aufgetreten",
|
"com_ui_import_conversation_error": "Beim Importieren Ihrer Konversationen ist ein Fehler aufgetreten",
|
||||||
"com_ui_import_conversation_file_type_error": "Nicht unterstützter Importtyp",
|
"com_ui_import_conversation_file_type_error": "Nicht unterstützter Importtyp",
|
||||||
"com_ui_import_conversation_info": "Konversationen aus einer JSON-Datei importieren",
|
"com_ui_import_conversation_info": "Konversationen aus einer JSON-Datei importieren",
|
||||||
"com_ui_import_conversation_success": "Konversationen erfolgreich importiert",
|
"com_ui_import_conversation_success": "Konversationen erfolgreich importiert",
|
||||||
"com_ui_include_shadcnui": "Anweisungen für shadcn/ui-Komponenten einschließen",
|
"com_ui_include_shadcnui": "Anweisungen für shadcn/ui-Komponenten einschließen",
|
||||||
|
"com_ui_include_shadcnui_agent": "shadcn/ui-Anweisungen einfügen",
|
||||||
"com_ui_input": "Eingabe",
|
"com_ui_input": "Eingabe",
|
||||||
"com_ui_instructions": "Anweisungen",
|
"com_ui_instructions": "Anweisungen",
|
||||||
"com_ui_latest_footer": "Alle KIs für alle.",
|
"com_ui_latest_footer": "Alle KIs für alle.",
|
||||||
|
"com_ui_latest_production_version": "Neueste Produktiv-Version",
|
||||||
|
"com_ui_latest_version": "Neueste Version",
|
||||||
"com_ui_librechat_code_api_key": "Hole dir deinen LibreChat Code Interpreter API-Schlüssel",
|
"com_ui_librechat_code_api_key": "Hole dir deinen LibreChat Code Interpreter API-Schlüssel",
|
||||||
"com_ui_librechat_code_api_subtitle": "Sicher. Mehrsprachig. Ein-/Ausgabedateien.",
|
"com_ui_librechat_code_api_subtitle": "Sicher. Mehrsprachig. Ein-/Ausgabedateien.",
|
||||||
"com_ui_librechat_code_api_title": "KI-Code ausführen",
|
"com_ui_librechat_code_api_title": "KI-Code ausführen",
|
||||||
"com_ui_llm_menu": "LLM-Menü",
|
"com_ui_llm_menu": "LLM-Menü",
|
||||||
"com_ui_llms_available": "Verfügbare LLMs",
|
"com_ui_llms_available": "Verfügbare LLMs",
|
||||||
|
"com_ui_loading": "Lade …",
|
||||||
"com_ui_locked": "Gesperrt",
|
"com_ui_locked": "Gesperrt",
|
||||||
"com_ui_logo": "{{0}} Logo",
|
"com_ui_logo": "{{0}} Logo",
|
||||||
"com_ui_manage": "Verwalten",
|
"com_ui_manage": "Verwalten",
|
||||||
"com_ui_max_tags": "Die maximale Anzahl ist {{0}}, es werden die neuesten Werte verwendet.",
|
"com_ui_max_tags": "Die maximale Anzahl ist {{0}}, es werden die neuesten Werte verwendet.",
|
||||||
"com_ui_mention": "Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln",
|
"com_ui_mention": "Erwähne einen Endpunkt, Assistenten oder eine Voreinstellung, um schnell dorthin zu wechseln",
|
||||||
"com_ui_min_tags": "Es können nicht mehr Werte entfernt werden, mindestens {{0}} sind erforderlich.",
|
"com_ui_min_tags": "Es können nicht mehr Werte entfernt werden, mindestens {{0}} sind erforderlich.",
|
||||||
|
"com_ui_misc": "Verschiedenes",
|
||||||
"com_ui_model": "KI-Modell",
|
"com_ui_model": "KI-Modell",
|
||||||
"com_ui_model_parameters": "Modell-Parameter",
|
"com_ui_model_parameters": "Modell-Parameter",
|
||||||
"com_ui_more_info": "Mehr Infos",
|
"com_ui_more_info": "Mehr Infos",
|
||||||
|
|
@ -613,15 +680,20 @@
|
||||||
"com_ui_new_chat": "Neuer Chat",
|
"com_ui_new_chat": "Neuer Chat",
|
||||||
"com_ui_next": "Weiter",
|
"com_ui_next": "Weiter",
|
||||||
"com_ui_no": "Nein",
|
"com_ui_no": "Nein",
|
||||||
|
"com_ui_no_backup_codes": "Keine Backup-Codes verfügbar. Bitte erstelle neue.",
|
||||||
"com_ui_no_bookmarks": "Du hast noch keine Lesezeichen. Klicke auf einen Chat und füge ein neues hinzu",
|
"com_ui_no_bookmarks": "Du hast noch keine Lesezeichen. Klicke auf einen Chat und füge ein neues hinzu",
|
||||||
"com_ui_no_category": "Keine Kategorie",
|
"com_ui_no_category": "Keine Kategorie",
|
||||||
"com_ui_no_changes": "Keine Änderungen zum Aktualisieren",
|
"com_ui_no_changes": "Keine Änderungen zum Aktualisieren",
|
||||||
"com_ui_no_terms_content": "Keine Inhalte der Allgemeinen Geschäftsbedingungen zum Anzeigen",
|
"com_ui_no_terms_content": "Keine Inhalte der Allgemeinen Geschäftsbedingungen zum Anzeigen",
|
||||||
|
"com_ui_none": "Keine",
|
||||||
"com_ui_none_selected": "Nichts ausgewählt",
|
"com_ui_none_selected": "Nichts ausgewählt",
|
||||||
|
"com_ui_not_used": "Nicht verwendet",
|
||||||
"com_ui_nothing_found": "Nichts gefunden",
|
"com_ui_nothing_found": "Nichts gefunden",
|
||||||
|
"com_ui_oauth": "OAuth",
|
||||||
"com_ui_of": "von",
|
"com_ui_of": "von",
|
||||||
"com_ui_off": "Aus",
|
"com_ui_off": "Aus",
|
||||||
"com_ui_on": "An",
|
"com_ui_on": "An",
|
||||||
|
"com_ui_openai": "OpenAI",
|
||||||
"com_ui_page": "Seite",
|
"com_ui_page": "Seite",
|
||||||
"com_ui_prev": "Zurück",
|
"com_ui_prev": "Zurück",
|
||||||
"com_ui_preview": "Vorschau",
|
"com_ui_preview": "Vorschau",
|
||||||
|
|
@ -641,9 +713,14 @@
|
||||||
"com_ui_prompts_allow_use": "Verwendung von Prompts erlauben",
|
"com_ui_prompts_allow_use": "Verwendung von Prompts erlauben",
|
||||||
"com_ui_provider": "Anbieter",
|
"com_ui_provider": "Anbieter",
|
||||||
"com_ui_read_aloud": "Vorlesen",
|
"com_ui_read_aloud": "Vorlesen",
|
||||||
|
"com_ui_refresh_link": "Link aktualisieren",
|
||||||
"com_ui_regenerate": "Neu generieren",
|
"com_ui_regenerate": "Neu generieren",
|
||||||
|
"com_ui_regenerate_backup": "Backup-Codes neu generieren",
|
||||||
|
"com_ui_regenerating": "Generiere neu ...",
|
||||||
"com_ui_region": "Region",
|
"com_ui_region": "Region",
|
||||||
"com_ui_rename": "Umbenennen",
|
"com_ui_rename": "Umbenennen",
|
||||||
|
"com_ui_rename_prompt": "Prompt umbenennen",
|
||||||
|
"com_ui_requires_auth": "Authentifizierung erforderlich",
|
||||||
"com_ui_reset_var": "{{0}} zurücksetzen",
|
"com_ui_reset_var": "{{0}} zurücksetzen",
|
||||||
"com_ui_result": "Ergebnis",
|
"com_ui_result": "Ergebnis",
|
||||||
"com_ui_revoke": "Widerrufen",
|
"com_ui_revoke": "Widerrufen",
|
||||||
|
|
@ -653,12 +730,16 @@
|
||||||
"com_ui_revoke_keys": "Schlüssel widerrufen",
|
"com_ui_revoke_keys": "Schlüssel widerrufen",
|
||||||
"com_ui_revoke_keys_confirm": "Bist du sicher, dass du alle Schlüssel widerrufen möchtest?",
|
"com_ui_revoke_keys_confirm": "Bist du sicher, dass du alle Schlüssel widerrufen möchtest?",
|
||||||
"com_ui_role_select": "Rolle auswählen",
|
"com_ui_role_select": "Rolle auswählen",
|
||||||
|
"com_ui_roleplay": "Rollenspiel",
|
||||||
"com_ui_run_code": "Code ausführen",
|
"com_ui_run_code": "Code ausführen",
|
||||||
"com_ui_run_code_error": "Bei der Ausführung des Codes ist ein Fehler aufgetreten",
|
"com_ui_run_code_error": "Bei der Ausführung des Codes ist ein Fehler aufgetreten",
|
||||||
"com_ui_save": "Speichern",
|
"com_ui_save": "Speichern",
|
||||||
"com_ui_save_submit": "Speichern & Absenden",
|
"com_ui_save_submit": "Speichern & Absenden",
|
||||||
"com_ui_saved": "Gespeichert!",
|
"com_ui_saved": "Gespeichert!",
|
||||||
"com_ui_schema": "Schema",
|
"com_ui_schema": "Schema",
|
||||||
|
"com_ui_scope": "Umfang",
|
||||||
|
"com_ui_search": "Suche",
|
||||||
|
"com_ui_secret_key": "Geheimschlüssel",
|
||||||
"com_ui_select": "Auswählen",
|
"com_ui_select": "Auswählen",
|
||||||
"com_ui_select_file": "Datei auswählen",
|
"com_ui_select_file": "Datei auswählen",
|
||||||
"com_ui_select_model": "Ein KI-Modell auswählen",
|
"com_ui_select_model": "Ein KI-Modell auswählen",
|
||||||
|
|
@ -677,9 +758,15 @@
|
||||||
"com_ui_share_to_all_users": "Mit allen Benutzern teilen",
|
"com_ui_share_to_all_users": "Mit allen Benutzern teilen",
|
||||||
"com_ui_share_update_message": "Ihr Name, benutzerdefinierte Anweisungen und alle Nachrichten, die du nach dem Teilen hinzufügen, bleiben privat.",
|
"com_ui_share_update_message": "Ihr Name, benutzerdefinierte Anweisungen und alle Nachrichten, die du nach dem Teilen hinzufügen, bleiben privat.",
|
||||||
"com_ui_share_var": "{{0}} teilen",
|
"com_ui_share_var": "{{0}} teilen",
|
||||||
|
"com_ui_shared_link_bulk_delete_success": "Geteilte Links erfolgreich gelöscht",
|
||||||
|
"com_ui_shared_link_delete_success": "Geteilter Link erfolgreich gelöscht",
|
||||||
"com_ui_shared_link_not_found": "Geteilter Link nicht gefunden",
|
"com_ui_shared_link_not_found": "Geteilter Link nicht gefunden",
|
||||||
"com_ui_shared_prompts": "Geteilte Prompts",
|
"com_ui_shared_prompts": "Geteilte Prompts",
|
||||||
|
"com_ui_shop": "Einkaufen",
|
||||||
|
"com_ui_show": "Anzeigen",
|
||||||
"com_ui_show_all": "Alle anzeigen",
|
"com_ui_show_all": "Alle anzeigen",
|
||||||
|
"com_ui_show_qr": "QR-Code anzeigen",
|
||||||
|
"com_ui_sign_in_to_domain": "Anmelden bei {{0}}",
|
||||||
"com_ui_simple": "Einfach",
|
"com_ui_simple": "Einfach",
|
||||||
"com_ui_size": "Größe",
|
"com_ui_size": "Größe",
|
||||||
"com_ui_special_variables": "Spezielle Variablen:",
|
"com_ui_special_variables": "Spezielle Variablen:",
|
||||||
|
|
@ -688,9 +775,16 @@
|
||||||
"com_ui_stop": "Stopp",
|
"com_ui_stop": "Stopp",
|
||||||
"com_ui_storage": "Speicher",
|
"com_ui_storage": "Speicher",
|
||||||
"com_ui_submit": "Absenden",
|
"com_ui_submit": "Absenden",
|
||||||
|
"com_ui_teach_or_explain": "Lernen",
|
||||||
|
"com_ui_temporary_chat": "Temporärer Chat",
|
||||||
"com_ui_terms_and_conditions": "Allgemeine Geschäftsbedingungen",
|
"com_ui_terms_and_conditions": "Allgemeine Geschäftsbedingungen",
|
||||||
"com_ui_terms_of_service": "Nutzungsbedingungen",
|
"com_ui_terms_of_service": "Nutzungsbedingungen",
|
||||||
|
"com_ui_thinking": "Nachdenken...",
|
||||||
|
"com_ui_thoughts": "Gedanken",
|
||||||
|
"com_ui_token_exchange_method": "Token-Austauschmethode",
|
||||||
|
"com_ui_token_url": "Token-URL",
|
||||||
"com_ui_tools": "Werkzeuge",
|
"com_ui_tools": "Werkzeuge",
|
||||||
|
"com_ui_travel": "Reisen",
|
||||||
"com_ui_unarchive": "Aus Archiv holen",
|
"com_ui_unarchive": "Aus Archiv holen",
|
||||||
"com_ui_unarchive_error": "Konversation konnte nicht aus dem Archiv geholt werden",
|
"com_ui_unarchive_error": "Konversation konnte nicht aus dem Archiv geholt werden",
|
||||||
"com_ui_unknown": "Unbekannt",
|
"com_ui_unknown": "Unbekannt",
|
||||||
|
|
@ -707,12 +801,18 @@
|
||||||
"com_ui_upload_invalid_var": "Ungültige Datei zum Hochladen. Muss ein Bild sein und {{0}} MB nicht überschreiten",
|
"com_ui_upload_invalid_var": "Ungültige Datei zum Hochladen. Muss ein Bild sein und {{0}} MB nicht überschreiten",
|
||||||
"com_ui_upload_success": "Datei erfolgreich hochgeladen",
|
"com_ui_upload_success": "Datei erfolgreich hochgeladen",
|
||||||
"com_ui_upload_type": "Upload-Typ auswählen",
|
"com_ui_upload_type": "Upload-Typ auswählen",
|
||||||
|
"com_ui_use_2fa_code": "Stattdessen 2FA-Code verwenden",
|
||||||
|
"com_ui_use_backup_code": "Stattdessen Backup-Code verwenden",
|
||||||
"com_ui_use_micrphone": "Mikrofon verwenden",
|
"com_ui_use_micrphone": "Mikrofon verwenden",
|
||||||
"com_ui_use_prompt": "Prompt verwenden",
|
"com_ui_use_prompt": "Prompt verwenden",
|
||||||
|
"com_ui_used": "Verwendet",
|
||||||
"com_ui_variables": "Variablen",
|
"com_ui_variables": "Variablen",
|
||||||
"com_ui_variables_info": "Verwende doppelte geschweifte Klammern in Ihrem Text, um Variablen zu erstellen, z.B. {{Beispielvariable}}, die du später beim Verwenden des Prompts ausfüllen kannst.",
|
"com_ui_variables_info": "Verwende doppelte geschweifte Klammern in Ihrem Text, um Variablen zu erstellen, z.B. {{Beispielvariable}}, die du später beim Verwenden des Prompts ausfüllen kannst.",
|
||||||
|
"com_ui_verify": "Überprüfen",
|
||||||
"com_ui_version_var": "Version {{0}}",
|
"com_ui_version_var": "Version {{0}}",
|
||||||
"com_ui_versions": "Versionen",
|
"com_ui_versions": "Versionen",
|
||||||
|
"com_ui_view_source": "Quell-Chat anzeigen",
|
||||||
|
"com_ui_write": "Schreiben",
|
||||||
"com_ui_yes": "Ja",
|
"com_ui_yes": "Ja",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "Du",
|
"com_user_message": "Du",
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,7 @@
|
||||||
"com_auth_email_verification_redirecting": "Redirecting in {{0}} seconds...",
|
"com_auth_email_verification_redirecting": "Redirecting in {{0}} seconds...",
|
||||||
"com_auth_email_verification_resend_prompt": "Didn't receive the email?",
|
"com_auth_email_verification_resend_prompt": "Didn't receive the email?",
|
||||||
"com_auth_email_verification_success": "Email verified successfully",
|
"com_auth_email_verification_success": "Email verified successfully",
|
||||||
|
"com_auth_email_verifying_ellipsis": "Verifying...",
|
||||||
"com_auth_error_create": "There was an error attempting to register your account. Please try again.",
|
"com_auth_error_create": "There was an error attempting to register your account. Please try again.",
|
||||||
"com_auth_error_invalid_reset_token": "This password reset token is no longer valid.",
|
"com_auth_error_invalid_reset_token": "This password reset token is no longer valid.",
|
||||||
"com_auth_error_login": "Unable to login with the information provided. Please check your credentials and try again.",
|
"com_auth_error_login": "Unable to login with the information provided. Please check your credentials and try again.",
|
||||||
|
|
@ -123,9 +124,11 @@
|
||||||
"com_auth_submit_registration": "Submit registration",
|
"com_auth_submit_registration": "Submit registration",
|
||||||
"com_auth_to_reset_your_password": "to reset your password.",
|
"com_auth_to_reset_your_password": "to reset your password.",
|
||||||
"com_auth_to_try_again": "to try again.",
|
"com_auth_to_try_again": "to try again.",
|
||||||
|
"com_auth_two_factor": "Check your preferred one-time password application for a code",
|
||||||
"com_auth_username": "Username (optional)",
|
"com_auth_username": "Username (optional)",
|
||||||
"com_auth_username_max_length": "Username must be less than 20 characters",
|
"com_auth_username_max_length": "Username must be less than 20 characters",
|
||||||
"com_auth_username_min_length": "Username must be at least 2 characters",
|
"com_auth_username_min_length": "Username must be at least 2 characters",
|
||||||
|
"com_auth_verify_your_identity": "Verify Your Identity",
|
||||||
"com_auth_welcome_back": "Welcome back",
|
"com_auth_welcome_back": "Welcome back",
|
||||||
"com_click_to_download": "(click here to download)",
|
"com_click_to_download": "(click here to download)",
|
||||||
"com_download_expired": "(download expired)",
|
"com_download_expired": "(download expired)",
|
||||||
|
|
@ -184,7 +187,6 @@
|
||||||
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
|
"com_endpoint_google_temp": "Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.",
|
||||||
"com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
|
"com_endpoint_google_topk": "Top-k changes how the model selects tokens for output. A top-k of 1 means the selected token is the most probable among all tokens in the model's vocabulary (also called greedy decoding), while a top-k of 3 means that the next token is selected from among the 3 most probable tokens (using temperature).",
|
||||||
"com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.",
|
"com_endpoint_google_topp": "Top-p changes how the model selects tokens for output. Tokens are selected from most K (see topK parameter) probable to least until the sum of their probabilities equals the top-p value.",
|
||||||
"com_endpoint_import": "Import",
|
|
||||||
"com_endpoint_instructions_assistants": "Override Instructions",
|
"com_endpoint_instructions_assistants": "Override Instructions",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.",
|
"com_endpoint_instructions_assistants_placeholder": "Overrides the instructions of the assistant. This is useful for modifying the behavior on a per-run basis.",
|
||||||
"com_endpoint_max_output_tokens": "Max Output Tokens",
|
"com_endpoint_max_output_tokens": "Max Output Tokens",
|
||||||
|
|
@ -266,13 +268,13 @@
|
||||||
"com_files_table": "something needs to go here. was empty",
|
"com_files_table": "something needs to go here. was empty",
|
||||||
"com_generated_files": "Generated files:",
|
"com_generated_files": "Generated files:",
|
||||||
"com_hide_examples": "Hide Examples",
|
"com_hide_examples": "Hide Examples",
|
||||||
|
"com_nav_2fa": "Two-Factor Authentication (2FA)",
|
||||||
"com_nav_account_settings": "Account Settings",
|
"com_nav_account_settings": "Account Settings",
|
||||||
"com_nav_always_make_prod": "Always make new versions production",
|
"com_nav_always_make_prod": "Always make new versions production",
|
||||||
"com_nav_archive_created_at": "Date Archived",
|
"com_nav_archive_created_at": "Date Archived",
|
||||||
"com_nav_archive_name": "Name",
|
"com_nav_archive_name": "Name",
|
||||||
"com_nav_archived_chats": "Archived chats",
|
"com_nav_archived_chats": "Archived chats",
|
||||||
"com_nav_archived_chats_empty": "You have no archived conversations.",
|
"com_nav_archived_chats_empty": "You have no archived conversations.",
|
||||||
"com_nav_archived_chats_manage": "Manage",
|
|
||||||
"com_nav_at_command": "@-Command",
|
"com_nav_at_command": "@-Command",
|
||||||
"com_nav_at_command_description": "Toggle command \"@\" for switching endpoints, models, presets, etc.",
|
"com_nav_at_command_description": "Toggle command \"@\" for switching endpoints, models, presets, etc.",
|
||||||
"com_nav_audio_play_error": "Error playing audio: {{0}}",
|
"com_nav_audio_play_error": "Error playing audio: {{0}}",
|
||||||
|
|
@ -348,6 +350,7 @@
|
||||||
"com_nav_lang_chinese": "中文",
|
"com_nav_lang_chinese": "中文",
|
||||||
"com_nav_lang_dutch": "Nederlands",
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
"com_nav_lang_english": "English",
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
"com_nav_lang_finnish": "Suomi",
|
"com_nav_lang_finnish": "Suomi",
|
||||||
"com_nav_lang_french": "Français ",
|
"com_nav_lang_french": "Français ",
|
||||||
"com_nav_lang_german": "Deutsch",
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
|
@ -357,10 +360,11 @@
|
||||||
"com_nav_lang_japanese": "日本語",
|
"com_nav_lang_japanese": "日本語",
|
||||||
"com_nav_lang_korean": "한국어",
|
"com_nav_lang_korean": "한국어",
|
||||||
"com_nav_lang_polish": "Polski",
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
"com_nav_lang_russian": "Русский",
|
"com_nav_lang_russian": "Русский",
|
||||||
"com_nav_lang_spanish": "Español",
|
"com_nav_lang_spanish": "Español",
|
||||||
"com_nav_lang_swedish": "Svenska",
|
"com_nav_lang_swedish": "Svenska",
|
||||||
"com_nav_lang_traditionalchinese": "繁體中文",
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
"com_nav_lang_turkish": "Türkçe",
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
"com_nav_language": "Language",
|
"com_nav_language": "Language",
|
||||||
|
|
@ -394,7 +398,6 @@
|
||||||
"com_nav_setting_speech": "Speech",
|
"com_nav_setting_speech": "Speech",
|
||||||
"com_nav_settings": "Settings",
|
"com_nav_settings": "Settings",
|
||||||
"com_nav_shared_links": "Shared links",
|
"com_nav_shared_links": "Shared links",
|
||||||
"com_nav_shared_links_manage": "Manage",
|
|
||||||
"com_nav_show_code": "Always show code when using code interpreter",
|
"com_nav_show_code": "Always show code when using code interpreter",
|
||||||
"com_nav_show_thinking": "Open Thinking Dropdowns by Default",
|
"com_nav_show_thinking": "Open Thinking Dropdowns by Default",
|
||||||
"com_nav_slash_command": "/-Command",
|
"com_nav_slash_command": "/-Command",
|
||||||
|
|
@ -435,6 +438,16 @@
|
||||||
"com_sidepanel_parameters": "Parameters",
|
"com_sidepanel_parameters": "Parameters",
|
||||||
"com_sidepanel_select_agent": "Select an Agent",
|
"com_sidepanel_select_agent": "Select an Agent",
|
||||||
"com_sidepanel_select_assistant": "Select an Assistant",
|
"com_sidepanel_select_assistant": "Select an Assistant",
|
||||||
|
"com_ui_2fa_account_security": "Two-factor authentication adds an extra layer of security to your account",
|
||||||
|
"com_ui_2fa_disable": "Disable 2FA",
|
||||||
|
"com_ui_2fa_disable_error": "There was an error disabling two-factor authentication",
|
||||||
|
"com_ui_2fa_disabled": "2FA has been disabled",
|
||||||
|
"com_ui_2fa_enable": "Enable 2FA",
|
||||||
|
"com_ui_2fa_enabled": "2FA has been enabled",
|
||||||
|
"com_ui_2fa_generate_error": "There was an error generating two-factor authentication settings",
|
||||||
|
"com_ui_2fa_invalid": "Invalid two-factor authentication code",
|
||||||
|
"com_ui_2fa_setup": "Setup 2FA",
|
||||||
|
"com_ui_2fa_verified": "Successfully verified Two-Factor Authentication",
|
||||||
"com_ui_accept": "I accept",
|
"com_ui_accept": "I accept",
|
||||||
"com_ui_add": "Add",
|
"com_ui_add": "Add",
|
||||||
"com_ui_add_model_preset": "Add a model or preset for an additional response",
|
"com_ui_add_model_preset": "Add a model or preset for an additional response",
|
||||||
|
|
@ -485,6 +498,9 @@
|
||||||
"com_ui_azure": "Azure",
|
"com_ui_azure": "Azure",
|
||||||
"com_ui_back_to_chat": "Back to Chat",
|
"com_ui_back_to_chat": "Back to Chat",
|
||||||
"com_ui_back_to_prompts": "Back to Prompts",
|
"com_ui_back_to_prompts": "Back to Prompts",
|
||||||
|
"com_ui_backup_codes": "Backup Codes",
|
||||||
|
"com_ui_backup_codes_regenerate_error": "There was an error regenerating backup codes",
|
||||||
|
"com_ui_backup_codes_regenerated": "Backup codes have been regenerated successfully",
|
||||||
"com_ui_basic": "Basic",
|
"com_ui_basic": "Basic",
|
||||||
"com_ui_basic_auth_header": "Basic authorization header",
|
"com_ui_basic_auth_header": "Basic authorization header",
|
||||||
"com_ui_bearer": "Bearer",
|
"com_ui_bearer": "Bearer",
|
||||||
|
|
@ -521,6 +537,7 @@
|
||||||
"com_ui_collapse_chat": "Collapse Chat",
|
"com_ui_collapse_chat": "Collapse Chat",
|
||||||
"com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used",
|
"com_ui_command_placeholder": "Optional: Enter a command for the prompt or name will be used",
|
||||||
"com_ui_command_usage_placeholder": "Select a Prompt by command or name",
|
"com_ui_command_usage_placeholder": "Select a Prompt by command or name",
|
||||||
|
"com_ui_complete_setup": "Complete Setup",
|
||||||
"com_ui_confirm_action": "Confirm Action",
|
"com_ui_confirm_action": "Confirm Action",
|
||||||
"com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?",
|
"com_ui_confirm_admin_use_change": "Changing this setting will block access for admins, including yourself. Are you sure you want to proceed?",
|
||||||
"com_ui_confirm_change": "Confirm Change",
|
"com_ui_confirm_change": "Confirm Change",
|
||||||
|
|
@ -574,8 +591,11 @@
|
||||||
"com_ui_descending": "Desc",
|
"com_ui_descending": "Desc",
|
||||||
"com_ui_description": "Description",
|
"com_ui_description": "Description",
|
||||||
"com_ui_description_placeholder": "Optional: Enter a description to display for the prompt",
|
"com_ui_description_placeholder": "Optional: Enter a description to display for the prompt",
|
||||||
|
"com_ui_disabling": "Disabling...",
|
||||||
"com_ui_download": "Download",
|
"com_ui_download": "Download",
|
||||||
"com_ui_download_artifact": "Download Artifact",
|
"com_ui_download_artifact": "Download Artifact",
|
||||||
|
"com_ui_download_backup": "Download Backup Codes",
|
||||||
|
"com_ui_download_backup_tooltip": "Before you continue, download your backup codes. You will need them to regain access if you lose your authenticator device",
|
||||||
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
"com_ui_download_error": "Error downloading file. The file may have been deleted.",
|
||||||
"com_ui_drag_drop": "something needs to go here. was empty",
|
"com_ui_drag_drop": "something needs to go here. was empty",
|
||||||
"com_ui_dropdown_variables": "Dropdown variables:",
|
"com_ui_dropdown_variables": "Dropdown variables:",
|
||||||
|
|
@ -585,6 +605,7 @@
|
||||||
"com_ui_duplication_processing": "Duplicating conversation...",
|
"com_ui_duplication_processing": "Duplicating conversation...",
|
||||||
"com_ui_duplication_success": "Successfully duplicated conversation",
|
"com_ui_duplication_success": "Successfully duplicated conversation",
|
||||||
"com_ui_edit": "Edit",
|
"com_ui_edit": "Edit",
|
||||||
|
"com_ui_empty_category": "-",
|
||||||
"com_ui_endpoint": "Endpoint",
|
"com_ui_endpoint": "Endpoint",
|
||||||
"com_ui_endpoint_menu": "LLM Endpoint Menu",
|
"com_ui_endpoint_menu": "LLM Endpoint Menu",
|
||||||
"com_ui_endpoints_available": "Available Endpoints",
|
"com_ui_endpoints_available": "Available Endpoints",
|
||||||
|
|
@ -600,6 +621,7 @@
|
||||||
"com_ui_field_required": "This field is required",
|
"com_ui_field_required": "This field is required",
|
||||||
"com_ui_filter_prompts": "Filter Prompts",
|
"com_ui_filter_prompts": "Filter Prompts",
|
||||||
"com_ui_filter_prompts_name": "Filter prompts by name",
|
"com_ui_filter_prompts_name": "Filter prompts by name",
|
||||||
|
"com_ui_finance": "Finance",
|
||||||
"com_ui_fork": "Fork",
|
"com_ui_fork": "Fork",
|
||||||
"com_ui_fork_all_target": "Include all to/from here",
|
"com_ui_fork_all_target": "Include all to/from here",
|
||||||
"com_ui_fork_branches": "Include related branches",
|
"com_ui_fork_branches": "Include related branches",
|
||||||
|
|
@ -622,14 +644,18 @@
|
||||||
"com_ui_fork_split_target_setting": "Start fork from target message by default",
|
"com_ui_fork_split_target_setting": "Start fork from target message by default",
|
||||||
"com_ui_fork_success": "Successfully forked conversation",
|
"com_ui_fork_success": "Successfully forked conversation",
|
||||||
"com_ui_fork_visible": "Visible messages only",
|
"com_ui_fork_visible": "Visible messages only",
|
||||||
|
"com_ui_generate_backup": "Generate Backup Codes",
|
||||||
|
"com_ui_generate_qrcode": "Generate QR Code",
|
||||||
|
"com_ui_generating": "Generating...",
|
||||||
"com_ui_global_group": "something needs to go here. was empty",
|
"com_ui_global_group": "something needs to go here. was empty",
|
||||||
"com_ui_go_back": "Go back",
|
"com_ui_go_back": "Go back",
|
||||||
"com_ui_go_to_conversation": "Go to conversation",
|
"com_ui_go_to_conversation": "Go to conversation",
|
||||||
"com_ui_happy_birthday": "It's my 1st birthday!",
|
"com_ui_happy_birthday": "It's my 1st birthday!",
|
||||||
"com_ui_hide_qr": "Hide QR Code",
|
"com_ui_hide_qr": "Hide QR Code",
|
||||||
"com_ui_host": "Host",
|
"com_ui_host": "Host",
|
||||||
|
"com_ui_idea": "Ideas",
|
||||||
"com_ui_image_gen": "Image Gen",
|
"com_ui_image_gen": "Image Gen",
|
||||||
"com_ui_import_conversation": "Import",
|
"com_ui_import": "Import",
|
||||||
"com_ui_import_conversation_error": "There was an error importing your conversations",
|
"com_ui_import_conversation_error": "There was an error importing your conversations",
|
||||||
"com_ui_import_conversation_file_type_error": "Unsupported import type",
|
"com_ui_import_conversation_file_type_error": "Unsupported import type",
|
||||||
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
"com_ui_import_conversation_info": "Import conversations from a JSON file",
|
||||||
|
|
@ -646,20 +672,24 @@
|
||||||
"com_ui_librechat_code_api_title": "Run AI Code",
|
"com_ui_librechat_code_api_title": "Run AI Code",
|
||||||
"com_ui_llm_menu": "LLM Menu",
|
"com_ui_llm_menu": "LLM Menu",
|
||||||
"com_ui_llms_available": "Available LLMs",
|
"com_ui_llms_available": "Available LLMs",
|
||||||
|
"com_ui_loading": "Loading...",
|
||||||
"com_ui_locked": "Locked",
|
"com_ui_locked": "Locked",
|
||||||
"com_ui_logo": "{{0}} Logo",
|
"com_ui_logo": "{{0}} Logo",
|
||||||
"com_ui_manage": "Manage",
|
"com_ui_manage": "Manage",
|
||||||
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
|
"com_ui_max_tags": "Maximum number allowed is {{0}}, using latest values.",
|
||||||
"com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it",
|
"com_ui_mention": "Mention an endpoint, assistant, or preset to quickly switch to it",
|
||||||
"com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.",
|
"com_ui_min_tags": "Cannot remove more values, a minimum of {{0}} are required.",
|
||||||
|
"com_ui_misc": "Misc.",
|
||||||
"com_ui_model": "Model",
|
"com_ui_model": "Model",
|
||||||
"com_ui_model_parameters": "Model Parameters",
|
"com_ui_model_parameters": "Model Parameters",
|
||||||
"com_ui_more_info": "More info",
|
"com_ui_more_info": "More info",
|
||||||
"com_ui_my_prompts": "My Prompts",
|
"com_ui_my_prompts": "My Prompts",
|
||||||
"com_ui_name": "Name",
|
"com_ui_name": "Name",
|
||||||
|
"com_ui_new": "New",
|
||||||
"com_ui_new_chat": "New chat",
|
"com_ui_new_chat": "New chat",
|
||||||
"com_ui_next": "Next",
|
"com_ui_next": "Next",
|
||||||
"com_ui_no": "No",
|
"com_ui_no": "No",
|
||||||
|
"com_ui_no_backup_codes": "No backup codes available. Please generate new ones",
|
||||||
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
|
"com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one",
|
||||||
"com_ui_no_category": "No category",
|
"com_ui_no_category": "No category",
|
||||||
"com_ui_no_changes": "No changes to update",
|
"com_ui_no_changes": "No changes to update",
|
||||||
|
|
@ -668,6 +698,7 @@
|
||||||
"com_ui_no_valid_items": "something needs to go here. was empty",
|
"com_ui_no_valid_items": "something needs to go here. was empty",
|
||||||
"com_ui_none": "None",
|
"com_ui_none": "None",
|
||||||
"com_ui_none_selected": "None selected",
|
"com_ui_none_selected": "None selected",
|
||||||
|
"com_ui_not_used": "Not Used",
|
||||||
"com_ui_nothing_found": "Nothing found",
|
"com_ui_nothing_found": "Nothing found",
|
||||||
"com_ui_oauth": "OAuth",
|
"com_ui_oauth": "OAuth",
|
||||||
"com_ui_of": "of",
|
"com_ui_of": "of",
|
||||||
|
|
@ -695,6 +726,8 @@
|
||||||
"com_ui_read_aloud": "Read aloud",
|
"com_ui_read_aloud": "Read aloud",
|
||||||
"com_ui_refresh_link": "Refresh link",
|
"com_ui_refresh_link": "Refresh link",
|
||||||
"com_ui_regenerate": "Regenerate",
|
"com_ui_regenerate": "Regenerate",
|
||||||
|
"com_ui_regenerate_backup": "Regenerate Backup Codes",
|
||||||
|
"com_ui_regenerating": "Regenerating...",
|
||||||
"com_ui_region": "Region",
|
"com_ui_region": "Region",
|
||||||
"com_ui_rename": "Rename",
|
"com_ui_rename": "Rename",
|
||||||
"com_ui_rename_prompt": "Rename Prompt",
|
"com_ui_rename_prompt": "Rename Prompt",
|
||||||
|
|
@ -708,6 +741,7 @@
|
||||||
"com_ui_revoke_keys": "Revoke Keys",
|
"com_ui_revoke_keys": "Revoke Keys",
|
||||||
"com_ui_revoke_keys_confirm": "Are you sure you want to revoke all keys?",
|
"com_ui_revoke_keys_confirm": "Are you sure you want to revoke all keys?",
|
||||||
"com_ui_role_select": "Role",
|
"com_ui_role_select": "Role",
|
||||||
|
"com_ui_roleplay": "Roleplay",
|
||||||
"com_ui_run_code": "Run Code",
|
"com_ui_run_code": "Run Code",
|
||||||
"com_ui_run_code_error": "There was an error running the code",
|
"com_ui_run_code_error": "There was an error running the code",
|
||||||
"com_ui_save": "Save",
|
"com_ui_save": "Save",
|
||||||
|
|
@ -716,6 +750,7 @@
|
||||||
"com_ui_schema": "Schema",
|
"com_ui_schema": "Schema",
|
||||||
"com_ui_scope": "Scope",
|
"com_ui_scope": "Scope",
|
||||||
"com_ui_search": "Search",
|
"com_ui_search": "Search",
|
||||||
|
"com_ui_secret_key": "Secret Key",
|
||||||
"com_ui_select": "Select",
|
"com_ui_select": "Select",
|
||||||
"com_ui_select_file": "Select a file",
|
"com_ui_select_file": "Select a file",
|
||||||
"com_ui_select_model": "Select a model",
|
"com_ui_select_model": "Select a model",
|
||||||
|
|
@ -739,6 +774,8 @@
|
||||||
"com_ui_shared_link_delete_success": "Successfully deleted shared link",
|
"com_ui_shared_link_delete_success": "Successfully deleted shared link",
|
||||||
"com_ui_shared_link_not_found": "Shared link not found",
|
"com_ui_shared_link_not_found": "Shared link not found",
|
||||||
"com_ui_shared_prompts": "Shared Prompts",
|
"com_ui_shared_prompts": "Shared Prompts",
|
||||||
|
"com_ui_shop": "Shopping",
|
||||||
|
"com_ui_show": "Show",
|
||||||
"com_ui_show_all": "Show All",
|
"com_ui_show_all": "Show All",
|
||||||
"com_ui_show_qr": "Show QR Code",
|
"com_ui_show_qr": "Show QR Code",
|
||||||
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
|
"com_ui_sign_in_to_domain": "Sign-in to {{0}}",
|
||||||
|
|
@ -750,6 +787,7 @@
|
||||||
"com_ui_stop": "Stop",
|
"com_ui_stop": "Stop",
|
||||||
"com_ui_storage": "Storage",
|
"com_ui_storage": "Storage",
|
||||||
"com_ui_submit": "Submit",
|
"com_ui_submit": "Submit",
|
||||||
|
"com_ui_teach_or_explain": "Learning",
|
||||||
"com_ui_temporary_chat": "Temporary Chat",
|
"com_ui_temporary_chat": "Temporary Chat",
|
||||||
"com_ui_terms_and_conditions": "Terms and Conditions",
|
"com_ui_terms_and_conditions": "Terms and Conditions",
|
||||||
"com_ui_terms_of_service": "Terms of service",
|
"com_ui_terms_of_service": "Terms of service",
|
||||||
|
|
@ -758,6 +796,7 @@
|
||||||
"com_ui_token_exchange_method": "Token Exchange Method",
|
"com_ui_token_exchange_method": "Token Exchange Method",
|
||||||
"com_ui_token_url": "Token URL",
|
"com_ui_token_url": "Token URL",
|
||||||
"com_ui_tools": "Tools",
|
"com_ui_tools": "Tools",
|
||||||
|
"com_ui_travel": "Travel",
|
||||||
"com_ui_unarchive": "Unarchive",
|
"com_ui_unarchive": "Unarchive",
|
||||||
"com_ui_unarchive_error": "Failed to unarchive conversation",
|
"com_ui_unarchive_error": "Failed to unarchive conversation",
|
||||||
"com_ui_unknown": "Unknown",
|
"com_ui_unknown": "Unknown",
|
||||||
|
|
@ -774,13 +813,18 @@
|
||||||
"com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB",
|
"com_ui_upload_invalid_var": "Invalid file for upload. Must be an image not exceeding {{0}} MB",
|
||||||
"com_ui_upload_success": "Successfully uploaded file",
|
"com_ui_upload_success": "Successfully uploaded file",
|
||||||
"com_ui_upload_type": "Select Upload Type",
|
"com_ui_upload_type": "Select Upload Type",
|
||||||
|
"com_ui_use_2fa_code": "Use 2FA Code Instead",
|
||||||
|
"com_ui_use_backup_code": "Use Backup Code Instead",
|
||||||
"com_ui_use_micrphone": "Use microphone",
|
"com_ui_use_micrphone": "Use microphone",
|
||||||
"com_ui_use_prompt": "Use prompt",
|
"com_ui_use_prompt": "Use prompt",
|
||||||
|
"com_ui_used": "Used",
|
||||||
"com_ui_variables": "Variables",
|
"com_ui_variables": "Variables",
|
||||||
"com_ui_variables_info": "Use double braces in your text to create variables, e.g. `{{example variable}}`, to later fill when using the prompt.",
|
"com_ui_variables_info": "Use double braces in your text to create variables, e.g. `{{example variable}}`, to later fill when using the prompt.",
|
||||||
|
"com_ui_verify": "Verify",
|
||||||
"com_ui_version_var": "Version {{0}}",
|
"com_ui_version_var": "Version {{0}}",
|
||||||
"com_ui_versions": "Versions",
|
"com_ui_versions": "Versions",
|
||||||
"com_ui_view_source": "View source chat",
|
"com_ui_view_source": "View source chat",
|
||||||
|
"com_ui_write": "Writing",
|
||||||
"com_ui_yes": "Yes",
|
"com_ui_yes": "Yes",
|
||||||
"com_ui_zoom": "Zoom",
|
"com_ui_zoom": "Zoom",
|
||||||
"com_user_message": "You",
|
"com_user_message": "You",
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,6 @@
|
||||||
"com_endpoint_google_temp": "Los valores más altos = más aleatorios, mientras que los valores más bajos = más enfocados y deterministas. Recomendamos alterar esto o Top P, pero no ambos.",
|
"com_endpoint_google_temp": "Los valores más altos = más aleatorios, mientras que los valores más bajos = más enfocados y deterministas. Recomendamos alterar esto o Top P, pero no ambos.",
|
||||||
"com_endpoint_google_topk": "Top-k cambia la forma en que el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el siguiente token se selecciona entre los 3 tokens más probables (usando temperatura).",
|
"com_endpoint_google_topk": "Top-k cambia la forma en que el modelo selecciona tokens para la salida. Un top-k de 1 significa que el token seleccionado es el más probable entre todos los tokens en el vocabulario del modelo (también llamado decodificación codiciosa), mientras que un top-k de 3 significa que el siguiente token se selecciona entre los 3 tokens más probables (usando temperatura).",
|
||||||
"com_endpoint_google_topp": "Top-p cambia la forma en que el modelo selecciona tokens para la salida. Los tokens se seleccionan desde los más K (ver parámetro topK) probables hasta los menos probables hasta que la suma de sus probabilidades sea igual al valor top-p.",
|
"com_endpoint_google_topp": "Top-p cambia la forma en que el modelo selecciona tokens para la salida. Los tokens se seleccionan desde los más K (ver parámetro topK) probables hasta los menos probables hasta que la suma de sus probabilidades sea igual al valor top-p.",
|
||||||
"com_endpoint_import": "Importar",
|
|
||||||
"com_endpoint_instructions_assistants": "Anular instrucciones",
|
"com_endpoint_instructions_assistants": "Anular instrucciones",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "Anula las instrucciones del asistente. Esto es útil para modificar el comportamiento por ejecución.",
|
"com_endpoint_instructions_assistants_placeholder": "Anula las instrucciones del asistente. Esto es útil para modificar el comportamiento por ejecución.",
|
||||||
"com_endpoint_max_output_tokens": "Tokens de Salida Máximos",
|
"com_endpoint_max_output_tokens": "Tokens de Salida Máximos",
|
||||||
|
|
@ -262,7 +261,6 @@
|
||||||
"com_nav_archive_name": "Nombre",
|
"com_nav_archive_name": "Nombre",
|
||||||
"com_nav_archived_chats": "Archivadas",
|
"com_nav_archived_chats": "Archivadas",
|
||||||
"com_nav_archived_chats_empty": "No tienes conversaciones archivadas.",
|
"com_nav_archived_chats_empty": "No tienes conversaciones archivadas.",
|
||||||
"com_nav_archived_chats_manage": "Gestionar",
|
|
||||||
"com_nav_at_command": "Comando @",
|
"com_nav_at_command": "Comando @",
|
||||||
"com_nav_at_command_description": "Alternar comando \"@\" para cambiar entre puntos de conexión, modelos, ajustes predefinidos, etc.",
|
"com_nav_at_command_description": "Alternar comando \"@\" para cambiar entre puntos de conexión, modelos, ajustes predefinidos, etc.",
|
||||||
"com_nav_audio_play_error": "Error al reproducir el audio: {{0}}",
|
"com_nav_audio_play_error": "Error al reproducir el audio: {{0}}",
|
||||||
|
|
@ -336,6 +334,7 @@
|
||||||
"com_nav_lang_chinese": "中文",
|
"com_nav_lang_chinese": "中文",
|
||||||
"com_nav_lang_dutch": "Nederlands",
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
"com_nav_lang_english": "English",
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
"com_nav_lang_finnish": "Suomi",
|
"com_nav_lang_finnish": "Suomi",
|
||||||
"com_nav_lang_french": "Français ",
|
"com_nav_lang_french": "Français ",
|
||||||
"com_nav_lang_german": "Deutsch",
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
|
@ -345,10 +344,11 @@
|
||||||
"com_nav_lang_japanese": "日本語",
|
"com_nav_lang_japanese": "日本語",
|
||||||
"com_nav_lang_korean": "한국어",
|
"com_nav_lang_korean": "한국어",
|
||||||
"com_nav_lang_polish": "Polski",
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
"com_nav_lang_russian": "Русский",
|
"com_nav_lang_russian": "Русский",
|
||||||
"com_nav_lang_spanish": "Español",
|
"com_nav_lang_spanish": "Español",
|
||||||
"com_nav_lang_swedish": "Svenska",
|
"com_nav_lang_swedish": "Svenska",
|
||||||
"com_nav_lang_traditionalchinese": "繁體中文",
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
"com_nav_lang_turkish": "Türkçe",
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
"com_nav_language": "Idioma",
|
"com_nav_language": "Idioma",
|
||||||
|
|
@ -381,7 +381,6 @@
|
||||||
"com_nav_setting_speech": "Voz y habla",
|
"com_nav_setting_speech": "Voz y habla",
|
||||||
"com_nav_settings": "Configuración",
|
"com_nav_settings": "Configuración",
|
||||||
"com_nav_shared_links": "Links Compartidos",
|
"com_nav_shared_links": "Links Compartidos",
|
||||||
"com_nav_shared_links_manage": "Gerenciar",
|
|
||||||
"com_nav_show_code": "Mostrar siempre el código cuando se use el intérprete de código",
|
"com_nav_show_code": "Mostrar siempre el código cuando se use el intérprete de código",
|
||||||
"com_nav_slash_command": "Comando /",
|
"com_nav_slash_command": "Comando /",
|
||||||
"com_nav_slash_command_description": "Alternar comando '/' para seleccionar un mensaje predefinido mediante el teclado",
|
"com_nav_slash_command_description": "Alternar comando '/' para seleccionar un mensaje predefinido mediante el teclado",
|
||||||
|
|
@ -585,7 +584,6 @@
|
||||||
"com_ui_happy_birthday": "¡Es mi primer cumpleaños!",
|
"com_ui_happy_birthday": "¡Es mi primer cumpleaños!",
|
||||||
"com_ui_host": "Host",
|
"com_ui_host": "Host",
|
||||||
"com_ui_image_gen": "Gen Imágenes",
|
"com_ui_image_gen": "Gen Imágenes",
|
||||||
"com_ui_import_conversation": "Importar",
|
|
||||||
"com_ui_import_conversation_error": "Hubo un error al importar tus chats",
|
"com_ui_import_conversation_error": "Hubo un error al importar tus chats",
|
||||||
"com_ui_import_conversation_file_type_error": "com_ui_import_conversation_file_type_error: Tipo de archivo no compatible para importar",
|
"com_ui_import_conversation_file_type_error": "com_ui_import_conversation_file_type_error: Tipo de archivo no compatible para importar",
|
||||||
"com_ui_import_conversation_info": "Importar chats de un archivo JSON",
|
"com_ui_import_conversation_info": "Importar chats de un archivo JSON",
|
||||||
|
|
|
||||||
796
client/src/locales/et/translation.json
Normal file
796
client/src/locales/et/translation.json
Normal file
|
|
@ -0,0 +1,796 @@
|
||||||
|
{
|
||||||
|
"chat_direction_left_to_right": "Joonda vestlus vasakult paremale.",
|
||||||
|
"chat_direction_right_to_left": "Joonda vestlus paremalt vasakule.",
|
||||||
|
"com_a11y_ai_composing": "AI genereerib vastust.",
|
||||||
|
"com_a11y_end": "AI on oma vastuse lõpetanud.",
|
||||||
|
"com_a11y_start": "AI on oma vastuse andmise alustanud.",
|
||||||
|
"com_agents_allow_editing": "Luba teistel kasutajatel sinu agenti muuta",
|
||||||
|
"com_agents_by_librechat": "LibreChatilt",
|
||||||
|
"com_agents_code_interpreter": "Kui see on lubatud, saab sinu agent kasutada LibreChati koodiinterpreteerimise API-t genereeritud koodi turvaliseks käivitamiseks, sealhulgas failide töötlemiseks. Vajalik on kehtiv API võti.",
|
||||||
|
"com_agents_code_interpreter_title": "Koodiinterpreteerimise API",
|
||||||
|
"com_agents_create_error": "Agendi loomisel tekkis viga.",
|
||||||
|
"com_agents_description_placeholder": "Valikuline: Kirjelda oma agenti siin",
|
||||||
|
"com_agents_enable_file_search": "Luba failiotsing",
|
||||||
|
"com_agents_file_search_disabled": "Agent tuleb luua enne failide üleslaadimist failiotsinguks.",
|
||||||
|
"com_agents_file_search_info": "Kui see on lubatud, teavitatakse agenti täpselt allpool loetletud failinimedest, mis võimaldab tal nendest failidest asjakohast konteksti hankida.",
|
||||||
|
"com_agents_instructions_placeholder": "Süsteemijuhised, mida agent kasutab",
|
||||||
|
"com_agents_missing_provider_model": "Enne agendi loomist valige teenusepakkuja ja mudel.",
|
||||||
|
"com_agents_name_placeholder": "Valikuline: Agendi nimi",
|
||||||
|
"com_agents_no_access": "Sul pole õigust seda agenti muuta.",
|
||||||
|
"com_agents_not_available": "Agent pole saadaval",
|
||||||
|
"com_agents_search_name": "Otsi agente nime järgi",
|
||||||
|
"com_agents_update_error": "Agendi uuendamisel tekkis viga.",
|
||||||
|
"com_assistants_action_attempt": "Assistent soovib suhelda: {{0}}",
|
||||||
|
"com_assistants_actions": "Tegevused",
|
||||||
|
"com_assistants_actions_disabled": "Enne tegevuste lisamist peate looma assistendi.",
|
||||||
|
"com_assistants_actions_info": "Lubage oma assistendil API-de kaudu teavet hankida või toiminguid teha",
|
||||||
|
"com_assistants_add_actions": "Lisa tegevusi",
|
||||||
|
"com_assistants_add_tools": "Lisa tööriistu",
|
||||||
|
"com_assistants_allow_sites_you_trust": "Luba ainult usaldusväärseid saite.",
|
||||||
|
"com_assistants_append_date": "Lisa praegune kuupäev ja kellaaeg",
|
||||||
|
"com_assistants_append_date_tooltip": "Kui see on lubatud, lisatakse praegune kliendi kuupäev ja kellaaeg assistendi süsteemijuhistele.",
|
||||||
|
"com_assistants_attempt_info": "Assistent soovib saata järgmist:",
|
||||||
|
"com_assistants_available_actions": "Saadaolevad tegevused",
|
||||||
|
"com_assistants_capabilities": "Võimed",
|
||||||
|
"com_assistants_code_interpreter": "Koodiinterpreteerija",
|
||||||
|
"com_assistants_code_interpreter_files": "Allpool olevad failid on ainult koodiinterpreteerijale:",
|
||||||
|
"com_assistants_code_interpreter_info": "Koodiinterpreteerija võimaldab assistendil kirjutada ja käivitada koodi. See tööriist saab töödelda mitmekesise andmestruktuuri ja vorminguga faile ning genereerida faile, näiteks graafikuid.",
|
||||||
|
"com_assistants_completed_action": "Suhtles {{0}}",
|
||||||
|
"com_assistants_completed_function": "Käivitas {{0}}",
|
||||||
|
"com_assistants_conversation_starters": "Vestluse alustajad",
|
||||||
|
"com_assistants_conversation_starters_placeholder": "Sisesta vestluse alustaja",
|
||||||
|
"com_assistants_create_error": "Assistendi loomisel tekkis viga.",
|
||||||
|
"com_assistants_create_success": "Loomine õnnestus",
|
||||||
|
"com_assistants_delete_actions_error": "Tegevuse kustutamisel tekkis viga.",
|
||||||
|
"com_assistants_delete_actions_success": "Tegevuse kustutamine assistendilt õnnestus",
|
||||||
|
"com_assistants_description_placeholder": "Valikuline: Kirjelda oma assistenti siin",
|
||||||
|
"com_assistants_domain_info": "Assistent saatis selle teabe aadressile {{0}}",
|
||||||
|
"com_assistants_file_search": "Failiotsing",
|
||||||
|
"com_assistants_file_search_info": "Failiotsing võimaldab assistendil kasutada teadmisi failidest, mille sina või sinu kasutajad üles laadite. Kui fail on üles laaditud, otsustab assistent automaatselt, millal kasutajate päringute põhjal sisu hankida. Vektorhoidlate lisamine failiotsinguks pole veel toetatud. Saate neid lisada teenusepakkuja mänguväljakult või lisada faile sõnumitele, et neid lõimes failiotsinguks kasutada.",
|
||||||
|
"com_assistants_function_use": "Assistent kasutas {{0}}",
|
||||||
|
"com_assistants_image_vision": "Pildi nägemine",
|
||||||
|
"com_assistants_instructions_placeholder": "Süsteemijuhised, mida assistent kasutab",
|
||||||
|
"com_assistants_knowledge": "Teadmised",
|
||||||
|
"com_assistants_knowledge_disabled": "Enne failide teadmistena üleslaadimist tuleb assistent luua ning koodiinterpreteerija või otsing lubada ja salvestada.",
|
||||||
|
"com_assistants_knowledge_info": "Kui laadite faile üles jaotise Teadmised alla, võivad vestlused teie assistendiga sisaldada failide sisu.",
|
||||||
|
"com_assistants_max_starters_reached": "Vestluse alustajate maksimaalne arv on saavutatud",
|
||||||
|
"com_assistants_name_placeholder": "Valikuline: Assistendi nimi",
|
||||||
|
"com_assistants_non_retrieval_model": "Selles mudelis pole failiotsing lubatud. Valige mõni muu mudel.",
|
||||||
|
"com_assistants_retrieval": "Otsing",
|
||||||
|
"com_assistants_running_action": "Käivitatakse tegevust",
|
||||||
|
"com_assistants_search_name": "Otsi assistente nime järgi",
|
||||||
|
"com_assistants_update_actions_error": "Tegevuse loomisel või uuendamisel tekkis viga.",
|
||||||
|
"com_assistants_update_actions_success": "Tegevuse loomine või uuendamine õnnestus",
|
||||||
|
"com_assistants_update_error": "Assistendi uuendamisel tekkis viga.",
|
||||||
|
"com_assistants_update_success": "Uuendamine õnnestus",
|
||||||
|
"com_auth_already_have_account": "Sul on juba konto?",
|
||||||
|
"com_auth_apple_login": "Logi sisse Apple'iga",
|
||||||
|
"com_auth_back_to_login": "Tagasi sisselogimisele",
|
||||||
|
"com_auth_click": "Vajuta",
|
||||||
|
"com_auth_click_here": "Vajuta siia",
|
||||||
|
"com_auth_continue": "Jätka",
|
||||||
|
"com_auth_create_account": "Loo oma konto",
|
||||||
|
"com_auth_discord_login": "Jätka Discordiga",
|
||||||
|
"com_auth_email": "E-post",
|
||||||
|
"com_auth_email_address": "E-posti aadress",
|
||||||
|
"com_auth_email_max_length": "E-post ei tohiks olla pikem kui 120 tähemärki",
|
||||||
|
"com_auth_email_min_length": "E-post peab olema vähemalt 6 tähemärki",
|
||||||
|
"com_auth_email_pattern": "Sa pead sisestama kehtiva e-posti aadressi",
|
||||||
|
"com_auth_email_required": "E-post on kohustuslik",
|
||||||
|
"com_auth_email_resend_link": "Saada e-kiri uuesti",
|
||||||
|
"com_auth_email_resent_failed": "E-kirja uuesti saatmine ebaõnnestus",
|
||||||
|
"com_auth_email_resent_success": "Kinnituskiri saadeti edukalt uuesti",
|
||||||
|
"com_auth_email_verification_failed": "E-posti kinnitamine ebaõnnestus",
|
||||||
|
"com_auth_email_verification_failed_token_missing": "Kinnitamine ebaõnnestus, tunnus puudub",
|
||||||
|
"com_auth_email_verification_in_progress": "Sinu e-posti kinnitamine, palun oota",
|
||||||
|
"com_auth_email_verification_invalid": "Vigane e-posti kinnitus",
|
||||||
|
"com_auth_email_verification_redirecting": "Suunatakse ümber {{0}} sekundi pärast...",
|
||||||
|
"com_auth_email_verification_resend_prompt": "Kas sa ei saanud e-kirja?",
|
||||||
|
"com_auth_email_verification_success": "E-post kinnitatud",
|
||||||
|
"com_auth_error_create": "Konto registreerimisel tekkis viga. Proovige uuesti.",
|
||||||
|
"com_auth_error_invalid_reset_token": "See parooli lähtestamise tunnus pole enam kehtiv.",
|
||||||
|
"com_auth_error_login": "Sisselogimine esitatud teabega ei õnnestu. Palun kontrolli oma andmeid ja proovi uuesti.",
|
||||||
|
"com_auth_error_login_ban": "Sinu konto on ajutiselt blokeeritud meie teenuse rikkumiste tõttu.",
|
||||||
|
"com_auth_error_login_rl": "Liiga palju sisselogimiskatseid lühikese aja jooksul. Palun proovi hiljem uuesti.",
|
||||||
|
"com_auth_error_login_server": "Tekkis sisemine serveriviga. Palun oota hetk ja proovi uuesti.",
|
||||||
|
"com_auth_error_login_unverified": "Sinu kontot pole veel kinnitatud. Palun leia kinnituslink enda e-postist.",
|
||||||
|
"com_auth_facebook_login": "Jätka Facebookiga",
|
||||||
|
"com_auth_full_name": "Täisnimi",
|
||||||
|
"com_auth_github_login": "Jätka Githubiga",
|
||||||
|
"com_auth_google_login": "Jätka Google'iga",
|
||||||
|
"com_auth_here": "SIIA",
|
||||||
|
"com_auth_login": "Logi sisse",
|
||||||
|
"com_auth_login_with_new_password": "Nüüd saad sisse logida oma uue parooliga.",
|
||||||
|
"com_auth_name_max_length": "Nimi peab olema vähem kui 80 tähemärki",
|
||||||
|
"com_auth_name_min_length": "Nimi peab olema vähemalt 3 tähemärki",
|
||||||
|
"com_auth_name_required": "Nimi on kohustuslik",
|
||||||
|
"com_auth_no_account": "Sul pole kontot?",
|
||||||
|
"com_auth_password": "Parool",
|
||||||
|
"com_auth_password_confirm": "Kinnita parool",
|
||||||
|
"com_auth_password_forgot": "Unustasid parooli?",
|
||||||
|
"com_auth_password_max_length": "Parool peab olema vähem kui 128 tähemärki",
|
||||||
|
"com_auth_password_min_length": "Parool peab olema vähemalt 8 tähemärki",
|
||||||
|
"com_auth_password_not_match": "Paroolid ei ühti",
|
||||||
|
"com_auth_password_required": "Parool on kohustuslik",
|
||||||
|
"com_auth_registration_success_generic": "Palun vaata oma e-posti, et oma e-posti aadress kinnitada.",
|
||||||
|
"com_auth_registration_success_insecure": "Registreerimine õnnestus.",
|
||||||
|
"com_auth_reset_password": "Lähtesta oma parool",
|
||||||
|
"com_auth_reset_password_if_email_exists": "Kui selle e-postiga konto on olemas, on saadetud parooli lähtestamise juhistega e-kiri. Palun kontrolli oma rämpsposti kausta.",
|
||||||
|
"com_auth_reset_password_link_sent": "E-kiri saadetud",
|
||||||
|
"com_auth_reset_password_success": "Parooli lähtestamine õnnestus",
|
||||||
|
"com_auth_sign_in": "Logi sisse",
|
||||||
|
"com_auth_sign_up": "Registreeru",
|
||||||
|
"com_auth_submit_registration": "Saada registreerimine",
|
||||||
|
"com_auth_to_reset_your_password": "parooli lähtestamiseks.",
|
||||||
|
"com_auth_to_try_again": "uuesti proovimiseks.",
|
||||||
|
"com_auth_username": "Kasutajanimi (valikuline)",
|
||||||
|
"com_auth_username_max_length": "Kasutajanimi peab olema vähem kui 20 tähemärki",
|
||||||
|
"com_auth_username_min_length": "Kasutajanimi peab olema vähemalt 2 tähemärki",
|
||||||
|
"com_auth_welcome_back": "Teretulemast tagasi",
|
||||||
|
"com_click_to_download": "(vajuta siia, et alla laadida)",
|
||||||
|
"com_download_expired": "(allalaadimine aegunud)",
|
||||||
|
"com_download_expires": "(vajuta siia, et alla laadida - aegub {{0}})",
|
||||||
|
"com_endpoint": "Otspunkt",
|
||||||
|
"com_endpoint_agent": "Agent",
|
||||||
|
"com_endpoint_agent_model": "Agendi mudel (soovitatav: GPT-3.5)",
|
||||||
|
"com_endpoint_agent_placeholder": "Palun vali agent",
|
||||||
|
"com_endpoint_ai": "AI",
|
||||||
|
"com_endpoint_anthropic_maxoutputtokens": "Maksimaalne märkide arv, mida vastuses genereerida saab. Lühemate vastuste jaoks määrake madalam väärtus ja pikemate vastuste jaoks kõrgem väärtus. Märkus: mudelid võivad peatuda enne selle maksimumi saavutamist.",
|
||||||
|
"com_endpoint_anthropic_prompt_cache": "Päringu vahemällu salvestamine võimaldab suurt konteksti või juhiseid API-kõnede vahel uuesti kasutada, vähendades kulusid ja latentsust",
|
||||||
|
"com_endpoint_anthropic_temp": "Vahemikus 0 kuni 1. Kasutage analüütiliste / valikvastustega küsimuste puhul väärtust, mis on lähemal 0-le, ning loominguliste ja genereerivate ülesannete puhul väärtust, mis on lähemal 1-le. Soovitame muuta kas seda või Top P-d, aga mitte mõlemat.",
|
||||||
|
"com_endpoint_anthropic_topk": "Top-k muudab seda, kuidas mudel valib väljundi jaoks märgid. Top-k väärtus 1 tähendab, et valitud märk on kõige tõenäolisem kõigi mudeli sõnavaras olevate märkide seas (nimetatakse ka ahneks dekodeerimiseks), samas kui top-k väärtus 3 tähendab, et järgmine märk valitakse 3 kõige tõenäolisema märgi seast (kasutades temperatuuri).",
|
||||||
|
"com_endpoint_anthropic_topp": "Top-p muudab seda, kuidas mudel valib väljundi jaoks märgid. Märgid valitakse kõige tõenäolisemast K (vt parameetrit topK) kuni vähim tõenäoliseni, kuni nende tõenäosuste summa on võrdne top-p väärtusega.",
|
||||||
|
"com_endpoint_assistant": "Assistent",
|
||||||
|
"com_endpoint_assistant_model": "Assistendi mudel",
|
||||||
|
"com_endpoint_assistant_placeholder": "Palun vali assistent parempoolsest külgpaneelist",
|
||||||
|
"com_endpoint_completion": "Lõpetamine",
|
||||||
|
"com_endpoint_completion_model": "Lõpetamise mudel (soovitatav: GPT-4)",
|
||||||
|
"com_endpoint_config_click_here": "Klõpsa siia",
|
||||||
|
"com_endpoint_config_google_api_info": "Et saada oma Generative Language API võtit (Gemini jaoks),",
|
||||||
|
"com_endpoint_config_google_api_key": "Google API võti",
|
||||||
|
"com_endpoint_config_google_cloud_platform": "(Google Cloud Platformilt)",
|
||||||
|
"com_endpoint_config_google_gemini_api": "(Gemini API)",
|
||||||
|
"com_endpoint_config_google_service_key": "Google'i teenusekonto võti",
|
||||||
|
"com_endpoint_config_key": "Määra API võti",
|
||||||
|
"com_endpoint_config_key_encryption": "Sinu võti krüpteeritakse ja kustutatakse",
|
||||||
|
"com_endpoint_config_key_for": "Määra API võti",
|
||||||
|
"com_endpoint_config_key_google_need_to": "Sa pead",
|
||||||
|
"com_endpoint_config_key_google_service_account": "Loo teenusekonto",
|
||||||
|
"com_endpoint_config_key_google_vertex_ai": "Luba Vertex AI",
|
||||||
|
"com_endpoint_config_key_google_vertex_api": "API Google Cloudis, seejärel",
|
||||||
|
"com_endpoint_config_key_google_vertex_api_role": "Veenduge, et klõpsate 'Loo ja jätka', et anda vähemalt roll 'Vertex AI kasutaja'. Lõpuks looge siia importimiseks JSON-võti.",
|
||||||
|
"com_endpoint_config_key_import_json_key": "Impordi teenusekonto JSON-võti.",
|
||||||
|
"com_endpoint_config_key_import_json_key_invalid": "Vigane teenusekonto JSON-võti, kas importisite õige faili?",
|
||||||
|
"com_endpoint_config_key_import_json_key_success": "Teenusekonto JSON-võtme importimine õnnestus",
|
||||||
|
"com_endpoint_config_key_name": "Võti",
|
||||||
|
"com_endpoint_config_key_never_expires": "Sinu võti ei aegu kunagi",
|
||||||
|
"com_endpoint_config_placeholder": "Määra oma võti päise menüüs vestlemiseks.",
|
||||||
|
"com_endpoint_config_value": "Sisesta väärtus",
|
||||||
|
"com_endpoint_context": "Kontekst",
|
||||||
|
"com_endpoint_context_info": "Maksimaalne märkide arv, mida konteksti jaoks kasutada saab. Kasutage seda selleks, et kontrollida, mitu märki taotluse kohta saadetakse. Kui seda pole määratud, kasutatakse süsteemi vaikesätteid, mis põhinevad teadaolevate mudelite konteksti suurusel. Kõrgemate väärtuste määramine võib põhjustada vigu ja/või suuremaid märgikulusid.",
|
||||||
|
"com_endpoint_context_tokens": "Maksimum kontekstimärgid",
|
||||||
|
"com_endpoint_custom_name": "Kohandatud nimi",
|
||||||
|
"com_endpoint_default": "vaikimisi",
|
||||||
|
"com_endpoint_default_blank": "vaikimisi: tühi",
|
||||||
|
"com_endpoint_default_empty": "vaikimisi: tühi",
|
||||||
|
"com_endpoint_default_with_num": "vaikimisi: {{0}}",
|
||||||
|
"com_endpoint_examples": "Eelseadistused",
|
||||||
|
"com_endpoint_export": "Ekspordi",
|
||||||
|
"com_endpoint_export_share": "Ekspordi/Jaga",
|
||||||
|
"com_endpoint_frequency_penalty": "Sageduse karistus",
|
||||||
|
"com_endpoint_func_hover": "Luba pluginate kasutamine OpenAI funktsioonidena",
|
||||||
|
"com_endpoint_google_custom_name_placeholder": "Määra Google'ile kohandatud nimi",
|
||||||
|
"com_endpoint_google_maxoutputtokens": "Maksimaalne märkide arv, mida vastuses genereerida saab. Lühemate vastuste jaoks määrake madalam väärtus ja pikemate vastuste jaoks kõrgem väärtus. Märkus: mudelid võivad peatuda enne selle maksimumi saavutamist.",
|
||||||
|
"com_endpoint_google_temp": "Kõrgemad väärtused = juhuslikum, samas kui madalamad väärtused = keskendunum ja deterministlikum. Soovitame muuta kas seda või Top P-d, aga mitte mõlemat.",
|
||||||
|
"com_endpoint_google_topk": "Top-k muudab seda, kuidas mudel valib väljundi jaoks märgid. Top-k väärtus 1 tähendab, et valitud märk on kõige tõenäolisem kõigi mudeli sõnavaras olevate märkide seas (nimetatakse ka ahneks dekodeerimiseks), samas kui top-k väärtus 3 tähendab, et järgmine märk valitakse 3 kõige tõenäolisema märgi seast (kasutades temperatuuri).",
|
||||||
|
"com_endpoint_google_topp": "Top-p muudab seda, kuidas mudel valib väljundi jaoks märgid. Märgid valitakse kõige tõenäolisemast K (vt parameetrit topK) kuni vähim tõenäoliseni, kuni nende tõenäosuste summa on võrdne top-p väärtusega.",
|
||||||
|
"com_endpoint_instructions_assistants": "Tühista juhised",
|
||||||
|
"com_endpoint_instructions_assistants_placeholder": "Tühistab assistendi juhised. See on kasulik käitumise muutmiseks käivituse kohta.",
|
||||||
|
"com_endpoint_max_output_tokens": "Maksimaalsed väljundmärgid",
|
||||||
|
"com_endpoint_message": "Sõnum",
|
||||||
|
"com_endpoint_message_new": "Sõnum {{0}}",
|
||||||
|
"com_endpoint_message_not_appendable": "Muuda oma sõnumit või genereeri uuesti.",
|
||||||
|
"com_endpoint_my_preset": "Minu eelseadistus",
|
||||||
|
"com_endpoint_no_presets": "Eelseadistusi pole veel, kasutage ühe loomiseks seadete nuppu",
|
||||||
|
"com_endpoint_open_menu": "Ava menüü",
|
||||||
|
"com_endpoint_openai_custom_name_placeholder": "Määra AI-le kohandatud nimi",
|
||||||
|
"com_endpoint_openai_detail": "Visioonitaotluste eraldusvõime. \"Madal\" on odavam ja kiirem, \"Kõrge\" on detailsem ja kallim ning \"Automaatne\" valib automaatselt kahe vahel vastavalt pildi eraldusvõimele.",
|
||||||
|
"com_endpoint_openai_freq": "Arv vahemikus -2,0 kuni 2,0. Positiivsed väärtused karistavad uusi märke nende senise sageduse alusel tekstis, vähendades mudeli tõenäosust sama rida sõnasõnaliselt korrata.",
|
||||||
|
"com_endpoint_openai_max": "Maksimaalsed genereeritavad märgid. Sisendmärkide ja genereeritud märkide kogupikkus on piiratud mudeli konteksti pikkusega.",
|
||||||
|
"com_endpoint_openai_max_tokens": "Valikuline väli 'max_tokens', mis esindab vestluse lõpetamisel genereeritavate märkide maksimaalset arvu. Sisendmärkide ja genereeritud märkide kogupikkus on piiratud mudelite konteksti pikkusega. Kui see arv ületab maksimaalseid kontekstimärke, võivad tekkida vead.",
|
||||||
|
"com_endpoint_openai_pres": "Arv vahemikus -2,0 kuni 2,0. Positiivsed väärtused karistavad uusi märke selle alusel, kas need esinevad senises tekstis, suurendades mudeli tõenäosust rääkida uutest teemadest.",
|
||||||
|
"com_endpoint_openai_prompt_prefix_placeholder": "Määra kohandatud juhised süsteemiteatesse lisamiseks. Vaikimisi: puudub",
|
||||||
|
"com_endpoint_openai_reasoning_effort": "Ainult o1 mudelid: piirab põhjendusmudelite põhjendustegevust. Põhjendustegevuse vähendamine võib tuua kaasa kiiremad vastused ja vähem põhjendamiseks kasutatud märke vastuses.",
|
||||||
|
"com_endpoint_openai_resend": "Saada kõik varem lisatud pildid uuesti. Märkus: see võib märgikulusid oluliselt suurendada ja paljude pildimanustega võivad tekkida vead.",
|
||||||
|
"com_endpoint_openai_resend_files": "Saada kõik varem lisatud failid uuesti. Märkus: see suurendab märgikulusid ja paljude manustega võivad tekkida vead.",
|
||||||
|
"com_endpoint_openai_stop": "Kuni 4 järjestust, mille korral API lõpetab täiendavate märkide genereerimise.",
|
||||||
|
"com_endpoint_openai_temp": "Kõrgemad väärtused = juhuslikum, samas kui madalamad väärtused = keskendunum ja deterministlikum. Soovitame muuta kas seda või Top P-d, aga mitte mõlemat.",
|
||||||
|
"com_endpoint_openai_topp": "Alternatiiviks temperatuuriga näidiste võtmisele on tuumaproovide võtmine, kus mudel arvestab märkide tulemustega, millel on top_p tõenäosuse mass. Seega 0,1 tähendab, et arvesse võetakse ainult märke, mis moodustavad 10% suurima tõenäosusega massi. Soovitame muuta kas seda või temperatuuri, aga mitte mõlemat.",
|
||||||
|
"com_endpoint_output": "Väljund",
|
||||||
|
"com_endpoint_plug_image_detail": "Pildi detailid",
|
||||||
|
"com_endpoint_plug_resend_files": "Saada failid uuesti",
|
||||||
|
"com_endpoint_plug_set_custom_instructions_for_gpt_placeholder": "Määra kohandatud juhised süsteemiteatesse lisamiseks. Vaikimisi: puudub",
|
||||||
|
"com_endpoint_plug_skip_completion": "Jäta lõpetamine vahele",
|
||||||
|
"com_endpoint_plug_use_functions": "Kasuta funktsioone",
|
||||||
|
"com_endpoint_presence_penalty": "Olekukaristus",
|
||||||
|
"com_endpoint_preset": "eelseadistus",
|
||||||
|
"com_endpoint_preset_custom_name_placeholder": "Otspunkti kohandatud nimi.",
|
||||||
|
"com_endpoint_preset_default": "on nüüd vaike-eelseadistus.",
|
||||||
|
"com_endpoint_preset_default_item": "Vaikimisi:",
|
||||||
|
"com_endpoint_preset_default_none": "Vaikimisi eelseadistus pole aktiivne.",
|
||||||
|
"com_endpoint_preset_default_removed": "pole enam vaike-eelseadistus.",
|
||||||
|
"com_endpoint_preset_delete_confirm": "Oled sa kindel, et sa soovid selle eelseadistuse kustutada?",
|
||||||
|
"com_endpoint_preset_delete_error": "Eelseadistuse kustutamisel tekkis viga. Palun proovi uuesti.",
|
||||||
|
"com_endpoint_preset_import": "Eelseadistus imporditud!",
|
||||||
|
"com_endpoint_preset_import_error": "Eelseadistuse importimisel tekkis viga. Palun proovi uuesti.",
|
||||||
|
"com_endpoint_preset_name": "Eelseadistuse nimi",
|
||||||
|
"com_endpoint_preset_save_error": "Eelseadistuse salvestamisel tekkis viga. Palun proovi uuesti.",
|
||||||
|
"com_endpoint_preset_selected": "Eelseadistus aktiivne!",
|
||||||
|
"com_endpoint_preset_selected_title": "Aktiivne!",
|
||||||
|
"com_endpoint_preset_title": "Eelseadistus",
|
||||||
|
"com_endpoint_presets": "eelseadistused",
|
||||||
|
"com_endpoint_presets_clear_warning": "Oled sa kindel, et sa soovid kõik eelseadistused kustutada? See on pöördumatu.",
|
||||||
|
"com_endpoint_prompt_cache": "Kasuta päringu vahemällu salvestamist",
|
||||||
|
"com_endpoint_prompt_prefix": "Kohandatud juhised",
|
||||||
|
"com_endpoint_prompt_prefix_assistants": "Täiendavad juhised",
|
||||||
|
"com_endpoint_prompt_prefix_assistants_placeholder": "Määra täiendavad juhised või kontekst lisaks assistendi peamistele juhistele. Kui see on tühi, ignoreeritakse seda.",
|
||||||
|
"com_endpoint_prompt_prefix_placeholder": "Määra kohandatud juhised või kontekst. Kui see on tühi, ignoreeritakse seda.",
|
||||||
|
"com_endpoint_reasoning_effort": "Põhjendustegevus",
|
||||||
|
"com_endpoint_save_as_preset": "Salvesta eelseadistusena",
|
||||||
|
"com_endpoint_search": "Otsi otspunkti nime järgi",
|
||||||
|
"com_endpoint_set_custom_name": "Määra kohandatud nimi, juhuks kui sa selle eelseadistuse leiad",
|
||||||
|
"com_endpoint_skip_hover": "Luba lõpetamise etapi vahelejätmine, mis vaatab üle lõpliku vastuse ja genereeritud sammud",
|
||||||
|
"com_endpoint_stop": "Peatamise järjestused",
|
||||||
|
"com_endpoint_stop_placeholder": "Eralda väärtused, vajutades `Enter`",
|
||||||
|
"com_endpoint_temperature": "Temperatuur",
|
||||||
|
"com_endpoint_top_k": "Top K",
|
||||||
|
"com_endpoint_top_p": "Top P",
|
||||||
|
"com_endpoint_use_active_assistant": "Kasuta aktiivset assistenti",
|
||||||
|
"com_error_expired_user_key": "Esitatud võti {{0}} aegus {{1}}. Esitage uus võti ja proovige uuesti.",
|
||||||
|
"com_error_files_dupe": "Leiti duplikaatfail.",
|
||||||
|
"com_error_files_empty": "Tühjad failid pole lubatud.",
|
||||||
|
"com_error_files_process": "Faili töötlemisel tekkis viga.",
|
||||||
|
"com_error_files_unsupported_capability": "Ühtegi seda failitüüpi toetavat võimalust pole lubatud.",
|
||||||
|
"com_error_files_upload": "Faili üleslaadimisel tekkis viga.",
|
||||||
|
"com_error_files_upload_canceled": "Faili üleslaadimise taotlus tühistati. Märkus: faili üleslaadimine võib endiselt olla pooleli ja see tuleb käsitsi kustutada.",
|
||||||
|
"com_error_files_validation": "Faili valideerimisel tekkis viga.",
|
||||||
|
"com_error_input_length": "Viimase sõnumi märkide arv on liiga pikk, ületades märkide limiidi (vastavalt {{0}}). Palun lühendage oma sõnumit, kohandage vestluse parameetrites maksimaalset konteksti suurust või jätkamiseks hargnege vestlus.",
|
||||||
|
"com_error_invalid_user_key": "Esitati vigane võti. Esitage kehtiv võti ja proovige uuesti.",
|
||||||
|
"com_error_moderation": "Tundub, et esitatud sisu on meie modereerimissüsteemi poolt märgistatud kui meie kogukonna juhistega mitte vastav. Me ei saa selle konkreetse teemaga jätkata. Kui teil on muid küsimusi või teemasid, mida soovite uurida, muutke oma sõnumit või looge uus vestlus.",
|
||||||
|
"com_error_no_base_url": "Baasaadressi ei leitud. Palun lisage see ja proovige uuesti.",
|
||||||
|
"com_error_no_user_key": "Võtit ei leitud. Palun sisestage võti ja proovige uuesti.",
|
||||||
|
"com_files_filter": "Filtreeri faile...",
|
||||||
|
"com_files_no_results": "Tulemusi pole.",
|
||||||
|
"com_files_number_selected": "{{0}} / {{1}} üksust valitud",
|
||||||
|
"com_files_table": "Failide tabel",
|
||||||
|
"com_generated_files": "Genereeritud failid:",
|
||||||
|
"com_hide_examples": "Peida näited",
|
||||||
|
"com_nav_account_settings": "Konto seaded",
|
||||||
|
"com_nav_always_make_prod": "Tee uued versioonid alati toodangusse",
|
||||||
|
"com_nav_archive_created_at": "Arhiveerimise kuupäev",
|
||||||
|
"com_nav_archive_name": "Nimi",
|
||||||
|
"com_nav_archived_chats": "Arhiveeritud vestlused",
|
||||||
|
"com_nav_archived_chats_empty": "Sul ei ole arhiveeritud vestlusi.",
|
||||||
|
"com_nav_at_command": "@-käsk",
|
||||||
|
"com_nav_at_command_description": "Lülita käsk \"@\" sisse/välja lõpp-punktide, mudelite, eelseadistuste jms vahetamiseks.",
|
||||||
|
"com_nav_audio_play_error": "Viga heli esitamisel: {{0}}",
|
||||||
|
"com_nav_audio_process_error": "Viga heli töötlemisel: {{0}}",
|
||||||
|
"com_nav_auto_scroll": "Automaatne kerimine vestluse avamisel viimase sõnumini",
|
||||||
|
"com_nav_auto_send_prompts": "Saada vihjed automaatselt",
|
||||||
|
"com_nav_auto_send_text": "Saada tekst automaatselt",
|
||||||
|
"com_nav_auto_send_text_disabled": "Keelamiseks määra -1",
|
||||||
|
"com_nav_auto_transcribe_audio": "Transkribeeri heli automaatselt",
|
||||||
|
"com_nav_automatic_playback": "Esita viimane sõnum automaatselt",
|
||||||
|
"com_nav_balance": "Saldo",
|
||||||
|
"com_nav_browser": "Brauser",
|
||||||
|
"com_nav_buffer_append_error": "Probleem heli voogedastusega. Taasesitus võib katkeda.",
|
||||||
|
"com_nav_change_picture": "Muuda pilti",
|
||||||
|
"com_nav_chat_commands": "Vestluskäsud",
|
||||||
|
"com_nav_chat_commands_info": "Need käsud aktiveeritakse konkreetsete märkide sisestamisel sõnumi alguses. Iga käsk käivitatakse sellele määratud eesliitega. Saate need keelata, kui kasutate neid märke sageli sõnumite alustamiseks.",
|
||||||
|
"com_nav_chat_direction": "Vestluse suund",
|
||||||
|
"com_nav_clear_all_chats": "Tühjenda kõik vestlused",
|
||||||
|
"com_nav_clear_cache_confirm_message": "Oled sa kindel, et sa soovid vahemälu tühjendada?",
|
||||||
|
"com_nav_clear_conversation": "Tühjenda vestlused",
|
||||||
|
"com_nav_clear_conversation_confirm_message": "Oled sa kindel, et sa soovid kõik vestlused tühjendada? See on pöördumatu.",
|
||||||
|
"com_nav_close_sidebar": "Sulge külgriba",
|
||||||
|
"com_nav_commands": "Käsud",
|
||||||
|
"com_nav_confirm_clear": "Kinnita tühjendamine",
|
||||||
|
"com_nav_conversation_mode": "Vestlusrežiim",
|
||||||
|
"com_nav_convo_menu_options": "Vestluse menüü valikud",
|
||||||
|
"com_nav_db_sensitivity": "Detsibelli tundlikkus",
|
||||||
|
"com_nav_delete_account": "Kustuta konto",
|
||||||
|
"com_nav_delete_account_button": "Kustuta minu konto jäädavalt",
|
||||||
|
"com_nav_delete_account_confirm": "Kustuta konto - oled sa kindel?",
|
||||||
|
"com_nav_delete_account_email_placeholder": "Palun sisesta oma konto e-post",
|
||||||
|
"com_nav_delete_cache_storage": "Kustuta TTS vahemälu",
|
||||||
|
"com_nav_delete_data_info": "Kõik sinu andmed kustutatakse.",
|
||||||
|
"com_nav_delete_warning": "HOIATUS: See kustutab sinu konto jäädavalt.",
|
||||||
|
"com_nav_edge": "Edge",
|
||||||
|
"com_nav_enable_cache_tts": "Luba TTS vahemälu",
|
||||||
|
"com_nav_enable_cloud_browser_voice": "Kasuta pilvepõhiseid hääli",
|
||||||
|
"com_nav_enabled": "Lubatud",
|
||||||
|
"com_nav_engine": "Mootor",
|
||||||
|
"com_nav_enter_to_send": "Vajuta Enter sõnumite saatmiseks",
|
||||||
|
"com_nav_export": "Ekspordi",
|
||||||
|
"com_nav_export_all_message_branches": "Ekspordi kõik sõnumite harud",
|
||||||
|
"com_nav_export_conversation": "Ekspordi vestlus",
|
||||||
|
"com_nav_export_filename": "Failinimi",
|
||||||
|
"com_nav_export_filename_placeholder": "Määra failinimi",
|
||||||
|
"com_nav_export_include_endpoint_options": "Kaasa otspunkti valikud",
|
||||||
|
"com_nav_export_recursive": "Rekursiivne",
|
||||||
|
"com_nav_export_recursive_or_sequential": "Rekursiivne või järjestikune?",
|
||||||
|
"com_nav_export_type": "Tüüp",
|
||||||
|
"com_nav_external": "Väline",
|
||||||
|
"com_nav_font_size": "Sõnumi fondi suurus",
|
||||||
|
"com_nav_font_size_base": "Keskmine",
|
||||||
|
"com_nav_font_size_lg": "Suur",
|
||||||
|
"com_nav_font_size_sm": "Väike",
|
||||||
|
"com_nav_font_size_xl": "Eriti suur",
|
||||||
|
"com_nav_font_size_xs": "Eriti väike",
|
||||||
|
"com_nav_help_faq": "Abi ja KKK",
|
||||||
|
"com_nav_hide_panel": "Peida kõige parempoolsem külgpaneel",
|
||||||
|
"com_nav_info_code_artifacts": "Võimaldab katsetuslike koodiartefaktide kuvamist vestluse kõrval",
|
||||||
|
"com_nav_info_code_artifacts_agent": "Võimaldab selle agendi jaoks koodiartefaktide kasutamist. Vaikimisi lisatakse artefaktide kasutamisele spetsiifilised täiendavad juhised, välja arvatud juhul, kui on lubatud \"Kohandatud viiparežiim\".",
|
||||||
|
"com_nav_info_custom_prompt_mode": "Kui see on lubatud, siis vaikimisi artefaktide süsteemiviipa ei lisata. Kõik artefakte genereerivad juhised tuleb selles režiimis käsitsi esitada.",
|
||||||
|
"com_nav_info_enter_to_send": "Kui see on lubatud, saadab `ENTER` sõnumi. Kui see on keelatud, lisab Enter uue rea ja sõnumi saatmiseks peate vajutama `CTRL + ENTER` / `⌘ + ENTER`.",
|
||||||
|
"com_nav_info_fork_change_default": "`Ainult nähtavad sõnumid` sisaldab ainult otsest teed valitud sõnumini. `Kaasa seotud harud` lisab harud mööda teed. `Kaasa kõik siia/siit` sisaldab kõiki ühendatud sõnumeid ja harusid.",
|
||||||
|
"com_nav_info_fork_split_target_setting": "Kui see on lubatud, algab hargnemine sihtsõnumist vestluse viimase sõnumini vastavalt valitud käitumisele.",
|
||||||
|
"com_nav_info_include_shadcnui": "Kui see on lubatud, siis lisatakse juhised shadcn/ui komponentide kasutamiseks. shadcn/ui on Radix UI ja Tailwind CSS-i abil loodud taaskasutatavate komponentide kogu. Märkus: need on pikad juhised, peaksid lubama ainult siis, kui LLM-i õigete importide ja komponentide teavitamine on sinu jaoks oluline. Nende komponentide kohta lisateabe saamiseks külastage: https://ui.shadcn.com/",
|
||||||
|
"com_nav_info_latex_parsing": "Kui see on lubatud, renderdatakse sõnumites olev LaTeX-kood matemaatiliste võrranditena. Selle keelamine võib jõudlust parandada, kui sa LaTeX-i renderdamist ei vaja.",
|
||||||
|
"com_nav_info_save_draft": "Kui see on lubatud, salvestatakse vestlusvormi sisestatud tekst ja manused automaatselt kohalikult mustanditena. Need mustandid on saadaval ka siis, kui lehe uuesti laadite või teisele vestlusele lülitute. Mustandid salvestatakse lokaalselt sinu seadmesse ja kustutatakse pärast sõnumi saatmist.",
|
||||||
|
"com_nav_info_show_thinking": "Kui see on lubatud, kuvatakse vestluses vaikimisi avatud mõtlemise rippmenüüd, võimaldades sul AI põhjendusi reaalajas vaadata. Kui see on keelatud, jäävad mõtlemise rippmenüüd vaikimisi suletuks, et saada puhtam ja sujuvam liides",
|
||||||
|
"com_nav_info_user_name_display": "Kui see on lubatud, kuvatakse saatja kasutajanimi iga sinu saadetud sõnumi kohal. Kui see on keelatud, näed oma sõnumite kohal ainult \"Sina\".",
|
||||||
|
"com_nav_lang_arabic": "العربية",
|
||||||
|
"com_nav_lang_auto": "Tuvasta automaatselt",
|
||||||
|
"com_nav_lang_brazilian_portuguese": "Português Brasileiro",
|
||||||
|
"com_nav_lang_chinese": "中文",
|
||||||
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
|
"com_nav_lang_finnish": "Suomi",
|
||||||
|
"com_nav_lang_french": "Français ",
|
||||||
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
"com_nav_lang_hebrew": "עברית",
|
||||||
|
"com_nav_lang_indonesia": "Indonesia",
|
||||||
|
"com_nav_lang_italian": "Italiano",
|
||||||
|
"com_nav_lang_japanese": "日本語",
|
||||||
|
"com_nav_lang_korean": "한국어",
|
||||||
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
|
"com_nav_lang_russian": "Русский",
|
||||||
|
"com_nav_lang_spanish": "Español",
|
||||||
|
"com_nav_lang_swedish": "Svenska",
|
||||||
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
|
"com_nav_language": "Keel",
|
||||||
|
"com_nav_latex_parsing": "LaTeXi parsimine sõnumites (võib mõjutada jõudlust)",
|
||||||
|
"com_nav_log_out": "Logi välja",
|
||||||
|
"com_nav_long_audio_warning": "Pikemate tekstide töötlemine võtab kauem aega.",
|
||||||
|
"com_nav_maximize_chat_space": "Maksimeeri vestluse ruumi",
|
||||||
|
"com_nav_modular_chat": "Luba otspunktide vahetamine vestluse keskel",
|
||||||
|
"com_nav_my_files": "Minu failid",
|
||||||
|
"com_nav_no_search_results": "Otsingutulemusi ei leitud",
|
||||||
|
"com_nav_not_supported": "Pole toetatud",
|
||||||
|
"com_nav_open_sidebar": "Ava külgriba",
|
||||||
|
"com_nav_playback_rate": "Heli taasesituse kiirus",
|
||||||
|
"com_nav_plugin_auth_error": "Selle pistikprogrammi autentimisel tekkis viga. Palun proovi uuesti.",
|
||||||
|
"com_nav_plugin_install": "Paigalda",
|
||||||
|
"com_nav_plugin_search": "Otsi pistikprogramme",
|
||||||
|
"com_nav_plugin_store": "Pistikprogrammide pood",
|
||||||
|
"com_nav_plugin_uninstall": "Eemalda",
|
||||||
|
"com_nav_plus_command": "+-käsk",
|
||||||
|
"com_nav_plus_command_description": "Lülita käsk \"+\" sisse/välja mitme vastuse seadistuse lisamiseks",
|
||||||
|
"com_nav_profile_picture": "Profiilipilt",
|
||||||
|
"com_nav_save_drafts": "Salvesta mustandid kohalikult",
|
||||||
|
"com_nav_scroll_button": "Keri nupu lõppu",
|
||||||
|
"com_nav_search_placeholder": "Otsi sõnumeid",
|
||||||
|
"com_nav_send_message": "Saada sõnum",
|
||||||
|
"com_nav_setting_account": "Konto",
|
||||||
|
"com_nav_setting_beta": "Beeta funktsioonid",
|
||||||
|
"com_nav_setting_chat": "Vestlus",
|
||||||
|
"com_nav_setting_data": "Andmekontroll",
|
||||||
|
"com_nav_setting_general": "Üldine",
|
||||||
|
"com_nav_setting_speech": "Kõne",
|
||||||
|
"com_nav_settings": "Seaded",
|
||||||
|
"com_nav_shared_links": "Jagatud lingid",
|
||||||
|
"com_nav_show_code": "Näita koodi alati, kui kasutatakse koodiinterpreteerijat",
|
||||||
|
"com_nav_show_thinking": "Ava mõtlemise rippmenüüd vaikimisi",
|
||||||
|
"com_nav_slash_command": "/-käsk",
|
||||||
|
"com_nav_slash_command_description": "Lülita käsk \"/\" sisse/välja, et valida klaviatuuri kaudu viipa",
|
||||||
|
"com_nav_source_buffer_error": "Viga heli taasesituse seadistamisel. Palun värskenda lehte.",
|
||||||
|
"com_nav_speech_cancel_error": "Heli taasesitust ei saa peatada. Võib-olla pead lehte värskendama.",
|
||||||
|
"com_nav_speech_to_text": "Kõnest tekstiks",
|
||||||
|
"com_nav_stop_generating": "Lõpeta genereerimine",
|
||||||
|
"com_nav_text_to_speech": "Tekst kõneks",
|
||||||
|
"com_nav_theme": "Teema",
|
||||||
|
"com_nav_theme_dark": "Tume",
|
||||||
|
"com_nav_theme_light": "Hele",
|
||||||
|
"com_nav_theme_system": "Süsteem",
|
||||||
|
"com_nav_tool_dialog": "Assistendi tööriistad",
|
||||||
|
"com_nav_tool_dialog_agents": "Agendi tööriistad",
|
||||||
|
"com_nav_tool_dialog_description": "Tööriistade valikute säilitamiseks tuleb assistent salvestada.",
|
||||||
|
"com_nav_tool_remove": "Eemalda",
|
||||||
|
"com_nav_tool_search": "Otsi tööriistu",
|
||||||
|
"com_nav_tts_init_error": "Teksti kõneks muutmise initsialiseerimine ebaõnnestus: {{0}}",
|
||||||
|
"com_nav_tts_unsupported_error": "Valitud mootori tekst kõneks muutmise funktsioon ei ole selles brauseris toetatud.",
|
||||||
|
"com_nav_user": "KASUTAJA",
|
||||||
|
"com_nav_user_msg_markdown": "Renderda kasutajasõnumid markdownina",
|
||||||
|
"com_nav_user_name_display": "Kuva kasutajanimi sõnumites",
|
||||||
|
"com_nav_voice_select": "Hääl",
|
||||||
|
"com_nav_voices_fetch_error": "Hääle valikuid ei saanud hankida. Palun kontrolli oma internetiühendust.",
|
||||||
|
"com_nav_welcome_agent": "Palun vali agent",
|
||||||
|
"com_nav_welcome_assistant": "Palun vali assistent",
|
||||||
|
"com_nav_welcome_message": "Kuidas ma saan täna sind aidata?",
|
||||||
|
"com_show_agent_settings": "Näita agendi seadeid",
|
||||||
|
"com_show_completion_settings": "Näita lõpetamise seadeid",
|
||||||
|
"com_show_examples": "Näita näiteid",
|
||||||
|
"com_sidepanel_agent_builder": "Agendi ehitaja",
|
||||||
|
"com_sidepanel_assistant_builder": "Assistendi ehitaja",
|
||||||
|
"com_sidepanel_attach_files": "Lisa faile",
|
||||||
|
"com_sidepanel_conversation_tags": "Järjehoidjad",
|
||||||
|
"com_sidepanel_hide_panel": "Peida paneel",
|
||||||
|
"com_sidepanel_manage_files": "Halda faile",
|
||||||
|
"com_sidepanel_parameters": "Parameetrid",
|
||||||
|
"com_sidepanel_select_agent": "Vali agent",
|
||||||
|
"com_sidepanel_select_assistant": "Vali assistent",
|
||||||
|
"com_ui_accept": "Nõustun",
|
||||||
|
"com_ui_add": "Lisa",
|
||||||
|
"com_ui_add_model_preset": "Lisa mudel või eelseadistus täiendava vastuse jaoks",
|
||||||
|
"com_ui_add_multi_conversation": "Lisa mitmevestlus",
|
||||||
|
"com_ui_admin": "Administraator",
|
||||||
|
"com_ui_admin_access_warning": "Administraatori juurdepääsu keelamine sellele funktsioonile võib põhjustada ootamatuid kasutajaliidese probleeme, mis nõuavad värskendamist. Kui see on salvestatud, on ainus viis taastada liideseseade kaudu librechat.yaml konfiguratsioonis, mis mõjutab kõiki rolle.",
|
||||||
|
"com_ui_admin_settings": "Administraatori seaded",
|
||||||
|
"com_ui_advanced": "Täpsemad",
|
||||||
|
"com_ui_agent": "Agent",
|
||||||
|
"com_ui_agent_delete_error": "Agendi kustutamisel tekkis viga",
|
||||||
|
"com_ui_agent_deleted": "Agendi kustutamine õnnestus",
|
||||||
|
"com_ui_agent_duplicate_error": "Agendi dubleerimisel tekkis viga",
|
||||||
|
"com_ui_agent_duplicated": "Agendi dubleerimine õnnestus",
|
||||||
|
"com_ui_agent_editing_allowed": "Teised kasutajad saavad seda agenti juba muuta",
|
||||||
|
"com_ui_agent_shared_to_all": "Seda agenti on jagatud kõigi kasutajatega",
|
||||||
|
"com_ui_agents": "Agendid",
|
||||||
|
"com_ui_agents_allow_create": "Luba agentide loomine",
|
||||||
|
"com_ui_agents_allow_share_global": "Luba agentide jagamine kõigile kasutajatele",
|
||||||
|
"com_ui_agents_allow_use": "Luba agentide kasutamine",
|
||||||
|
"com_ui_all": "kõik",
|
||||||
|
"com_ui_all_proper": "Kõik",
|
||||||
|
"com_ui_analyzing": "Analüüsimine",
|
||||||
|
"com_ui_analyzing_finished": "Analüüs lõpetatud",
|
||||||
|
"com_ui_api_key": "API võti",
|
||||||
|
"com_ui_archive": "Arhiveeri",
|
||||||
|
"com_ui_archive_error": "Vestluse arhiveerimine ebaõnnestus",
|
||||||
|
"com_ui_artifact_click": "Klõpsa avamiseks",
|
||||||
|
"com_ui_artifacts": "Artefaktid",
|
||||||
|
"com_ui_artifacts_toggle": "Lülita artefaktide kasutajaliides sisse/välja",
|
||||||
|
"com_ui_artifacts_toggle_agent": "Luba artefaktid",
|
||||||
|
"com_ui_ascending": "Asc",
|
||||||
|
"com_ui_assistant": "Assistent",
|
||||||
|
"com_ui_assistant_delete_error": "Assistendi kustutamisel tekkis viga",
|
||||||
|
"com_ui_assistant_deleted": "Assistendi kustutamine õnnestus",
|
||||||
|
"com_ui_assistants": "Assistendid",
|
||||||
|
"com_ui_assistants_output": "Assistentide väljund",
|
||||||
|
"com_ui_attach_error": "Faili ei saa lisada. Loo või vali vestlus või proovi lehte värskendada.",
|
||||||
|
"com_ui_attach_error_openai": "Assistendi faile ei saa teistele otspunktidele lisada",
|
||||||
|
"com_ui_attach_error_size": "Failisuuruse limiit on otspunkti jaoks ületatud:",
|
||||||
|
"com_ui_attach_error_type": "Otspunkti jaoks toetamatu failitüüp:",
|
||||||
|
"com_ui_attach_warn_endpoint": "Sobiva tööriista puudumisel võidakse mitte-assistendi faile ignoreerida",
|
||||||
|
"com_ui_attachment": "Manus",
|
||||||
|
"com_ui_auth_type": "Autentimise tüüp",
|
||||||
|
"com_ui_auth_url": "Autentimise URL",
|
||||||
|
"com_ui_authentication": "Autentimine",
|
||||||
|
"com_ui_authentication_type": "Autentimise tüüp",
|
||||||
|
"com_ui_avatar": "Avatar",
|
||||||
|
"com_ui_azure": "Azure",
|
||||||
|
"com_ui_back_to_chat": "Tagasi vestlusesse",
|
||||||
|
"com_ui_back_to_prompts": "Tagasi sisendite juurde",
|
||||||
|
"com_ui_basic": "Põhiline",
|
||||||
|
"com_ui_basic_auth_header": "Põhiline autentimise päis",
|
||||||
|
"com_ui_bearer": "Bearer",
|
||||||
|
"com_ui_bookmark_delete_confirm": "Oled sa kindel, et sa soovid selle järjehoidja kustutada?",
|
||||||
|
"com_ui_bookmarks": "Järjehoidjad",
|
||||||
|
"com_ui_bookmarks_add": "Lisa järjehoidjaid",
|
||||||
|
"com_ui_bookmarks_add_to_conversation": "Lisa praegusesse vestlusse",
|
||||||
|
"com_ui_bookmarks_count": "Hulk",
|
||||||
|
"com_ui_bookmarks_create_error": "Järjehoidja loomisel tekkis viga",
|
||||||
|
"com_ui_bookmarks_create_exists": "See järjehoidja on juba olemas",
|
||||||
|
"com_ui_bookmarks_create_success": "Järjehoidja loomine õnnestus",
|
||||||
|
"com_ui_bookmarks_delete": "Kustuta järjehoidja",
|
||||||
|
"com_ui_bookmarks_delete_error": "Järjehoidja kustutamisel tekkis viga",
|
||||||
|
"com_ui_bookmarks_delete_success": "Järjehoidja kustutamine õnnestus",
|
||||||
|
"com_ui_bookmarks_description": "Kirjeldus",
|
||||||
|
"com_ui_bookmarks_edit": "Muuda järjehoidjat",
|
||||||
|
"com_ui_bookmarks_filter": "Filtreeri järjehoidjaid...",
|
||||||
|
"com_ui_bookmarks_new": "Uus järjehoidja",
|
||||||
|
"com_ui_bookmarks_title": "Pealkiri",
|
||||||
|
"com_ui_bookmarks_update_error": "Järjehoidja uuendamisel tekkis viga",
|
||||||
|
"com_ui_bookmarks_update_success": "Järjehoidja uuendamine õnnestus",
|
||||||
|
"com_ui_bulk_delete_error": "Jagatud linkide kustutamine ebaõnnestus",
|
||||||
|
"com_ui_callback_url": "Tagasikutsumise URL",
|
||||||
|
"com_ui_cancel": "Tühista",
|
||||||
|
"com_ui_chat": "Vestlus",
|
||||||
|
"com_ui_chat_history": "Vestluse ajalugu",
|
||||||
|
"com_ui_clear": "Tühjenda",
|
||||||
|
"com_ui_clear_all": "Tühjenda kõik",
|
||||||
|
"com_ui_client_id": "Kliendi ID",
|
||||||
|
"com_ui_client_secret": "Kliendi saladus",
|
||||||
|
"com_ui_close": "Sulge",
|
||||||
|
"com_ui_close_menu": "Sulge menüü",
|
||||||
|
"com_ui_code": "Kood",
|
||||||
|
"com_ui_collapse_chat": "Ahenda vestlus",
|
||||||
|
"com_ui_command_placeholder": "Valikuline: sisesta sisendi jaoks käsk või kasutatakse nime",
|
||||||
|
"com_ui_command_usage_placeholder": "Vali sisend käsu või nime järgi",
|
||||||
|
"com_ui_confirm_action": "Kinnita tegevus",
|
||||||
|
"com_ui_confirm_admin_use_change": "Selle seadistuse muutmine blokeerib juurdepääsu administraatoritele, sealhulgas sinule endale. Oled sa kindel, et sa soovid jätkata?",
|
||||||
|
"com_ui_confirm_change": "Kinnita muudatus",
|
||||||
|
"com_ui_context": "Kontekst",
|
||||||
|
"com_ui_continue": "Jätka",
|
||||||
|
"com_ui_controls": "Juhtelemendid",
|
||||||
|
"com_ui_copied": "Kopeeritud!",
|
||||||
|
"com_ui_copied_to_clipboard": "Kopeeritud lõikepuhvrisse",
|
||||||
|
"com_ui_copy_code": "Kopeeri kood",
|
||||||
|
"com_ui_copy_link": "Kopeeri link",
|
||||||
|
"com_ui_copy_to_clipboard": "Kopeeri lõikepuhvrisse",
|
||||||
|
"com_ui_create": "Loo",
|
||||||
|
"com_ui_create_link": "Loo link",
|
||||||
|
"com_ui_create_prompt": "Loo sisend",
|
||||||
|
"com_ui_currently_production": "Praegu tootmises",
|
||||||
|
"com_ui_custom": "Kohandatud",
|
||||||
|
"com_ui_custom_header_name": "Kohandatud päise nimi",
|
||||||
|
"com_ui_custom_prompt_mode": "Kohandatud viiparežiim",
|
||||||
|
"com_ui_dashboard": "Armatuurlaud",
|
||||||
|
"com_ui_date": "Kuupäev",
|
||||||
|
"com_ui_date_april": "Aprill",
|
||||||
|
"com_ui_date_august": "August",
|
||||||
|
"com_ui_date_december": "Detsember",
|
||||||
|
"com_ui_date_february": "Veebruar",
|
||||||
|
"com_ui_date_january": "Jaanuar",
|
||||||
|
"com_ui_date_july": "Juuli",
|
||||||
|
"com_ui_date_june": "Juuni",
|
||||||
|
"com_ui_date_march": "Märts",
|
||||||
|
"com_ui_date_may": "Mai",
|
||||||
|
"com_ui_date_november": "November",
|
||||||
|
"com_ui_date_october": "Oktoober",
|
||||||
|
"com_ui_date_previous_30_days": "Eelmised 30 päeva",
|
||||||
|
"com_ui_date_previous_7_days": "Eelmised 7 päeva",
|
||||||
|
"com_ui_date_september": "September",
|
||||||
|
"com_ui_date_today": "Täna",
|
||||||
|
"com_ui_date_yesterday": "Eile",
|
||||||
|
"com_ui_decline": "Ma ei nõustu",
|
||||||
|
"com_ui_default_post_request": "Vaikimisi (POST päring)",
|
||||||
|
"com_ui_delete": "Kustuta",
|
||||||
|
"com_ui_delete_action": "Kustuta tegevus",
|
||||||
|
"com_ui_delete_action_confirm": "Oled sa kindel, et sa soovid selle tegevuse kustutada?",
|
||||||
|
"com_ui_delete_agent_confirm": "Oled sa kindel, et sa soovid selle agendi kustutada?",
|
||||||
|
"com_ui_delete_assistant_confirm": "Oled sa kindel, et sa soovid selle assistendi kustutada? Seda ei saa tagasi võtta.",
|
||||||
|
"com_ui_delete_confirm": "See kustutab",
|
||||||
|
"com_ui_delete_confirm_prompt_version_var": "See kustutab valitud versiooni \"{{0}}\" jaoks. Kui muid versioone ei eksisteeri, kustutatakse sisend.",
|
||||||
|
"com_ui_delete_conversation": "Kustuta vestlus?",
|
||||||
|
"com_ui_delete_prompt": "Kustuta sisend?",
|
||||||
|
"com_ui_delete_shared_link": "Kustuta jagatud link?",
|
||||||
|
"com_ui_delete_tool": "Kustuta tööriist",
|
||||||
|
"com_ui_delete_tool_confirm": "Oled sa kindel, et sa soovid selle tööriista kustutada?",
|
||||||
|
"com_ui_descending": "Desc",
|
||||||
|
"com_ui_description": "Kirjeldus",
|
||||||
|
"com_ui_description_placeholder": "Valikuline: sisesta sisendi jaoks kuvatav kirjeldus",
|
||||||
|
"com_ui_download": "Laadi alla",
|
||||||
|
"com_ui_download_artifact": "Laadi artefakt alla",
|
||||||
|
"com_ui_download_error": "Viga faili allalaadimisel. Fail võib olla kustutatud.",
|
||||||
|
"com_ui_drag_drop": "Lohistage",
|
||||||
|
"com_ui_dropdown_variables": "Rippmenüü muutujad:",
|
||||||
|
"com_ui_dropdown_variables_info": "Loo sisendite jaoks kohandatud rippmenüüd: `{{muutuja_nimi:valik1|valik2|valik3}}`",
|
||||||
|
"com_ui_duplicate": "Dubleeri",
|
||||||
|
"com_ui_duplication_error": "Vestluse dubleerimisel tekkis viga",
|
||||||
|
"com_ui_duplication_processing": "Vestlust dubleeritakse...",
|
||||||
|
"com_ui_duplication_success": "Vestluse dubleerimine õnnestus",
|
||||||
|
"com_ui_edit": "Muuda",
|
||||||
|
"com_ui_empty_category": "-",
|
||||||
|
"com_ui_endpoint": "Otspunkt",
|
||||||
|
"com_ui_endpoint_menu": "LLM otspunkti menüü",
|
||||||
|
"com_ui_endpoints_available": "Saadaolevad otspunktid",
|
||||||
|
"com_ui_enter": "Sisesta",
|
||||||
|
"com_ui_enter_api_key": "Sisesta API võti",
|
||||||
|
"com_ui_enter_openapi_schema": "Sisesta siia oma OpenAPI skeem",
|
||||||
|
"com_ui_enter_var": "Sisesta {{0}}",
|
||||||
|
"com_ui_error": "Viga",
|
||||||
|
"com_ui_error_connection": "Viga serveriga ühendamisel, proovi lehte värskendada.",
|
||||||
|
"com_ui_error_save_admin_settings": "Administraatori seadete salvestamisel tekkis viga.",
|
||||||
|
"com_ui_examples": "Näited",
|
||||||
|
"com_ui_export_convo_modal": "Ekspordi vestluse modaal",
|
||||||
|
"com_ui_field_required": "See väli on kohustuslik",
|
||||||
|
"com_ui_filter_prompts": "Filtreeri sisendid",
|
||||||
|
"com_ui_filter_prompts_name": "Filtreeri sisendeid nime järgi",
|
||||||
|
"com_ui_finance": "Raha",
|
||||||
|
"com_ui_fork": "Hargne",
|
||||||
|
"com_ui_fork_all_target": "Kaasa kõik siia/siit",
|
||||||
|
"com_ui_fork_branches": "Kaasa seotud harud",
|
||||||
|
"com_ui_fork_change_default": "Vaikimisi hargnemise valik",
|
||||||
|
"com_ui_fork_default": "Kasuta vaikimisi hargnemise valikut",
|
||||||
|
"com_ui_fork_error": "Vestluse hargnemisel tekkis viga",
|
||||||
|
"com_ui_fork_from_message": "Vali hargnemise valik",
|
||||||
|
"com_ui_fork_info_1": "Kasuta seda seadistust sõnumite hargnemiseks soovitud käitumisega.",
|
||||||
|
"com_ui_fork_info_2": "\"Hargnemine\" viitab uue vestluse loomisele, mis algab/lõpeb praeguse vestluse konkreetsetest sõnumitest, luues koopia vastavalt valitud valikutele.",
|
||||||
|
"com_ui_fork_info_3": "\"Sihtsõnum\" viitab kas sõnumile, millest see hüpikaken avati, või, kui märgid \"{{0}}\", vestluse viimasele sõnumile.",
|
||||||
|
"com_ui_fork_info_branches": "See valik hargneb nähtavad sõnumid koos seotud harudega; teisisõnu, otsene tee sihtsõnumini, sealhulgas harud mööda teed.",
|
||||||
|
"com_ui_fork_info_remember": "Märgi see, et jätta meelde valitud valikud edaspidiseks kasutamiseks, muutes vestluste hargnemise eelistatud viisil kiiremaks.",
|
||||||
|
"com_ui_fork_info_start": "Kui see on märgitud, algab hargnemine sellest sõnumist vestluse viimase sõnumini vastavalt ülalvalitud käitumisele.",
|
||||||
|
"com_ui_fork_info_target": "See valik hargneb kõik sõnumid, mis viivad sihtsõnumini, kaasa arvatud selle naabrid; teisisõnu, kõik sõnumiharud, olenemata sellest, kas need on nähtavad või samal teel, on kaasatud.",
|
||||||
|
"com_ui_fork_info_visible": "See valik hargneb ainult nähtavad sõnumid; teisisõnu, otsene tee sihtsõnumini, ilma harudeta.",
|
||||||
|
"com_ui_fork_processing": "Vestlust hargnetakse...",
|
||||||
|
"com_ui_fork_remember": "Jäta meelde",
|
||||||
|
"com_ui_fork_remember_checked": "Sinu valik jäetakse pärast kasutamist meelde. Muuda seda igal ajal seadetes.",
|
||||||
|
"com_ui_fork_split_target": "Alusta hargnemist siit",
|
||||||
|
"com_ui_fork_split_target_setting": "Alusta vaikimisi sihtsõnumist hargnemist",
|
||||||
|
"com_ui_fork_success": "Vestluse hargnemine õnnestus",
|
||||||
|
"com_ui_fork_visible": "Ainult nähtavad sõnumid",
|
||||||
|
"com_ui_global_group": "Ülene grupp",
|
||||||
|
"com_ui_go_back": "Mine tagasi",
|
||||||
|
"com_ui_go_to_conversation": "Mine vestlusesse",
|
||||||
|
"com_ui_happy_birthday": "Mul on 1. sünnipäev!",
|
||||||
|
"com_ui_hide_qr": "Peida QR-kood",
|
||||||
|
"com_ui_host": "Host",
|
||||||
|
"com_ui_idea": "Ideed",
|
||||||
|
"com_ui_image_gen": "Pildi genereerimine",
|
||||||
|
"com_ui_import_conversation_error": "Vestluste importimisel tekkis viga",
|
||||||
|
"com_ui_import_conversation_file_type_error": "Toetamatu imporditüüp",
|
||||||
|
"com_ui_import_conversation_info": "Impordi vestlused JSON-failist",
|
||||||
|
"com_ui_import_conversation_success": "Vestluste importimine õnnestus",
|
||||||
|
"com_ui_include_shadcnui": "Kaasa shadcn/ui komponentide juhised",
|
||||||
|
"com_ui_include_shadcnui_agent": "Kaasa shadcn/ui juhised",
|
||||||
|
"com_ui_input": "Sisend",
|
||||||
|
"com_ui_instructions": "Juhised",
|
||||||
|
"com_ui_latest_footer": "Igaühele oma AI.",
|
||||||
|
"com_ui_latest_production_version": "Viimane tootmisversioon",
|
||||||
|
"com_ui_latest_version": "Viimane versioon",
|
||||||
|
"com_ui_librechat_code_api_key": "Hangi oma LibreChati koodiinterpreteerimise API võti",
|
||||||
|
"com_ui_librechat_code_api_subtitle": "Turvaline. Mitmekeelne. Sisend-/väljundfailid.",
|
||||||
|
"com_ui_librechat_code_api_title": "Käivita AI koodi",
|
||||||
|
"com_ui_llm_menu": "LLM menüü",
|
||||||
|
"com_ui_llms_available": "Saadaolevad LLM-id",
|
||||||
|
"com_ui_loading": "Laeb...",
|
||||||
|
"com_ui_locked": "Lukus",
|
||||||
|
"com_ui_logo": "{{0}} logo",
|
||||||
|
"com_ui_manage": "Halda",
|
||||||
|
"com_ui_max_tags": "Maksimaalne lubatud arv on {{0}}, kasutades viimaseid väärtusi.",
|
||||||
|
"com_ui_mention": "Maini otspunkti, assistenti või eelseadistust, et sellele kiiresti üle minna",
|
||||||
|
"com_ui_min_tags": "Rohkem väärtusi ei saa eemaldada, vaja on vähemalt {{0}}.",
|
||||||
|
"com_ui_misc": "Muu",
|
||||||
|
"com_ui_model": "Mudel",
|
||||||
|
"com_ui_model_parameters": "Mudeli parameetrid",
|
||||||
|
"com_ui_more_info": "Rohkem infot",
|
||||||
|
"com_ui_my_prompts": "Minu sisendid",
|
||||||
|
"com_ui_name": "Nimi",
|
||||||
|
"com_ui_new_chat": "Uus vestlus",
|
||||||
|
"com_ui_next": "Järgmine",
|
||||||
|
"com_ui_no": "Ei",
|
||||||
|
"com_ui_no_bookmarks": "Tundub, et sul pole veel järjehoidjaid. Klõpsa vestlusele ja lisa uus",
|
||||||
|
"com_ui_no_category": "Kategooriat pole",
|
||||||
|
"com_ui_no_changes": "Uuendamiseks pole muudatusi",
|
||||||
|
"com_ui_no_data": "Andmed puuduvad!",
|
||||||
|
"com_ui_no_terms_content": "Kuvamiseks puudub kasutustingimuste sisu",
|
||||||
|
"com_ui_no_valid_items": "Sobivad üksused puuduvad!",
|
||||||
|
"com_ui_none": "Puudub",
|
||||||
|
"com_ui_none_selected": "Ühtegi pole valitud",
|
||||||
|
"com_ui_nothing_found": "Midagi ei leitud",
|
||||||
|
"com_ui_oauth": "OAuth",
|
||||||
|
"com_ui_of": "kohta",
|
||||||
|
"com_ui_off": "Väljas",
|
||||||
|
"com_ui_on": "Sees",
|
||||||
|
"com_ui_openai": "OpenAI",
|
||||||
|
"com_ui_page": "Leht",
|
||||||
|
"com_ui_prev": "Eelmine",
|
||||||
|
"com_ui_preview": "Eelvaade",
|
||||||
|
"com_ui_privacy_policy": "Privaatsuspoliitika",
|
||||||
|
"com_ui_privacy_policy_url": "Privaatsuspoliitika URL",
|
||||||
|
"com_ui_prompt": "Sisend",
|
||||||
|
"com_ui_prompt_already_shared_to_all": "See sisend on juba kõigile kasutajatele jagatud",
|
||||||
|
"com_ui_prompt_name": "Sisendi nimi",
|
||||||
|
"com_ui_prompt_name_required": "Sisendi nimi on kohustuslik",
|
||||||
|
"com_ui_prompt_preview_not_shared": "Autor ei ole selle sisendi jaoks koostööd lubanud.",
|
||||||
|
"com_ui_prompt_text": "Tekst",
|
||||||
|
"com_ui_prompt_text_required": "Tekst on kohustuslik",
|
||||||
|
"com_ui_prompt_update_error": "Sisendi uuendamisel tekkis viga",
|
||||||
|
"com_ui_prompts": "Sisendid",
|
||||||
|
"com_ui_prompts_allow_create": "Luba sisendite loomine",
|
||||||
|
"com_ui_prompts_allow_share_global": "Luba sisendite jagamine kõigile kasutajatele",
|
||||||
|
"com_ui_prompts_allow_use": "Luba sisendite kasutamine",
|
||||||
|
"com_ui_provider": "Teenusepakkuja",
|
||||||
|
"com_ui_read_aloud": "Loe valjusti",
|
||||||
|
"com_ui_refresh_link": "Värskenda linki",
|
||||||
|
"com_ui_regenerate": "Genereeri uuesti",
|
||||||
|
"com_ui_region": "Piirkond",
|
||||||
|
"com_ui_rename": "Nimeta ümber",
|
||||||
|
"com_ui_rename_prompt": "Nimeta sisend ümber",
|
||||||
|
"com_ui_requires_auth": "Vajab autentimist",
|
||||||
|
"com_ui_reset_var": "Lähtesta {{0}}",
|
||||||
|
"com_ui_result": "Tulemus",
|
||||||
|
"com_ui_revoke": "Tühista",
|
||||||
|
"com_ui_revoke_info": "Tühista kõik kasutaja esitatud mandaadid",
|
||||||
|
"com_ui_revoke_key_confirm": "Oled sa kindel, et sa soovid selle võtme tühistada?",
|
||||||
|
"com_ui_revoke_key_endpoint": "Tühista võti {{0}} jaoks",
|
||||||
|
"com_ui_revoke_keys": "Tühista võtmed",
|
||||||
|
"com_ui_revoke_keys_confirm": "Oled sa kindel, et sa soovid kõik võtmed tühistada?",
|
||||||
|
"com_ui_role_select": "Roll",
|
||||||
|
"com_ui_roleplay": "Rollimäng",
|
||||||
|
"com_ui_run_code": "Käivita kood",
|
||||||
|
"com_ui_run_code_error": "Koodi käivitamisel tekkis viga",
|
||||||
|
"com_ui_save": "Salvesta",
|
||||||
|
"com_ui_save_submit": "Salvesta ja esita",
|
||||||
|
"com_ui_saved": "Salvestatud!",
|
||||||
|
"com_ui_schema": "Skeem",
|
||||||
|
"com_ui_scope": "Ulatus",
|
||||||
|
"com_ui_search": "Otsi",
|
||||||
|
"com_ui_select": "Vali",
|
||||||
|
"com_ui_select_file": "Vali fail",
|
||||||
|
"com_ui_select_model": "Vali mudel",
|
||||||
|
"com_ui_select_provider": "Vali teenusepakkuja",
|
||||||
|
"com_ui_select_provider_first": "Vali esmalt teenusepakkuja",
|
||||||
|
"com_ui_select_region": "Vali piirkond",
|
||||||
|
"com_ui_select_search_model": "Otsi mudelit nime järgi",
|
||||||
|
"com_ui_select_search_plugin": "Otsi pistikprogrammi nime järgi",
|
||||||
|
"com_ui_select_search_provider": "Otsi teenusepakkujat nime järgi",
|
||||||
|
"com_ui_select_search_region": "Otsi piirkonda nime järgi",
|
||||||
|
"com_ui_share": "Jaga",
|
||||||
|
"com_ui_share_create_message": "Sinu nimi ja kõik sõnumid, mille sa pärast jagamist lisad, jäävad privaatseks.",
|
||||||
|
"com_ui_share_delete_error": "Jagatud lingi kustutamisel tekkis viga",
|
||||||
|
"com_ui_share_error": "Vestluslingi jagamisel tekkis viga",
|
||||||
|
"com_ui_share_form_description": "Jaga kasutamist.",
|
||||||
|
"com_ui_share_link_to_chat": "Jaga linki vestlusele",
|
||||||
|
"com_ui_share_to_all_users": "Jaga kõigile kasutajatele",
|
||||||
|
"com_ui_share_update_message": "Sinu nimi, kohandatud juhised ja kõik sõnumid, mille sa pärast jagamist lisad, jäävad privaatseks.",
|
||||||
|
"com_ui_share_var": "Jaga {{0}}",
|
||||||
|
"com_ui_shared_link_bulk_delete_success": "Jagatud linkide kustutamine õnnestus",
|
||||||
|
"com_ui_shared_link_delete_success": "Jagatud lingi kustutamine õnnestus",
|
||||||
|
"com_ui_shared_link_not_found": "Jagatud linki ei leitud",
|
||||||
|
"com_ui_shared_prompts": "Jagatud sisendid",
|
||||||
|
"com_ui_shop": "Ostlemine",
|
||||||
|
"com_ui_show_all": "Näita kõiki",
|
||||||
|
"com_ui_show_qr": "Näita QR-koodi",
|
||||||
|
"com_ui_sign_in_to_domain": "Logi sisse {{0}}",
|
||||||
|
"com_ui_simple": "Lihtne",
|
||||||
|
"com_ui_size": "Suurus",
|
||||||
|
"com_ui_special_variables": "Erilised muutujad:",
|
||||||
|
"com_ui_special_variables_info": "Kasuta `{{praegune_kuupäev}}` praeguse kuupäeva jaoks ja `{{praegune_kasutaja}}` oma konto nime jaoks.",
|
||||||
|
"com_ui_speech_while_submitting": "Kõnet ei saa esitada, kui vastust genereeritakse",
|
||||||
|
"com_ui_stop": "Peata",
|
||||||
|
"com_ui_storage": "Salvestusruum",
|
||||||
|
"com_ui_submit": "Esita",
|
||||||
|
"com_ui_teach_or_explain": "Õppimine",
|
||||||
|
"com_ui_temporary_chat": "Ajutine vestlus",
|
||||||
|
"com_ui_terms_and_conditions": "Kasutustingimused",
|
||||||
|
"com_ui_terms_of_service": "Teenuse tingimused",
|
||||||
|
"com_ui_thinking": "Mõtleb...",
|
||||||
|
"com_ui_thoughts": "Mõtted",
|
||||||
|
"com_ui_token_exchange_method": "Märgi vahetamise meetod",
|
||||||
|
"com_ui_token_url": "Märgi URL",
|
||||||
|
"com_ui_tools": "Tööriistad",
|
||||||
|
"com_ui_travel": "Reisimine",
|
||||||
|
"com_ui_unarchive": "Arhiveeri lahti",
|
||||||
|
"com_ui_unarchive_error": "Vestluse arhiveerimine lahti ebaõnnestus",
|
||||||
|
"com_ui_unknown": "Tundmatu",
|
||||||
|
"com_ui_update": "Uuenda",
|
||||||
|
"com_ui_upload": "Laadi üles",
|
||||||
|
"com_ui_upload_code_files": "Laadi üles koodiinterpreteerija jaoks",
|
||||||
|
"com_ui_upload_delay": "Faili \"{{0}}\" üleslaadimine võtab oodatust kauem aega. Palun oota, kuni faili indekseerimine hankimiseks lõpeb.",
|
||||||
|
"com_ui_upload_error": "Faili üleslaadimisel tekkis viga",
|
||||||
|
"com_ui_upload_file_search": "Laadi üles failiotsingu jaoks",
|
||||||
|
"com_ui_upload_files": "Laadi faile üles",
|
||||||
|
"com_ui_upload_image": "Laadi pilt üles",
|
||||||
|
"com_ui_upload_image_input": "Laadi pilt üles",
|
||||||
|
"com_ui_upload_invalid": "Fail on üleslaadimiseks vigane. Peab olema pilt, mis ei ületa piirangut",
|
||||||
|
"com_ui_upload_invalid_var": "Fail on üleslaadimiseks vigane. Peab olema pilt, mis ei ületa {{0}} MB",
|
||||||
|
"com_ui_upload_success": "Faili üleslaadimine õnnestus",
|
||||||
|
"com_ui_upload_type": "Vali üleslaadimise tüüp",
|
||||||
|
"com_ui_use_micrphone": "Kasuta mikrofoni",
|
||||||
|
"com_ui_use_prompt": "Kasuta sisendit",
|
||||||
|
"com_ui_variables": "Muutujad",
|
||||||
|
"com_ui_variables_info": "Kasuta oma tekstis topelt sulgusid, et luua muutujaid, nt `{{näidismuutuja}}`, et hiljem sisendi kasutamisel täita.",
|
||||||
|
"com_ui_version_var": "Versioon {{0}}",
|
||||||
|
"com_ui_versions": "Versioonid",
|
||||||
|
"com_ui_view_source": "Vaata algset vestlust",
|
||||||
|
"com_ui_write": "Kirjutamine",
|
||||||
|
"com_ui_yes": "Jah",
|
||||||
|
"com_ui_zoom": "Suumi",
|
||||||
|
"com_user_message": "Sina",
|
||||||
|
"com_warning_resubmit_unsupported": "AI sõnumi uuesti esitamine pole selle otspunkti jaoks toetatud."
|
||||||
|
}
|
||||||
|
|
@ -149,7 +149,6 @@
|
||||||
"com_endpoint_google_temp": "Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.",
|
"com_endpoint_google_temp": "Korkeampi arvo = satunnaisempi; matalampi arvo = keskittyneempi ja deterministisempi. Suosittelemme, että muokkaat tätä tai Top P:tä, mutta ei molempia.",
|
||||||
"com_endpoint_google_topk": "Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennäköisen mallin sanastossa (tunnetaan myös nimellä ahne dekoodaus), kun taas top-k 3 tarkoittaisi, että seuraavat token valitaan 3 todennäköisimmän tokenin joukosta, lämpötilaa hyödyntäen.",
|
"com_endpoint_google_topk": "Top-k vaikuttaa siihen, miten malli valitsee tokeineita tulokseen. Jos Top-k on 1, valitaan se token, joka on kaikkien todennäköisen mallin sanastossa (tunnetaan myös nimellä ahne dekoodaus), kun taas top-k 3 tarkoittaisi, että seuraavat token valitaan 3 todennäköisimmän tokenin joukosta, lämpötilaa hyödyntäen.",
|
||||||
"com_endpoint_google_topp": "Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennäköisimmistä vähiten todennäköseen, kunnes niiden todennäköisyyksien summa ylittää Top-P -arvon.",
|
"com_endpoint_google_topp": "Top-P vaikuttaa siihen kuinka malli valitsee tokeneita tulokseen. Tokenit valitaan top-k:sta (ks. Top-k -parametri) todennäköisimmistä vähiten todennäköseen, kunnes niiden todennäköisyyksien summa ylittää Top-P -arvon.",
|
||||||
"com_endpoint_import": "Tuo",
|
|
||||||
"com_endpoint_instructions_assistants": "Yliaja ohjeet",
|
"com_endpoint_instructions_assistants": "Yliaja ohjeet",
|
||||||
"com_endpoint_instructions_assistants_placeholder": "Yliajaa Avustajan ohjeet. Tätä voi hyödyntää käytöksen muuttamiseen keskustelukohtaisesti.",
|
"com_endpoint_instructions_assistants_placeholder": "Yliajaa Avustajan ohjeet. Tätä voi hyödyntää käytöksen muuttamiseen keskustelukohtaisesti.",
|
||||||
"com_endpoint_max_output_tokens": "Tulos-tokeneiden maksimimäärä",
|
"com_endpoint_max_output_tokens": "Tulos-tokeneiden maksimimäärä",
|
||||||
|
|
@ -220,7 +219,6 @@
|
||||||
"com_nav_archive_name": "Nimi",
|
"com_nav_archive_name": "Nimi",
|
||||||
"com_nav_archived_chats": "Arkistoidut keskustelut",
|
"com_nav_archived_chats": "Arkistoidut keskustelut",
|
||||||
"com_nav_archived_chats_empty": "Sinulla ei ole arkistoituja keskusteluita.",
|
"com_nav_archived_chats_empty": "Sinulla ei ole arkistoituja keskusteluita.",
|
||||||
"com_nav_archived_chats_manage": "Hallinnoi",
|
|
||||||
"com_nav_audio_play_error": "Virhe ääntä toistaessa: {{0}}",
|
"com_nav_audio_play_error": "Virhe ääntä toistaessa: {{0}}",
|
||||||
"com_nav_audio_process_error": "Virhe ääntä käsitellessä: {{0}}",
|
"com_nav_audio_process_error": "Virhe ääntä käsitellessä: {{0}}",
|
||||||
"com_nav_auto_scroll": "Vieritä automaattisesti viimeisimpään viestiin keskustelua avatessa",
|
"com_nav_auto_scroll": "Vieritä automaattisesti viimeisimpään viestiin keskustelua avatessa",
|
||||||
|
|
@ -273,6 +271,7 @@
|
||||||
"com_nav_lang_chinese": "中文",
|
"com_nav_lang_chinese": "中文",
|
||||||
"com_nav_lang_dutch": "Nederlands",
|
"com_nav_lang_dutch": "Nederlands",
|
||||||
"com_nav_lang_english": "English",
|
"com_nav_lang_english": "English",
|
||||||
|
"com_nav_lang_estonian": "Eesti keel",
|
||||||
"com_nav_lang_finnish": "Suomi",
|
"com_nav_lang_finnish": "Suomi",
|
||||||
"com_nav_lang_french": "Français ",
|
"com_nav_lang_french": "Français ",
|
||||||
"com_nav_lang_german": "Deutsch",
|
"com_nav_lang_german": "Deutsch",
|
||||||
|
|
@ -282,10 +281,11 @@
|
||||||
"com_nav_lang_japanese": "日本語",
|
"com_nav_lang_japanese": "日本語",
|
||||||
"com_nav_lang_korean": "한국어",
|
"com_nav_lang_korean": "한국어",
|
||||||
"com_nav_lang_polish": "Polski",
|
"com_nav_lang_polish": "Polski",
|
||||||
|
"com_nav_lang_portuguese": "Português",
|
||||||
"com_nav_lang_russian": "Русский",
|
"com_nav_lang_russian": "Русский",
|
||||||
"com_nav_lang_spanish": "Español",
|
"com_nav_lang_spanish": "Español",
|
||||||
"com_nav_lang_swedish": "Svenska",
|
"com_nav_lang_swedish": "Svenska",
|
||||||
"com_nav_lang_traditionalchinese": "繁體中文",
|
"com_nav_lang_traditional_chinese": "繁體中文",
|
||||||
"com_nav_lang_turkish": "Türkçe",
|
"com_nav_lang_turkish": "Türkçe",
|
||||||
"com_nav_lang_vietnamese": "Tiếng Việt",
|
"com_nav_lang_vietnamese": "Tiếng Việt",
|
||||||
"com_nav_language": "Kieli",
|
"com_nav_language": "Kieli",
|
||||||
|
|
@ -313,7 +313,6 @@
|
||||||
"com_nav_setting_speech": "Puhe",
|
"com_nav_setting_speech": "Puhe",
|
||||||
"com_nav_settings": "Asetukset",
|
"com_nav_settings": "Asetukset",
|
||||||
"com_nav_shared_links": "Jaetut linkit",
|
"com_nav_shared_links": "Jaetut linkit",
|
||||||
"com_nav_shared_links_manage": "Hallinnoi",
|
|
||||||
"com_nav_show_code": "Kooditulkkia käyttäessä näytä aina koodi",
|
"com_nav_show_code": "Kooditulkkia käyttäessä näytä aina koodi",
|
||||||
"com_nav_speech_to_text": "Puheesta tekstiksi",
|
"com_nav_speech_to_text": "Puheesta tekstiksi",
|
||||||
"com_nav_text_to_speech": "Tekstistä puheeksi",
|
"com_nav_text_to_speech": "Tekstistä puheeksi",
|
||||||
|
|
@ -459,7 +458,6 @@
|
||||||
"com_ui_happy_birthday": "On 1. syntymäpäiväni!",
|
"com_ui_happy_birthday": "On 1. syntymäpäiväni!",
|
||||||
"com_ui_host": "Host",
|
"com_ui_host": "Host",
|
||||||
"com_ui_image_gen": "Kuvanluonti",
|
"com_ui_image_gen": "Kuvanluonti",
|
||||||
"com_ui_import_conversation": "Tuo",
|
|
||||||
"com_ui_import_conversation_error": "Keskustelujesi tuonnissa tapahtui virhe",
|
"com_ui_import_conversation_error": "Keskustelujesi tuonnissa tapahtui virhe",
|
||||||
"com_ui_import_conversation_file_type_error": "Tiedostotyyppi ei ole tuettu tuonnissa",
|
"com_ui_import_conversation_file_type_error": "Tiedostotyyppi ei ole tuettu tuonnissa",
|
||||||
"com_ui_import_conversation_info": "Tuo keskusteluja JSON-tiedostosta",
|
"com_ui_import_conversation_info": "Tuo keskusteluja JSON-tiedostosta",
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue