Merge branch 'main' into added-codeql

This commit is contained in:
Ruben Talstra 2025-02-12 18:17:59 +01:00 committed by GitHub
commit 4b72518813
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
255 changed files with 20660 additions and 41548 deletions

View file

@ -1,213 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
commonjs: true,
es6: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:jest/recommended',
'prettier',
'plugin:jsx-a11y/recommended',
],
ignorePatterns: [
'client/dist/**/*',
'client/public/**/*',
'e2e/playwright-report/**/*',
'packages/mcp/types/**/*',
'packages/mcp/dist/**/*',
'packages/mcp/test_bundle/**/*',
'api/demo/**/*',
'packages/data-provider/types/**/*',
'packages/data-provider/dist/**/*',
'packages/data-provider/test_bundle/**/*',
'data-node/**/*',
'meili_data/**/*',
'node_modules/**/*',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
plugins: ['react', 'react-hooks', '@typescript-eslint', 'import', 'jsx-a11y'],
rules: {
'react/react-in-jsx-scope': 'off',
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow' }],
indent: ['error', 2, { SwitchCase: 1 }],
'max-len': [
'error',
{
code: 120,
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreComments: true,
},
],
'linebreak-style': 0,
curly: ['error', 'all'],
semi: ['error', 'always'],
'object-curly-spacing': ['error', 'always'],
'no-multiple-empty-lines': ['error', { max: 1 }],
'no-trailing-spaces': 'error',
'comma-dangle': ['error', 'always-multiline'],
// "arrow-parens": [2, "as-needed", { requireForBlockBody: true }],
// 'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
'no-console': 'off',
'import/no-cycle': 'error',
'import/no-self-import': 'error',
'import/extensions': 'off',
'no-promise-executor-return': 'off',
'no-param-reassign': 'off',
'no-continue': 'off',
'no-restricted-syntax': 'off',
'react/prop-types': ['off'],
'react/display-name': ['off'],
'no-nested-ternary': 'error',
'no-unused-vars': ['error', { varsIgnorePattern: '^_' }],
quotes: ['error', 'single'],
},
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-unused-vars': 'off', // off because it conflicts with '@typescript-eslint/no-unused-vars'
'react/display-name': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
},
{
files: ['rollup.config.js', '.eslintrc.js', 'jest.config.js'],
env: {
node: true,
},
},
{
files: [
'**/*.test.js',
'**/*.test.jsx',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.js',
'**/*.spec.jsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'setupTests.js',
],
env: {
jest: true,
node: true,
},
rules: {
'react/display-name': 'off',
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off',
},
},
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './client/tsconfig.json',
},
plugins: ['@typescript-eslint/eslint-plugin', 'jest'],
extends: [
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unnecessary-condition': 'warn',
'@typescript-eslint/strict-boolean-expressions': 'warn',
},
},
{
files: './packages/data-provider/**/*.ts',
overrides: [
{
files: '**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './packages/data-provider/tsconfig.json',
},
},
],
},
{
files: './api/demo/**/*.ts',
overrides: [
{
files: '**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './packages/data-provider/tsconfig.json',
},
},
],
},
{
files: './packages/mcp/**/*.ts',
overrides: [
{
files: '**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './packages/mcp/tsconfig.json',
},
},
],
},
{
files: './config/translations/**/*.ts',
parser: '@typescript-eslint/parser',
parserOptions: {
project: './config/translations/tsconfig.json',
},
},
{
files: ['./packages/data-provider/specs/**/*.ts'],
parserOptions: {
project: './packages/data-provider/tsconfig.spec.json',
},
},
{
files: ['./api/demo/specs/**/*.ts'],
parserOptions: {
project: './packages/data-provider/tsconfig.spec.json',
},
},
{
files: ['./packages/mcp/specs/**/*.ts'],
parserOptions: {
project: './packages/mcp/tsconfig.spec.json',
},
},
],
settings: {
react: {
createClass: 'createReactClass', // Regex for Component Factory to use,
// default to "createReactClass"
pragma: 'React', // Pragma to use, default to "React"
fragment: 'Fragment', // Fragment to use (may be a property of <pragma>), default to "Fragment"
version: 'detect', // React version. "detect" automatically picks the version you have installed.
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./client/tsconfig.json'],
},
node: {
project: ['./client/tsconfig.json'],
},
},
},
};

View file

@ -1,12 +1,19 @@
name: Bug Report name: Bug Report
description: File a bug report description: File a bug report
title: "[Bug]: " title: "[Bug]: "
labels: ["bug"] labels: ["🐛 bug"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Thanks for taking the time to fill out this bug report!
Before submitting, please:
- Search existing [Issues and Discussions](https://github.com/danny-avila/LibreChat/discussions) to see if your bug has already been reported
- Use [Discussions](https://github.com/danny-avila/LibreChat/discussions) instead of Issues for:
- General inquiries
- Help with setup
- Questions about whether you're experiencing a bug
- type: textarea - type: textarea
id: what-happened id: what-happened
attributes: attributes:
@ -15,6 +22,23 @@ body:
placeholder: Please give as many details as possible placeholder: Please give as many details as possible
validations: validations:
required: true required: true
- type: textarea
id: version-info
attributes:
label: Version Information
description: |
If using Docker, please run and provide the output of:
```bash
docker images | grep librechat
```
If running from source, please run and provide the output of:
```bash
git rev-parse HEAD
```
placeholder: Paste the output here
validations:
required: true
- type: textarea - type: textarea
id: steps-to-reproduce id: steps-to-reproduce
attributes: attributes:
@ -39,7 +63,21 @@ body:
id: logs id: logs
attributes: attributes:
label: Relevant log output label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. description: |
Please paste relevant logs that were created when reproducing the error.
Log locations:
- Docker: Project root directory ./logs
- npm: ./api/logs
There are two types of logs that can help diagnose the issue:
- debug logs (debug-YYYY-MM-DD.log)
- error logs (error-YYYY-MM-DD.log)
Error logs contain exact stack traces and are especially helpful, but both can provide valuable information.
Please only include the relevant portions of logs that correspond to when you reproduced the error.
For UI-related issues, browser console logs can be very helpful. You can provide these as screenshots or paste the text here.
render: shell render: shell
- type: textarea - type: textarea
id: screenshots id: screenshots

View file

@ -1,7 +1,7 @@
name: Feature Request name: Feature Request
description: File a feature request description: File a feature request
title: "Enhancement: " title: "[Enhancement]: "
labels: ["enhancement"] labels: ["enhancement"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -0,0 +1,33 @@
name: New Language Request
description: Request to add a new language for LibreChat translations.
title: "New Language Request: "
labels: ["✨ enhancement", "🌍 i18n"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to submit a new language request! Please fill out the following details so we can review your request.
- type: input
id: language_name
attributes:
label: Language Name
description: Please provide the full name of the language (e.g., Spanish, Mandarin).
placeholder: e.g., Spanish
validations:
required: true
- type: input
id: iso_code
attributes:
label: ISO 639-1 Code
description: Please provide the ISO 639-1 code for the language (e.g., es for Spanish). You can refer to [this list](https://www.w3schools.com/tags/ref_language_codes.asp) for valid codes.
placeholder: e.g., es
validations:
required: true
- 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

View file

@ -1,7 +1,7 @@
name: Question name: Question
description: Ask your question description: Ask your question
title: "[Question]: " title: "[Question]: "
labels: ["question"] labels: ["question"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -62,8 +62,3 @@ jobs:
- name: Run librechat-data-provider unit tests - name: Run librechat-data-provider unit tests
run: cd packages/data-provider && npm run test:ci run: cd packages/data-provider && npm run test:ci
- name: Run linters
uses: wearerequired/lint-action@v2
with:
eslint: true

73
.github/workflows/eslint-ci.yml vendored Normal file
View file

@ -0,0 +1,73 @@
name: ESLint Code Quality Checks
on:
pull_request:
branches:
- main
- dev
- release/*
paths:
- 'api/**'
- 'client/**'
jobs:
eslint_checks:
name: Run ESLint Linting
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
# Run ESLint on changed files within the api/ and client/ directories.
- name: Run ESLint on changed files
env:
SARIF_ESLINT_IGNORE_SUPPRESSED: "true"
run: |
# Extract the base commit SHA from the pull_request event payload.
BASE_SHA=$(jq --raw-output .pull_request.base.sha "$GITHUB_EVENT_PATH")
echo "Base commit SHA: $BASE_SHA"
# Get changed files (only JS/TS files in api/ or client/)
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRTUXB "$BASE_SHA" HEAD | grep -E '^(api|client)/.*\.(js|jsx|ts|tsx)$' || true)
# Debug output
echo "Changed files:"
echo "$CHANGED_FILES"
# Ensure there are files to lint before running ESLint
if [[ -z "$CHANGED_FILES" ]]; then
echo "No matching files changed. Skipping ESLint."
echo "UPLOAD_SARIF=false" >> $GITHUB_ENV
exit 0
fi
# Set variable to allow SARIF upload
echo "UPLOAD_SARIF=true" >> $GITHUB_ENV
# Run ESLint
npx eslint --no-error-on-unmatched-pattern \
--config eslint.config.mjs \
--format @microsoft/eslint-formatter-sarif \
--output-file eslint-results.sarif $CHANGED_FILES || true
- name: Upload analysis results to GitHub
if: env.UPLOAD_SARIF == 'true'
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: eslint-results.sarif
wait-for-processing: true

84
.github/workflows/i18n-unused-keys.yml vendored Normal file
View file

@ -0,0 +1,84 @@
name: Detect Unused i18next Strings
on:
pull_request:
paths:
- "client/src/**"
jobs:
detect-unused-i18n-keys:
runs-on: ubuntu-latest
permissions:
pull-requests: write # Required for posting PR comments
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Find unused i18next keys
id: find-unused
run: |
echo "🔍 Scanning for unused i18next keys..."
# Define paths
I18N_FILE="client/src/locales/en/translation.json"
SOURCE_DIR="client/src"
# Check if translation file exists
if [[ ! -f "$I18N_FILE" ]]; then
echo "::error title=Missing i18n File::Translation file not found: $I18N_FILE"
exit 1
fi
# Extract all keys from the JSON file
KEYS=$(jq -r 'keys[]' "$I18N_FILE")
# Track unused keys
UNUSED_KEYS=()
# Check if each key is used in the source code
for KEY in $KEYS; do
if ! grep -r --include=\*.{js,jsx,ts,tsx} -q "$KEY" "$SOURCE_DIR"; then
UNUSED_KEYS+=("$KEY")
fi
done
# Output results
if [[ ${#UNUSED_KEYS[@]} -gt 0 ]]; then
echo "🛑 Found ${#UNUSED_KEYS[@]} unused i18n keys:"
echo "unused_keys=$(echo "${UNUSED_KEYS[@]}" | jq -R -s -c 'split(" ")')" >> $GITHUB_ENV
for KEY in "${UNUSED_KEYS[@]}"; do
echo "::warning title=Unused i18n Key::'$KEY' is defined but not used in the codebase."
done
else
echo "✅ No unused i18n keys detected!"
echo "unused_keys=[]" >> $GITHUB_ENV
fi
- name: Post verified comment on PR
if: env.unused_keys != '[]'
run: |
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
# Format the unused keys list correctly, filtering out empty entries
FILTERED_KEYS=$(echo "$unused_keys" | jq -r '.[]' | grep -v '^\s*$' | sed 's/^/- `/;s/$/`/' )
COMMENT_BODY=$(cat <<EOF
### 🚨 Unused i18next Keys Detected
The following translation keys are defined in \`translation.json\` but are **not used** in the codebase:
$FILTERED_KEYS
⚠️ **Please remove these unused keys to keep the translation files clean.**
EOF
)
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$COMMENT_BODY" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fail workflow if unused keys found
if: env.unused_keys != '[]'
run: exit 1 # This makes the PR fail if unused keys exist

72
.github/workflows/locize-i18n-sync.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Sync Locize Translations & Create Translation PR
on:
push:
branches: [main]
repository_dispatch:
types: [locize/versionPublished]
jobs:
sync-translations:
name: Sync Translation Keys with Locize
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set Up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install locize CLI
run: npm install -g locize-cli
# Sync translations (Push missing keys & remove deleted ones)
- name: Sync Locize with Repository
if: ${{ github.event_name == 'push' }}
run: |
cd client/src/locales
locize sync --api-key ${{ secrets.LOCIZE_API_KEY }} --project-id ${{ secrets.LOCIZE_PROJECT_ID }} --language en
# When triggered by repository_dispatch, skip sync step.
- name: Skip sync step on non-push events
if: ${{ github.event_name != 'push' }}
run: echo "Skipping sync as the event is not a push."
create-pull-request:
name: Create Translation PR on Version Published
runs-on: ubuntu-latest
needs: sync-translations
permissions:
contents: write
pull-requests: write
steps:
# 1. Check out the repository.
- name: Checkout Repository
uses: actions/checkout@v4
# 2. Download translation files from locize.
- name: Download Translations from locize
uses: locize/download@v1
with:
project-id: ${{ secrets.LOCIZE_PROJECT_ID }}
path: "client/src/locales"
# 3. Create a Pull Request using built-in functionality.
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
sign-commits: true
commit-message: "🌍 i18n: Update translation.json with latest translations"
base: main
branch: i18n/locize-translation-update
reviewers: danny-avila
title: "🌍 i18n: Update translation.json with latest translations"
body: |
**Description**:
- 🎯 **Objective**: Update `translation.json` with the latest translations from locize.
- 🔍 **Details**: This PR is automatically generated upon receiving a versionPublished event with version "latest". It reflects the newest translations provided by locize.
- ✅ **Status**: Ready for review.
labels: "🌍 i18n"

147
.github/workflows/unused-packages.yml vendored Normal file
View file

@ -0,0 +1,147 @@
name: Detect Unused NPM Packages
on: [pull_request]
jobs:
detect-unused-packages:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install depcheck
run: npm install -g depcheck
- name: Validate JSON files
run: |
for FILE in package.json client/package.json api/package.json; do
if [[ -f "$FILE" ]]; then
jq empty "$FILE" || (echo "::error title=Invalid JSON::$FILE is invalid" && exit 1)
fi
done
- name: Extract Dependencies Used in Scripts
id: extract-used-scripts
run: |
extract_deps_from_scripts() {
local package_file=$1
if [[ -f "$package_file" ]]; then
jq -r '.scripts | to_entries[].value' "$package_file" | \
grep -oE '([a-zA-Z0-9_-]+)' | sort -u > used_scripts.txt
else
touch used_scripts.txt
fi
}
extract_deps_from_scripts "package.json"
mv used_scripts.txt root_used_deps.txt
extract_deps_from_scripts "client/package.json"
mv used_scripts.txt client_used_deps.txt
extract_deps_from_scripts "api/package.json"
mv used_scripts.txt api_used_deps.txt
- name: Extract Dependencies Used in Source Code
id: extract-used-code
run: |
extract_deps_from_code() {
local folder=$1
local output_file=$2
if [[ -d "$folder" ]]; then
grep -rEho "require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)" "$folder" --include=\*.{js,ts,mjs,cjs} | \
sed -E "s/require\\(['\"]([a-zA-Z0-9@/._-]+)['\"]\\)/\1/" > "$output_file"
grep -rEho "import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]" "$folder" --include=\*.{js,ts,mjs,cjs} | \
sed -E "s/import .* from ['\"]([a-zA-Z0-9@/._-]+)['\"]/\1/" >> "$output_file"
sort -u "$output_file" -o "$output_file"
else
touch "$output_file"
fi
}
extract_deps_from_code "." root_used_code.txt
extract_deps_from_code "client" client_used_code.txt
extract_deps_from_code "api" api_used_code.txt
- name: Run depcheck for root package.json
id: check-root
run: |
if [[ -f "package.json" ]]; then
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat root_used_deps.txt root_used_code.txt | sort) || echo "")
echo "ROOT_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
fi
- name: Run depcheck for client/package.json
id: check-client
run: |
if [[ -f "client/package.json" ]]; then
chmod -R 755 client
cd client
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../client_used_deps.txt ../client_used_code.txt | sort) || echo "")
echo "CLIENT_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
cd ..
fi
- name: Run depcheck for api/package.json
id: check-api
run: |
if [[ -f "api/package.json" ]]; then
chmod -R 755 api
cd api
UNUSED=$(depcheck --json | jq -r '.dependencies | join("\n")' || echo "")
UNUSED=$(comm -23 <(echo "$UNUSED" | sort) <(cat ../api_used_deps.txt ../api_used_code.txt | sort) || echo "")
echo "API_UNUSED<<EOF" >> $GITHUB_ENV
echo "$UNUSED" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
cd ..
fi
- name: Post comment on PR if unused dependencies are found
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
run: |
PR_NUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
ROOT_LIST=$(echo "$ROOT_UNUSED" | awk '{print "- `" $0 "`"}')
CLIENT_LIST=$(echo "$CLIENT_UNUSED" | awk '{print "- `" $0 "`"}')
API_LIST=$(echo "$API_UNUSED" | awk '{print "- `" $0 "`"}')
COMMENT_BODY=$(cat <<EOF
### 🚨 Unused NPM Packages Detected
The following **unused dependencies** were found:
$(if [[ ! -z "$ROOT_UNUSED" ]]; then echo "#### 📂 Root \`package.json\`"; echo ""; echo "$ROOT_LIST"; echo ""; fi)
$(if [[ ! -z "$CLIENT_UNUSED" ]]; then echo "#### 📂 Client \`client/package.json\`"; echo ""; echo "$CLIENT_LIST"; echo ""; fi)
$(if [[ ! -z "$API_UNUSED" ]]; then echo "#### 📂 API \`api/package.json\`"; echo ""; echo "$API_LIST"; echo ""; fi)
⚠️ **Please remove these unused dependencies to keep your project clean.**
EOF
)
gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
-f body="$COMMENT_BODY" \
-H "Authorization: token ${{ secrets.GITHUB_TOKEN }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Fail workflow if unused dependencies found
if: env.ROOT_UNUSED != '' || env.CLIENT_UNUSED != '' || env.API_UNUSED != ''
run: exit 1

19
.prettierrc Normal file
View file

@ -0,0 +1,19 @@
{
"tailwindConfig": "./client/tailwind.config.cjs",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always",
"embeddedLanguageFormatting": "auto",
"insertPragma": false,
"proseWrap": "preserve",
"quoteProps": "as-needed",
"requirePragma": false,
"rangeStart": 0,
"endOfLine": "auto",
"jsxSingleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"]
}

View file

@ -1,4 +1,4 @@
# v0.7.6 # v0.7.7-rc1
# Base node image # Base node image
FROM node:20-alpine AS node FROM node:20-alpine AS node

View file

@ -1,5 +1,5 @@
# Dockerfile.multi # Dockerfile.multi
# v0.7.6 # v0.7.7-rc1
# Base for all builds # Base for all builds
FROM node:20-alpine AS base-min FROM node:20-alpine AS base-min

View file

@ -38,6 +38,15 @@
</a> </a>
</p> </p>
<p align="center">
<a href="https://www.librechat.ai/docs/translation">
<img
src="https://img.shields.io/badge/dynamic/json.svg?style=for-the-badge&color=2096F3&label=locize&query=%24.translatedPercentage&url=https://api.locize.app/badgedata/4cb2598b-ed4d-469c-9b04-2ed531a8cb45&suffix=%+translated"
alt="Translation Progress">
</a>
</p>
# ✨ Features # ✨ Features
- 🖥️ **UI & Experience** inspired by ChatGPT with enhanced design and features - 🖥️ **UI & Experience** inspired by ChatGPT with enhanced design and features
@ -79,6 +88,9 @@
- English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro - English, 中文, Deutsch, Español, Français, Italiano, Polski, Português Brasileiro
- Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית - Русский, 日本語, Svenska, 한국어, Tiếng Việt, 繁體中文, العربية, Türkçe, Nederlands, עברית
- 🧠 **Reasoning UI**:
- Dynamic Reasoning UI for Chain-of-Thought/Reasoning AI models like DeepSeek-R1
- 🎨 **Customizable Interface**: - 🎨 **Customizable Interface**:
- Customizable Dropdown & Interface that adapts to both power users and newcomers - Customizable Dropdown & Interface that adapts to both power users and newcomers
@ -167,6 +179,8 @@ Contributions, suggestions, bug reports and fixes are welcome!
For new features, components, or extensions, please open an issue and discuss before sending a PR. For new features, components, or extensions, please open an issue and discuss before sending a PR.
If you'd like to help translate LibreChat into your language, we'd love your contribution! Improving our translations not only makes LibreChat more accessible to users around the world but also enhances the overall user experience. Please check out our [Translation Guide](https://www.librechat.ai/docs/translation).
--- ---
## 💖 This project exists in its current state thanks to all the people who contribute ## 💖 This project exists in its current state thanks to all the people who contribute
@ -174,3 +188,15 @@ For new features, components, or extensions, please open an issue and discuss be
<a href="https://github.com/danny-avila/LibreChat/graphs/contributors"> <a href="https://github.com/danny-avila/LibreChat/graphs/contributors">
<img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" /> <img src="https://contrib.rocks/image?repo=danny-avila/LibreChat" />
</a> </a>
---
## 🎉 Special Thanks
We thank [Locize](https://locize.com) for their translation management tools that support multiple languages in LibreChat.
<p align="center">
<a href="https://locize.com" target="_blank" rel="noopener noreferrer">
<img src="https://locize.com/img/locize_color.svg" alt="Locize Logo" height="50">
</a>
</p>

View file

@ -506,9 +506,8 @@ class OpenAIClient extends BaseClient {
if (promptPrefix && this.isOmni === true) { if (promptPrefix && this.isOmni === true) {
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user'); const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
if (lastUserMessageIndex !== -1) { if (lastUserMessageIndex !== -1) {
payload[ payload[lastUserMessageIndex].content =
lastUserMessageIndex `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
].content = `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
} }
} }
@ -1067,14 +1066,36 @@ ${convo}
}); });
} }
getStreamText() { /**
*
* @param {string[]} [intermediateReply]
* @returns {string}
*/
getStreamText(intermediateReply) {
if (!this.streamHandler) { if (!this.streamHandler) {
return ''; return intermediateReply?.join('') ?? '';
}
let thinkMatch;
let remainingText;
let reasoningText = '';
if (this.streamHandler.reasoningTokens.length > 0) {
reasoningText = this.streamHandler.reasoningTokens.join('');
thinkMatch = reasoningText.match(/<think>([\s\S]*?)<\/think>/)?.[1]?.trim();
if (thinkMatch != null && thinkMatch) {
const reasoningTokens = `:::thinking\n${thinkMatch}\n:::\n`;
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
return `${reasoningTokens}${remainingText}${this.streamHandler.tokens.join('')}`;
} else if (thinkMatch === '') {
remainingText = reasoningText.split(/<\/think>/)?.[1]?.trim() || '';
return `${remainingText}${this.streamHandler.tokens.join('')}`;
}
} }
const reasoningTokens = const reasoningTokens =
this.streamHandler.reasoningTokens.length > 0 reasoningText.length > 0
? `:::thinking\n${this.streamHandler.reasoningTokens.join('')}\n:::\n` ? `:::thinking\n${reasoningText.replace('<think>', '').replace('</think>', '').trim()}\n:::\n`
: ''; : '';
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`; return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
@ -1314,11 +1335,19 @@ ${convo}
streamPromise = new Promise((resolve) => { streamPromise = new Promise((resolve) => {
streamResolve = resolve; streamResolve = resolve;
}); });
/** @type {OpenAI.OpenAI.CompletionCreateParamsStreaming} */
const params = {
...modelOptions,
stream: true,
};
if (
this.options.endpoint === EModelEndpoint.openAI ||
this.options.endpoint === EModelEndpoint.azureOpenAI
) {
params.stream_options = { include_usage: true };
}
const stream = await openai.beta.chat.completions const stream = await openai.beta.chat.completions
.stream({ .stream(params)
...modelOptions,
stream: true,
})
.on('abort', () => { .on('abort', () => {
/* Do nothing here */ /* Do nothing here */
}) })
@ -1449,7 +1478,7 @@ ${convo}
this.options.context !== 'title' && this.options.context !== 'title' &&
message.content.startsWith('<think>') message.content.startsWith('<think>')
) { ) {
return message.content.replace('<think>', ':::thinking').replace('</think>', ':::'); return this.getStreamText();
} }
return message.content; return message.content;
@ -1458,7 +1487,7 @@ ${convo}
err?.message?.includes('abort') || err?.message?.includes('abort') ||
(err instanceof OpenAI.APIError && err?.message?.includes('abort')) (err instanceof OpenAI.APIError && err?.message?.includes('abort'))
) { ) {
return intermediateReply.join(''); return this.getStreamText(intermediateReply);
} }
if ( if (
err?.message?.includes( err?.message?.includes(
@ -1473,14 +1502,18 @@ ${convo}
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason')) (err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
) { ) {
logger.error('[OpenAIClient] Known OpenAI error:', err); logger.error('[OpenAIClient] Known OpenAI error:', err);
if (intermediateReply.length > 0) { if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return intermediateReply.join(''); return this.getStreamText();
} else if (intermediateReply.length > 0) {
return this.getStreamText(intermediateReply);
} else { } else {
throw err; throw err;
} }
} else if (err instanceof OpenAI.APIError) { } else if (err instanceof OpenAI.APIError) {
if (intermediateReply.length > 0) { if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
return intermediateReply.join(''); return this.getStreamText();
} else if (intermediateReply.length > 0) {
return this.getStreamText(intermediateReply);
} else { } else {
throw err; throw err;
} }

View file

@ -37,6 +37,10 @@ const messages = isRedisEnabled
? new Keyv({ store: keyvRedis, ttl: Time.ONE_MINUTE }) ? new Keyv({ store: keyvRedis, ttl: Time.ONE_MINUTE })
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.ONE_MINUTE }); : new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.ONE_MINUTE });
const flows = isRedisEnabled
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
: new Keyv({ namespace: CacheKeys.FLOWS, ttl: Time.ONE_MINUTE * 3 });
const tokenConfig = isRedisEnabled const tokenConfig = isRedisEnabled
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES }) ? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES }); : new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
@ -88,6 +92,7 @@ const namespaces = {
[CacheKeys.MODEL_QUERIES]: modelQueries, [CacheKeys.MODEL_QUERIES]: modelQueries,
[CacheKeys.AUDIO_RUNS]: audioRuns, [CacheKeys.AUDIO_RUNS]: audioRuns,
[CacheKeys.MESSAGES]: messages, [CacheKeys.MESSAGES]: messages,
[CacheKeys.FLOWS]: flows,
}; };
/** /**

View file

@ -1,6 +1,6 @@
const KeyvRedis = require('@keyv/redis'); const KeyvRedis = require('@keyv/redis');
const { logger } = require('~/config');
const { isEnabled } = require('~/server/utils'); const { isEnabled } = require('~/server/utils');
const logger = require('~/config/winston');
const { REDIS_URI, USE_REDIS } = process.env; const { REDIS_URI, USE_REDIS } = process.env;

View file

@ -1,9 +1,11 @@
const { EventSource } = require('eventsource'); const { EventSource } = require('eventsource');
const { Time, CacheKeys } = require('librechat-data-provider');
const logger = require('./winston'); const logger = require('./winston');
global.EventSource = EventSource; global.EventSource = EventSource;
let mcpManager = null; let mcpManager = null;
let flowManager = null;
/** /**
* @returns {Promise<MCPManager>} * @returns {Promise<MCPManager>}
@ -16,6 +18,21 @@ async function getMCPManager() {
return mcpManager; return mcpManager;
} }
/**
* @param {(key: string) => Keyv} getLogStores
* @returns {Promise<FlowStateManager>}
*/
async function getFlowStateManager(getLogStores) {
if (!flowManager) {
const { FlowStateManager } = await import('librechat-mcp');
flowManager = new FlowStateManager(getLogStores(CacheKeys.FLOWS), {
ttl: Time.ONE_MINUTE * 3,
logger,
});
}
return flowManager;
}
/** /**
* Sends message data in Server Sent Events format. * Sends message data in Server Sent Events format.
* @param {ServerResponse} res - The server response. * @param {ServerResponse} res - The server response.
@ -34,4 +51,5 @@ module.exports = {
logger, logger,
sendEvent, sendEvent,
getMCPManager, getMCPManager,
getFlowStateManager,
}; };

View file

@ -1,5 +1,6 @@
const tokenSchema = require('./schema/tokenSchema');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const { encryptV2 } = require('~/server/utils/crypto');
const tokenSchema = require('./schema/tokenSchema');
const { logger } = require('~/config'); const { logger } = require('~/config');
/** /**
@ -7,6 +8,32 @@ const { logger } = require('~/config');
* @type {mongoose.Model} * @type {mongoose.Model}
*/ */
const Token = mongoose.model('Token', tokenSchema); const Token = mongoose.model('Token', tokenSchema);
/**
* Fixes the indexes for the Token collection from legacy TTL indexes to the new expiresAt index.
*/
async function fixIndexes() {
try {
const indexes = await Token.collection.indexes();
logger.debug('Existing Token Indexes:', JSON.stringify(indexes, null, 2));
const unwantedTTLIndexes = indexes.filter(
(index) => index.key.createdAt === 1 && index.expireAfterSeconds !== undefined,
);
if (unwantedTTLIndexes.length === 0) {
logger.debug('No unwanted Token indexes found.');
return;
}
for (const index of unwantedTTLIndexes) {
logger.debug(`Dropping unwanted Token index: ${index.name}`);
await Token.collection.dropIndex(index.name);
logger.debug(`Dropped Token index: ${index.name}`);
}
logger.debug('Token index cleanup completed successfully.');
} catch (error) {
logger.error('An error occurred while fixing Token indexes:', error);
}
}
fixIndexes();
/** /**
* Creates a new Token instance. * Creates a new Token instance.
@ -29,8 +56,7 @@ async function createToken(tokenData) {
expiresAt, expiresAt,
}; };
const newToken = new Token(newTokenData); return await Token.create(newTokenData);
return await newToken.save();
} catch (error) { } catch (error) {
logger.debug('An error occurred while creating token:', error); logger.debug('An error occurred while creating token:', error);
throw error; throw error;
@ -42,7 +68,8 @@ async function createToken(tokenData) {
* @param {Object} query - The query to match against. * @param {Object} query - The query to match against.
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @param {String} query.token - The token value. * @param {String} query.token - The token value.
* @param {String} query.email - The email of the user. * @param {String} [query.email] - The email of the user.
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
* @returns {Promise<Object|null>} The matched Token document, or null if not found. * @returns {Promise<Object|null>} The matched Token document, or null if not found.
* @throws Will throw an error if the find operation fails. * @throws Will throw an error if the find operation fails.
*/ */
@ -59,6 +86,9 @@ async function findToken(query) {
if (query.email) { if (query.email) {
conditions.push({ email: query.email }); conditions.push({ email: query.email });
} }
if (query.identifier) {
conditions.push({ identifier: query.identifier });
}
const token = await Token.findOne({ const token = await Token.findOne({
$and: conditions, $and: conditions,
@ -76,6 +106,8 @@ async function findToken(query) {
* @param {Object} query - The query to match against. * @param {Object} query - The query to match against.
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @param {String} query.token - The token value. * @param {String} query.token - The token value.
* @param {String} [query.email] - The email of the user.
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
* @param {Object} updateData - The data to update the Token with. * @param {Object} updateData - The data to update the Token with.
* @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found. * @returns {Promise<mongoose.Document|null>} The updated Token document, or null if not found.
* @throws Will throw an error if the update operation fails. * @throws Will throw an error if the update operation fails.
@ -94,14 +126,20 @@ async function updateToken(query, updateData) {
* @param {Object} query - The query to match against. * @param {Object} query - The query to match against.
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user. * @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @param {String} query.token - The token value. * @param {String} query.token - The token value.
* @param {String} query.email - The email of the user. * @param {String} [query.email] - The email of the user.
* @param {String} [query.identifier] - Unique, alternative identifier for the token.
* @returns {Promise<Object>} The result of the delete operation. * @returns {Promise<Object>} The result of the delete operation.
* @throws Will throw an error if the delete operation fails. * @throws Will throw an error if the delete operation fails.
*/ */
async function deleteTokens(query) { async function deleteTokens(query) {
try { try {
return await Token.deleteMany({ return await Token.deleteMany({
$or: [{ userId: query.userId }, { token: query.token }, { email: query.email }], $or: [
{ userId: query.userId },
{ token: query.token },
{ email: query.email },
{ identifier: query.identifier },
],
}); });
} catch (error) { } catch (error) {
logger.debug('An error occurred while deleting tokens:', error); logger.debug('An error occurred while deleting tokens:', error);
@ -109,9 +147,46 @@ async function deleteTokens(query) {
} }
} }
/**
* Handles the OAuth token by creating or updating the token.
* @param {object} fields
* @param {string} fields.userId - The user's ID.
* @param {string} fields.token - The full token to store.
* @param {string} fields.identifier - Unique, alternative identifier for the token.
* @param {number} fields.expiresIn - The number of seconds until the token expires.
* @param {object} fields.metadata - Additional metadata to store with the token.
* @param {string} [fields.type="oauth"] - The type of token. Default is 'oauth'.
*/
async function handleOAuthToken({
token,
userId,
identifier,
expiresIn,
metadata,
type = 'oauth',
}) {
const encrypedToken = await encryptV2(token);
const tokenData = {
type,
userId,
metadata,
identifier,
token: encrypedToken,
expiresIn: parseInt(expiresIn, 10) || 3600,
};
const existingToken = await findToken({ userId, identifier });
if (existingToken) {
return await updateToken({ identifier }, tokenData);
} else {
return await createToken(tokenData);
}
}
module.exports = { module.exports = {
createToken,
findToken, findToken,
createToken,
updateToken, updateToken,
deleteTokens, deleteTokens,
handleOAuthToken,
}; };

View file

@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema(
model_parameters: { model_parameters: {
type: Object, type: Object,
}, },
artifacts: {
type: String,
},
access_level: { access_level: {
type: Number, type: Number,
}, },

View file

@ -10,6 +10,10 @@ const tokenSchema = new Schema({
email: { email: {
type: String, type: String,
}, },
type: String,
identifier: {
type: String,
},
token: { token: {
type: String, type: String,
required: true, required: true,
@ -23,6 +27,10 @@ const tokenSchema = new Schema({
type: Date, type: Date,
required: true, required: true,
}, },
metadata: {
type: Map,
of: Schema.Types.Mixed,
},
}); });
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 }); tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/backend", "name": "@librechat/backend",
"version": "v0.7.6", "version": "v0.7.7-rc1",
"description": "", "description": "",
"scripts": { "scripts": {
"start": "echo 'please run this from the root directory'", "start": "echo 'please run this from the root directory'",
@ -45,11 +45,10 @@
"@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.2", "@librechat/agents": "^2.0.4",
"@waylaidwanderer/fetch-event-source": "^3.0.1", "@waylaidwanderer/fetch-event-source": "^3.0.1",
"axios": "^1.7.7", "axios": "1.7.8",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"cheerio": "^1.0.0-rc.12",
"cohere-ai": "^7.9.1", "cohere-ai": "^7.9.1",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-redis": "^7.1.0", "connect-redis": "^7.1.0",
@ -66,7 +65,6 @@
"firebase": "^11.0.2", "firebase": "^11.0.2",
"googleapis": "^126.0.1", "googleapis": "^126.0.1",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html": "^1.0.0",
"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",
@ -91,7 +89,6 @@
"openid-client": "^5.4.2", "openid-client": "^5.4.2",
"passport": "^0.6.0", "passport": "^0.6.0",
"passport-apple": "^2.0.2", "passport-apple": "^2.0.2",
"passport-custom": "^1.1.1",
"passport-discord": "^0.1.4", "passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0", "passport-facebook": "^3.0.0",
"passport-github2": "^0.1.12", "passport-github2": "^0.1.12",
@ -99,7 +96,6 @@
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pino": "^8.12.1",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"tiktoken": "^1.0.15", "tiktoken": "^1.0.15",
"traverse": "^0.6.7", "traverse": "^0.6.7",
@ -111,8 +107,8 @@
}, },
"devDependencies": { "devDependencies": {
"jest": "^29.7.0", "jest": "^29.7.0",
"mongodb-memory-server": "^10.0.0", "mongodb-memory-server": "^10.1.3",
"nodemon": "^3.0.1", "nodemon": "^3.0.3",
"supertest": "^6.3.3" "supertest": "^7.0.0"
} }
} }

View file

@ -155,6 +155,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
sender, sender,
messageId: responseMessageId, messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId, parentMessageId: userMessageId ?? parentMessageId,
}).catch((err) => {
logger.error('[AskController] Error in `handleAbortError`', err);
}); });
} }
}; };

View file

@ -140,6 +140,8 @@ const EditController = async (req, res, next, initializeClient) => {
sender, sender,
messageId: responseMessageId, messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId, parentMessageId: userMessageId ?? parentMessageId,
}).catch((err) => {
logger.error('[EditController] Error in `handleAbortError`', err);
}); });
} }
}; };

View file

@ -143,6 +143,8 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
sender, sender,
messageId: responseMessageId, messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId, parentMessageId: userMessageId ?? parentMessageId,
}).catch((err) => {
logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
}); });
} }
}; };

View file

@ -84,6 +84,7 @@ const startServer = async () => {
app.use('/oauth', routes.oauth); app.use('/oauth', routes.oauth);
/* API Endpoints */ /* API Endpoints */
app.use('/api/auth', routes.auth); app.use('/api/auth', routes.auth);
app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys); app.use('/api/keys', routes.keys);
app.use('/api/user', routes.user); app.use('/api/user', routes.user);
app.use('/api/search', routes.search); app.use('/api/search', routes.search);

View file

@ -0,0 +1,136 @@
const express = require('express');
const jwt = require('jsonwebtoken');
const { getAccessToken } = require('~/server/services/TokenService');
const { logger, getFlowStateManager } = require('~/config');
const { getLogStores } = require('~/cache');
const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET;
/**
* Handles the OAuth callback and exchanges the authorization code for tokens.
*
* @route GET /actions/:action_id/oauth/callback
* @param {string} req.params.action_id - The ID of the action.
* @param {string} req.query.code - The authorization code returned by the provider.
* @param {string} req.query.state - The state token to verify the authenticity of the request.
* @returns {void} Sends a success message after updating the action with OAuth tokens.
*/
router.get('/:action_id/oauth/callback', async (req, res) => {
const { action_id } = req.params;
const { code, state } = req.query;
const flowManager = await getFlowStateManager(getLogStores);
let identifier = action_id;
try {
let decodedState;
try {
decodedState = jwt.verify(state, JWT_SECRET);
} catch (err) {
await flowManager.failFlow(identifier, 'oauth', 'Invalid or expired state parameter');
return res.status(400).send('Invalid or expired state parameter');
}
if (decodedState.action_id !== action_id) {
await flowManager.failFlow(identifier, 'oauth', 'Mismatched action ID in state parameter');
return res.status(400).send('Mismatched action ID in state parameter');
}
if (!decodedState.user) {
await flowManager.failFlow(identifier, 'oauth', 'Invalid user ID in state parameter');
return res.status(400).send('Invalid user ID in state parameter');
}
identifier = `${decodedState.user}:${action_id}`;
const flowState = await flowManager.getFlowState(identifier, 'oauth');
if (!flowState) {
throw new Error('OAuth flow not found');
}
const tokenData = await getAccessToken({
code,
userId: decodedState.user,
identifier,
client_url: flowState.metadata.client_url,
redirect_uri: flowState.metadata.redirect_uri,
/** Encrypted values */
encrypted_oauth_client_id: flowState.metadata.encrypted_oauth_client_id,
encrypted_oauth_client_secret: flowState.metadata.encrypted_oauth_client_secret,
});
await flowManager.completeFlow(identifier, 'oauth', tokenData);
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Authentication Successful</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont;
background-color: rgb(249, 250, 251);
margin: 0;
padding: 2rem;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.card {
background-color: white;
border-radius: 0.5rem;
padding: 2rem;
max-width: 28rem;
width: 100%;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
text-align: center;
}
.heading {
color: rgb(17, 24, 39);
font-size: 1.875rem;
font-weight: 700;
margin: 0 0 1rem;
}
.description {
color: rgb(75, 85, 99);
font-size: 0.875rem;
margin: 0.5rem 0;
}
.countdown {
color: rgb(99, 102, 241);
font-weight: 500;
}
</style>
</head>
<body>
<div class="card">
<h1 class="heading">Authentication Successful</h1>
<p class="description">
Your authentication was successful. This window will close in
<span class="countdown" id="countdown">3</span> seconds.
</p>
</div>
<script>
let secondsLeft = 3;
const countdownElement = document.getElementById('countdown');
const countdown = setInterval(() => {
secondsLeft--;
countdownElement.textContent = secondsLeft;
if (secondsLeft <= 0) {
clearInterval(countdown);
window.close();
}
}, 1000);
</script>
</body>
</html>
`);
} catch (error) {
logger.error('Error in OAuth callback:', error);
await flowManager.failFlow(identifier, 'oauth', error);
res.status(500).send('Authentication failed. Please try again.');
}
});
module.exports = router;

View file

@ -1,6 +1,6 @@
const express = require('express'); const express = require('express');
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const { actionDelimiter, SystemRoles } = require('librechat-data-provider'); const { actionDelimiter, SystemRoles, removeNullishValues } = require('librechat-data-provider');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { updateAction, getActions, deleteAction } = require('~/models/Action');
const { isActionDomainAllowed } = require('~/server/services/domains'); const { isActionDomainAllowed } = require('~/server/services/domains');
@ -51,7 +51,7 @@ router.post('/:agent_id', async (req, res) => {
return res.status(400).json({ message: 'No functions provided' }); return res.status(400).json({ message: 'No functions provided' });
} }
let metadata = await encryptMetadata(_metadata); let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
const isDomainAllowed = await isActionDomainAllowed(metadata.domain); const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
if (!isDomainAllowed) { if (!isDomainAllowed) {
return res.status(400).json({ message: 'Domain not allowed' }); return res.status(400).json({ message: 'Domain not allowed' });
@ -117,10 +117,7 @@ router.post('/:agent_id', async (req, res) => {
} }
/** @type {[Action]} */ /** @type {[Action]} */
const updatedAction = await updateAction( const updatedAction = await updateAction({ action_id }, actionUpdateData);
{ action_id },
actionUpdateData,
);
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret']; const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
for (let field of sensitiveFields) { for (let field of sensitiveFields) {

View file

@ -1,6 +1,6 @@
const express = require('express'); const express = require('express');
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const { actionDelimiter, EModelEndpoint } = require('librechat-data-provider'); const { actionDelimiter, EModelEndpoint, removeNullishValues } = require('librechat-data-provider');
const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); const { encryptMetadata, domainParser } = require('~/server/services/ActionService');
const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { updateAction, getActions, deleteAction } = require('~/models/Action');
@ -29,7 +29,7 @@ router.post('/:assistant_id', async (req, res) => {
return res.status(400).json({ message: 'No functions provided' }); return res.status(400).json({ message: 'No functions provided' });
} }
let metadata = await encryptMetadata(_metadata); let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
const isDomainAllowed = await isActionDomainAllowed(metadata.domain); const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
if (!isDomainAllowed) { if (!isDomainAllowed) {
return res.status(400).json({ message: 'Domain not allowed' }); return res.status(400).json({ message: 'Domain not allowed' });

View file

@ -9,6 +9,7 @@ const prompts = require('./prompts');
const balance = require('./balance'); const balance = require('./balance');
const plugins = require('./plugins'); const plugins = require('./plugins');
const bedrock = require('./bedrock'); const bedrock = require('./bedrock');
const actions = require('./actions');
const search = require('./search'); const search = require('./search');
const models = require('./models'); const models = require('./models');
const convos = require('./convos'); const convos = require('./convos');
@ -45,6 +46,7 @@ module.exports = {
config, config,
models, models,
plugins, plugins,
actions,
presets, presets,
balance, balance,
messages, messages,

View file

@ -1,20 +1,28 @@
const jwt = require('jsonwebtoken');
const { nanoid } = require('nanoid');
const { tool } = require('@langchain/core/tools');
const { GraphEvents, sleep } = require('@librechat/agents');
const { const {
Time,
CacheKeys, CacheKeys,
StepTypes,
Constants, Constants,
AuthTypeEnum, AuthTypeEnum,
actionDelimiter, actionDelimiter,
isImageVisionTool, isImageVisionTool,
actionDomainSeparator, actionDomainSeparator,
} = require('librechat-data-provider'); } = require('librechat-data-provider');
const { tool } = require('@langchain/core/tools'); const { refreshAccessToken } = require('~/server/services/TokenService');
const { isActionDomainAllowed } = require('~/server/services/domains'); const { isActionDomainAllowed } = require('~/server/services/domains');
const { logger, getFlowStateManager, sendEvent } = require('~/config');
const { encryptV2, decryptV2 } = require('~/server/utils/crypto'); const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
const { getActions, deleteActions } = require('~/models/Action'); const { getActions, deleteActions } = require('~/models/Action');
const { deleteAssistant } = require('~/models/Assistant'); const { deleteAssistant } = require('~/models/Assistant');
const { findToken } = require('~/models/Token');
const { logAxiosError } = require('~/utils'); const { logAxiosError } = require('~/utils');
const { getLogStores } = require('~/cache'); const { getLogStores } = require('~/cache');
const { logger } = require('~/config');
const JWT_SECRET = process.env.JWT_SECRET;
const toolNameRegex = /^[a-zA-Z0-9_-]+$/; const toolNameRegex = /^[a-zA-Z0-9_-]+$/;
const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g'); const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g');
@ -115,6 +123,8 @@ async function loadActionSets(searchParams) {
* Creates a general tool for an entire action set. * Creates a general tool for an entire action set.
* *
* @param {Object} params - The parameters for loading action sets. * @param {Object} params - The parameters for loading action sets.
* @param {ServerRequest} params.req
* @param {ServerResponse} params.res
* @param {Action} params.action - The action set. Necessary for decrypting authentication values. * @param {Action} params.action - The action set. Necessary for decrypting authentication values.
* @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call. * @param {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
* @param {string | undefined} [params.name] - The name of the tool. * @param {string | undefined} [params.name] - The name of the tool.
@ -122,33 +132,185 @@ async function loadActionSets(searchParams) {
* @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition * @param {import('zod').ZodTypeAny | undefined} [params.zodSchema] - The Zod schema for tool input validation/definition
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input. * @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
*/ */
async function createActionTool({ action, requestBuilder, zodSchema, name, description }) { async function createActionTool({
action.metadata = await decryptMetadata(action.metadata); req,
res,
action,
requestBuilder,
zodSchema,
name,
description,
}) {
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain); const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
if (!isDomainAllowed) { if (!isDomainAllowed) {
return null; return null;
} }
/** @type {(toolInput: Object | string) => Promise<unknown>} */ const encrypted = {
const _call = async (toolInput) => { oauth_client_id: action.metadata.oauth_client_id,
try { oauth_client_secret: action.metadata.oauth_client_secret,
const executor = requestBuilder.createExecutor(); };
action.metadata = await decryptMetadata(action.metadata);
// Chain the operations /** @type {(toolInput: Object | string, config: GraphRunnableConfig) => Promise<unknown>} */
const _call = async (toolInput, config) => {
try {
/** @type {import('librechat-data-provider').ActionMetadataRuntime} */
const metadata = action.metadata;
const executor = requestBuilder.createExecutor();
const preparedExecutor = executor.setParams(toolInput); const preparedExecutor = executor.setParams(toolInput);
if (action.metadata.auth && action.metadata.auth.type !== AuthTypeEnum.None) { if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
await preparedExecutor.setAuth(action.metadata); try {
const action_id = action.action_id;
const identifier = `${req.user.id}:${action.action_id}`;
if (metadata.auth.type === AuthTypeEnum.OAuth && metadata.auth.authorization_url) {
const requestLogin = async () => {
const { args: _args, stepId, ...toolCall } = config.toolCall ?? {};
if (!stepId) {
throw new Error('Tool call is missing stepId');
}
const statePayload = {
nonce: nanoid(),
user: req.user.id,
action_id,
};
const stateToken = jwt.sign(statePayload, JWT_SECRET, { expiresIn: '10m' });
try {
const redirectUri = `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`;
const params = new URLSearchParams({
client_id: metadata.oauth_client_id,
scope: metadata.auth.scope,
redirect_uri: redirectUri,
access_type: 'offline',
response_type: 'code',
state: stateToken,
});
const authURL = `${metadata.auth.authorization_url}?${params.toString()}`;
/** @type {{ id: string; delta: AgentToolCallDelta }} */
const data = {
id: stepId,
delta: {
type: StepTypes.TOOL_CALLS,
tool_calls: [{ ...toolCall, args: '' }],
auth: authURL,
expires_at: Date.now() + Time.TWO_MINUTES,
},
};
const flowManager = await getFlowStateManager(getLogStores);
await flowManager.createFlowWithHandler(
`${identifier}:login`,
'oauth_login',
async () => {
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
logger.debug('Sent OAuth login request to client', { action_id, identifier });
return true;
},
);
logger.debug('Waiting for OAuth Authorization response', { action_id, identifier });
const result = await flowManager.createFlow(identifier, 'oauth', {
state: stateToken,
userId: req.user.id,
client_url: metadata.auth.client_url,
redirect_uri: `${process.env.DOMAIN_CLIENT}/api/actions/${action_id}/oauth/callback`,
/** Encrypted values */
encrypted_oauth_client_id: encrypted.oauth_client_id,
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
});
logger.debug('Received OAuth Authorization response', { action_id, identifier });
data.delta.auth = undefined;
data.delta.expires_at = undefined;
sendEvent(res, { event: GraphEvents.ON_RUN_STEP_DELTA, data });
await sleep(3000);
metadata.oauth_access_token = result.access_token;
metadata.oauth_refresh_token = result.refresh_token;
const expiresAt = new Date(Date.now() + result.expires_in * 1000);
metadata.oauth_token_expires_at = expiresAt.toISOString();
} catch (error) {
const errorMessage = 'Failed to authenticate OAuth tool';
logger.error(errorMessage, error);
throw new Error(errorMessage);
}
};
const tokenPromises = [];
tokenPromises.push(findToken({ userId: req.user.id, type: 'oauth', identifier }));
tokenPromises.push(
findToken({
userId: req.user.id,
type: 'oauth_refresh',
identifier: `${identifier}:refresh`,
}),
);
const [tokenData, refreshTokenData] = await Promise.all(tokenPromises);
if (tokenData) {
// Valid token exists, add it to metadata for setAuth
metadata.oauth_access_token = await decryptV2(tokenData.token);
if (refreshTokenData) {
metadata.oauth_refresh_token = await decryptV2(refreshTokenData.token);
}
metadata.oauth_token_expires_at = tokenData.expiresAt.toISOString();
} else if (!refreshTokenData) {
// No tokens exist, need to authenticate
await requestLogin();
} else if (refreshTokenData) {
// Refresh token is still valid, use it to get new access token
try {
const refresh_token = await decryptV2(refreshTokenData.token);
const refreshTokens = async () =>
await refreshAccessToken({
identifier,
refresh_token,
userId: req.user.id,
client_url: metadata.auth.client_url,
encrypted_oauth_client_id: encrypted.oauth_client_id,
encrypted_oauth_client_secret: encrypted.oauth_client_secret,
});
const flowManager = await getFlowStateManager(getLogStores);
const refreshData = await flowManager.createFlowWithHandler(
`${identifier}:refresh`,
'oauth_refresh',
refreshTokens,
);
metadata.oauth_access_token = refreshData.access_token;
if (refreshData.refresh_token) {
metadata.oauth_refresh_token = refreshData.refresh_token;
}
const expiresAt = new Date(Date.now() + refreshData.expires_in * 1000);
metadata.oauth_token_expires_at = expiresAt.toISOString();
} catch (error) {
logger.error('Failed to refresh token, requesting new login:', error);
await requestLogin();
}
} else {
await requestLogin();
}
}
await preparedExecutor.setAuth(metadata);
} catch (error) {
if (
error.message.includes('No access token found') ||
error.message.includes('Access token is expired')
) {
throw error;
}
throw new Error(`Authentication failed: ${error.message}`);
}
} }
const res = await preparedExecutor.execute(); const response = await preparedExecutor.execute();
if (typeof res.data === 'object') { if (typeof response.data === 'object') {
return JSON.stringify(res.data); return JSON.stringify(response.data);
} }
return res.data; return response.data;
} catch (error) { } catch (error) {
const logMessage = `API call to ${action.metadata.domain} failed`; const logMessage = `API call to ${action.metadata.domain} failed`;
logAxiosError({ message: logMessage, error }); logAxiosError({ message: logMessage, error });
throw error;
} }
}; };

View file

@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options')
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize'); const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
const initCustom = require('~/server/services/Endpoints/custom/initialize'); const initCustom = require('~/server/services/Endpoints/custom/initialize');
const initGoogle = require('~/server/services/Endpoints/google/initialize'); const initGoogle = require('~/server/services/Endpoints/google/initialize');
const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getCustomEndpointConfig } = require('~/server/services/Config'); const { getCustomEndpointConfig } = require('~/server/services/Config');
const { loadAgentTools } = require('~/server/services/ToolService'); const { loadAgentTools } = require('~/server/services/ToolService');
const AgentClient = require('~/server/controllers/agents/client'); const AgentClient = require('~/server/controllers/agents/client');
@ -72,6 +73,16 @@ const primeResources = async (_attachments, _tool_resources) => {
} }
}; };
/**
* @param {object} params
* @param {ServerRequest} params.req
* @param {ServerResponse} params.res
* @param {Agent} params.agent
* @param {object} [params.endpointOption]
* @param {AgentToolResources} [params.tool_resources]
* @param {boolean} [params.isInitialAgent]
* @returns {Promise<Agent>}
*/
const initializeAgentOptions = async ({ const initializeAgentOptions = async ({
req, req,
res, res,
@ -82,6 +93,7 @@ const initializeAgentOptions = async ({
}) => { }) => {
const { tools, toolContextMap } = await loadAgentTools({ const { tools, toolContextMap } = await loadAgentTools({
req, req,
res,
agent, agent,
tool_resources, tool_resources,
}); });
@ -131,6 +143,13 @@ const initializeAgentOptions = async ({
agent.model_parameters.model = agent.model; agent.model_parameters.model = agent.model;
} }
if (typeof agent.artifacts === 'string' && agent.artifacts !== '') {
agent.additional_instructions = generateArtifactsPrompt({
endpoint: agent.provider,
artifacts: agent.artifacts,
});
}
const tokensModel = const tokensModel =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model; agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;

View file

@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant'); const { getAssistant } = require('~/models/Assistant');
const buildOptions = async (endpoint, parsedBody) => { const buildOptions = async (endpoint, parsedBody) => {
// eslint-disable-next-line no-unused-vars
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } = const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody; parsedBody;
const endpointOption = removeNullishValues({ const endpointOption = removeNullishValues({

View file

@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant'); const { getAssistant } = require('~/models/Assistant');
const buildOptions = async (endpoint, parsedBody) => { const buildOptions = async (endpoint, parsedBody) => {
// eslint-disable-next-line no-unused-vars
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } = const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody; parsedBody;
const endpointOption = removeNullishValues({ const endpointOption = removeNullishValues({

View file

@ -0,0 +1,170 @@
const axios = require('axios');
const { handleOAuthToken } = require('~/models/Token');
const { decryptV2 } = require('~/server/utils/crypto');
const { logAxiosError } = require('~/utils');
const { logger } = require('~/config');
/**
* Processes the access tokens and stores them in the database.
* @param {object} tokenData
* @param {string} tokenData.access_token
* @param {number} tokenData.expires_in
* @param {string} [tokenData.refresh_token]
* @param {number} [tokenData.refresh_token_expires_in]
* @param {object} metadata
* @param {string} metadata.userId
* @param {string} metadata.identifier
* @returns {Promise<void>}
*/
async function processAccessTokens(tokenData, { userId, identifier }) {
const { access_token, expires_in = 3600, refresh_token, refresh_token_expires_in } = tokenData;
if (!access_token) {
logger.error('Access token not found: ', tokenData);
throw new Error('Access token not found');
}
await handleOAuthToken({
identifier,
token: access_token,
expiresIn: expires_in,
userId,
});
if (refresh_token != null) {
logger.debug('Processing refresh token');
await handleOAuthToken({
token: refresh_token,
type: 'oauth_refresh',
userId,
identifier: `${identifier}:refresh`,
expiresIn: refresh_token_expires_in ?? null,
});
}
logger.debug('Access tokens processed');
}
/**
* Refreshes the access token using the refresh token.
* @param {object} fields
* @param {string} fields.userId - The ID of the user.
* @param {string} fields.client_url - The URL of the OAuth provider.
* @param {string} fields.identifier - The identifier for the token.
* @param {string} fields.refresh_token - The refresh token to use.
* @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
* @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
* @returns {Promise<{
* access_token: string,
* expires_in: number,
* refresh_token?: string,
* refresh_token_expires_in?: number,
* }>}
*/
const refreshAccessToken = async ({
userId,
client_url,
identifier,
refresh_token,
encrypted_oauth_client_id,
encrypted_oauth_client_secret,
}) => {
try {
const oauth_client_id = await decryptV2(encrypted_oauth_client_id);
const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret);
const params = new URLSearchParams({
client_id: oauth_client_id,
client_secret: oauth_client_secret,
grant_type: 'refresh_token',
refresh_token,
});
const response = await axios({
method: 'POST',
url: client_url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
data: params.toString(),
});
await processAccessTokens(response.data, {
userId,
identifier,
});
logger.debug(`Access token refreshed successfully for ${identifier}`);
return response.data;
} catch (error) {
const message = 'Error refreshing OAuth tokens';
logAxiosError({
message,
error,
});
throw new Error(message);
}
};
/**
* Handles the OAuth callback and exchanges the authorization code for tokens.
* @param {object} fields
* @param {string} fields.code - The authorization code returned by the provider.
* @param {string} fields.userId - The ID of the user.
* @param {string} fields.identifier - The identifier for the token.
* @param {string} fields.client_url - The URL of the OAuth provider.
* @param {string} fields.redirect_uri - The redirect URI for the OAuth provider.
* @param {string} fields.encrypted_oauth_client_id - The client ID for the OAuth provider.
* @param {string} fields.encrypted_oauth_client_secret - The client secret for the OAuth provider.
* @returns {Promise<{
* access_token: string,
* expires_in: number,
* refresh_token?: string,
* refresh_token_expires_in?: number,
* }>}
*/
const getAccessToken = async ({
code,
userId,
identifier,
client_url,
redirect_uri,
encrypted_oauth_client_id,
encrypted_oauth_client_secret,
}) => {
const oauth_client_id = await decryptV2(encrypted_oauth_client_id);
const oauth_client_secret = await decryptV2(encrypted_oauth_client_secret);
const params = new URLSearchParams({
code,
client_id: oauth_client_id,
client_secret: oauth_client_secret,
grant_type: 'authorization_code',
redirect_uri,
});
try {
const response = await axios({
method: 'POST',
url: client_url,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
},
data: params.toString(),
});
await processAccessTokens(response.data, {
userId,
identifier,
});
logger.debug(`Access tokens successfully created for ${identifier}`);
return response.data;
} catch (error) {
const message = 'Error exchanging OAuth code';
logAxiosError({
message,
error,
});
throw new Error(message);
}
};
module.exports = {
getAccessToken,
refreshAccessToken,
};

View file

@ -409,11 +409,12 @@ async function processRequiredActions(client, requiredActions) {
* Processes the runtime tool calls and returns the tool classes. * Processes the runtime tool calls and returns the tool classes.
* @param {Object} params - Run params containing user and request information. * @param {Object} params - Run params containing user and request information.
* @param {ServerRequest} params.req - The request object. * @param {ServerRequest} params.req - The request object.
* @param {ServerResponse} params.res - The request object.
* @param {Agent} params.agent - The agent to load tools for. * @param {Agent} params.agent - The agent to load tools for.
* @param {string | undefined} [params.openAIApiKey] - The OpenAI API key. * @param {string | undefined} [params.openAIApiKey] - The OpenAI API key.
* @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools. * @returns {Promise<{ tools?: StructuredTool[] }>} The agent tools.
*/ */
async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) { async function loadAgentTools({ req, res, agent, tool_resources, openAIApiKey }) {
if (!agent.tools || agent.tools.length === 0) { if (!agent.tools || agent.tools.length === 0) {
return {}; return {};
} }
@ -546,6 +547,8 @@ async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) {
if (requestBuilder) { if (requestBuilder) {
const tool = await createActionTool({ const tool = await createActionTool({
req,
res,
action: actionSet, action: actionSet,
requestBuilder, requestBuilder,
zodSchema, zodSchema,

View file

@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) {
config.capabilities = [ config.capabilities = [
AgentCapabilities.execute_code, AgentCapabilities.execute_code,
AgentCapabilities.file_search, AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions, AgentCapabilities.actions,
AgentCapabilities.tools, AgentCapabilities.tools,
]; ];

View file

@ -98,6 +98,12 @@
* @memberof typedefs * @memberof typedefs
*/ */
/**
* @exports LangChainToolCall
* @typedef {import('@langchain/core/messages/tool').ToolCall} LangChainToolCall
* @memberof typedefs
*/
/** /**
* @exports GraphRunnableConfig * @exports GraphRunnableConfig
* @typedef {import('@langchain/core/runnables').RunnableConfig<{ * @typedef {import('@langchain/core/runnables').RunnableConfig<{
@ -109,7 +115,9 @@
* agent_index: number; * agent_index: number;
* last_agent_index: number; * last_agent_index: number;
* hide_sequential_outputs: boolean; * hide_sequential_outputs: boolean;
* }>} GraphRunnableConfig * }> & {
* toolCall?: LangChainToolCall & { stepId?: string };
* }} GraphRunnableConfig
* @memberof typedefs * @memberof typedefs
*/ */
@ -383,6 +391,12 @@
* @memberof typedefs * @memberof typedefs
*/ */
/**
* @exports AgentToolCallDelta
* @typedef {import('librechat-data-provider').Agents.ToolCallDelta} AgentToolCallDelta
* @memberof typedefs
*/
/** Prompts */ /** Prompts */
/** /**
* @exports TPrompt * @exports TPrompt
@ -947,12 +961,24 @@
* @memberof typedefs * @memberof typedefs
*/ */
/**
* @exports Keyv
* @typedef {import('keyv')} Keyv
* @memberof typedefs
*/
/** /**
* @exports MCPManager * @exports MCPManager
* @typedef {import('librechat-mcp').MCPManager} MCPManager * @typedef {import('librechat-mcp').MCPManager} MCPManager
* @memberof typedefs * @memberof typedefs
*/ */
/**
* @exports FlowStateManager
* @typedef {import('librechat-mcp').FlowStateManager} FlowStateManager
* @memberof typedefs
*/
/** /**
* @exports LCAvailableTools * @exports LCAvailableTools
* @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools * @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/frontend", "name": "@librechat/frontend",
"version": "v0.7.6", "version": "v0.7.7-rc1",
"description": "", "description": "",
"type": "module", "type": "module",
"scripts": { "scripts": {
@ -50,11 +50,8 @@
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3", "@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.28.0", "@tanstack/react-query": "^4.28.0",
"@tanstack/react-table": "^8.11.7", "@tanstack/react-table": "^8.11.7",
"@zattoo/use-double-click": "1.2.0",
"axios": "^1.7.7",
"class-variance-authority": "^0.6.0", "class-variance-authority": "^0.6.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
@ -65,7 +62,7 @@
"filenamify": "^6.0.0", "filenamify": "^6.0.0",
"framer-motion": "^11.5.4", "framer-motion": "^11.5.4",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"image-blob-reduce": "^4.1.0", "i18next": "^24.2.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",
@ -79,10 +76,10 @@
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^5.0.0",
"react-flip-toolkit": "^7.1.0", "react-flip-toolkit": "^7.1.0",
"react-gtm-module": "^2.0.11", "react-gtm-module": "^2.0.11",
"react-hook-form": "^7.43.9", "react-hook-form": "^7.43.9",
"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.1",
@ -103,7 +100,6 @@
"tailwind-merge": "^1.9.1", "tailwind-merge": "^1.9.1",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",
"tailwindcss-radix": "^2.8.0", "tailwindcss-radix": "^2.8.0",
"url": "^0.11.0",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
@ -116,31 +112,31 @@
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0", "@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3", "@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.5.2", "@types/jest": "^29.5.14",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/node": "^20.3.0", "@types/node": "^20.3.0",
"@types/react": "^18.2.11", "@types/react": "^18.2.11",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.4",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.20",
"babel-plugin-replace-ts-export-assignment": "^0.0.2", "babel-plugin-replace-ts-export-assignment": "^0.0.2",
"babel-plugin-root-import": "^6.6.0", "babel-plugin-root-import": "^6.6.0",
"babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-import-meta": "^2.3.2",
"babel-plugin-transform-vite-meta-env": "^1.0.3", "babel-plugin-transform-vite-meta-env": "^1.0.3",
"eslint-plugin-jest": "^27.2.1", "eslint-plugin-jest": "^28.11.0",
"identity-obj-proxy": "^3.0.0", "identity-obj-proxy": "^3.0.0",
"jest": "^29.5.0", "jest": "^29.7.0",
"jest-canvas-mock": "^2.5.1", "jest-canvas-mock": "^2.5.2",
"jest-environment-jsdom": "^29.5.0", "jest-environment-jsdom": "^29.7.0",
"jest-file-loader": "^1.0.3", "jest-file-loader": "^1.0.3",
"jest-junit": "^16.0.0", "jest-junit": "^16.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postcss-loader": "^7.1.0", "postcss-loader": "^7.1.0",
"postcss-preset-env": "^8.2.0", "postcss-preset-env": "^8.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"ts-jest": "^29.1.0", "ts-jest": "^29.2.5",
"typescript": "^5.0.4", "typescript": "^5.3.3",
"vite": "^5.4.14", "vite": "^6.1.0",
"vite-plugin-node-polyfills": "^0.17.0", "vite-plugin-node-polyfills": "^0.17.0",
"vite-plugin-pwa": "^0.21.1" "vite-plugin-pwa": "^0.21.1"
} }

View file

@ -1,4 +1,4 @@
import { AgentCapabilities } from 'librechat-data-provider'; import { AgentCapabilities, ArtifactModes } from 'librechat-data-provider';
import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider'; import type { Agent, AgentProvider, AgentModelParameters } from 'librechat-data-provider';
import type { OptionWithIcon, ExtendedFile } from './types'; import type { OptionWithIcon, ExtendedFile } from './types';
@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
}; };
export type TAgentCapabilities = { export type TAgentCapabilities = {
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean; [AgentCapabilities.file_search]: boolean;
[AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.end_after_tools]?: boolean; [AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean; [AgentCapabilities.hide_sequential_outputs]?: boolean;
}; };
@ -26,4 +26,5 @@ export type AgentForm = {
tools?: string[]; tools?: string[];
provider?: AgentProvider | OptionWithIcon; provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[]; agent_ids?: string[];
[AgentCapabilities.artifacts]?: ArtifactModes | string;
} & TAgentCapabilities; } & TAgentCapabilities;

View file

@ -6,6 +6,7 @@ import type { SetterOrUpdater } from 'recoil';
import type * as t from 'librechat-data-provider'; import type * as t from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
import type { TranslationKeys } from '~/hooks';
export type CodeBarProps = { export type CodeBarProps = {
lang: string; lang: string;
@ -66,7 +67,10 @@ export type GenericSetter<T> = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record<t.EModelEndpoint, string>; export type LastSelectedModels = Record<t.EModelEndpoint, string>;
export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string; export type LocalizeFunction = (
phraseKey: TranslationKeys,
options?: Record<string, string | number>,
) => string;
export type ChatFormValues = { text: string }; export type ChatFormValues = { text: string };
@ -85,6 +89,7 @@ export type IconMapProps = {
iconURL?: string; iconURL?: string;
context?: 'landing' | 'menu-item' | 'nav' | 'message'; context?: 'landing' | 'menu-item' | 'nav' | 'message';
endpoint?: string | null; endpoint?: string | null;
endpointType?: string;
assistantName?: string; assistantName?: string;
agentName?: string; agentName?: string;
avatar?: string; avatar?: string;

View file

@ -1,4 +1,4 @@
import { useLocalize } from '~/hooks'; import { TranslationKeys, useLocalize } from '~/hooks';
import { BlinkAnimation } from './BlinkAnimation'; import { BlinkAnimation } from './BlinkAnimation';
import { TStartupConfig } from 'librechat-data-provider'; import { TStartupConfig } from 'librechat-data-provider';
import SocialLoginRender from './SocialLoginRender'; import SocialLoginRender from './SocialLoginRender';
@ -33,7 +33,7 @@ function AuthLayout({
startupConfig: TStartupConfig | null | undefined; startupConfig: TStartupConfig | null | undefined;
startupConfigError: unknown | null | undefined; startupConfigError: unknown | null | undefined;
pathname: string; pathname: string;
error: string | null; error: TranslationKeys | null;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
@ -65,7 +65,7 @@ function AuthLayout({
<img <img
src="/assets/logo.svg" src="/assets/logo.svg"
className="h-full w-full object-contain" className="h-full w-full object-contain"
alt={localize('com_ui_logo', startupConfig?.appTitle ?? 'LibreChat')} alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/> />
</div> </div>
</BlinkAnimation> </BlinkAnimation>

View file

@ -6,7 +6,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common'; import type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from './ErrorMessage'; import { ErrorMessage } from './ErrorMessage';
import { Spinner } from '~/components/svg'; import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks'; import { useLocalize, TranslationKeys } from '~/hooks';
const Registration: React.FC = () => { const Registration: React.FC = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -56,7 +56,7 @@ const Registration: React.FC = () => {
}, },
}); });
const renderInput = (id: string, label: string, type: string, validation: object) => ( const renderInput = (id: string, label: TranslationKeys, type: string, validation: object) => (
<div className="mb-4"> <div className="mb-4">
<div className="relative"> <div className="relative">
<input <input
@ -114,7 +114,7 @@ const Registration: React.FC = () => {
: 'com_auth_registration_success_insecure', : 'com_auth_registration_success_insecure',
) + ) +
' ' + ' ' +
localize('com_auth_email_verification_redirecting', countdown.toString())} localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
</div> </div>
)} )}
{!startupConfigError && !isFetching && ( {!startupConfigError && !isFetching && (

View file

@ -84,7 +84,7 @@ function RequestPasswordReset() {
</h1> </h1>
{countdown > 0 && ( {countdown > 0 && (
<p className="text-center text-lg text-gray-600 dark:text-gray-400"> <p className="text-center text-lg text-gray-600 dark:text-gray-400">
{localize('com_auth_email_verification_redirecting', countdown.toString())} {localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
</p> </p>
)} )}
{showResendLink && countdown === 0 && ( {showResendLink && countdown === 0 && (

View file

@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
select: useCallback( select: useCallback(
(data: TMessage[]) => { (data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap }); const dataTree = buildTree({ messages: data, fileMap });
return dataTree?.length === 0 ? null : dataTree ?? null; return dataTree?.length === 0 ? null : (dataTree ?? null);
}, },
[fileMap], [fileMap],
), ),
@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
<ChatFormProvider {...methods}> <ChatFormProvider {...methods}>
<ChatContext.Provider value={chatHelpers}> <ChatContext.Provider value={chatHelpers}>
<AddedChatContext.Provider value={addedChatHelpers}> <AddedChatContext.Provider value={addedChatHelpers}>
<Presentation useSidePanel={true}> <Presentation>
{content} {content}
<div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent"> <div className="w-full border-t-0 pl-0 pt-2 dark:border-white/20 md:w-[calc(100%-.5rem)] md:border-t-0 md:border-transparent md:pl-0 md:pt-0 md:dark:border-transparent">
<ChatForm index={index} /> <ChatForm index={index} />

View file

@ -220,8 +220,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
<span className="hidden sm:inline"> <span className="hidden sm:inline">
{localize( {localize(
'com_files_number_selected', 'com_files_number_selected',
`${table.getFilteredSelectedRowModel().rows.length}`, {
`${table.getFilteredRowModel().rows.length}`, 0: `${table.getFilteredSelectedRowModel().rows.length}`,
1: `${table.getFilteredRowModel().rows.length}`,
},
)} )}
</span> </span>
<span className="sm:hidden"> <span className="sm:hidden">

View file

@ -9,7 +9,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '~/components/ui/DropdownMenu'; } from '~/components/ui/DropdownMenu';
import { Button } from '~/components/ui/Button'; import { Button } from '~/components/ui/Button';
import useLocalize from '~/hooks/useLocalize'; import { useLocalize, TranslationKeys } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> { interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
@ -78,9 +78,12 @@ export function SortFilterHeader<TData, TValue>({
<DropdownMenuSeparator className="dark:bg-gray-500" /> <DropdownMenuSeparator className="dark:bg-gray-500" />
{filters && {filters &&
Object.entries(filters).map(([key, values]) => Object.entries(filters).map(([key, values]) =>
values.map((value: string | number) => { values.map((value?: string | number) => {
const localizedValue = localize(valueMap?.[value] ?? ''); const translationKey = valueMap?.[value ?? ''];
const filterValue = localizedValue.length ? localizedValue : valueMap?.[value]; const filterValue =
translationKey != null && translationKey.length
? localize(translationKey as TranslationKeys)
: String(value);
if (!filterValue) { if (!filterValue) {
return null; return null;
} }

View file

@ -44,8 +44,8 @@ export default function PopoverButtons({
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? ''; const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
const model = overrideModel ?? _model; const model = overrideModel ?? _model;
const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false; const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false; const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? ''); const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
const { showExamples } = optionSettings; const { showExamples } = optionSettings;

View file

@ -18,7 +18,7 @@ export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: Temporary
<div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm"> <div className="flex items-start gap-4 py-2.5 pl-3 pr-1.5 text-sm">
<span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center"> <span className="mt-0 flex h-6 w-6 flex-shrink-0 items-center justify-center">
<div className="icon-md"> <div className="icon-md">
<MessageCircleDashed className="icon-md" /> <MessageCircleDashed className="icon-md" aria-hidden="true" />
</div> </div>
</span> </span>
<span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold"> <span className="text-token-text-secondary line-clamp-3 flex-1 py-0.5 font-semibold">

View file

@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite'; import MarkdownLite from './MarkdownLite';
import store from '~/store'; import store from '~/store';
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function CodeAnalyze({ export default function CodeAnalyze({
initialProgress = 0.1, initialProgress = 0.1,
code, code,
@ -22,9 +25,6 @@ export default function CodeAnalyze({
const progress = useProgress(initialProgress); const progress = useProgress(initialProgress);
const showAnalysisCode = useRecoilValue(store.showCode); const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode); const [showCode, setShowCode] = useState(showAnalysisCode);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference; const offset = circumference - progress * circumference;
const logs = outputs.reduce((acc, output) => { const logs = outputs.reduce((acc, output) => {
@ -53,9 +53,10 @@ export default function CodeAnalyze({
<ProgressText <ProgressText
progress={progress} progress={progress}
onClick={() => setShowCode((prev) => !prev)} onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing" inProgressText={localize('com_ui_analyzing')}
finishedText="Finished analyzing" finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length} hasInput={!!code.length}
isExpanded={showCode}
/> />
</div> </div>
{showCode && ( {showCode && (

View file

@ -50,11 +50,24 @@ const ContentParts = memo(
[attachments, messageAttachmentsMap, messageId], [attachments, messageAttachmentsMap, messageId],
); );
const hasReasoningParts = useMemo( const hasReasoningParts = useMemo(() => {
() => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false, const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
[content], const allThinkPartsHaveContent =
); content?.every((part) => {
if (part?.type !== ContentTypes.THINK) {
return true;
}
if (typeof part.think === 'string') {
const cleanedContent = part.think.replace(/<\/?think>/g, '').trim();
return cleanedContent.length > 0;
}
return false;
}) ?? false;
return hasThinkPart && allThinkPartsHaveContent;
}, [content]);
if (!content) { if (!content) {
return null; return null;
} }

View file

@ -9,14 +9,14 @@ const Files = ({ message }: { message?: TMessage }) => {
}, [message?.files]); }, [message?.files]);
const otherFiles = useMemo(() => { const otherFiles = useMemo(() => {
return message?.files?.filter((file) => !file.type?.startsWith('image/')) || []; return message?.files?.filter((file) => !(file.type?.startsWith('image/') === true)) || [];
}, [message?.files]); }, [message?.files]);
return ( return (
<> <>
{otherFiles.length > 0 && {otherFiles.length > 0 &&
otherFiles.map((file) => <FileContainer key={file.file_id} file={file as TFile} />)} otherFiles.map((file) => <FileContainer key={file.file_id} file={file as TFile} />)}
{imageFiles && {imageFiles.length > 0 &&
imageFiles.map((file) => ( imageFiles.map((file) => (
<Image <Image
key={file.file_id} key={file.file_id}

View file

@ -9,12 +9,12 @@ const scaleImage = ({
originalHeight, originalHeight,
containerRef, containerRef,
}: { }: {
originalWidth: number; originalWidth?: number;
originalHeight: number; originalHeight?: number;
containerRef: React.RefObject<HTMLDivElement>; containerRef: React.RefObject<HTMLDivElement>;
}) => { }) => {
const containerWidth = containerRef.current?.offsetWidth ?? 0; const containerWidth = containerRef.current?.offsetWidth ?? 0;
if (containerWidth === 0 || originalWidth === undefined || originalHeight === undefined) { if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
return { width: 'auto', height: 'auto' }; return { width: 'auto', height: 'auto' };
} }
const aspectRatio = originalWidth / originalHeight; const aspectRatio = originalWidth / originalHeight;
@ -35,8 +35,8 @@ const Image = ({
height: number; height: number;
width: number; width: number;
placeholderDimensions?: { placeholderDimensions?: {
height: string; height?: string;
width: string; width?: string;
}; };
}) => { }) => {
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(false);
@ -47,8 +47,8 @@ const Image = ({
const { width: scaledWidth, height: scaledHeight } = useMemo( const { width: scaledWidth, height: scaledHeight } = useMemo(
() => () =>
scaleImage({ scaleImage({
originalWidth: Number(placeholderDimensions?.width?.split('px')[0]) ?? width, originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width),
originalHeight: Number(placeholderDimensions?.height?.split('px')[0]) ?? height, originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height),
containerRef, containerRef,
}), }),
[placeholderDimensions, height, width], [placeholderDimensions, height, width],

View file

@ -11,7 +11,7 @@ export default function InProgressCall({
progress: number; progress: number;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
if ((!isSubmitting && progress < 1) || error) { if ((!isSubmitting && progress < 1) || error === true) {
return <CancelledIcon />; return <CancelledIcon />;
} }

View file

@ -159,7 +159,9 @@ const MessageContent = ({
return ( return (
<> <>
{thinkingContent && <Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>} {thinkingContent.length > 0 && (
<Thinking key={`thinking-${messageId}`}>{thinkingContent}</Thinking>
)}
<DisplayMessage <DisplayMessage
key={`display-${messageId}`} key={`display-${messageId}`}
showCursor={showRegularCursor} showCursor={showRegularCursor}

View file

@ -81,6 +81,8 @@ const Part = memo(({ part, isSubmitting, attachments, showCursor, isCreatedByUse
initialProgress={toolCall.progress ?? 0.1} initialProgress={toolCall.progress ?? 0.1}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
attachments={attachments} attachments={attachments}
auth={toolCall.auth}
expires_at={toolCall.expires_at}
/> />
); );
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) { } else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {

View file

@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText'; import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon'; import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress'; import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment'; import Attachment from './Attachment';
import LogContent from './LogContent'; import LogContent from './LogContent';
import { useProgress } from '~/hooks';
import store from '~/store'; import store from '~/store';
interface ParsedArgs { interface ParsedArgs {
@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
}, [args]); }, [args]);
} }
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function ExecuteCode({ export default function ExecuteCode({
initialProgress = 0.1, initialProgress = 0.1,
args, args,
@ -49,14 +52,12 @@ export default function ExecuteCode({
isSubmitting: boolean; isSubmitting: boolean;
attachments?: TAttachment[]; attachments?: TAttachment[];
}) { }) {
const localize = useLocalize();
const showAnalysisCode = useRecoilValue(store.showCode); const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode); const [showCode, setShowCode] = useState(showAnalysisCode);
const { lang, code } = useParseArgs(args); const { lang, code } = useParseArgs(args);
const progress = useProgress(initialProgress); const progress = useProgress(initialProgress);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference; const offset = circumference - progress * circumference;
return ( return (
@ -78,9 +79,10 @@ export default function ExecuteCode({
<ProgressText <ProgressText
progress={progress} progress={progress}
onClick={() => setShowCode((prev) => !prev)} onClick={() => setShowCode((prev) => !prev)}
inProgressText="Analyzing" inProgressText={localize('com_ui_analyzing')}
finishedText="Finished analyzing" finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length} hasInput={!!code.length}
isExpanded={showCode}
/> />
</div> </div>
{showCode && ( {showCode && (
@ -105,9 +107,7 @@ export default function ExecuteCode({
)} )}
</div> </div>
)} )}
{attachments?.map((attachment, index) => ( {attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
<Attachment attachment={attachment} key={index} />
))}
</> </>
); );
} }

View file

@ -64,7 +64,7 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, atta
} }
// const expirationText = expiresAt // const expirationText = expiresAt
// ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}` // ? ` ${localize('com_download_expires', { 0: format(expiresAt, 'MM/dd/yy HH:mm') })}`
// : ` ${localize('com_click_to_download')}`; // : ` ${localize('com_click_to_download')}`;
return ( return (

View file

@ -11,9 +11,16 @@ type ReasoningProps = {
const Reasoning = memo(({ reasoning }: ReasoningProps) => { const Reasoning = memo(({ reasoning }: ReasoningProps) => {
const { isExpanded, nextType } = useMessageContext(); const { isExpanded, nextType } = useMessageContext();
const reasoningText = useMemo(() => { const reasoningText = useMemo(() => {
return reasoning.replace(/^<think>\s*/, '').replace(/\s*<\/think>$/, ''); return reasoning
.replace(/^<think>\s*/, '')
.replace(/\s*<\/think>$/, '')
.trim();
}, [reasoning]); }, [reasoning]);
if (!reasoningText) {
return null;
}
return ( return (
<div <div
className={cn( className={cn(

View file

@ -39,16 +39,21 @@ export default function ProgressText({
onClick, onClick,
inProgressText, inProgressText,
finishedText, finishedText,
authText,
hasInput = true, hasInput = true,
popover = false, popover = false,
isExpanded = false,
}: { }: {
progress: number; progress: number;
onClick: () => void; onClick?: () => void;
inProgressText: string; inProgressText: string;
finishedText: string; finishedText: string;
authText?: string;
hasInput?: boolean; hasInput?: boolean;
popover?: boolean; popover?: boolean;
isExpanded?: boolean;
}) { }) {
const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
return ( return (
<Wrapper popover={popover}> <Wrapper popover={popover}>
<button <button
@ -57,8 +62,14 @@ export default function ProgressText({
disabled={!hasInput} disabled={!hasInput}
onClick={onClick} onClick={onClick}
> >
{progress < 1 ? inProgressText : finishedText} {text}
<svg width="16" height="17" viewBox="0 0 16 17" fill="none"> <svg
width="16"
height="17"
viewBox="0 0 16 17"
fill="none"
className={isExpanded ? 'rotate-180' : 'rotate-0'}
>
<path <path
className={hasInput ? '' : 'stroke-transparent'} className={hasInput ? '' : 'stroke-transparent'}
d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203" d="M11.3346 7.83203L8.00131 11.1654L4.66797 7.83203"

View file

@ -1,5 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import * as Popover from '@radix-ui/react-popover'; import * as Popover from '@radix-ui/react-popover';
import { ShieldCheck, TriangleAlert } from 'lucide-react';
import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider'; import { actionDelimiter, actionDomainSeparator, Constants } from 'librechat-data-provider';
import type { TAttachment } from 'librechat-data-provider'; import type { TAttachment } from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
@ -14,6 +15,9 @@ import WrenchIcon from './WrenchIcon';
import { useProgress } from '~/hooks'; import { useProgress } from '~/hooks';
import { logger } from '~/utils'; import { logger } from '~/utils';
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
export default function ToolCall({ export default function ToolCall({
initialProgress = 0.1, initialProgress = 0.1,
isSubmitting, isSubmitting,
@ -21,6 +25,7 @@ export default function ToolCall({
args: _args = '', args: _args = '',
output, output,
attachments, attachments,
auth,
}: { }: {
initialProgress: number; initialProgress: number;
isSubmitting: boolean; isSubmitting: boolean;
@ -28,13 +33,10 @@ export default function ToolCall({
args: string | Record<string, unknown>; args: string | Record<string, unknown>;
output?: string | null; output?: string | null;
attachments?: TAttachment[]; attachments?: TAttachment[];
auth?: string;
expires_at?: number;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const progress = useProgress(initialProgress);
const radius = 56.08695652173913;
const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
const { function_name, domain, isMCPToolCall } = useMemo(() => { const { function_name, domain, isMCPToolCall } = useMemo(() => {
if (typeof name !== 'string') { if (typeof name !== 'string') {
return { function_name: '', domain: null, isMCPToolCall: false }; return { function_name: '', domain: null, isMCPToolCall: false };
@ -83,8 +85,37 @@ export default function ToolCall({
[args, output], [args, output],
); );
const authDomain = useMemo(() => {
const authURL = auth ?? '';
if (!authURL) {
return '';
}
try {
const url = new URL(authURL);
return url.hostname;
} catch (e) {
return '';
}
}, [auth]);
const progress = useProgress(error === true ? 1 : initialProgress);
const cancelled = (!isSubmitting && progress < 1) || error === true;
const offset = circumference - progress * circumference;
const renderIcon = () => { const renderIcon = () => {
if (progress < 1) { if (progress < 1 && authDomain.length > 0) {
return (
<div
className="absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-full bg-transparent text-text-secondary"
style={{ opacity: 1, transform: 'none' }}
data-projection-id="849"
>
<div>
<ShieldCheck />
</div>
</div>
);
} else if (progress < 1) {
return ( return (
<InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}> <InProgressCall progress={progress} isSubmitting={isSubmitting} error={error}>
<div <div
@ -101,43 +132,67 @@ export default function ToolCall({
); );
} }
return error === true ? <CancelledIcon /> : <FinishedIcon />; return cancelled ? <CancelledIcon /> : <FinishedIcon />;
}; };
const getFinishedText = () => { const getFinishedText = () => {
if (cancelled) {
return localize('com_ui_error');
}
if (isMCPToolCall === true) { if (isMCPToolCall === true) {
return localize('com_assistants_completed_function', function_name); return localize('com_assistants_completed_function', { 0: function_name });
} }
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) { if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
return localize('com_assistants_completed_action', domain); return localize('com_assistants_completed_action', { 0: domain });
} }
return localize('com_assistants_completed_function', function_name); return localize('com_assistants_completed_function', { 0: function_name });
}; };
return ( return (
<Popover.Root> <Popover.Root>
<div className="my-2.5 flex items-center gap-2.5"> <div className="my-2.5 flex flex-wrap items-center gap-2.5">
<div className="relative h-5 w-5 shrink-0">{renderIcon()}</div> <div className="flex w-full items-center gap-2.5">
<ProgressText <div className="relative h-5 w-5 shrink-0">{renderIcon()}</div>
progress={progress} <ProgressText
onClick={() => ({})} progress={cancelled ? 1 : progress}
inProgressText={localize('com_assistants_running_action')} inProgressText={localize('com_assistants_running_action')}
finishedText={getFinishedText()} authText={
hasInput={hasInfo} !cancelled && authDomain.length > 0 ? localize('com_ui_requires_auth') : undefined
popover={true} }
/> finishedText={getFinishedText()}
{hasInfo && ( hasInput={hasInfo}
<ToolPopover popover={true}
input={args ?? ''}
output={output}
domain={domain ?? ''}
function_name={function_name}
/> />
{hasInfo && (
<ToolPopover
input={args ?? ''}
output={output}
domain={authDomain || (domain ?? '')}
function_name={function_name}
pendingAuth={authDomain.length > 0 && !cancelled && progress < 1}
/>
)}
</div>
{auth != null && auth && progress < 1 && !cancelled && (
<div className="flex w-full flex-col gap-2.5">
<div className="mb-1 mt-2">
<a
className="inline-flex items-center justify-center gap-2 rounded-3xl bg-surface-tertiary px-4 py-2 text-sm font-medium hover:bg-surface-hover"
href={auth}
target="_blank"
rel="noopener noreferrer"
>
{localize('com_ui_sign_in_to_domain', { 0: authDomain })}
</a>
</div>
<p className="flex items-center text-xs text-text-secondary">
<TriangleAlert className="mr-1.5 inline-block h-4 w-4" />
{localize('com_assistants_allow_sites_you_trust')}
</p>
</div>
)} )}
</div> </div>
{attachments?.map((attachment, index) => ( {attachments?.map((attachment, index) => <Attachment attachment={attachment} key={index} />)}
<Attachment attachment={attachment} key={index} />
))}
</Popover.Root> </Popover.Root>
); );
} }

View file

@ -4,13 +4,15 @@ import useLocalize from '~/hooks/useLocalize';
export default function ToolPopover({ export default function ToolPopover({
input, input,
output, output,
function_name,
domain, domain,
function_name,
pendingAuth,
}: { }: {
input: string; input: string;
function_name: string; function_name: string;
output?: string | null; output?: string | null;
domain?: string; domain?: string;
pendingAuth?: boolean;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const formatText = (text: string) => { const formatText = (text: string) => {
@ -21,6 +23,17 @@ export default function ToolPopover({
} }
}; };
let title =
domain != null && domain
? localize('com_assistants_domain_info', { 0: domain })
: localize('com_assistants_function_use', { 0: function_name });
if (pendingAuth === true) {
title =
domain != null && domain
? localize('com_assistants_action_attempt', { 0: domain })
: localize('com_assistants_attempt_info');
}
return ( return (
<Popover.Portal> <Popover.Portal>
<Popover.Content <Popover.Content
@ -28,27 +41,23 @@ export default function ToolPopover({
align="start" align="start"
sideOffset={12} sideOffset={12}
alignOffset={-5} alignOffset={-5}
className="w-18 min-w-[180px] max-w-sm rounded-lg bg-white dark:bg-gray-900" className="w-18 min-w-[180px] max-w-sm rounded-lg bg-surface-primary px-1"
> >
<div tabIndex={-1}> <div tabIndex={-1}>
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]"> <div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
<div className="mb-2 text-sm font-medium dark:text-gray-100"> <div className="mb-2 text-sm font-medium text-text-primary">{title}</div>
{domain != null && domain
? localize('com_assistants_domain_info', domain)
: localize('com_assistants_function_use', function_name)}
</div>
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs"> <div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700"> <div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
<code className="!whitespace-pre-wrap ">{formatText(input)}</code> <code className="!whitespace-pre-wrap ">{formatText(input)}</code>
</div> </div>
</div> </div>
{output != null && output && ( {output != null && output && (
<> <>
<div className="mb-2 mt-2 text-sm font-medium dark:text-gray-100"> <div className="mb-2 mt-2 text-sm font-medium text-text-primary">
{localize('com_ui_result')} {localize('com_ui_result')}
</div> </div>
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs"> <div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700"> <div className="max-h-32 overflow-y-auto rounded-md bg-surface-tertiary p-2">
<code className="!whitespace-pre-wrap ">{formatText(output)}</code> <code className="!whitespace-pre-wrap ">{formatText(output)}</code>
</div> </div>
</div> </div>

View file

@ -1,37 +1,19 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider'; import { FileSources, LocalStorageKeys } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common'; import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider'; import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper'; import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import Artifacts from '~/components/Artifacts/Artifacts'; import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel'; import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks'; import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers'; import { EditorProvider } from '~/Providers';
import store from '~/store'; import store from '~/store';
const defaultInterface = getConfigDefaults().interface; export default function Presentation({ children }: { children: React.ReactNode }) {
export default function Presentation({
children,
useSidePanel = false,
panel,
}: {
children: React.ReactNode;
panel?: React.ReactNode;
useSidePanel?: boolean;
}) {
const { data: startupConfig } = useGetStartupConfig();
const artifacts = useRecoilValue(store.artifactsState); const artifacts = useRecoilValue(store.artifactsState);
const codeArtifacts = useRecoilValue(store.codeArtifacts);
const hideSidePanel = useRecoilValue(store.hideSidePanel);
const artifactsVisible = useRecoilValue(store.artifactsVisible); const artifactsVisible = useRecoilValue(store.artifactsVisible);
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
);
const setFilesToDelete = useSetFilesToDelete(); const setFilesToDelete = useSetFilesToDelete();
const { mutateAsync } = useDeleteFilesMutation({ const { mutateAsync } = useDeleteFilesMutation({
@ -83,35 +65,24 @@ export default function Presentation({
</div> </div>
); );
if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
<SidePanel
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
fullPanelCollapse={fullCollapse}
artifacts={
artifactsVisible === true &&
codeArtifacts === true &&
Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanel>
</DragDropWrapper>
);
}
return ( return (
<DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation"> <DragDropWrapper className="relative flex w-full grow overflow-hidden bg-presentation">
{layout()} <SidePanelGroup
{panel != null && panel} defaultLayout={defaultLayout}
fullPanelCollapse={fullCollapse}
defaultCollapsed={defaultCollapsed}
artifacts={
artifactsVisible === true && Object.keys(artifacts ?? {}).length > 0 ? (
<EditorProvider>
<Artifacts />
</EditorProvider>
) : null
}
>
<main className="flex h-full flex-col overflow-y-auto" role="main">
{children}
</main>
</SidePanelGroup>
</DragDropWrapper> </DragDropWrapper>
); );
} }

View file

@ -1,14 +1,14 @@
import { TPromptGroup } from 'librechat-data-provider'; import { TPromptGroup } from 'librechat-data-provider';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) { export default function PromptCard({ promptGroup }: { promptGroup?: TPromptGroup }) {
return ( return (
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700"> <div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
<div className=""> <div className="">
<CategoryIcon className="size-4" category={promptGroup.category || ''} /> <CategoryIcon className="size-4" category={promptGroup?.category ?? ''} />
</div> </div>
<p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400"> <p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400">
{promptGroup?.oneliner || promptGroup?.productionPrompt?.prompt} {(promptGroup?.oneliner ?? '') || promptGroup?.productionPrompt?.prompt}
</p> </p>
</div> </div>
); );

View file

@ -1,8 +1,8 @@
import { useMemo, memo } from 'react'; import { useMemo, memo } from 'react';
import { parseISO, isToday } from 'date-fns'; import { parseISO, isToday } from 'date-fns';
import { TConversation } from 'librechat-data-provider'; import { TConversation } from 'librechat-data-provider';
import { useLocalize, TranslationKeys } from '~/hooks';
import { groupConversationsByDate } from '~/utils'; import { groupConversationsByDate } from '~/utils';
import { useLocalize } from '~/hooks';
import Convo from './Convo'; import Convo from './Convo';
const Conversations = ({ const Conversations = ({
@ -41,8 +41,7 @@ const Conversations = ({
paddingLeft: '10px', paddingLeft: '10px',
}} }}
> >
{/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */} {localize(groupName as TranslationKeys) || groupName}
{localize(groupName) || groupName}
</div> </div>
{convos.map((convo, i) => ( {convos.map((convo, i) => (
<Convo <Convo

View file

@ -12,7 +12,7 @@ import {
HoverCardContent, HoverCardContent,
} from '~/components/ui'; } from '~/components/ui';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover'; import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { useLocalize, useNavigateToConvo } from '~/hooks'; import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
import { useForkConvoMutation } from '~/data-provider'; import { useForkConvoMutation } from '~/data-provider';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { ESide } from '~/common'; import { ESide } from '~/common';
@ -201,7 +201,7 @@ export default function Fork({
align="center" align="center"
> >
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200"> <div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
{localize(activeSetting)} {localize(activeSetting as TranslationKeys)}
<HoverCard openDelay={50}> <HoverCard openDelay={50}>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" /> <InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
@ -216,7 +216,9 @@ export default function Fork({
<span>{localize('com_ui_fork_info_1')}</span> <span>{localize('com_ui_fork_info_1')}</span>
<span>{localize('com_ui_fork_info_2')}</span> <span>{localize('com_ui_fork_info_2')}</span>
<span> <span>
{localize('com_ui_fork_info_3', localize('com_ui_fork_split_target'))} {localize('com_ui_fork_info_3', {
0: localize('com_ui_fork_split_target'),
})}
</span> </span>
</div> </div>
</HoverCardContent> </HoverCardContent>
@ -233,7 +235,7 @@ export default function Fork({
hoverTitle={ hoverTitle={
<> <>
<GitCommit className="h-5 w-5 rotate-90" /> <GitCommit className="h-5 w-5 rotate-90" />
{localize(optionLabels[ForkOptions.DIRECT_PATH])} {localize(optionLabels[ForkOptions.DIRECT_PATH] as TranslationKeys)}
</> </>
} }
hoverDescription={localize('com_ui_fork_info_visible')} hoverDescription={localize('com_ui_fork_info_visible')}
@ -251,7 +253,7 @@ export default function Fork({
hoverTitle={ hoverTitle={
<> <>
<GitBranchPlus className="h-4 w-4 rotate-180" /> <GitBranchPlus className="h-4 w-4 rotate-180" />
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])} {localize(optionLabels[ForkOptions.INCLUDE_BRANCHES] as TranslationKeys)}
</> </>
} }
hoverDescription={localize('com_ui_fork_info_branches')} hoverDescription={localize('com_ui_fork_info_branches')}
@ -269,9 +271,9 @@ export default function Fork({
hoverTitle={ hoverTitle={
<> <>
<ListTree className="h-5 w-5" /> <ListTree className="h-5 w-5" />
{`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize( {`${localize(
'com_endpoint_default', optionLabels[ForkOptions.TARGET_LEVEL] as TranslationKeys,
)})`} )} (${localize('com_endpoint_default')})`}
</> </>
} }
hoverDescription={localize('com_ui_fork_info_target')} hoverDescription={localize('com_ui_fork_info_target')}

View file

@ -69,6 +69,7 @@ const MinimalIcon: React.FC<IconProps> = (props) => {
<div <div
data-testid="convo-icon" data-testid="convo-icon"
title={name} title={name}
aria-hidden="true"
style={{ style={{
width: size, width: size,
height: size, height: size,

View file

@ -121,7 +121,7 @@ export default function Settings({
<Label htmlFor="temp-int" className="text-left text-sm font-medium"> <Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '} {localize('com_endpoint_temperature')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', '1')}) ({localize('com_endpoint_default_with_num', { 0: '1' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber

View file

@ -27,12 +27,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
conversation ?? {}; conversation ?? {};
const currentList = useMemo( const currentList = useMemo(
() => Object.values(assistantListMap?.[endpoint ?? ''] ?? {}) as Assistant[], () => Object.values(assistantListMap[endpoint ?? ''] ?? {}) as Assistant[],
[assistantListMap, endpoint], [assistantListMap, endpoint],
); );
const assistants = useMemo(() => { const assistants = useMemo(() => {
const currentAssistants = (currentList ?? []).map(({ id, name }) => ({ const currentAssistants = currentList.map(({ id, name }) => ({
label: name, label: name,
value: id, value: id,
})); }));
@ -52,8 +52,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
}); });
const activeAssistant = useMemo(() => { const activeAssistant = useMemo(() => {
if (assistant_id) { if (assistant_id != null && assistant_id) {
return assistantListMap[endpoint ?? '']?.[assistant_id]; return assistantListMap[endpoint ?? '']?.[assistant_id] as Assistant | null;
} }
return null; return null;
@ -70,11 +70,13 @@ export default function Settings({ conversation, setOption, models, readonly }:
}, [models, activeAssistant, localize]); }, [models, activeAssistant, localize]);
const [assistantValue, setAssistantValue] = useState<Option>( const [assistantValue, setAssistantValue] = useState<Option>(
activeAssistant ? { label: activeAssistant.name, value: activeAssistant.id } : defaultOption, activeAssistant != null
? { label: activeAssistant.name ?? '', value: activeAssistant.id }
: defaultOption,
); );
useEffect(() => { useEffect(() => {
if (assistantValue && assistantValue.value === '') { if (assistantValue.value === '') {
setOption('presetOverride')({ setOption('presetOverride')({
assistant_id: assistantValue.value, assistant_id: assistantValue.value,
} as Partial<TPreset>); } as Partial<TPreset>);
@ -95,7 +97,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
return; return;
} }
const assistant = assistantListMap[endpoint ?? '']?.[value]; const assistant = assistantListMap[endpoint ?? '']?.[value] as Assistant | null;
if (!assistant) { if (!assistant) {
setAssistantValue(defaultOption); setAssistantValue(defaultOption);
return; return;
@ -103,7 +105,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
setAssistantValue({ setAssistantValue({
label: assistant.name ?? '', label: assistant.name ?? '',
value: assistant.id ?? '', value: assistant.id || '',
}); });
setOption('assistant_id')(assistant.id); setOption('assistant_id')(assistant.id);
if (assistant.model) { if (assistant.model) {

View file

@ -37,7 +37,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
<TextareaAutosize <TextareaAutosize
id={`input-${idx}`} id={`input-${idx}`}
disabled={readonly} disabled={readonly}
value={example?.input?.content || ''} value={example.input.content || ''}
onChange={(e) => setExample(idx, 'input', e.target.value ?? null)} onChange={(e) => setExample(idx, 'input', e.target.value ?? null)}
placeholder="Set example input. Example is ignored if empty." placeholder="Set example input. Example is ignored if empty."
className={cn( className={cn(
@ -62,7 +62,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
<TextareaAutosize <TextareaAutosize
id={`output-${idx}`} id={`output-${idx}`}
disabled={readonly} disabled={readonly}
value={example?.output?.content || ''} value={example.output.content || ''}
onChange={(e) => setExample(idx, 'output', e.target.value ?? null)} onChange={(e) => setExample(idx, 'output', e.target.value ?? null)}
placeholder={'Set example output. Example is ignored if empty.'} placeholder={'Set example output. Example is ignored if empty.'}
className={cn( className={cn(

View file

@ -180,7 +180,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-p-int" className="text-left text-sm font-medium"> <Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '} {localize('com_endpoint_top_p')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', google.topP.default + '')}) ({localize('com_endpoint_default_with_num', { 0: google.topP.default + '' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber
@ -221,7 +221,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-k-int" className="text-left text-sm font-medium"> <Label htmlFor="top-k-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_k')}{' '} {localize('com_endpoint_top_k')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', google.topK.default + '')}) ({localize('com_endpoint_default_with_num',{ 0: google.topK.default + '' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber
@ -261,7 +261,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium"> <Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
{localize('com_endpoint_max_output_tokens')}{' '} {localize('com_endpoint_max_output_tokens')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', google.maxOutputTokens.default + '')}) ({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber

View file

@ -226,7 +226,7 @@ export default function Settings({
<Label htmlFor="temp-int" className="text-left text-sm font-medium"> <Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '} {localize('com_endpoint_temperature')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', '0.8')}) ({localize('com_endpoint_default_with_num', { 0: '0.8' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber
@ -266,7 +266,7 @@ export default function Settings({
<Label htmlFor="top-p-int" className="text-left text-sm font-medium"> <Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '} {localize('com_endpoint_top_p')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', '1')}) ({localize('com_endpoint_default_with_num', { 0: '1' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber
@ -307,7 +307,7 @@ export default function Settings({
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium"> <Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_frequency_penalty')}{' '} {localize('com_endpoint_frequency_penalty')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', '0')}) ({localize('com_endpoint_default_with_num', { 0: '0' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber
@ -348,7 +348,7 @@ export default function Settings({
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium"> <Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_presence_penalty')}{' '} {localize('com_endpoint_presence_penalty')}{' '}
<small className="opacity-40"> <small className="opacity-40">
({localize('com_endpoint_default_with_num', '0')}) ({localize('com_endpoint_default_with_num', { 0: '0' })})
</small> </small>
</Label> </Label>
<InputNumber <InputNumber

View file

@ -9,7 +9,7 @@ const FileDashboardView = () => {
return ( return (
<div className="bg-[#f9f9f9] p-0 lg:p-7"> <div className="bg-[#f9f9f9] p-0 lg:p-7">
<div className="ml-3 mt-3 flex flex-row justify-between"> <div className="ml-3 mt-3 flex flex-row justify-between">
{params?.vectorStoreId && ( {params.vectorStoreId && (
<Button <Button
className="block lg:hidden" className="block lg:hidden"
variant={'outline'} variant={'outline'}

View file

@ -244,9 +244,10 @@ export default function DataTableFile<TData, TValue>({
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0"> <div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
<div className="text-muted-foreground ml-2 flex-1 text-sm"> <div className="text-muted-foreground ml-2 flex-1 text-sm">
{localize( {localize(
'com_files_number_selected', 'com_files_number_selected', {
`${table.getFilteredSelectedRowModel().rows.length}`, 0: `${table.getFilteredSelectedRowModel().rows.length}`,
`${table.getFilteredRowModel().rows.length}`, 1: `${table.getFilteredRowModel().rows.length}`,
},
)} )}
</div> </div>
<Button <Button

View file

@ -2,16 +2,18 @@ import React from 'react';
import FileSidePanel from './FileList/FileSidePanel'; import FileSidePanel from './FileList/FileSidePanel';
import { Outlet, useNavigate, useParams } from 'react-router-dom'; import { Outlet, useNavigate, useParams } from 'react-router-dom';
import FilesSectionSelector from './FilesSectionSelector'; import FilesSectionSelector from './FilesSectionSelector';
import { useLocalize } from '~/hooks';
import { Button } from '../ui'; import { Button } from '../ui';
export default function FilesListView() { export default function FilesListView() {
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const localize = useLocalize();
return ( return (
<div className="bg-[#f9f9f9] p-0 lg:p-7"> <div className="bg-[#f9f9f9] p-0 lg:p-7">
<div className="m-4 flex w-full flex-row justify-between md:m-2"> <div className="m-4 flex w-full flex-row justify-between md:m-2">
<FilesSectionSelector /> <FilesSectionSelector />
{params?.fileId && ( {params.fileId != null && params.fileId && (
<Button <Button
className="block lg:hidden" className="block lg:hidden"
variant={'outline'} variant={'outline'}
@ -20,21 +22,21 @@ export default function FilesListView() {
navigate('/d/files'); navigate('/d/files');
}} }}
> >
Go back {localize('com_ui_go_back')}
</Button> </Button>
)} )}
</div> </div>
<div className="flex w-full flex-row divide-x"> <div className="flex w-full flex-row divide-x">
<div <div
className={`mr-2 w-full xl:w-1/3 ${ className={`mr-2 w-full xl:w-1/3 ${
params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full' params.fileId != null && params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
}`} }`}
> >
<FileSidePanel /> <FileSidePanel />
</div> </div>
<div <div
className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${ className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${
params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block' params.fileId != null && params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
}`} }`}
> >
<Outlet /> <Outlet />

View file

@ -178,7 +178,7 @@ export default function VectorStorePreview() {
<Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" /> <Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" />
&nbsp;Created At &nbsp;Created At
</span> </span>
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt?.toString()}</span> <span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt.toString()}</span>
</div> </div>
</div> </div>

View file

@ -29,7 +29,7 @@ export default function ModelSelect({
} }
const { endpoint: _endpoint, endpointType } = conversation; const { endpoint: _endpoint, endpointType } = conversation;
const models = modelsQuery?.data?.[_endpoint] ?? []; const models = modelsQuery.data?.[_endpoint] ?? [];
const endpoint = endpointType ?? _endpoint; const endpoint = endpointType ?? _endpoint;
const OptionComponent = multiChatOptions[endpoint]; const OptionComponent = multiChatOptions[endpoint];

View file

@ -40,13 +40,15 @@ export const TemporaryChat = () => {
}; };
return ( return (
<div className="sticky bottom-0 border-none bg-surface-tertiary px-6 py-4 "> <div className="sticky bottom-0 mt-auto w-full border-none bg-surface-tertiary px-6 py-4">
<div className="flex items-center"> <div className="flex items-center justify-between">
<div className={cn('flex flex-1 items-center gap-2', isActiveConvo && 'opacity-40')}> <div className={cn('flex items-center gap-2', isActiveConvo && 'opacity-40')}>
<MessageCircleDashed className="icon-sm" /> <MessageCircleDashed className="icon-sm" aria-hidden="true" />
<span className="text-sm text-text-primary">{localize('com_ui_temporary_chat')}</span> <span className="truncate text-sm text-text-primary">
{localize('com_ui_temporary_chat')}
</span>
</div> </div>
<div className="ml-auto flex items-center"> <div className="flex flex-shrink-0 items-center">
<Switch <Switch
id="temporary-chat-switch" id="temporary-chat-switch"
checked={isTemporary} checked={isTemporary}

View file

@ -47,11 +47,11 @@ const errorMessages = {
[ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`, [ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`,
[ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => { [ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => {
const { expiredAt, endpoint } = json; const { expiredAt, endpoint } = json;
return localize('com_error_expired_user_key', endpoint, expiredAt); return localize('com_error_expired_user_key', { 0: endpoint, 1: expiredAt });
}, },
[ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => { [ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => {
const { info } = json; const { info } = json;
return localize('com_error_input_length', info); return localize('com_error_input_length', { 0: info });
}, },
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => { [ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
const { info } = json; const { info } = json;

View file

@ -35,9 +35,9 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
<div className="h-7 w-7 flex-shrink-0"> <div className="h-7 w-7 flex-shrink-0">
<div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary"> <div className="relative flex h-full items-center justify-center rounded-full border border-border-medium bg-surface-primary-alt text-text-primary">
{tags.length > 0 ? ( {tags.length > 0 ? (
<BookmarkFilledIcon className="h-4 w-4" /> <BookmarkFilledIcon className="h-4 w-4" aria-hidden="true" />
) : ( ) : (
<BookmarkIcon className="h-4 w-4" /> <BookmarkIcon className="h-4 w-4" aria-hidden="true" />
)} )}
</div> </div>
</div> </div>

View file

@ -70,7 +70,7 @@ function Avatar() {
const megabytes = const megabytes =
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2; fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
showToast({ showToast({
message: localize('com_ui_upload_invalid_var', megabytes + ''), message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error', status: 'error',
}); });
} }

View file

@ -41,7 +41,7 @@ export const RevokeKeysButton = ({
const dialogTitle = all const dialogTitle = all
? localize('com_ui_revoke_keys') ? localize('com_ui_revoke_keys')
: localize('com_ui_revoke_key_endpoint', endpoint); : localize('com_ui_revoke_key_endpoint', { 0: endpoint });
const dialogMessage = all const dialogMessage = all
? localize('com_ui_revoke_keys_confirm') ? localize('com_ui_revoke_keys_confirm')

View file

@ -15,7 +15,7 @@ export default function DecibelSelector() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div>{localize('com_nav_db_sensitivity')}</div> <div>{localize('com_nav_db_sensitivity')}</div>
<div className="w-2" /> <div className="w-2" />
<small className="opacity-40">({localize('com_endpoint_default_with_num', '-45')})</small> <small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '-45' })})</small>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Slider <Slider

View file

@ -15,7 +15,7 @@ export default function DecibelSelector() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div>{localize('com_nav_playback_rate')}</div> <div>{localize('com_nav_playback_rate')}</div>
<div className="w-2" /> <div className="w-2" />
<small className="opacity-40">({localize('com_endpoint_default_with_num', '1')})</small> <small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '1' })})</small>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Slider <Slider

View file

@ -153,7 +153,7 @@ const AdminSettings = () => {
variant="outline" variant="outline"
className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0" className="mr-2 h-10 w-fit gap-1 border transition-all dark:bg-transparent dark:hover:bg-surface-tertiary sm:m-0"
> >
<ShieldEllipsis className="cursor-pointer" /> <ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
<span className="hidden sm:flex">{localize('com_ui_admin')}</span> <span className="hidden sm:flex">{localize('com_ui_admin')}</span>
</Button> </Button>
</OGDialogTrigger> </OGDialogTrigger>

View file

@ -46,7 +46,7 @@ const Command = ({
return ( return (
<div className="rounded-xl border border-border-light"> <div className="rounded-xl border border-border-light">
<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" /> <SquareSlash className="icon-sm" aria-hidden="true" />
<Input <Input
type="text" type="text"
tabIndex={tabIndex} tabIndex={tabIndex}

View file

@ -42,7 +42,7 @@ const DeleteVersion = ({
htmlFor="dialog-delete-confirm-prompt" htmlFor="dialog-delete-confirm-prompt"
className="text-left text-sm font-medium" className="text-left text-sm font-medium"
> >
{localize('com_ui_delete_confirm_prompt_version_var', name)} {localize('com_ui_delete_confirm_prompt_version_var', { 0: name })}
</Label> </Label>
</div> </div>
</div> </div>

View file

@ -43,7 +43,7 @@ const Description = ({
return ( return (
<div className="rounded-xl border border-border-light"> <div className="rounded-xl border border-border-light">
<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" /> <Info className="icon-sm" aria-hidden="true" />
<Input <Input
type="text" type="text"
tabIndex={tabIndex} tabIndex={tabIndex}

View file

@ -48,5 +48,5 @@ export default function CategoryIcon({
if (!IconComponent) { if (!IconComponent) {
return null; return null;
} }
return <IconComponent className={cn(colorClass, className)} />; return <IconComponent className={cn(colorClass, className)} aria-hidden="true" />;
} }

View file

@ -30,17 +30,13 @@ export default function GroupSidePanel({
<div <div
className={cn( className={cn(
'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4', 'mr-2 flex h-auto w-auto min-w-72 flex-col gap-2 lg:w-1/4 xl:w-1/4',
isDetailView && isSmallerScreen ? 'hidden' : '', isDetailView === true && isSmallerScreen ? 'hidden' : '',
className, className,
)} )}
> >
{children} {children}
<div className="flex-grow overflow-y-auto"> <div className="flex-grow overflow-y-auto">
<List <List groups={promptGroups} isChatRoute={isChatRoute} isLoading={!!groupsQuery.isLoading} />
groups={promptGroups}
isChatRoute={isChatRoute}
isLoading={!!groupsQuery.isLoading}
/>
</div> </div>
<PanelNavigation <PanelNavigation
nextPage={nextPage} nextPage={nextPage}

View file

@ -169,7 +169,7 @@ export default function VariableForm({
return ( return (
<InputCombobox <InputCombobox
options={field.config.options || []} options={field.config.options || []}
placeholder={localize('com_ui_enter_var', field.config.variable)} placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
className={cn( className={cn(
defaultTextProps, defaultTextProps,
'rounded px-3 py-2 focus:bg-surface-tertiary', 'rounded px-3 py-2 focus:bg-surface-tertiary',
@ -192,7 +192,7 @@ export default function VariableForm({
defaultTextProps, defaultTextProps,
'rounded px-3 py-2 focus:bg-surface-tertiary', 'rounded px-3 py-2 focus:bg-surface-tertiary',
)} )}
placeholder={localize('com_ui_enter_var', field.config.variable)} placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
maxRows={8} maxRows={8}
/> />
); );

View file

@ -70,7 +70,7 @@ const PromptForm = () => {
const selectedPrompt = useMemo( const selectedPrompt = useMemo(
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined), () => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
[prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex], [prompts, selectionIndex],
); );
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>(); const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
@ -102,7 +102,7 @@ const PromptForm = () => {
); );
}, },
onSuccess(data) { onSuccess(data) {
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) { if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) {
makeProductionMutation.mutate({ makeProductionMutation.mutate({
id: data.prompt._id, id: data.prompt._id,
groupId: data.prompt.groupId, groupId: data.prompt.groupId,
@ -336,7 +336,7 @@ const PromptForm = () => {
variant="ghost" variant="ghost"
className="h-10 w-10 border border-border-light p-0 lg:hidden" className="h-10 w-10 border border-border-light p-0 lg:hidden"
onClick={() => setShowSidePanel(true)} onClick={() => setShowSidePanel(true)}
aria-label={localize('com_ui_open_menu')} aria-label={localize('com_endpoint_open_menu')}
> >
<Menu className="size-5" /> <Menu className="size-5" />
</Button> </Button>
@ -382,8 +382,8 @@ const PromptForm = () => {
onClick={() => setShowSidePanel(false)} onClick={() => setShowSidePanel(false)}
aria-hidden={!showSidePanel} aria-hidden={!showSidePanel}
tabIndex={showSidePanel ? 0 : -1} tabIndex={showSidePanel ? 0 : -1}
aria-label={localize('com_ui_close_menu')}
/> />
<div <div
className="absolute inset-y-0 right-0 z-50 lg:hidden" className="absolute inset-y-0 right-0 z-50 lg:hidden"
style={{ style={{

View file

@ -30,7 +30,7 @@ const PromptVariables = ({
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" /> <Variable className="icon-sm" aria-hidden="true" />
{localize('com_ui_variables')} {localize('com_ui_variables')}
</h3> </h3>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">

View file

@ -111,12 +111,12 @@ const VersionCard = ({
onClick={onClick} onClick={onClick}
aria-selected={isSelected} aria-selected={isSelected}
role="tab" role="tab"
aria-label={localize('com_ui_version_var', `${totalVersions - index}`)} aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
> >
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex items-start justify-between lg:flex-col xl:flex-row"> <div className="flex items-start justify-between lg:flex-col xl:flex-row">
<h3 className="font-bold text-text-primary"> <h3 className="font-bold text-text-primary">
{localize('com_ui_version_var', `${totalVersions - index}`)} {localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
</h3> </h3>
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}> <time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')} {format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}

View file

@ -97,7 +97,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
</OGDialogTrigger> </OGDialogTrigger>
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title"> <OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}> <OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
{localize('com_ui_share_var', `"${group.name}"`)} {localize('com_ui_share_var', { 0: `"${group.name}"` })}
</OGDialogTitle> </OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description"> <form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
<div id="form-description" className="sr-only"> <div id="form-description" className="sr-only">

View file

@ -21,7 +21,7 @@ export default function MessagesView({
}} }}
> >
<div className="flex flex-col pb-9 text-sm dark:bg-transparent"> <div className="flex flex-col pb-9 text-sm dark:bg-transparent">
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? ( {(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300"> <div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
Nothing found Nothing found
</div> </div>

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common'; import type { TMessageProps } from '~/common';
// eslint-disable-next-line import/no-cycle // eslint-disable-next-line import/no-cycle
import Message from './Message'; import Message from './Message';
@ -25,17 +26,16 @@ export default function MultiMessage({
}, [messagesTree?.length]); }, [messagesTree?.length]);
useEffect(() => { useEffect(() => {
if (messagesTree?.length && siblingIdx >= messagesTree?.length) { if (messagesTree?.length != null && siblingIdx >= messagesTree.length) {
setSiblingIdx(0); setSiblingIdx(0);
} }
}, [siblingIdx, messagesTree?.length, setSiblingIdx]); }, [siblingIdx, messagesTree?.length, setSiblingIdx]);
if (!(messagesTree && messagesTree?.length)) { if (!(messagesTree && messagesTree.length)) {
return null; return null;
} }
const message = messagesTree[messagesTree.length - siblingIdx - 1]; const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | null;
if (!message) { if (!message) {
return null; return null;
} }

View file

@ -1,296 +0,0 @@
import { useFormContext } from 'react-hook-form';
import * as RadioGroup from '@radix-ui/react-radio-group';
import * as DialogPrimitive from '@radix-ui/react-dialog';
import {
AuthTypeEnum,
AuthorizationTypeEnum,
TokenExchangeMethodEnum,
} from 'librechat-data-provider';
import { DialogContent } from '~/components/ui/';
export default function ActionsAuth({
setOpenAuthDialog,
}: {
setOpenAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
}) {
const { watch, setValue, trigger } = useFormContext();
const type = watch('type');
return (
<DialogContent
role="dialog"
id="radix-:rf5:"
aria-describedby="radix-:rf7:"
aria-labelledby="radix-:rf6:"
data-state="open"
className="left-1/2 col-auto col-start-2 row-auto row-start-2 w-full max-w-md -translate-x-1/2 rounded-xl bg-white pb-0 text-left shadow-xl transition-all dark:bg-gray-700 dark:text-gray-100"
tabIndex={-1}
style={{ pointerEvents: 'auto' }}
>
<div className="flex items-center justify-between border-b border-black/10 px-4 pb-4 pt-5 dark:border-white/10 sm:p-6">
<div className="flex">
<div className="flex items-center">
<div className="flex grow flex-col gap-1">
<h2
id="radix-:rf6:"
className="text-token-text-primary text-lg font-medium leading-6"
>
Authentication
</h2>
</div>
</div>
</div>
</div>
<div className="p-4 sm:p-6 sm:pt-0">
<div className="mb-4">
<label className="mb-1 block text-sm font-medium">Authentication Type</label>
<RadioGroup.Root
defaultValue={AuthTypeEnum.None}
onValueChange={(value) => setValue('type', value)}
value={type}
role="radiogroup"
aria-required="false"
dir="ltr"
className="flex gap-4"
tabIndex={0}
style={{ outline: 'none' }}
>
<div className="flex items-center gap-2">
<label htmlFor=":rf8:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthTypeEnum.None}
id=":rf8:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
None
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor=":rfa:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthTypeEnum.ServiceHttp}
id=":rfa:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
tabIndex={0}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
API Key
</label>
</div>
<div className="flex items-center gap-2 text-gray-500">
<label htmlFor=":rfc:" className="flex cursor-not-allowed items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
disabled={true}
value={AuthTypeEnum.OAuth}
id=":rfc:"
className="mr-1 flex h-5 w-5 cursor-not-allowed items-center justify-center rounded-full border border-gray-500 bg-gray-300 dark:border-gray-600 dark:bg-gray-700"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
OAuth
</label>
</div>
</RadioGroup.Root>
</div>
{type === 'none' ? null : type === 'service_http' ? <ApiKey /> : <OAuth />}
{/* Cancel/Save */}
<div className="mt-5 flex flex-col gap-3 sm:mt-4 sm:flex-row-reverse">
<button
className="btn relative bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600"
onClick={async () => {
const result = await trigger(undefined, { shouldFocus: true });
setValue('saved_auth_fields', result);
setOpenAuthDialog(!result);
}}
>
<div className="flex w-full items-center justify-center gap-2">Save</div>
</button>
<DialogPrimitive.Close className="btn btn-neutral relative">
<div className="flex w-full items-center justify-center gap-2">Cancel</div>
</DialogPrimitive.Close>
</div>
</div>
</DialogContent>
);
}
const ApiKey = () => {
const { register, watch, setValue } = useFormContext();
const authorization_type = watch('authorization_type');
const type = watch('type');
return (
<>
<label className="mb-1 block text-sm font-medium">API Key</label>
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="new-password"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
{...register('api_key', { required: type === AuthTypeEnum.ServiceHttp })}
/>
<label className="mb-1 block text-sm font-medium">Auth Type</label>
<RadioGroup.Root
defaultValue={AuthorizationTypeEnum.Basic}
onValueChange={(value) => setValue('authorization_type', value)}
value={authorization_type}
role="radiogroup"
aria-required="true"
dir="ltr"
className="mb-2 flex gap-6 overflow-hidden rounded-lg"
tabIndex={0}
style={{ outline: 'none' }}
>
<div className="flex items-center gap-2">
<label htmlFor=":rfu:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthorizationTypeEnum.Basic}
id=":rfu:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
Basic
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor=":rg0:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthorizationTypeEnum.Bearer}
id=":rg0:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
Bearer
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor=":rg2:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={AuthorizationTypeEnum.Custom}
id=":rg2:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-500 dark:bg-gray-500"
tabIndex={0}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
Custom
</label>
</div>
</RadioGroup.Root>
{authorization_type === AuthorizationTypeEnum.Custom && (
<div className="mt-2">
<label className="mb-1 block text-sm font-medium">Custom Header Name</label>
<input
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-600"
placeholder="X-Api-Key"
{...register('custom_auth_header', {
required: authorization_type === AuthorizationTypeEnum.Custom,
})}
/>
</div>
)}
</>
);
};
const OAuth = () => {
const { register, watch, setValue } = useFormContext();
const token_exchange_method = watch('token_exchange_method');
const type = watch('type');
return (
<>
<label className="mb-1 block text-sm font-medium">Client ID</label>
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="off"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
{...register('oauth_client_id', { required: type === AuthTypeEnum.OAuth })}
/>
<label className="mb-1 block text-sm font-medium">Client Secret</label>
<input
placeholder="<HIDDEN>"
type="password"
autoComplete="off"
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
{...register('oauth_client_secret', { required: type === AuthTypeEnum.OAuth })}
/>
<label className="mb-1 block text-sm font-medium">Authorization URL</label>
<input
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
{...register('authorization_url', { required: type === AuthTypeEnum.OAuth })}
/>
<label className="mb-1 block text-sm font-medium">Token URL</label>
<input
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
{...register('client_url', { required: type === AuthTypeEnum.OAuth })}
/>
<label className="mb-1 block text-sm font-medium">Scope</label>
<input
className="border-token-border-medium mb-2 h-9 w-full resize-none overflow-y-auto rounded-lg border px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-400 dark:bg-gray-800"
{...register('scope', { required: type === AuthTypeEnum.OAuth })}
/>
<label className="mb-1 block text-sm font-medium">Token Exchange Method</label>
<RadioGroup.Root
defaultValue={AuthorizationTypeEnum.Basic}
onValueChange={(value) => setValue('token_exchange_method', value)}
value={token_exchange_method}
role="radiogroup"
aria-required="true"
dir="ltr"
tabIndex={0}
style={{ outline: 'none' }}
>
<div className="flex items-center gap-2">
<label htmlFor=":rj1:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={TokenExchangeMethodEnum.DefaultPost}
id=":rj1:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
Default (POST request)
</label>
</div>
<div className="flex items-center gap-2">
<label htmlFor=":rj3:" className="flex cursor-pointer items-center gap-1">
<RadioGroup.Item
type="button"
role="radio"
value={TokenExchangeMethodEnum.BasicAuthHeader}
id=":rj3:"
className="mr-1 flex h-5 w-5 items-center justify-center rounded-full border border-gray-500 bg-white dark:border-gray-700 dark:bg-gray-700"
tabIndex={-1}
>
<RadioGroup.Indicator className="h-2 w-2 rounded-full bg-gray-950 dark:bg-white"></RadioGroup.Indicator>
</RadioGroup.Item>
Basic authorization header
</label>
</div>
</RadioGroup.Root>
</>
);
};

View file

@ -14,6 +14,7 @@ import type {
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import type { ActionAuthForm } from '~/common'; import type { ActionAuthForm } from '~/common';
import type { Spec } from './ActionsTable'; import type { Spec } from './ActionsTable';
import ActionCallback from '~/components/SidePanel/Builder/ActionCallback';
import { ActionsTable, columns } from './ActionsTable'; import { ActionsTable, columns } from './ActionsTable';
import { useUpdateAgentAction } from '~/data-provider'; import { useUpdateAgentAction } from '~/data-provider';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
@ -248,8 +249,8 @@ export default function ActionsInput({
</div> </div>
</div> </div>
{!!data && ( {!!data && (
<div> <div className="my-2">
<div className="mb-1.5 flex items-center"> <div className="flex items-center">
<label className="text-token-text-primary block font-medium"> <label className="text-token-text-primary block font-medium">
{localize('com_assistants_available_actions')} {localize('com_assistants_available_actions')}
</label> </label>
@ -258,6 +259,7 @@ export default function ActionsInput({
</div> </div>
)} )}
<div className="relative my-1"> <div className="relative my-1">
<ActionCallback action_id={action?.action_id} />
<div className="mb-1.5 flex items-center"> <div className="mb-1.5 flex items-center">
<label className="text-token-text-primary block font-medium"> <label className="text-token-text-primary block font-medium">
{localize('com_ui_privacy_policy_url')} {localize('com_ui_privacy_policy_url')}
@ -267,7 +269,7 @@ export default function ActionsInput({
<input <input
type="text" type="text"
placeholder="https://api.example-weather-app.com/privacy" placeholder="https://api.example-weather-app.com/privacy"
className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none focus:ring-1 focus:ring-border-light" className="flex-1 rounded-lg bg-transparent px-3 py-1.5 text-sm outline-none placeholder:text-text-secondary-alt focus:ring-1 focus:ring-border-light"
/> />
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { useForm, FormProvider } from 'react-hook-form'; import { useForm, FormProvider } from 'react-hook-form';
import { import {
AuthTypeEnum, AuthTypeEnum,
@ -7,14 +7,14 @@ import {
} from 'librechat-data-provider'; } from 'librechat-data-provider';
import { ChevronLeft } from 'lucide-react'; import { ChevronLeft } from 'lucide-react';
import type { AgentPanelProps, ActionAuthForm } from '~/common'; import type { AgentPanelProps, ActionAuthForm } from '~/common';
import { Dialog, DialogTrigger, OGDialog, OGDialogTrigger, Label } from '~/components/ui'; import ActionsAuth from '~/components/SidePanel/Builder/ActionsAuth';
import { OGDialog, OGDialogTrigger, Label } from '~/components/ui';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useDeleteAgentAction } from '~/data-provider'; import { useDeleteAgentAction } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize'; import useLocalize from '~/hooks/useLocalize';
import { useToastContext } from '~/Providers'; import { useToastContext } from '~/Providers';
import { TrashIcon } from '~/components/svg'; import { TrashIcon } from '~/components/svg';
import ActionsInput from './ActionsInput'; import ActionsInput from './ActionsInput';
import ActionsAuth from './ActionsAuth';
import { Panel } from '~/common'; import { Panel } from '~/common';
export default function ActionsPanel({ export default function ActionsPanel({
@ -26,8 +26,6 @@ export default function ActionsPanel({
}: AgentPanelProps) { }: AgentPanelProps) {
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const [openAuthDialog, setOpenAuthDialog] = useState(false);
const deleteAgentAction = useDeleteAgentAction({ const deleteAgentAction = useDeleteAgentAction({
onSuccess: () => { onSuccess: () => {
showToast({ showToast({
@ -65,7 +63,6 @@ export default function ActionsPanel({
}); });
const { reset, watch } = methods; const { reset, watch } = methods;
const type = watch('type');
useEffect(() => { useEffect(() => {
if (action?.metadata.auth) { if (action?.metadata.auth) {
@ -156,40 +153,7 @@ export default function ActionsPanel({
<a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a> <a href="https://help.openai.com/en/articles/8554397-creating-a-gpt" target="_blank" rel="noreferrer" className="font-medium">Learn more.</a>
</div> */} </div> */}
</div> </div>
<Dialog open={openAuthDialog} onOpenChange={setOpenAuthDialog}> <ActionsAuth />
<DialogTrigger asChild>
<div className="relative mb-4">
<div className="mb-1.5 flex items-center">
<label className="text-token-text-primary block font-medium">
{localize('com_ui_authentication')}
</label>
</div>
<div className="border-token-border-medium flex rounded-lg border text-sm hover:cursor-pointer">
<div className="h-9 grow px-3 py-2">{type}</div>
<div className="bg-token-border-medium w-px"></div>
<button type="button" color="neutral" className="flex items-center gap-2 px-3">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="icon-sm"
>
<path
d="M11.6439 3C10.9352 3 10.2794 3.37508 9.92002 3.98596L9.49644 4.70605C8.96184 5.61487 7.98938 6.17632 6.93501 6.18489L6.09967 6.19168C5.39096 6.19744 4.73823 6.57783 4.38386 7.19161L4.02776 7.80841C3.67339 8.42219 3.67032 9.17767 4.01969 9.7943L4.43151 10.5212C4.95127 11.4386 4.95127 12.5615 4.43151 13.4788L4.01969 14.2057C3.67032 14.8224 3.67339 15.5778 4.02776 16.1916L4.38386 16.8084C4.73823 17.4222 5.39096 17.8026 6.09966 17.8083L6.93502 17.8151C7.98939 17.8237 8.96185 18.3851 9.49645 19.294L9.92002 20.014C10.2794 20.6249 10.9352 21 11.6439 21H12.3561C13.0648 21 13.7206 20.6249 14.08 20.014L14.5035 19.294C15.0381 18.3851 16.0106 17.8237 17.065 17.8151L17.9004 17.8083C18.6091 17.8026 19.2618 17.4222 19.6162 16.8084L19.9723 16.1916C20.3267 15.5778 20.3298 14.8224 19.9804 14.2057L19.5686 13.4788C19.0488 12.5615 19.0488 11.4386 19.5686 10.5212L19.9804 9.7943C20.3298 9.17767 20.3267 8.42219 19.9723 7.80841L19.6162 7.19161C19.2618 6.57783 18.6091 6.19744 17.9004 6.19168L17.065 6.18489C16.0106 6.17632 15.0382 5.61487 14.5036 4.70605L14.08 3.98596C13.7206 3.37508 13.0648 3 12.3561 3H11.6439Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<circle cx="12" cy="12" r="2.5" stroke="currentColor" strokeWidth="2" />
</svg>
</button>
</div>
</div>
</DialogTrigger>
<ActionsAuth setOpenAuthDialog={setOpenAuthDialog} />
</Dialog>
<ActionsInput action={action} agent_id={agent_id} setAction={setAction} /> <ActionsInput action={action} agent_id={agent_id} setAction={setAction} />
</div> </div>
</form> </form>

Some files were not shown because too many files have changed in this diff Show more