diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 539c642650..0000000000
--- a/.eslintrc.js
+++ /dev/null
@@ -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 ), 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'],
- },
- },
- },
-};
diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
index 5c88b9f70d..3a3b828ee1 100644
--- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
+++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml
@@ -1,12 +1,19 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
-labels: ["bug"]
+labels: ["🐛 bug"]
body:
- type: markdown
attributes:
value: |
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
id: what-happened
attributes:
@@ -15,6 +22,23 @@ body:
placeholder: Please give as many details as possible
validations:
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
id: steps-to-reproduce
attributes:
@@ -39,7 +63,21 @@ body:
id: logs
attributes:
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
- type: textarea
id: screenshots
@@ -53,4 +91,4 @@ body:
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
+ required: true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
index d85957fd22..613c9e0a01 100644
--- a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
+++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml
@@ -1,7 +1,7 @@
name: Feature Request
description: File a feature request
-title: "Enhancement: "
-labels: ["enhancement"]
+title: "[Enhancement]: "
+labels: ["✨ enhancement"]
body:
- type: markdown
attributes:
diff --git a/.github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml b/.github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml
new file mode 100644
index 0000000000..5fddced9f8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/NEW-LANGUAGE-REQUEST.yml
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/QUESTION.yml b/.github/ISSUE_TEMPLATE/QUESTION.yml
index 0669fd6724..c66e6baa3b 100644
--- a/.github/ISSUE_TEMPLATE/QUESTION.yml
+++ b/.github/ISSUE_TEMPLATE/QUESTION.yml
@@ -1,7 +1,7 @@
name: Question
description: Ask your question
title: "[Question]: "
-labels: ["question"]
+labels: ["❓ question"]
body:
- type: markdown
attributes:
diff --git a/.github/workflows/backend-review.yml b/.github/workflows/backend-review.yml
index 33316731a1..5bc3d3b2db 100644
--- a/.github/workflows/backend-review.yml
+++ b/.github/workflows/backend-review.yml
@@ -38,7 +38,7 @@ jobs:
- name: Install MCP Package
run: npm run build:mcp
-
+
- name: Create empty auth.json file
run: |
mkdir -p api/data
@@ -61,9 +61,4 @@ jobs:
run: cd api && npm run test:ci
- name: Run librechat-data-provider unit tests
- run: cd packages/data-provider && npm run test:ci
-
- - name: Run linters
- uses: wearerequired/lint-action@v2
- with:
- eslint: true
+ run: cd packages/data-provider && npm run test:ci
\ No newline at end of file
diff --git a/.github/workflows/eslint-ci.yml b/.github/workflows/eslint-ci.yml
new file mode 100644
index 0000000000..ea1a5f2416
--- /dev/null
+++ b/.github/workflows/eslint-ci.yml
@@ -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
\ No newline at end of file
diff --git a/.github/workflows/i18n-unused-keys.yml b/.github/workflows/i18n-unused-keys.yml
new file mode 100644
index 0000000000..79f95d3b27
--- /dev/null
+++ b/.github/workflows/i18n-unused-keys.yml
@@ -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 < 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<> $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<> $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<> $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 <
+
+
+
+
+
+
+
# ✨ 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
- Русский, 日本語, 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 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.
+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
@@ -174,3 +188,15 @@ For new features, components, or extensions, please open an issue and discuss be
+
+---
+
+## 🎉 Special Thanks
+
+We thank [Locize](https://locize.com) for their translation management tools that support multiple languages in LibreChat.
+
+
+
+
+
+
diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js
index 9334f1c28b..368e7d6e84 100644
--- a/api/app/clients/OpenAIClient.js
+++ b/api/app/clients/OpenAIClient.js
@@ -506,9 +506,8 @@ class OpenAIClient extends BaseClient {
if (promptPrefix && this.isOmni === true) {
const lastUserMessageIndex = payload.findLastIndex((message) => message.role === 'user');
if (lastUserMessageIndex !== -1) {
- payload[
- lastUserMessageIndex
- ].content = `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
+ payload[lastUserMessageIndex].content =
+ `${promptPrefix}\n${payload[lastUserMessageIndex].content}`;
}
}
@@ -1067,14 +1066,36 @@ ${convo}
});
}
- getStreamText() {
+ /**
+ *
+ * @param {string[]} [intermediateReply]
+ * @returns {string}
+ */
+ getStreamText(intermediateReply) {
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(/([\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 =
- this.streamHandler.reasoningTokens.length > 0
- ? `:::thinking\n${this.streamHandler.reasoningTokens.join('')}\n:::\n`
+ reasoningText.length > 0
+ ? `:::thinking\n${reasoningText.replace('', '').replace(' ', '').trim()}\n:::\n`
: '';
return `${reasoningTokens}${this.streamHandler.tokens.join('')}`;
@@ -1314,11 +1335,19 @@ ${convo}
streamPromise = new Promise((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
- .stream({
- ...modelOptions,
- stream: true,
- })
+ .stream(params)
.on('abort', () => {
/* Do nothing here */
})
@@ -1449,7 +1478,7 @@ ${convo}
this.options.context !== 'title' &&
message.content.startsWith('')
) {
- return message.content.replace('', ':::thinking').replace(' ', ':::');
+ return this.getStreamText();
}
return message.content;
@@ -1458,7 +1487,7 @@ ${convo}
err?.message?.includes('abort') ||
(err instanceof OpenAI.APIError && err?.message?.includes('abort'))
) {
- return intermediateReply.join('');
+ return this.getStreamText(intermediateReply);
}
if (
err?.message?.includes(
@@ -1473,14 +1502,18 @@ ${convo}
(err instanceof OpenAI.OpenAIError && err?.message?.includes('missing finish_reason'))
) {
logger.error('[OpenAIClient] Known OpenAI error:', err);
- if (intermediateReply.length > 0) {
- return intermediateReply.join('');
+ if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
+ return this.getStreamText();
+ } else if (intermediateReply.length > 0) {
+ return this.getStreamText(intermediateReply);
} else {
throw err;
}
} else if (err instanceof OpenAI.APIError) {
- if (intermediateReply.length > 0) {
- return intermediateReply.join('');
+ if (this.streamHandler && this.streamHandler.reasoningTokens.length) {
+ return this.getStreamText();
+ } else if (intermediateReply.length > 0) {
+ return this.getStreamText(intermediateReply);
} else {
throw err;
}
diff --git a/api/cache/getLogStores.js b/api/cache/getLogStores.js
index b7ff50150e..6592371f02 100644
--- a/api/cache/getLogStores.js
+++ b/api/cache/getLogStores.js
@@ -37,6 +37,10 @@ const messages = isRedisEnabled
? new Keyv({ store: keyvRedis, 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
? new Keyv({ store: keyvRedis, 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.AUDIO_RUNS]: audioRuns,
[CacheKeys.MESSAGES]: messages,
+ [CacheKeys.FLOWS]: flows,
};
/**
diff --git a/api/cache/keyvRedis.js b/api/cache/keyvRedis.js
index 9501045e4e..d544b50a11 100644
--- a/api/cache/keyvRedis.js
+++ b/api/cache/keyvRedis.js
@@ -1,6 +1,6 @@
const KeyvRedis = require('@keyv/redis');
-const { logger } = require('~/config');
const { isEnabled } = require('~/server/utils');
+const logger = require('~/config/winston');
const { REDIS_URI, USE_REDIS } = process.env;
diff --git a/api/config/index.js b/api/config/index.js
index c2b21cfc07..aaf8bb2764 100644
--- a/api/config/index.js
+++ b/api/config/index.js
@@ -1,9 +1,11 @@
const { EventSource } = require('eventsource');
+const { Time, CacheKeys } = require('librechat-data-provider');
const logger = require('./winston');
global.EventSource = EventSource;
let mcpManager = null;
+let flowManager = null;
/**
* @returns {Promise}
@@ -16,6 +18,21 @@ async function getMCPManager() {
return mcpManager;
}
+/**
+ * @param {(key: string) => Keyv} getLogStores
+ * @returns {Promise}
+ */
+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.
* @param {ServerResponse} res - The server response.
@@ -34,4 +51,5 @@ module.exports = {
logger,
sendEvent,
getMCPManager,
+ getFlowStateManager,
};
diff --git a/api/models/Token.js b/api/models/Token.js
index cdd156b6b4..210666ddd7 100644
--- a/api/models/Token.js
+++ b/api/models/Token.js
@@ -1,5 +1,6 @@
-const tokenSchema = require('./schema/tokenSchema');
const mongoose = require('mongoose');
+const { encryptV2 } = require('~/server/utils/crypto');
+const tokenSchema = require('./schema/tokenSchema');
const { logger } = require('~/config');
/**
@@ -7,6 +8,32 @@ const { logger } = require('~/config');
* @type {mongoose.Model}
*/
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.
@@ -29,8 +56,7 @@ async function createToken(tokenData) {
expiresAt,
};
- const newToken = new Token(newTokenData);
- return await newToken.save();
+ return await Token.create(newTokenData);
} catch (error) {
logger.debug('An error occurred while creating token:', error);
throw error;
@@ -42,7 +68,8 @@ async function createToken(tokenData) {
* @param {Object} query - The query to match against.
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @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} The matched Token document, or null if not found.
* @throws Will throw an error if the find operation fails.
*/
@@ -59,6 +86,9 @@ async function findToken(query) {
if (query.email) {
conditions.push({ email: query.email });
}
+ if (query.identifier) {
+ conditions.push({ identifier: query.identifier });
+ }
const token = await Token.findOne({
$and: conditions,
@@ -76,6 +106,8 @@ async function findToken(query) {
* @param {Object} query - The query to match against.
* @param {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @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.
* @returns {Promise} The updated Token document, or null if not found.
* @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 {mongoose.Types.ObjectId|String} query.userId - The ID of the user.
* @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} The result of the delete operation.
* @throws Will throw an error if the delete operation fails.
*/
async function deleteTokens(query) {
try {
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) {
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 = {
- createToken,
findToken,
+ createToken,
updateToken,
deleteTokens,
+ handleOAuthToken,
};
diff --git a/api/models/schema/agent.js b/api/models/schema/agent.js
index 2006859ab6..53e49e1cfd 100644
--- a/api/models/schema/agent.js
+++ b/api/models/schema/agent.js
@@ -35,6 +35,9 @@ const agentSchema = mongoose.Schema(
model_parameters: {
type: Object,
},
+ artifacts: {
+ type: String,
+ },
access_level: {
type: Number,
},
diff --git a/api/models/schema/tokenSchema.js b/api/models/schema/tokenSchema.js
index bb223ff450..1b45b2ff33 100644
--- a/api/models/schema/tokenSchema.js
+++ b/api/models/schema/tokenSchema.js
@@ -10,6 +10,10 @@ const tokenSchema = new Schema({
email: {
type: String,
},
+ type: String,
+ identifier: {
+ type: String,
+ },
token: {
type: String,
required: true,
@@ -23,6 +27,10 @@ const tokenSchema = new Schema({
type: Date,
required: true,
},
+ metadata: {
+ type: Map,
+ of: Schema.Types.Mixed,
+ },
});
tokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
diff --git a/api/package.json b/api/package.json
index 10264309c9..8d5a997e6e 100644
--- a/api/package.json
+++ b/api/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/backend",
- "version": "v0.7.6",
+ "version": "v0.7.7-rc1",
"description": "",
"scripts": {
"start": "echo 'please run this from the root directory'",
@@ -45,11 +45,10 @@
"@langchain/google-genai": "^0.1.7",
"@langchain/google-vertexai": "^0.1.8",
"@langchain/textsplitters": "^0.1.0",
- "@librechat/agents": "^2.0.2",
+ "@librechat/agents": "^2.0.4",
"@waylaidwanderer/fetch-event-source": "^3.0.1",
- "axios": "^1.7.7",
+ "axios": "1.7.8",
"bcryptjs": "^2.4.3",
- "cheerio": "^1.0.0-rc.12",
"cohere-ai": "^7.9.1",
"compression": "^1.7.4",
"connect-redis": "^7.1.0",
@@ -66,7 +65,6 @@
"firebase": "^11.0.2",
"googleapis": "^126.0.1",
"handlebars": "^4.7.7",
- "html": "^1.0.0",
"ioredis": "^5.3.2",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
@@ -91,7 +89,6 @@
"openid-client": "^5.4.2",
"passport": "^0.6.0",
"passport-apple": "^2.0.2",
- "passport-custom": "^1.1.1",
"passport-discord": "^0.1.4",
"passport-facebook": "^3.0.0",
"passport-github2": "^0.1.12",
@@ -99,7 +96,6 @@
"passport-jwt": "^4.0.1",
"passport-ldapauth": "^3.0.1",
"passport-local": "^1.0.0",
- "pino": "^8.12.1",
"sharp": "^0.32.6",
"tiktoken": "^1.0.15",
"traverse": "^0.6.7",
@@ -111,8 +107,8 @@
},
"devDependencies": {
"jest": "^29.7.0",
- "mongodb-memory-server": "^10.0.0",
- "nodemon": "^3.0.1",
- "supertest": "^6.3.3"
+ "mongodb-memory-server": "^10.1.3",
+ "nodemon": "^3.0.3",
+ "supertest": "^7.0.0"
}
}
diff --git a/api/server/controllers/AskController.js b/api/server/controllers/AskController.js
index b952ab0042..55fe2fa717 100644
--- a/api/server/controllers/AskController.js
+++ b/api/server/controllers/AskController.js
@@ -155,6 +155,8 @@ const AskController = async (req, res, next, initializeClient, addTitle) => {
sender,
messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId,
+ }).catch((err) => {
+ logger.error('[AskController] Error in `handleAbortError`', err);
});
}
};
diff --git a/api/server/controllers/EditController.js b/api/server/controllers/EditController.js
index ec618eabcf..2a2f8c28de 100644
--- a/api/server/controllers/EditController.js
+++ b/api/server/controllers/EditController.js
@@ -140,6 +140,8 @@ const EditController = async (req, res, next, initializeClient) => {
sender,
messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId,
+ }).catch((err) => {
+ logger.error('[EditController] Error in `handleAbortError`', err);
});
}
};
diff --git a/api/server/controllers/agents/request.js b/api/server/controllers/agents/request.js
index 8ceadd977d..288ae8f37f 100644
--- a/api/server/controllers/agents/request.js
+++ b/api/server/controllers/agents/request.js
@@ -143,6 +143,8 @@ const AgentController = async (req, res, next, initializeClient, addTitle) => {
sender,
messageId: responseMessageId,
parentMessageId: userMessageId ?? parentMessageId,
+ }).catch((err) => {
+ logger.error('[api/server/controllers/agents/request] Error in `handleAbortError`', err);
});
}
};
diff --git a/api/server/index.js b/api/server/index.js
index 7278273600..30d36d9a9f 100644
--- a/api/server/index.js
+++ b/api/server/index.js
@@ -84,6 +84,7 @@ const startServer = async () => {
app.use('/oauth', routes.oauth);
/* API Endpoints */
app.use('/api/auth', routes.auth);
+ app.use('/api/actions', routes.actions);
app.use('/api/keys', routes.keys);
app.use('/api/user', routes.user);
app.use('/api/search', routes.search);
diff --git a/api/server/routes/actions.js b/api/server/routes/actions.js
new file mode 100644
index 0000000000..454f4be6c7
--- /dev/null
+++ b/api/server/routes/actions.js
@@ -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(`
+
+
+
+ Authentication Successful
+
+
+
+
+
+
+
Authentication Successful
+
+ Your authentication was successful. This window will close in
+ 3 seconds.
+
+
+
+
+
+ `);
+ } 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;
diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js
index 5d5456c29f..786f44dd8e 100644
--- a/api/server/routes/agents/actions.js
+++ b/api/server/routes/agents/actions.js
@@ -1,6 +1,6 @@
const express = require('express');
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 { updateAction, getActions, deleteAction } = require('~/models/Action');
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' });
}
- let metadata = await encryptMetadata(_metadata);
+ let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
if (!isDomainAllowed) {
return res.status(400).json({ message: 'Domain not allowed' });
@@ -117,10 +117,7 @@ router.post('/:agent_id', async (req, res) => {
}
/** @type {[Action]} */
- const updatedAction = await updateAction(
- { action_id },
- actionUpdateData,
- );
+ const updatedAction = await updateAction({ action_id }, actionUpdateData);
const sensitiveFields = ['api_key', 'oauth_client_id', 'oauth_client_secret'];
for (let field of sensitiveFields) {
diff --git a/api/server/routes/assistants/actions.js b/api/server/routes/assistants/actions.js
index eb65749086..9f4db5d6b8 100644
--- a/api/server/routes/assistants/actions.js
+++ b/api/server/routes/assistants/actions.js
@@ -1,6 +1,6 @@
const express = require('express');
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 { getOpenAIClient } = require('~/server/controllers/assistants/helpers');
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' });
}
- let metadata = await encryptMetadata(_metadata);
+ let metadata = await encryptMetadata(removeNullishValues(_metadata, true));
const isDomainAllowed = await isActionDomainAllowed(metadata.domain);
if (!isDomainAllowed) {
return res.status(400).json({ message: 'Domain not allowed' });
diff --git a/api/server/routes/index.js b/api/server/routes/index.js
index 4aba91e954..4b34029c7b 100644
--- a/api/server/routes/index.js
+++ b/api/server/routes/index.js
@@ -9,6 +9,7 @@ const prompts = require('./prompts');
const balance = require('./balance');
const plugins = require('./plugins');
const bedrock = require('./bedrock');
+const actions = require('./actions');
const search = require('./search');
const models = require('./models');
const convos = require('./convos');
@@ -45,6 +46,7 @@ module.exports = {
config,
models,
plugins,
+ actions,
presets,
balance,
messages,
diff --git a/api/server/services/ActionService.js b/api/server/services/ActionService.js
index 712157bf29..660e7aeb0d 100644
--- a/api/server/services/ActionService.js
+++ b/api/server/services/ActionService.js
@@ -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 {
+ Time,
CacheKeys,
+ StepTypes,
Constants,
AuthTypeEnum,
actionDelimiter,
isImageVisionTool,
actionDomainSeparator,
} = require('librechat-data-provider');
-const { tool } = require('@langchain/core/tools');
+const { refreshAccessToken } = require('~/server/services/TokenService');
const { isActionDomainAllowed } = require('~/server/services/domains');
+const { logger, getFlowStateManager, sendEvent } = require('~/config');
const { encryptV2, decryptV2 } = require('~/server/utils/crypto');
const { getActions, deleteActions } = require('~/models/Action');
const { deleteAssistant } = require('~/models/Assistant');
+const { findToken } = require('~/models/Token');
const { logAxiosError } = require('~/utils');
const { getLogStores } = require('~/cache');
-const { logger } = require('~/config');
+const JWT_SECRET = process.env.JWT_SECRET;
const toolNameRegex = /^[a-zA-Z0-9_-]+$/;
const replaceSeparatorRegex = new RegExp(actionDomainSeparator, 'g');
@@ -115,6 +123,8 @@ async function loadActionSets(searchParams) {
* Creates a general tool for an entire action set.
*
* @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 {ActionRequest} params.requestBuilder - The ActionRequest builder class to execute the API call.
* @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
* @returns { Promise unknown}> } An object with `_call` method to execute the tool input.
*/
-async function createActionTool({ action, requestBuilder, zodSchema, name, description }) {
- action.metadata = await decryptMetadata(action.metadata);
+async function createActionTool({
+ req,
+ res,
+ action,
+ requestBuilder,
+ zodSchema,
+ name,
+ description,
+}) {
const isDomainAllowed = await isActionDomainAllowed(action.metadata.domain);
if (!isDomainAllowed) {
return null;
}
- /** @type {(toolInput: Object | string) => Promise} */
- const _call = async (toolInput) => {
- try {
- const executor = requestBuilder.createExecutor();
+ const encrypted = {
+ oauth_client_id: action.metadata.oauth_client_id,
+ oauth_client_secret: action.metadata.oauth_client_secret,
+ };
+ action.metadata = await decryptMetadata(action.metadata);
- // Chain the operations
+ /** @type {(toolInput: Object | string, config: GraphRunnableConfig) => Promise} */
+ 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);
- if (action.metadata.auth && action.metadata.auth.type !== AuthTypeEnum.None) {
- await preparedExecutor.setAuth(action.metadata);
+ if (metadata.auth && metadata.auth.type !== AuthTypeEnum.None) {
+ 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') {
- return JSON.stringify(res.data);
+ if (typeof response.data === 'object') {
+ return JSON.stringify(response.data);
}
- return res.data;
+ return response.data;
} catch (error) {
const logMessage = `API call to ${action.metadata.domain} failed`;
logAxiosError({ message: logMessage, error });
+ throw error;
}
};
diff --git a/api/server/services/Endpoints/agents/initialize.js b/api/server/services/Endpoints/agents/initialize.js
index d1b6916854..3e03a45125 100644
--- a/api/server/services/Endpoints/agents/initialize.js
+++ b/api/server/services/Endpoints/agents/initialize.js
@@ -13,6 +13,7 @@ const getBedrockOptions = require('~/server/services/Endpoints/bedrock/options')
const initOpenAI = require('~/server/services/Endpoints/openAI/initialize');
const initCustom = require('~/server/services/Endpoints/custom/initialize');
const initGoogle = require('~/server/services/Endpoints/google/initialize');
+const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getCustomEndpointConfig } = require('~/server/services/Config');
const { loadAgentTools } = require('~/server/services/ToolService');
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}
+ */
const initializeAgentOptions = async ({
req,
res,
@@ -82,6 +93,7 @@ const initializeAgentOptions = async ({
}) => {
const { tools, toolContextMap } = await loadAgentTools({
req,
+ res,
agent,
tool_resources,
});
@@ -131,6 +143,13 @@ const initializeAgentOptions = async ({
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 =
agent.provider === EModelEndpoint.azureOpenAI ? agent.model : agent.model_parameters.model;
diff --git a/api/server/services/Endpoints/assistants/build.js b/api/server/services/Endpoints/assistants/build.js
index b5eb03a94f..544567dd01 100644
--- a/api/server/services/Endpoints/assistants/build.js
+++ b/api/server/services/Endpoints/assistants/build.js
@@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant');
const buildOptions = async (endpoint, parsedBody) => {
- // eslint-disable-next-line no-unused-vars
+
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody;
const endpointOption = removeNullishValues({
diff --git a/api/server/services/Endpoints/azureAssistants/build.js b/api/server/services/Endpoints/azureAssistants/build.js
index 3785014ca1..54a32e4d3c 100644
--- a/api/server/services/Endpoints/azureAssistants/build.js
+++ b/api/server/services/Endpoints/azureAssistants/build.js
@@ -3,7 +3,7 @@ const generateArtifactsPrompt = require('~/app/clients/prompts/artifacts');
const { getAssistant } = require('~/models/Assistant');
const buildOptions = async (endpoint, parsedBody) => {
- // eslint-disable-next-line no-unused-vars
+
const { promptPrefix, assistant_id, iconURL, greeting, spec, artifacts, ...modelOptions } =
parsedBody;
const endpointOption = removeNullishValues({
diff --git a/api/server/services/TokenService.js b/api/server/services/TokenService.js
new file mode 100644
index 0000000000..ec0f990a47
--- /dev/null
+++ b/api/server/services/TokenService.js
@@ -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}
+ */
+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,
+};
diff --git a/api/server/services/ToolService.js b/api/server/services/ToolService.js
index cf88c0b199..f3e4efb6e3 100644
--- a/api/server/services/ToolService.js
+++ b/api/server/services/ToolService.js
@@ -409,11 +409,12 @@ async function processRequiredActions(client, requiredActions) {
* Processes the runtime tool calls and returns the tool classes.
* @param {Object} params - Run params containing user and request information.
* @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 {string | undefined} [params.openAIApiKey] - The OpenAI API key.
* @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) {
return {};
}
@@ -546,6 +547,8 @@ async function loadAgentTools({ req, agent, tool_resources, openAIApiKey }) {
if (requestBuilder) {
const tool = await createActionTool({
+ req,
+ res,
action: actionSet,
requestBuilder,
zodSchema,
diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js
index e46584c805..8c681d8f4e 100644
--- a/api/server/utils/handleText.js
+++ b/api/server/utils/handleText.js
@@ -200,6 +200,7 @@ function generateConfig(key, baseURL, endpoint) {
config.capabilities = [
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
+ AgentCapabilities.artifacts,
AgentCapabilities.actions,
AgentCapabilities.tools,
];
diff --git a/api/typedefs.js b/api/typedefs.js
index c88e57719a..bd97bd93fa 100644
--- a/api/typedefs.js
+++ b/api/typedefs.js
@@ -98,6 +98,12 @@
* @memberof typedefs
*/
+/**
+ * @exports LangChainToolCall
+ * @typedef {import('@langchain/core/messages/tool').ToolCall} LangChainToolCall
+ * @memberof typedefs
+ */
+
/**
* @exports GraphRunnableConfig
* @typedef {import('@langchain/core/runnables').RunnableConfig<{
@@ -109,7 +115,9 @@
* agent_index: number;
* last_agent_index: number;
* hide_sequential_outputs: boolean;
- * }>} GraphRunnableConfig
+ * }> & {
+ * toolCall?: LangChainToolCall & { stepId?: string };
+ * }} GraphRunnableConfig
* @memberof typedefs
*/
@@ -383,6 +391,12 @@
* @memberof typedefs
*/
+/**
+ * @exports AgentToolCallDelta
+ * @typedef {import('librechat-data-provider').Agents.ToolCallDelta} AgentToolCallDelta
+ * @memberof typedefs
+ */
+
/** Prompts */
/**
* @exports TPrompt
@@ -947,12 +961,24 @@
* @memberof typedefs
*/
+/**
+ * @exports Keyv
+ * @typedef {import('keyv')} Keyv
+ * @memberof typedefs
+ */
+
/**
* @exports MCPManager
* @typedef {import('librechat-mcp').MCPManager} MCPManager
* @memberof typedefs
*/
+/**
+ * @exports FlowStateManager
+ * @typedef {import('librechat-mcp').FlowStateManager} FlowStateManager
+ * @memberof typedefs
+ */
+
/**
* @exports LCAvailableTools
* @typedef {import('librechat-mcp').LCAvailableTools} LCAvailableTools
diff --git a/client/package.json b/client/package.json
index 057b150459..993cf30071 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "@librechat/frontend",
- "version": "v0.7.6",
+ "version": "v0.7.7-rc1",
"description": "",
"type": "module",
"scripts": {
@@ -50,11 +50,8 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
- "@radix-ui/react-tooltip": "^1.0.6",
"@tanstack/react-query": "^4.28.0",
"@tanstack/react-table": "^8.11.7",
- "@zattoo/use-double-click": "1.2.0",
- "axios": "^1.7.7",
"class-variance-authority": "^0.6.0",
"clsx": "^1.2.1",
"copy-to-clipboard": "^3.3.3",
@@ -65,7 +62,7 @@
"filenamify": "^6.0.0",
"framer-motion": "^11.5.4",
"html-to-image": "^1.11.11",
- "image-blob-reduce": "^4.1.0",
+ "i18next": "^24.2.2",
"js-cookie": "^3.0.5",
"librechat-data-provider": "*",
"lodash": "^4.17.21",
@@ -79,10 +76,10 @@
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
- "react-error-boundary": "^5.0.0",
"react-flip-toolkit": "^7.1.0",
"react-gtm-module": "^2.0.11",
"react-hook-form": "^7.43.9",
+ "react-i18next": "^15.4.0",
"react-lazy-load-image-component": "^1.6.0",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.1",
@@ -103,7 +100,6 @@
"tailwind-merge": "^1.9.1",
"tailwindcss-animate": "^1.0.5",
"tailwindcss-radix": "^2.8.0",
- "url": "^0.11.0",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -116,31 +112,31 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^14.4.3",
- "@types/jest": "^29.5.2",
+ "@types/jest": "^29.5.14",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.3.0",
"@types/react": "^18.2.11",
"@types/react-dom": "^18.2.4",
"@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-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",
- "eslint-plugin-jest": "^27.2.1",
+ "eslint-plugin-jest": "^28.11.0",
"identity-obj-proxy": "^3.0.0",
- "jest": "^29.5.0",
- "jest-canvas-mock": "^2.5.1",
- "jest-environment-jsdom": "^29.5.0",
+ "jest": "^29.7.0",
+ "jest-canvas-mock": "^2.5.2",
+ "jest-environment-jsdom": "^29.7.0",
"jest-file-loader": "^1.0.3",
"jest-junit": "^16.0.0",
"postcss": "^8.4.31",
"postcss-loader": "^7.1.0",
"postcss-preset-env": "^8.2.0",
"tailwindcss": "^3.4.1",
- "ts-jest": "^29.1.0",
- "typescript": "^5.0.4",
- "vite": "^5.4.14",
+ "ts-jest": "^29.2.5",
+ "typescript": "^5.3.3",
+ "vite": "^6.1.0",
"vite-plugin-node-polyfills": "^0.17.0",
"vite-plugin-pwa": "^0.21.1"
}
diff --git a/client/src/common/agents-types.ts b/client/src/common/agents-types.ts
index 7f64f07882..a9c24106bc 100644
--- a/client/src/common/agents-types.ts
+++ b/client/src/common/agents-types.ts
@@ -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 { OptionWithIcon, ExtendedFile } from './types';
@@ -9,8 +9,8 @@ export type TAgentOption = OptionWithIcon &
};
export type TAgentCapabilities = {
- [AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.file_search]: boolean;
+ [AgentCapabilities.execute_code]: boolean;
[AgentCapabilities.end_after_tools]?: boolean;
[AgentCapabilities.hide_sequential_outputs]?: boolean;
};
@@ -26,4 +26,5 @@ export type AgentForm = {
tools?: string[];
provider?: AgentProvider | OptionWithIcon;
agent_ids?: string[];
+ [AgentCapabilities.artifacts]?: ArtifactModes | string;
} & TAgentCapabilities;
diff --git a/client/src/common/types.ts b/client/src/common/types.ts
index b296dcc090..3d61eccb1c 100644
--- a/client/src/common/types.ts
+++ b/client/src/common/types.ts
@@ -6,6 +6,7 @@ import type { SetterOrUpdater } from 'recoil';
import type * as t from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type { LucideIcon } from 'lucide-react';
+import type { TranslationKeys } from '~/hooks';
export type CodeBarProps = {
lang: string;
@@ -66,7 +67,10 @@ export type GenericSetter = (value: T | ((currentValue: T) => T)) => void;
export type LastSelectedModels = Record;
-export type LocalizeFunction = (phraseKey: string, ...values: string[]) => string;
+export type LocalizeFunction = (
+ phraseKey: TranslationKeys,
+ options?: Record,
+) => string;
export type ChatFormValues = { text: string };
@@ -85,6 +89,7 @@ export type IconMapProps = {
iconURL?: string;
context?: 'landing' | 'menu-item' | 'nav' | 'message';
endpoint?: string | null;
+ endpointType?: string;
assistantName?: string;
agentName?: string;
avatar?: string;
diff --git a/client/src/components/Auth/AuthLayout.tsx b/client/src/components/Auth/AuthLayout.tsx
index 6df73d2cf9..a7e890517a 100644
--- a/client/src/components/Auth/AuthLayout.tsx
+++ b/client/src/components/Auth/AuthLayout.tsx
@@ -1,4 +1,4 @@
-import { useLocalize } from '~/hooks';
+import { TranslationKeys, useLocalize } from '~/hooks';
import { BlinkAnimation } from './BlinkAnimation';
import { TStartupConfig } from 'librechat-data-provider';
import SocialLoginRender from './SocialLoginRender';
@@ -33,7 +33,7 @@ function AuthLayout({
startupConfig: TStartupConfig | null | undefined;
startupConfigError: unknown | null | undefined;
pathname: string;
- error: string | null;
+ error: TranslationKeys | null;
}) {
const localize = useLocalize();
@@ -65,7 +65,7 @@ function AuthLayout({
diff --git a/client/src/components/Auth/Registration.tsx b/client/src/components/Auth/Registration.tsx
index 4ae4e03b79..047808ffea 100644
--- a/client/src/components/Auth/Registration.tsx
+++ b/client/src/components/Auth/Registration.tsx
@@ -6,7 +6,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from './ErrorMessage';
import { Spinner } from '~/components/svg';
-import { useLocalize } from '~/hooks';
+import { useLocalize, TranslationKeys } from '~/hooks';
const Registration: React.FC = () => {
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) => (
{
: 'com_auth_registration_success_insecure',
) +
' ' +
- localize('com_auth_email_verification_redirecting', countdown.toString())}
+ localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
)}
{!startupConfigError && !isFetching && (
diff --git a/client/src/components/Auth/VerifyEmail.tsx b/client/src/components/Auth/VerifyEmail.tsx
index 0af2f41959..9c143224b2 100644
--- a/client/src/components/Auth/VerifyEmail.tsx
+++ b/client/src/components/Auth/VerifyEmail.tsx
@@ -84,7 +84,7 @@ function RequestPasswordReset() {
{countdown > 0 && (
- {localize('com_auth_email_verification_redirecting', countdown.toString())}
+ {localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
)}
{showResendLink && countdown === 0 && (
diff --git a/client/src/components/Chat/ChatView.tsx b/client/src/components/Chat/ChatView.tsx
index 0ee64ef62b..dbf39ee845 100644
--- a/client/src/components/Chat/ChatView.tsx
+++ b/client/src/components/Chat/ChatView.tsx
@@ -28,7 +28,7 @@ function ChatView({ index = 0 }: { index?: number }) {
select: useCallback(
(data: TMessage[]) => {
const dataTree = buildTree({ messages: data, fileMap });
- return dataTree?.length === 0 ? null : dataTree ?? null;
+ return dataTree?.length === 0 ? null : (dataTree ?? null);
},
[fileMap],
),
@@ -62,7 +62,7 @@ function ChatView({ index = 0 }: { index?: number }) {
-
+
{content}
diff --git a/client/src/components/Chat/Input/Files/Table/DataTable.tsx b/client/src/components/Chat/Input/Files/Table/DataTable.tsx
index 6b9800b472..31e7296af3 100644
--- a/client/src/components/Chat/Input/Files/Table/DataTable.tsx
+++ b/client/src/components/Chat/Input/Files/Table/DataTable.tsx
@@ -220,8 +220,10 @@ export default function DataTable
({ columns, data }: DataTablePro
{localize(
'com_files_number_selected',
- `${table.getFilteredSelectedRowModel().rows.length}`,
- `${table.getFilteredRowModel().rows.length}`,
+ {
+ 0: `${table.getFilteredSelectedRowModel().rows.length}`,
+ 1: `${table.getFilteredRowModel().rows.length}`,
+ },
)}
diff --git a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx
index 5bccd2c6a0..1b9a0cbe42 100644
--- a/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx
+++ b/client/src/components/Chat/Input/Files/Table/SortFilterHeader.tsx
@@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '~/components/ui/DropdownMenu';
import { Button } from '~/components/ui/Button';
-import useLocalize from '~/hooks/useLocalize';
+import { useLocalize, TranslationKeys } from '~/hooks';
import { cn } from '~/utils';
interface SortFilterHeaderProps extends React.HTMLAttributes {
@@ -78,9 +78,12 @@ export function SortFilterHeader({
{filters &&
Object.entries(filters).map(([key, values]) =>
- values.map((value: string | number) => {
- const localizedValue = localize(valueMap?.[value] ?? '');
- const filterValue = localizedValue.length ? localizedValue : valueMap?.[value];
+ values.map((value?: string | number) => {
+ const translationKey = valueMap?.[value ?? ''];
+ const filterValue =
+ translationKey != null && translationKey.length
+ ? localize(translationKey as TranslationKeys)
+ : String(value);
if (!filterValue) {
return null;
}
diff --git a/client/src/components/Chat/Input/PopoverButtons.tsx b/client/src/components/Chat/Input/PopoverButtons.tsx
index 3b52799445..2143a870e6 100644
--- a/client/src/components/Chat/Input/PopoverButtons.tsx
+++ b/client/src/components/Chat/Input/PopoverButtons.tsx
@@ -44,8 +44,8 @@ export default function PopoverButtons({
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
const model = overrideModel ?? _model;
- const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false;
- const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false;
+ const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
+ const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
const { showExamples } = optionSettings;
diff --git a/client/src/components/Chat/Input/TemporaryChat.tsx b/client/src/components/Chat/Input/TemporaryChat.tsx
index 4c9ad898c1..c1f571ba47 100644
--- a/client/src/components/Chat/Input/TemporaryChat.tsx
+++ b/client/src/components/Chat/Input/TemporaryChat.tsx
@@ -18,7 +18,7 @@ export const TemporaryChat = ({ isTemporaryChat, setIsTemporaryChat }: Temporary
-
+
diff --git a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx
index d062f4276a..f0918240ba 100644
--- a/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx
+++ b/client/src/components/Chat/Messages/Content/CodeAnalyze.tsx
@@ -7,6 +7,9 @@ import FinishedIcon from './FinishedIcon';
import MarkdownLite from './MarkdownLite';
import store from '~/store';
+const radius = 56.08695652173913;
+const circumference = 2 * Math.PI * radius;
+
export default function CodeAnalyze({
initialProgress = 0.1,
code,
@@ -22,9 +25,6 @@ export default function CodeAnalyze({
const progress = useProgress(initialProgress);
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
-
- const radius = 56.08695652173913;
- const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
const logs = outputs.reduce((acc, output) => {
@@ -53,9 +53,10 @@ export default function CodeAnalyze({
setShowCode((prev) => !prev)}
- inProgressText="Analyzing"
- finishedText="Finished analyzing"
+ inProgressText={localize('com_ui_analyzing')}
+ finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length}
+ isExpanded={showCode}
/>
{showCode && (
diff --git a/client/src/components/Chat/Messages/Content/ContentParts.tsx b/client/src/components/Chat/Messages/Content/ContentParts.tsx
index ce77e3bfdb..b997060c61 100644
--- a/client/src/components/Chat/Messages/Content/ContentParts.tsx
+++ b/client/src/components/Chat/Messages/Content/ContentParts.tsx
@@ -50,11 +50,24 @@ const ContentParts = memo(
[attachments, messageAttachmentsMap, messageId],
);
- const hasReasoningParts = useMemo(
- () => content?.some((part) => part?.type === ContentTypes.THINK && part.think) ?? false,
- [content],
- );
+ const hasReasoningParts = useMemo(() => {
+ const hasThinkPart = content?.some((part) => part?.type === ContentTypes.THINK) ?? false;
+ 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) {
return null;
}
diff --git a/client/src/components/Chat/Messages/Content/Files.tsx b/client/src/components/Chat/Messages/Content/Files.tsx
index 09801d92c1..8997d5e822 100644
--- a/client/src/components/Chat/Messages/Content/Files.tsx
+++ b/client/src/components/Chat/Messages/Content/Files.tsx
@@ -9,14 +9,14 @@ const Files = ({ message }: { message?: TMessage }) => {
}, [message?.files]);
const otherFiles = useMemo(() => {
- return message?.files?.filter((file) => !file.type?.startsWith('image/')) || [];
+ return message?.files?.filter((file) => !(file.type?.startsWith('image/') === true)) || [];
}, [message?.files]);
return (
<>
{otherFiles.length > 0 &&
otherFiles.map((file) => )}
- {imageFiles &&
+ {imageFiles.length > 0 &&
imageFiles.map((file) => (
;
}) => {
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' };
}
const aspectRatio = originalWidth / originalHeight;
@@ -35,8 +35,8 @@ const Image = ({
height: number;
width: number;
placeholderDimensions?: {
- height: string;
- width: string;
+ height?: string;
+ width?: string;
};
}) => {
const [isLoaded, setIsLoaded] = useState(false);
@@ -47,8 +47,8 @@ const Image = ({
const { width: scaledWidth, height: scaledHeight } = useMemo(
() =>
scaleImage({
- originalWidth: Number(placeholderDimensions?.width?.split('px')[0]) ?? width,
- originalHeight: Number(placeholderDimensions?.height?.split('px')[0]) ?? height,
+ originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width),
+ originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height),
containerRef,
}),
[placeholderDimensions, height, width],
diff --git a/client/src/components/Chat/Messages/Content/InProgressCall.tsx b/client/src/components/Chat/Messages/Content/InProgressCall.tsx
index c2e9fc35f8..019a3e2d0a 100644
--- a/client/src/components/Chat/Messages/Content/InProgressCall.tsx
+++ b/client/src/components/Chat/Messages/Content/InProgressCall.tsx
@@ -11,7 +11,7 @@ export default function InProgressCall({
progress: number;
children: React.ReactNode;
}) {
- if ((!isSubmitting && progress < 1) || error) {
+ if ((!isSubmitting && progress < 1) || error === true) {
return ;
}
diff --git a/client/src/components/Chat/Messages/Content/MessageContent.tsx b/client/src/components/Chat/Messages/Content/MessageContent.tsx
index 21bbe231e6..1547a01d80 100644
--- a/client/src/components/Chat/Messages/Content/MessageContent.tsx
+++ b/client/src/components/Chat/Messages/Content/MessageContent.tsx
@@ -159,7 +159,9 @@ const MessageContent = ({
return (
<>
- {thinkingContent && {thinkingContent} }
+ {thinkingContent.length > 0 && (
+ {thinkingContent}
+ )}
);
} else if (toolCall.type === ToolCallTypes.CODE_INTERPRETER) {
diff --git a/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx
index 49a15fc71c..93fdab434e 100644
--- a/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx
+++ b/client/src/components/Chat/Messages/Content/Parts/ExecuteCode.tsx
@@ -4,10 +4,10 @@ import type { TAttachment } from 'librechat-data-provider';
import ProgressText from '~/components/Chat/Messages/Content/ProgressText';
import FinishedIcon from '~/components/Chat/Messages/Content/FinishedIcon';
import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite';
+import { useProgress, useLocalize } from '~/hooks';
import { CodeInProgress } from './CodeProgress';
import Attachment from './Attachment';
import LogContent from './LogContent';
-import { useProgress } from '~/hooks';
import store from '~/store';
interface ParsedArgs {
@@ -36,6 +36,9 @@ export function useParseArgs(args: string): ParsedArgs {
}, [args]);
}
+const radius = 56.08695652173913;
+const circumference = 2 * Math.PI * radius;
+
export default function ExecuteCode({
initialProgress = 0.1,
args,
@@ -49,14 +52,12 @@ export default function ExecuteCode({
isSubmitting: boolean;
attachments?: TAttachment[];
}) {
+ const localize = useLocalize();
const showAnalysisCode = useRecoilValue(store.showCode);
const [showCode, setShowCode] = useState(showAnalysisCode);
const { lang, code } = useParseArgs(args);
const progress = useProgress(initialProgress);
-
- const radius = 56.08695652173913;
- const circumference = 2 * Math.PI * radius;
const offset = circumference - progress * circumference;
return (
@@ -78,9 +79,10 @@ export default function ExecuteCode({
setShowCode((prev) => !prev)}
- inProgressText="Analyzing"
- finishedText="Finished analyzing"
+ inProgressText={localize('com_ui_analyzing')}
+ finishedText={localize('com_ui_analyzing_finished')}
hasInput={!!code.length}
+ isExpanded={showCode}
/>
{showCode && (
@@ -105,9 +107,7 @@ export default function ExecuteCode({
)}
)}
- {attachments?.map((attachment, index) => (
-
- ))}
+ {attachments?.map((attachment, index) => )}
>
);
}
diff --git a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx
index 9a6e3fc99d..0d53fb50ee 100644
--- a/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx
+++ b/client/src/components/Chat/Messages/Content/Parts/LogContent.tsx
@@ -64,7 +64,7 @@ const LogContent: React.FC = ({ output = '', renderImages, atta
}
// 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')}`;
return (
diff --git a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx
index 447bf2f2c4..fd84b03618 100644
--- a/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx
+++ b/client/src/components/Chat/Messages/Content/Parts/Reasoning.tsx
@@ -11,9 +11,16 @@ type ReasoningProps = {
const Reasoning = memo(({ reasoning }: ReasoningProps) => {
const { isExpanded, nextType } = useMessageContext();
const reasoningText = useMemo(() => {
- return reasoning.replace(/^\s*/, '').replace(/\s*<\/think>$/, '');
+ return reasoning
+ .replace(/^\s*/, '')
+ .replace(/\s*<\/think>$/, '')
+ .trim();
}, [reasoning]);
+ if (!reasoningText) {
+ return null;
+ }
+
return (
void;
+ onClick?: () => void;
inProgressText: string;
finishedText: string;
+ authText?: string;
hasInput?: boolean;
popover?: boolean;
+ isExpanded?: boolean;
}) {
+ const text = progress < 1 ? (authText ?? inProgressText) : finishedText;
return (
- {progress < 1 ? inProgressText : finishedText}
-
+ {text}
+
;
output?: string | null;
attachments?: TAttachment[];
+ auth?: string;
+ expires_at?: number;
}) {
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(() => {
if (typeof name !== 'string') {
return { function_name: '', domain: null, isMCPToolCall: false };
@@ -83,8 +85,37 @@ export default function ToolCall({
[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 = () => {
- if (progress < 1) {
+ if (progress < 1 && authDomain.length > 0) {
+ return (
+
+ );
+ } else if (progress < 1) {
return (
: ;
+ return cancelled ? : ;
};
const getFinishedText = () => {
+ if (cancelled) {
+ return localize('com_ui_error');
+ }
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) {
- 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 (
-
-
{renderIcon()}
-
({})}
- inProgressText={localize('com_assistants_running_action')}
- finishedText={getFinishedText()}
- hasInput={hasInfo}
- popover={true}
- />
- {hasInfo && (
-
+
+
{renderIcon()}
+
0 ? localize('com_ui_requires_auth') : undefined
+ }
+ finishedText={getFinishedText()}
+ hasInput={hasInfo}
+ popover={true}
/>
+ {hasInfo && (
+ 0 && !cancelled && progress < 1}
+ />
+ )}
+
+ {auth != null && auth && progress < 1 && !cancelled && (
+
+
+
+
+ {localize('com_assistants_allow_sites_you_trust')}
+
+
)}
- {attachments?.map((attachment, index) => (
-
- ))}
+ {attachments?.map((attachment, index) => )}
);
}
diff --git a/client/src/components/Chat/Messages/Content/ToolPopover.tsx b/client/src/components/Chat/Messages/Content/ToolPopover.tsx
index 97e5aefa41..198f64b3e0 100644
--- a/client/src/components/Chat/Messages/Content/ToolPopover.tsx
+++ b/client/src/components/Chat/Messages/Content/ToolPopover.tsx
@@ -4,13 +4,15 @@ import useLocalize from '~/hooks/useLocalize';
export default function ToolPopover({
input,
output,
- function_name,
domain,
+ function_name,
+ pendingAuth,
}: {
input: string;
function_name: string;
output?: string | null;
domain?: string;
+ pendingAuth?: boolean;
}) {
const localize = useLocalize();
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 (
-
- {domain != null && domain
- ? localize('com_assistants_domain_info', domain)
- : localize('com_assistants_function_use', function_name)}
-
+
{title}
-
{output != null && output && (
<>
-
+
{localize('com_ui_result')}
-
diff --git a/client/src/components/Chat/Presentation.tsx b/client/src/components/Chat/Presentation.tsx
index c92f604ee1..9228fd880c 100644
--- a/client/src/components/Chat/Presentation.tsx
+++ b/client/src/components/Chat/Presentation.tsx
@@ -1,37 +1,19 @@
import { useRecoilValue } from 'recoil';
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 { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider';
+import { useDeleteFilesMutation } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import Artifacts from '~/components/Artifacts/Artifacts';
-import { SidePanel } from '~/components/SidePanel';
+import { SidePanelGroup } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks';
import { EditorProvider } from '~/Providers';
import store from '~/store';
-const defaultInterface = getConfigDefaults().interface;
-
-export default function Presentation({
- children,
- useSidePanel = false,
- panel,
-}: {
- children: React.ReactNode;
- panel?: React.ReactNode;
- useSidePanel?: boolean;
-}) {
- const { data: startupConfig } = useGetStartupConfig();
+export default function Presentation({ children }: { children: React.ReactNode }) {
const artifacts = useRecoilValue(store.artifactsState);
- const codeArtifacts = useRecoilValue(store.codeArtifacts);
- const hideSidePanel = useRecoilValue(store.hideSidePanel);
const artifactsVisible = useRecoilValue(store.artifactsVisible);
- const interfaceConfig = useMemo(
- () => startupConfig?.interface ?? defaultInterface,
- [startupConfig],
- );
-
const setFilesToDelete = useSetFilesToDelete();
const { mutateAsync } = useDeleteFilesMutation({
@@ -83,35 +65,24 @@ export default function Presentation({
);
- if (useSidePanel && !hideSidePanel && interfaceConfig.sidePanel === true) {
- return (
-
- 0 ? (
-
-
-
- ) : null
- }
- >
-
- {children}
-
-
-
- );
- }
-
return (
- {layout()}
- {panel != null && panel}
+ 0 ? (
+
+
+
+ ) : null
+ }
+ >
+
+ {children}
+
+
);
}
diff --git a/client/src/components/Chat/PromptCard.tsx b/client/src/components/Chat/PromptCard.tsx
index e348b24d24..4a37fd998c 100644
--- a/client/src/components/Chat/PromptCard.tsx
+++ b/client/src/components/Chat/PromptCard.tsx
@@ -1,14 +1,14 @@
import { TPromptGroup } from 'librechat-data-provider';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
-export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
+export default function PromptCard({ promptGroup }: { promptGroup?: TPromptGroup }) {
return (
-
+
- {promptGroup?.oneliner || promptGroup?.productionPrompt?.prompt}
+ {(promptGroup?.oneliner ?? '') || promptGroup?.productionPrompt?.prompt}
);
diff --git a/client/src/components/Conversations/Conversations.tsx b/client/src/components/Conversations/Conversations.tsx
index 941dd848d2..67d79c704c 100644
--- a/client/src/components/Conversations/Conversations.tsx
+++ b/client/src/components/Conversations/Conversations.tsx
@@ -1,8 +1,8 @@
import { useMemo, memo } from 'react';
import { parseISO, isToday } from 'date-fns';
import { TConversation } from 'librechat-data-provider';
+import { useLocalize, TranslationKeys } from '~/hooks';
import { groupConversationsByDate } from '~/utils';
-import { useLocalize } from '~/hooks';
import Convo from './Convo';
const Conversations = ({
@@ -41,8 +41,7 @@ const Conversations = ({
paddingLeft: '10px',
}}
>
- {/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */}
- {localize(groupName) || groupName}
+ {localize(groupName as TranslationKeys) || groupName}
{convos.map((convo, i) => (
- {localize(activeSetting)}
+ {localize(activeSetting as TranslationKeys)}
@@ -216,7 +216,9 @@ export default function Fork({
{localize('com_ui_fork_info_1')}
{localize('com_ui_fork_info_2')}
- {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'),
+ })}
@@ -233,7 +235,7 @@ export default function Fork({
hoverTitle={
<>
- {localize(optionLabels[ForkOptions.DIRECT_PATH])}
+ {localize(optionLabels[ForkOptions.DIRECT_PATH] as TranslationKeys)}
>
}
hoverDescription={localize('com_ui_fork_info_visible')}
@@ -251,7 +253,7 @@ export default function Fork({
hoverTitle={
<>
- {localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
+ {localize(optionLabels[ForkOptions.INCLUDE_BRANCHES] as TranslationKeys)}
>
}
hoverDescription={localize('com_ui_fork_info_branches')}
@@ -269,9 +271,9 @@ export default function Fork({
hoverTitle={
<>
- {`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize(
- 'com_endpoint_default',
- )})`}
+ {`${localize(
+ optionLabels[ForkOptions.TARGET_LEVEL] as TranslationKeys,
+ )} (${localize('com_endpoint_default')})`}
>
}
hoverDescription={localize('com_ui_fork_info_target')}
diff --git a/client/src/components/Endpoints/MinimalIcon.tsx b/client/src/components/Endpoints/MinimalIcon.tsx
index f5a5778a72..0c7989755e 100644
--- a/client/src/components/Endpoints/MinimalIcon.tsx
+++ b/client/src/components/Endpoints/MinimalIcon.tsx
@@ -69,6 +69,7 @@ const MinimalIcon: React.FC = (props) => {
{localize('com_endpoint_temperature')}{' '}
- ({localize('com_endpoint_default_with_num', '1')})
+ ({localize('com_endpoint_default_with_num', { 0: '1' })})
Object.values(assistantListMap?.[endpoint ?? ''] ?? {}) as Assistant[],
+ () => Object.values(assistantListMap[endpoint ?? ''] ?? {}) as Assistant[],
[assistantListMap, endpoint],
);
const assistants = useMemo(() => {
- const currentAssistants = (currentList ?? []).map(({ id, name }) => ({
+ const currentAssistants = currentList.map(({ id, name }) => ({
label: name,
value: id,
}));
@@ -52,8 +52,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
});
const activeAssistant = useMemo(() => {
- if (assistant_id) {
- return assistantListMap[endpoint ?? '']?.[assistant_id];
+ if (assistant_id != null && assistant_id) {
+ return assistantListMap[endpoint ?? '']?.[assistant_id] as Assistant | null;
}
return null;
@@ -70,11 +70,13 @@ export default function Settings({ conversation, setOption, models, readonly }:
}, [models, activeAssistant, localize]);
const [assistantValue, setAssistantValue] = useState(
- activeAssistant ? { label: activeAssistant.name, value: activeAssistant.id } : defaultOption,
+ activeAssistant != null
+ ? { label: activeAssistant.name ?? '', value: activeAssistant.id }
+ : defaultOption,
);
useEffect(() => {
- if (assistantValue && assistantValue.value === '') {
+ if (assistantValue.value === '') {
setOption('presetOverride')({
assistant_id: assistantValue.value,
} as Partial);
@@ -95,7 +97,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
return;
}
- const assistant = assistantListMap[endpoint ?? '']?.[value];
+ const assistant = assistantListMap[endpoint ?? '']?.[value] as Assistant | null;
if (!assistant) {
setAssistantValue(defaultOption);
return;
@@ -103,7 +105,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
setAssistantValue({
label: assistant.name ?? '',
- value: assistant.id ?? '',
+ value: assistant.id || '',
});
setOption('assistant_id')(assistant.id);
if (assistant.model) {
diff --git a/client/src/components/Endpoints/Settings/Examples.tsx b/client/src/components/Endpoints/Settings/Examples.tsx
index 362eaac706..98dac9ea94 100644
--- a/client/src/components/Endpoints/Settings/Examples.tsx
+++ b/client/src/components/Endpoints/Settings/Examples.tsx
@@ -37,7 +37,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
setExample(idx, 'input', e.target.value ?? null)}
placeholder="Set example input. Example is ignored if empty."
className={cn(
@@ -62,7 +62,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
setExample(idx, 'output', e.target.value ?? null)}
placeholder={'Set example output. Example is ignored if empty.'}
className={cn(
diff --git a/client/src/components/Endpoints/Settings/Google.tsx b/client/src/components/Endpoints/Settings/Google.tsx
index 318eecb756..063d5f098e 100644
--- a/client/src/components/Endpoints/Settings/Google.tsx
+++ b/client/src/components/Endpoints/Settings/Google.tsx
@@ -180,7 +180,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
{localize('com_endpoint_top_p')}{' '}
- ({localize('com_endpoint_default_with_num', google.topP.default + '')})
+ ({localize('com_endpoint_default_with_num', { 0: google.topP.default + '' })})
{localize('com_endpoint_top_k')}{' '}
- ({localize('com_endpoint_default_with_num', google.topK.default + '')})
+ ({localize('com_endpoint_default_with_num',{ 0: google.topK.default + '' })})
{localize('com_endpoint_max_output_tokens')}{' '}
- ({localize('com_endpoint_default_with_num', google.maxOutputTokens.default + '')})
+ ({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })})
{localize('com_endpoint_temperature')}{' '}
- ({localize('com_endpoint_default_with_num', '0.8')})
+ ({localize('com_endpoint_default_with_num', { 0: '0.8' })})
{localize('com_endpoint_top_p')}{' '}
- ({localize('com_endpoint_default_with_num', '1')})
+ ({localize('com_endpoint_default_with_num', { 0: '1' })})
{localize('com_endpoint_frequency_penalty')}{' '}
- ({localize('com_endpoint_default_with_num', '0')})
+ ({localize('com_endpoint_default_with_num', { 0: '0' })})
{localize('com_endpoint_presence_penalty')}{' '}
- ({localize('com_endpoint_default_with_num', '0')})
+ ({localize('com_endpoint_default_with_num', { 0: '0' })})
{
return (
- {params?.vectorStoreId && (
+ {params.vectorStoreId && (
({
{localize(
- 'com_files_number_selected',
- `${table.getFilteredSelectedRowModel().rows.length}`,
- `${table.getFilteredRowModel().rows.length}`,
+ 'com_files_number_selected', {
+ 0: `${table.getFilteredSelectedRowModel().rows.length}`,
+ 1: `${table.getFilteredRowModel().rows.length}`,
+ },
)}
- {params?.fileId && (
+ {params.fileId != null && params.fileId && (
- Go back
+ {localize('com_ui_go_back')}
)}
diff --git a/client/src/components/Files/VectorStore/VectorStorePreview.tsx b/client/src/components/Files/VectorStore/VectorStorePreview.tsx
index 74e7fe8b0a..c1d70107fa 100644
--- a/client/src/components/Files/VectorStore/VectorStorePreview.tsx
+++ b/client/src/components/Files/VectorStore/VectorStorePreview.tsx
@@ -178,7 +178,7 @@ export default function VectorStorePreview() {
Created At
- {vectorStore.createdAt?.toString()}
+ {vectorStore.createdAt.toString()}
diff --git a/client/src/components/Input/ModelSelect/ModelSelect.tsx b/client/src/components/Input/ModelSelect/ModelSelect.tsx
index c4081db6ac..959fd60ecc 100644
--- a/client/src/components/Input/ModelSelect/ModelSelect.tsx
+++ b/client/src/components/Input/ModelSelect/ModelSelect.tsx
@@ -29,7 +29,7 @@ export default function ModelSelect({
}
const { endpoint: _endpoint, endpointType } = conversation;
- const models = modelsQuery?.data?.[_endpoint] ?? [];
+ const models = modelsQuery.data?.[_endpoint] ?? [];
const endpoint = endpointType ?? _endpoint;
const OptionComponent = multiChatOptions[endpoint];
diff --git a/client/src/components/Input/ModelSelect/TemporaryChat.tsx b/client/src/components/Input/ModelSelect/TemporaryChat.tsx
index 1f4168d953..3b0cae4912 100644
--- a/client/src/components/Input/ModelSelect/TemporaryChat.tsx
+++ b/client/src/components/Input/ModelSelect/TemporaryChat.tsx
@@ -40,13 +40,15 @@ export const TemporaryChat = () => {
};
return (
-
-
-
-
-
{localize('com_ui_temporary_chat')}
+
+
+
+
+
+ {localize('com_ui_temporary_chat')}
+
-
+
{
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) => {
const { info } = json;
- return localize('com_error_input_length', info);
+ return localize('com_error_input_length', { 0: info });
},
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
const { info } = json;
diff --git a/client/src/components/Nav/Bookmarks/BookmarkNav.tsx b/client/src/components/Nav/Bookmarks/BookmarkNav.tsx
index a05643d536..c1de20f1ad 100644
--- a/client/src/components/Nav/Bookmarks/BookmarkNav.tsx
+++ b/client/src/components/Nav/Bookmarks/BookmarkNav.tsx
@@ -35,9 +35,9 @@ const BookmarkNav: FC = ({ tags, setTags, isSmallScreen }: Boo
{tags.length > 0 ? (
-
+
) : (
-
+
)}
diff --git a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx
index fb75b451d9..48f585bdbb 100644
--- a/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx
+++ b/client/src/components/Nav/SettingsTabs/Account/Avatar.tsx
@@ -70,7 +70,7 @@ function Avatar() {
const megabytes =
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
showToast({
- message: localize('com_ui_upload_invalid_var', megabytes + ''),
+ message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error',
});
}
diff --git a/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx b/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx
index 806ab8fbf4..19f706eded 100644
--- a/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx
+++ b/client/src/components/Nav/SettingsTabs/Data/RevokeKeysButton.tsx
@@ -41,7 +41,7 @@ export const RevokeKeysButton = ({
const dialogTitle = all
? localize('com_ui_revoke_keys')
- : localize('com_ui_revoke_key_endpoint', endpoint);
+ : localize('com_ui_revoke_key_endpoint', { 0: endpoint });
const dialogMessage = all
? localize('com_ui_revoke_keys_confirm')
diff --git a/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx b/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx
index 8683523e4d..b7fa279ece 100755
--- a/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx
+++ b/client/src/components/Nav/SettingsTabs/Speech/STT/DecibelSelector.tsx
@@ -15,7 +15,7 @@ export default function DecibelSelector() {
{localize('com_nav_db_sensitivity')}
-
({localize('com_endpoint_default_with_num', '-45')})
+
({localize('com_endpoint_default_with_num', { 0: '-45' })})
{localize('com_nav_playback_rate')}
- ({localize('com_endpoint_default_with_num', '1')})
+ ({localize('com_endpoint_default_with_num', { 0: '1' })})
diff --git a/client/src/components/Prompts/Description.tsx b/client/src/components/Prompts/Description.tsx
index 6ea3388af4..d99d7c3b58 100644
--- a/client/src/components/Prompts/Description.tsx
+++ b/client/src/components/Prompts/Description.tsx
@@ -43,7 +43,7 @@ const Description = ({
return (
-
+
;
+ return ;
}
diff --git a/client/src/components/Prompts/Groups/GroupSidePanel.tsx b/client/src/components/Prompts/Groups/GroupSidePanel.tsx
index 74e3de1745..84a5c3a7a5 100644
--- a/client/src/components/Prompts/Groups/GroupSidePanel.tsx
+++ b/client/src/components/Prompts/Groups/GroupSidePanel.tsx
@@ -30,17 +30,13 @@ export default function GroupSidePanel({
{children}
-
+
);
diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx
index 6e497fa195..edb38735ae 100644
--- a/client/src/components/Prompts/PromptForm.tsx
+++ b/client/src/components/Prompts/PromptForm.tsx
@@ -70,7 +70,7 @@ const PromptForm = () => {
const selectedPrompt = useMemo(
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
- [prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex],
+ [prompts, selectionIndex],
);
const { groupsQuery } = useOutletContext
>();
@@ -102,7 +102,7 @@ const PromptForm = () => {
);
},
onSuccess(data) {
- if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
+ if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) {
makeProductionMutation.mutate({
id: data.prompt._id,
groupId: data.prompt.groupId,
@@ -336,7 +336,7 @@ const PromptForm = () => {
variant="ghost"
className="h-10 w-10 border border-border-light p-0 lg:hidden"
onClick={() => setShowSidePanel(true)}
- aria-label={localize('com_ui_open_menu')}
+ aria-label={localize('com_endpoint_open_menu')}
>
@@ -382,8 +382,8 @@ const PromptForm = () => {
onClick={() => setShowSidePanel(false)}
aria-hidden={!showSidePanel}
tabIndex={showSidePanel ? 0 : -1}
+ aria-label={localize('com_ui_close_menu')}
/>
-
-
+
{localize('com_ui_variables')}
diff --git a/client/src/components/Prompts/PromptVersions.tsx b/client/src/components/Prompts/PromptVersions.tsx
index 2f34a8d2e2..b221087de4 100644
--- a/client/src/components/Prompts/PromptVersions.tsx
+++ b/client/src/components/Prompts/PromptVersions.tsx
@@ -111,12 +111,12 @@ const VersionCard = ({
onClick={onClick}
aria-selected={isSelected}
role="tab"
- aria-label={localize('com_ui_version_var', `${totalVersions - index}`)}
+ aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
>
- {localize('com_ui_version_var', `${totalVersions - index}`)}
+ {localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}
diff --git a/client/src/components/Prompts/SharePrompt.tsx b/client/src/components/Prompts/SharePrompt.tsx
index 76ad63e60b..2aa9f23b70 100644
--- a/client/src/components/Prompts/SharePrompt.tsx
+++ b/client/src/components/Prompts/SharePrompt.tsx
@@ -97,7 +97,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
- {localize('com_ui_share_var', `"${group.name}"`)}
+ {localize('com_ui_share_var', { 0: `"${group.name}"` })}
diff --git a/client/src/components/SidePanel/Agents/ActionsTable/Table.tsx b/client/src/components/SidePanel/Agents/ActionsTable/Table.tsx
index 921c9571ae..231badf53a 100644
--- a/client/src/components/SidePanel/Agents/ActionsTable/Table.tsx
+++ b/client/src/components/SidePanel/Agents/ActionsTable/Table.tsx
@@ -22,7 +22,7 @@ export default function DataTable({ columns, data }: DataTablePro
className="border-token-border-light text-token-text-tertiary border-b text-left text-xs"
>
{headerGroup.headers.map((header, j) => (
-
+
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
diff --git a/client/src/components/SidePanel/Agents/AdminSettings.tsx b/client/src/components/SidePanel/Agents/AdminSettings.tsx
index 2b38a72e1d..6ca21d1317 100644
--- a/client/src/components/SidePanel/Agents/AdminSettings.tsx
+++ b/client/src/components/SidePanel/Agents/AdminSettings.tsx
@@ -144,7 +144,7 @@ const AdminSettings = () => {
variant={'outline'}
className="btn btn-neutral border-token-border-light relative mb-4 h-9 w-full gap-1 rounded-lg font-medium"
>
-
+
{localize('com_ui_admin_settings')}
diff --git a/client/src/components/SidePanel/Agents/AgentAvatar.tsx b/client/src/components/SidePanel/Agents/AgentAvatar.tsx
index aa0ca44f04..80b58e787b 100644
--- a/client/src/components/SidePanel/Agents/AgentAvatar.tsx
+++ b/client/src/components/SidePanel/Agents/AgentAvatar.tsx
@@ -171,7 +171,7 @@ function Avatar({
} else {
const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2;
showToast({
- message: localize('com_ui_upload_invalid_var', megabytes + ''),
+ message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error',
});
}
diff --git a/client/src/components/SidePanel/Agents/AgentConfig.tsx b/client/src/components/SidePanel/Agents/AgentConfig.tsx
index fe90ebd910..9fc7674158 100644
--- a/client/src/components/SidePanel/Agents/AgentConfig.tsx
+++ b/client/src/components/SidePanel/Agents/AgentConfig.tsx
@@ -10,7 +10,7 @@ import {
AgentCapabilities,
} from 'librechat-data-provider';
import type { TPlugin } from 'librechat-data-provider';
-import type { AgentForm, AgentPanelProps } from '~/common';
+import type { AgentForm, AgentPanelProps, IconComponentTypes } from '~/common';
import { cn, defaultTextProps, removeFocusOutlines, getEndpointField, getIconKey } from '~/utils';
import { useCreateAgentMutation, useUpdateAgentMutation } from '~/data-provider';
import { useLocalize, useAuthContext, useHasAccess } from '~/hooks';
@@ -26,6 +26,7 @@ import AgentAvatar from './AgentAvatar';
import { Spinner } from '~/components';
import FileSearch from './FileSearch';
import ShareAgent from './ShareAgent';
+import Artifacts from './Artifacts';
import AgentTool from './AgentTool';
import CodeForm from './Code/Form';
import { Panel } from '~/common';
@@ -77,6 +78,10 @@ export default function AgentConfig({
() => agentsConfig?.capabilities.includes(AgentCapabilities.actions),
[agentsConfig],
);
+ const artifactsEnabled = useMemo(
+ () => agentsConfig?.capabilities.includes(AgentCapabilities.artifacts) ?? false,
+ [agentsConfig],
+ );
const fileSearchEnabled = useMemo(
() => agentsConfig?.capabilities.includes(AgentCapabilities.file_search) ?? false,
[agentsConfig],
@@ -150,7 +155,7 @@ export default function AgentConfig({
onSuccess: (data) => {
setCurrentAgentId(data.id);
showToast({
- message: `${localize('com_assistants_create_success ')} ${
+ message: `${localize('com_assistants_create_success')} ${
data.name ?? localize('com_ui_agent')
}`,
});
@@ -178,18 +183,10 @@ export default function AgentConfig({
}, [agent_id, setActivePanel, showToast, localize]);
const providerValue = typeof provider === 'string' ? provider : provider?.value;
+ let Icon: IconComponentTypes | null | undefined;
let endpointType: EModelEndpoint | undefined;
let endpointIconURL: string | undefined;
let iconKey: string | undefined;
- let Icon:
- | React.ComponentType<
- React.SVGProps & {
- endpoint: string;
- endpointType: EModelEndpoint | undefined;
- iconURL: string | undefined;
- }
- >
- | undefined;
if (providerValue !== undefined) {
endpointType = getEndpointField(endpointsConfig, providerValue as string, 'type');
@@ -337,7 +334,7 @@ export default function AgentConfig({
- {(codeEnabled || fileSearchEnabled) && (
+ {(codeEnabled || fileSearchEnabled || artifactsEnabled) && (
{localize('com_assistants_capabilities')}
@@ -346,6 +343,8 @@ export default function AgentConfig({
{codeEnabled && }
{/* File Search */}
{fileSearchEnabled && }
+ {/* Artifacts */}
+ {artifactsEnabled && }
)}
{/* Agent Tools & Actions */}
diff --git a/client/src/components/SidePanel/Agents/AgentPanel.tsx b/client/src/components/SidePanel/Agents/AgentPanel.tsx
index 0f49dc30e7..cea3265ebc 100644
--- a/client/src/components/SidePanel/Agents/AgentPanel.tsx
+++ b/client/src/components/SidePanel/Agents/AgentPanel.tsx
@@ -120,6 +120,7 @@ export default function AgentPanel({
const {
name,
+ artifacts,
description,
instructions,
model: _model,
@@ -139,6 +140,7 @@ export default function AgentPanel({
agent_id,
data: {
name,
+ artifacts,
description,
instructions,
model,
@@ -162,6 +164,7 @@ export default function AgentPanel({
create.mutate({
name,
+ artifacts,
description,
instructions,
model,
@@ -184,7 +187,7 @@ export default function AgentPanel({
const canEditAgent = useMemo(() => {
const canEdit =
- agentQuery.data?.isCollaborative ?? false
+ (agentQuery.data?.isCollaborative ?? false)
? true
: agentQuery.data?.author === user?.id || user?.role === SystemRoles.ADMIN;
diff --git a/client/src/components/SidePanel/Agents/AgentSelect.tsx b/client/src/components/SidePanel/Agents/AgentSelect.tsx
index 5d406c48b3..caeb0457e8 100644
--- a/client/src/components/SidePanel/Agents/AgentSelect.tsx
+++ b/client/src/components/SidePanel/Agents/AgentSelect.tsx
@@ -55,8 +55,8 @@ export default function AgentSelect({
};
const capabilities: TAgentCapabilities = {
- [AgentCapabilities.execute_code]: false,
[AgentCapabilities.file_search]: false,
+ [AgentCapabilities.execute_code]: false,
[AgentCapabilities.end_after_tools]: false,
[AgentCapabilities.hide_sequential_outputs]: false,
};
diff --git a/client/src/components/SidePanel/Agents/Artifacts.tsx b/client/src/components/SidePanel/Agents/Artifacts.tsx
new file mode 100644
index 0000000000..2a814cc7f1
--- /dev/null
+++ b/client/src/components/SidePanel/Agents/Artifacts.tsx
@@ -0,0 +1,124 @@
+import { useFormContext } from 'react-hook-form';
+import { ArtifactModes, AgentCapabilities } from 'librechat-data-provider';
+import type { AgentForm } from '~/common';
+import {
+ Switch,
+ HoverCard,
+ HoverCardPortal,
+ HoverCardContent,
+ HoverCardTrigger,
+} from '~/components/ui';
+import { useLocalize } from '~/hooks';
+import { CircleHelpIcon } from '~/components/svg';
+import { ESide } from '~/common';
+
+export default function Artifacts() {
+ const localize = useLocalize();
+ const methods = useFormContext();
+ const { setValue, watch } = methods;
+
+ const artifactsMode = watch(AgentCapabilities.artifacts);
+
+ const handleArtifactsChange = (value: boolean) => {
+ setValue(AgentCapabilities.artifacts, value ? ArtifactModes.DEFAULT : '', {
+ shouldDirty: true,
+ });
+ };
+
+ const handleShadcnuiChange = (value: boolean) => {
+ setValue(AgentCapabilities.artifacts, value ? ArtifactModes.SHADCNUI : ArtifactModes.DEFAULT, {
+ shouldDirty: true,
+ });
+ };
+
+ const handleCustomModeChange = (value: boolean) => {
+ setValue(AgentCapabilities.artifacts, value ? ArtifactModes.CUSTOM : ArtifactModes.DEFAULT, {
+ shouldDirty: true,
+ });
+ };
+
+ const isEnabled = artifactsMode !== undefined && artifactsMode !== '';
+ const isCustomEnabled = artifactsMode === ArtifactModes.CUSTOM;
+ const isShadcnEnabled = artifactsMode === ArtifactModes.SHADCNUI;
+
+ return (
+
+
+
+
+ {localize('com_ui_artifacts')}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SwitchItem({
+ id,
+ label,
+ checked,
+ onCheckedChange,
+ hoverCardText,
+ disabled = false,
+}: {
+ id: string;
+ label: string;
+ checked: boolean;
+ onCheckedChange: (value: boolean) => void;
+ hoverCardText: string;
+ disabled?: boolean;
+}) {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/components/SidePanel/Agents/Code/Action.tsx b/client/src/components/SidePanel/Agents/Code/Action.tsx
index a2a16d4419..e655101b76 100644
--- a/client/src/components/SidePanel/Agents/Code/Action.tsx
+++ b/client/src/components/SidePanel/Agents/Code/Action.tsx
@@ -86,7 +86,7 @@ export default function Action({ authType = '', isToolAuthenticated = false }) {
)}
-
+
diff --git a/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx
index 4d9dd85f5f..f006d97691 100644
--- a/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx
+++ b/client/src/components/SidePanel/Agents/FileSearchCheckbox.tsx
@@ -29,7 +29,7 @@ export default function FileSearchCheckbox() {
{...field}
checked={field.value}
onCheckedChange={field.onChange}
- className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
+ className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field.value.toString()}
/>
)}
@@ -38,7 +38,6 @@ export default function FileSearchCheckbox() {
type="button"
className="flex items-center space-x-2"
onClick={() =>
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true,
})
@@ -51,7 +50,7 @@ export default function FileSearchCheckbox() {
{localize('com_agents_enable_file_search')}
-
+
diff --git a/client/src/components/SidePanel/Agents/ImageVision.tsx b/client/src/components/SidePanel/Agents/ImageVision.tsx
index 7536e8470a..4be2443145 100644
--- a/client/src/components/SidePanel/Agents/ImageVision.tsx
+++ b/client/src/components/SidePanel/Agents/ImageVision.tsx
@@ -20,7 +20,7 @@ export default function ImageVision() {
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
- value={field?.value?.toString()}
+ value={field.value?.toString()}
/>
)}
/>
diff --git a/client/src/components/SidePanel/Agents/ModelPanel.tsx b/client/src/components/SidePanel/Agents/ModelPanel.tsx
index c1a26ebf9a..475b0341f6 100644
--- a/client/src/components/SidePanel/Agents/ModelPanel.tsx
+++ b/client/src/components/SidePanel/Agents/ModelPanel.tsx
@@ -221,8 +221,8 @@ export default function Parameters({
onClick={handleResetParameters}
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
-
- {localize('com_ui_reset_var', localize('com_ui_model_parameters'))}
+
+ {localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
diff --git a/client/src/components/SidePanel/Agents/Retrieval.tsx b/client/src/components/SidePanel/Agents/Retrieval.tsx
index 97e9789af7..1900ae9f44 100644
--- a/client/src/components/SidePanel/Agents/Retrieval.tsx
+++ b/client/src/components/SidePanel/Agents/Retrieval.tsx
@@ -43,7 +43,7 @@ export default function Retrieval({ retrievalModels }: { retrievalModels: Set
)}
/>
diff --git a/client/src/components/SidePanel/Agents/Sequential/HideSequential.tsx b/client/src/components/SidePanel/Agents/Sequential/HideSequential.tsx
index f52207eabc..8fdc50f876 100644
--- a/client/src/components/SidePanel/Agents/Sequential/HideSequential.tsx
+++ b/client/src/components/SidePanel/Agents/Sequential/HideSequential.tsx
@@ -38,7 +38,7 @@ export default function HideSequential() {
type="button"
className="flex items-center space-x-2"
onClick={() =>
- // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),
diff --git a/client/src/components/SidePanel/Agents/ShareAgent.tsx b/client/src/components/SidePanel/Agents/ShareAgent.tsx
index 8f65a60fdd..43aba233d2 100644
--- a/client/src/components/SidePanel/Agents/ShareAgent.tsx
+++ b/client/src/components/SidePanel/Agents/ShareAgent.tsx
@@ -137,7 +137,7 @@ export default function ShareAgent({
)}
aria-label={localize(
'com_ui_share_var',
- agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
+ { 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
)}
type="button"
>
@@ -150,7 +150,7 @@ export default function ShareAgent({
{localize(
'com_ui_share_var',
- agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
+ { 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
)}